910e62b5创建于 1月15日历史提交
// Copyright 2020 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_controller.h"

#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "ash/capture_mode/base_capture_mode_session.h"
#include "ash/capture_mode/capture_mode_ash_notification_view.h"
#include "ash/capture_mode/capture_mode_behavior.h"
#include "ash/capture_mode/capture_mode_camera_controller.h"
#include "ash/capture_mode/capture_mode_education_controller.h"
#include "ash/capture_mode/capture_mode_metrics.h"
#include "ash/capture_mode/capture_mode_observer.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/null_capture_mode_session.h"
#include "ash/capture_mode/search_results_panel.h"
#include "ash/capture_mode/sunfish_scanner_feature_watcher.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/public/cpp/capture_mode/capture_mode_api.h"
#include "ash/public/cpp/holding_space/holding_space_client.h"
#include "ash/public/cpp/holding_space/holding_space_controller.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/saved_desk_delegate.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/root_window_controller.h"
#include "ash/scanner/scanner_action_view_model.h"
#include "ash/scanner/scanner_controller.h"
#include "ash/scanner/scanner_disclaimer.h"
#include "ash/scanner/scanner_metrics.h"
#include "ash/scanner/scanner_session.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/message_view_factory.h"
#include "ash/system/toast/anchored_nudge_manager_impl.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/window_state.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/location.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/current_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "capture_mode_util.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_type.h"
#include "components/vector_icons/vector_icons.h"
#include "components/viz/host/host_frame_sink_manager.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "ui/aura/env.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_util.h"

#undef ENABLED_VLOG_LEVEL
#define ENABLED_VLOG_LEVEL 1

namespace ash {

namespace {

CaptureModeController* g_instance = nullptr;

// The amount of time that can elapse from the prior screenshot to be considered
// consecutive.
constexpr base::TimeDelta kConsecutiveScreenshotThreshold = base::Seconds(5);

constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification";
constexpr char kScreenCaptureStoppedNotificationId[] =
    "capture_mode_stopped_notification";
constexpr char kScreenCaptureNotifierId[] = "ash.capture_mode_controller";
constexpr char kScreenShotNotificationType[] = "screen_shot_notification_type";
constexpr char kScreenRecordingNotificationType[] =
    "screen_recording_notification_type";

// The base file names of captured images.
// TODO(afakhry): Discuss with UX localizing "Screenshot" and "Screen
// recording".
constexpr char kScreenshotFileName[] = "Screenshot";
constexpr char kVideoFileName[] = "Screen recording";

// Duration to clear the capture region selection from the previous session.
constexpr base::TimeDelta kResetCaptureRegionDuration = base::Minutes(8);

// The name of a file path pref for the user-selected custom path to which
// captured images and videos should be saved.
constexpr char kCustomCapturePathPrefName[] =
    "ash.capture_mode.custom_save_path";

// The name of a boolean pref that indicates whether the default downloads path
// is currently selected even if a custom capture path is set.
constexpr char kUsesDefaultCapturePathPrefName[] =
    "ash.capture_mode.uses_default_capture_path";

constexpr char kShareToYouTubeURL[] = "https://youtube.com/upload";

// TODO: crbug.com/388287849 - Clear this pref.
// The name of a boolean pref that determines whether we can show the demo tools
// user nudge. When this pref is false, it means that we showed the nudge at
// some point and the user interacted with the capture mode session UI in such a
// way that the nudge no longer needs to be displayed again.
constexpr char kCanShowDemoToolsNudge[] =
    "ash.capture_mode.can_show_demo_tools_nudge";

// The name of a boolean pref that determines whether we can show the sunfish
// region user nudge. When this pref is false, it means that we showed the nudge
// at some point and the user interacted with the capture mode session UI in
// such a way that the nudge no longer needs to be displayed again.
constexpr char kCanShowSunfishRegionNudge[] =
    "ash.capture_mode.can_show_sunfish_region_nudge";

// The ID for the toast shown when text is copied to clipboard.
constexpr char kCaptureModeTextCopiedToastId[] = "capture_mode_text_copied";

// The ID for the anchored nudge that shows when a user tries to perform a
// Sunfish image search while an Incognito Chrome window is open.
constexpr char kSunfishIncognitoNudgeId[] = "kSunfishIncognitoNudge";

// An invalid IDS value used as a placeholder to not show a message in a
// notification.
constexpr int kNoMessage = -1;

// The screenshot notification button index.
enum ScreenshotNotificationButtonIndex {
  kButtonEdit = 0,
  kButtonDelete,
};

// The video notification button index.
enum GameDashboardVideoNotificationButtonIndex {
  kButtonShareToYoutube = 0,
  kButtonDeleteGameVideo,
};
enum VideoNotificationButtonIndex {
  kButtonDeleteVideo = 0,
};

// Returns the file extension for the given `recording_type` and the current
// capture `source`.
std::string GetVideoExtension(RecordingType recording_type,
                              CaptureModeSource source) {
  switch (recording_type) {
    case RecordingType::kGif:
      // Currently, we only support recording GIF for partial regions, so we
      // ignore the recording type if the source is fullscreen or window, and
      // force recording in webm.
      return source == CaptureModeSource::kRegion ? "gif" : "webm";
    case RecordingType::kWebM:
      return "webm";
  }
}

// Returns true if the given `video_file_path` is of a type that supports audio
// recording (e.g. ".webm" files).
bool SupportsAudioRecording(const base::FilePath& video_file_path) {
  return video_file_path.MatchesExtension(".webm");
}

bool IsVideoFileExtensionSupported(const base::FilePath& video_file_path) {
  for (const auto* const extension : {".webm", ".gif"}) {
    if (video_file_path.MatchesExtension(extension)) {
      return true;
    }
  }
  return false;
}

// Selects a file path for captured files (image/video) from `current_path` and
// `fallback_path`. If `current_path` is valid, use `current_path`, otherwise
// use `fallback_path`.
base::FilePath SelectFilePathForCapturedFile(
    const base::FilePath& current_path,
    const base::FilePath& fallback_path) {
  if (base::PathExists(current_path.DirName()))
    return current_path;
  DCHECK(base::PathExists(fallback_path.DirName()));
  return fallback_path;
}

// Writes the given `data` in a file with `path`. Returns true if saving
// succeeded, or false otherwise.
base::FilePath DoSaveFile(scoped_refptr<base::RefCountedMemory> data,
                          const base::FilePath& path) {
  DCHECK(data);
  DCHECK(data->size());
  if (!base::WriteFile(path, *data)) {
    LOG(ERROR) << "Failed to save file: " << path;
    return base::FilePath();
  }
  return path;
}

// Attempts to write the given `data` with the file path returned from
// `SelectAFilePathForCapturedFile`.
base::FilePath SaveFile(scoped_refptr<base::RefCountedMemory> data,
                        const base::FilePath& current_path,
                        const base::FilePath& fallback_path) {
  DCHECK(!base::CurrentUIThread::IsSet());
  DCHECK(!current_path.empty());
  DCHECK(!fallback_path.empty());

  return DoSaveFile(data,
                    SelectFilePathForCapturedFile(current_path, fallback_path));
}

// Called when the "Share to YouTube" button is pressed to
// open the YouTube share video page.
void OnShareToYouTubeButtonPressed() {
  NewWindowDelegate::GetInstance()->OpenUrl(
      GURL(kShareToYouTubeURL),
      NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

// Adds the given `notification` to the message center after it removes any
// existing notification that has the same ID.
void AddNotificationToMessageCenter(
    std::unique_ptr<message_center::Notification> notification) {
  auto* message_center = message_center::MessageCenter::Get();
  message_center->RemoveNotification(notification->id(),
                                     /*by_user=*/false);
  message_center->AddNotification(std::move(notification));
}

// Shows a Capture Mode related notification with the given parameters.
// |for_video_thumbnail| will be considered only if |optional_fields| contain
// an image to show in the notification as a thumbnail for what was captured.
void ShowNotification(
    const std::string& notification_id,
    int title_id,
    int message_id,
    const message_center::RichNotificationData& optional_fields,
    scoped_refptr<message_center::NotificationDelegate> delegate,
    message_center::SystemNotificationWarningLevel warning_level =
        message_center::SystemNotificationWarningLevel::NORMAL,
    const gfx::VectorIcon& notification_icon = kCaptureModeIcon,
    bool for_video_thumbnail = false) {
  const auto type = optional_fields.image.IsEmpty()
                        ? message_center::NOTIFICATION_TYPE_SIMPLE
                        : message_center::NOTIFICATION_TYPE_CUSTOM;
  const std::u16string message = message_id == kNoMessage
                                     ? std::u16string()
                                     : l10n_util::GetStringUTF16(message_id);
  std::unique_ptr<message_center::Notification> notification =
      CreateSystemNotificationPtr(
          type, notification_id, l10n_util::GetStringUTF16(title_id), message,
          l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE),
          GURL(),
          message_center::NotifierId(
              message_center::NotifierType::SYSTEM_COMPONENT,
              kScreenCaptureNotifierId,
              NotificationCatalogName::kScreenCapture),
          optional_fields, delegate, notification_icon, warning_level);
  if (type == message_center::NOTIFICATION_TYPE_CUSTOM) {
    notification->set_custom_view_type(for_video_thumbnail
                                           ? kScreenRecordingNotificationType
                                           : kScreenShotNotificationType);
  }

  AddNotificationToMessageCenter(std::move(notification));
}

// Shows a notification informing the user that a Capture Mode operation has
// failed.
void ShowFailureNotification() {
  ShowNotification(kScreenCaptureStoppedNotificationId,
                   IDS_ASH_SCREEN_CAPTURE_FAILURE_TITLE,
                   IDS_ASH_SCREEN_CAPTURE_FAILURE_MESSAGE,
                   /*optional_fields=*/{}, /*delegate=*/nullptr);
}

// Shows a notification that indicates to the user that the GIF file is being
// processed and will be ready shortly.
void ShowGifProgressNotification() {
  message_center::RichNotificationData optional_fields;
  optional_fields.progress = -1;  // Infinite progress.
  optional_fields.never_timeout = true;
  AddNotificationToMessageCenter(CreateSystemNotificationPtr(
      message_center::NOTIFICATION_TYPE_PROGRESS, kScreenCaptureNotificationId,
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_GIF_PROGRESS_TITLE),
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_GIF_PROGRESS_MESSAGE),
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE), GURL(),
      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
                                 kScreenCaptureNotifierId,
                                 NotificationCatalogName::kScreenCapture),
      optional_fields, /*delegate=*/nullptr, kCaptureModeIcon,
      message_center::SystemNotificationWarningLevel::NORMAL));
}

// Returns the ID of the message or the title for the notification based on
// |allowance| and |for_title|.
int GetDisabledNotificationMessageId(CaptureAllowance allowance,
                                     bool for_title) {
  switch (allowance) {
    case CaptureAllowance::kDisallowedByPolicy:
      return for_title ? IDS_ASH_SCREEN_CAPTURE_POLICY_DISABLED_TITLE
                       : IDS_ASH_SCREEN_CAPTURE_POLICY_DISABLED_MESSAGE;
    case CaptureAllowance::kDisallowedByHdcp:
      return for_title ? IDS_ASH_SCREEN_CAPTURE_HDCP_STOPPED_TITLE
                       : IDS_ASH_SCREEN_CAPTURE_HDCP_BLOCKED_MESSAGE;
    case CaptureAllowance::kAllowed:
      NOTREACHED();
  }
}

// Shows a notification informing the user that Capture Mode operations are
// currently disabled. |allowance| identifies the reason why the operation is
// currently disabled.
void ShowDisabledNotification(CaptureAllowance allowance) {
  DCHECK(allowance != CaptureAllowance::kAllowed);
  ShowNotification(
      kScreenCaptureNotificationId,
      GetDisabledNotificationMessageId(allowance, /*for_title=*/true),
      GetDisabledNotificationMessageId(allowance, /*for_title=*/false),
      /*optional_fields=*/{}, /*delegate=*/nullptr,
      message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
      allowance == CaptureAllowance::kDisallowedByHdcp
          ? kCaptureModeIcon
          : vector_icons::kBusinessIcon);
}

// Shows a notification informing the user that video recording was stopped due
// to a content-enforced protection.
void ShowVideoRecordingStoppedByHdcpNotification() {
  ShowNotification(
      kScreenCaptureStoppedNotificationId,
      IDS_ASH_SCREEN_CAPTURE_HDCP_STOPPED_TITLE,
      IDS_ASH_SCREEN_CAPTURE_HDCP_BLOCKED_MESSAGE,
      /*optional_fields=*/{}, /*delegate=*/nullptr,
      message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
      kCaptureModeIcon);
}

// Shows a toast informing the user that text has been copied to clipboard.
void ShowTextCopiedToast() {
  ToastManager::Get()->Show(ToastData(
      kCaptureModeTextCopiedToastId, ToastCatalogName::kCaptureModeTextCopied,
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_TEXT_COPIED_TOAST)));
}

// Copies the bitmap representation of the given |image| to the clipboard.
void CopyImageToClipboard(const gfx::Image& image) {
  ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste)
      .WriteImage(image.AsBitmap());
}

void CopyTextToClipboard(const std::u16string& text) {
  ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste).WriteText(text);
}

// Emits UMA samples for the |status| of the recording as reported by the
// recording service.
void EmitServiceRecordingStatus(recording::mojom::RecordingStatus status) {
  using recording::mojom::RecordingStatus;
  switch (status) {
    case RecordingStatus::kSuccess:
      // We emit no samples for success status, as in this case the recording
      // was ended normally by the client, and the end reason for that is
      // emitted else where.
      break;
    case RecordingStatus::kServiceClosing:
      RecordEndRecordingReason(EndRecordingReason::kServiceClosing);
      break;
    case RecordingStatus::kVizVideoCapturerDisconnected:
      RecordEndRecordingReason(
          EndRecordingReason::kVizVideoCaptureDisconnected);
      break;
    case RecordingStatus::kAudioEncoderInitializationFailure:
      RecordEndRecordingReason(
          EndRecordingReason::kAudioEncoderInitializationFailure);
      break;
    case RecordingStatus::kVideoEncoderInitializationFailure:
      RecordEndRecordingReason(
          EndRecordingReason::kVideoEncoderInitializationFailure);
      break;
    case RecordingStatus::kAudioEncodingError:
      RecordEndRecordingReason(EndRecordingReason::kAudioEncodingError);
      break;
    case RecordingStatus::kVideoEncodingError:
      RecordEndRecordingReason(EndRecordingReason::kVideoEncodingError);
      break;
    case RecordingStatus::kIoError:
      RecordEndRecordingReason(EndRecordingReason::kFileIoError);
      break;
    case RecordingStatus::kLowDiskSpace:
      RecordEndRecordingReason(EndRecordingReason::kLowDiskSpace);
      break;
    case RecordingStatus::kLowDriveFsQuota:
      RecordEndRecordingReason(EndRecordingReason::kLowDriveFsQuota);
      break;
    case RecordingStatus::kVideoEncoderReconfigurationFailure:
      RecordEndRecordingReason(
          EndRecordingReason::kVideoEncoderReconfigurationFailure);
      break;
  }
}

base::FilePath GetTempDir() {
  base::FilePath temp_dir;
  if (!base::GetTempDir(&temp_dir))
    LOG(ERROR) << "Failed to find the temporary directory.";
  return temp_dir;
}

int GetNotificationTitleIdForFile(const base::FilePath& file_path) {
  if (file_path.MatchesExtension(".gif")) {
    return IDS_ASH_SCREEN_CAPTURE_GIF_RECORDING_TITLE;
  }

  if (file_path.MatchesExtension(".webm")) {
    return IDS_ASH_SCREEN_CAPTURE_RECORDING_TITLE;
  }

  DCHECK(file_path.MatchesExtension(".png"));
  return IDS_ASH_SCREEN_CAPTURE_SCREENSHOT_TITLE;
}

// Returns the size of the file at the given `file_path` in KBs. Returns -1 when
// a failure occurs.
int GetFileSizeInKB(const base::FilePath& file_path) {
  std::optional<int64_t> size_in_bytes = base::GetFileSize(file_path);
  if (!size_in_bytes.has_value()) {
    return -1;
  }
  // Convert the value to KBs.
  return size_in_bytes.value() / 1024;
}

// Creates a new `CaptureModeSession` based on the given `session_type`. Can be
// a regular session or a null session.
std::unique_ptr<BaseCaptureModeSession> CreateSession(
    SessionType session_type,
    CaptureModeController* controller,
    CaptureModeBehavior* active_behavior) {
  switch (session_type) {
    case SessionType::kReal:
      return std::make_unique<CaptureModeSession>(controller, active_behavior);

    case SessionType::kNull:
      return std::make_unique<NullCaptureModeSession>(controller,
                                                      active_behavior);
  }

  NOTREACHED();
}

// Hides the cursor to avoid capturing it in the screenshot. Returns true if the
// cursor is already locked, in which case there is no need to unlock it after.
bool MaybeLockCursor() {
  auto* cursor_manager = Shell::Get()->cursor_manager();
  bool was_cursor_originally_blocked = cursor_manager->IsCursorLocked();
  if (!was_cursor_originally_blocked) {
    cursor_manager->HideCursor();
    cursor_manager->LockCursor();
  }
  return was_cursor_originally_blocked;
}

// Re-shows the cursor after the image capture, if the cursor was locked by us.
void MaybeUnlockCursor(bool was_cursor_originally_blocked) {
  if (!was_cursor_originally_blocked) {
    auto* cursor_manager = Shell::Get()->cursor_manager();
    if (!display::Screen::Get()->InTabletMode()) {
      cursor_manager->ShowCursor();
    }
    // TODO(crbug.com/376171009): Investigate why the cursor may have already
    // been unlocked even though image capture should have locked the cursor.
    if (cursor_manager->IsCursorLocked()) {
      cursor_manager->UnlockCursor();
    }
  }
}

// Given a `CaptureModeEntryType`, returns the `BehaviorType` associated with
// it, or default behavior if none exists.
BehaviorType ToBehaviorType(CaptureModeEntryType entry_type) {
  switch (entry_type) {
    case CaptureModeEntryType::kProjector:
      return BehaviorType::kProjector;
    case CaptureModeEntryType::kGameDashboard:
      return BehaviorType::kGameDashboard;
    case CaptureModeEntryType::kSunfish:
      DCHECK(CanShowSunfishOrScannerUi());
      return BehaviorType::kSunfish;
    default:
      return BehaviorType::kDefault;
  }
}

// Returns true if text detection should be performed on a captured image with
// the given `capture_type`.
bool ShouldPerformTextDetection(PerformCaptureType capture_type) {
  return features::IsCaptureModeOnDeviceOcrEnabled() &&
         capture_type == PerformCaptureType::kTextDetection;
}

// Returns true if Scanner actions should be fetched for a captured image with
// the given `capture_type`.
// This will return true even if Scanner is disabled so the appropriate metrics
// can be emitted.
bool ShouldFetchScannerActions(PerformCaptureType capture_type) {
  return capture_type == PerformCaptureType::kSunfish ||
         capture_type == PerformCaptureType::kScanner;
}

bool CaptureTypeIsImageSearch(PerformCaptureType capture_type) {
  return capture_type == PerformCaptureType::kSunfish ||
         capture_type == PerformCaptureType::kSearch;
}

// Returns true if region search should be performed on a captured image with
// the given `capture_type`.
bool ShouldSendRegionSearch(PerformCaptureType capture_type) {
  return CanShowSunfishUi() && CaptureTypeIsImageSearch(capture_type);
}

// Returns true if the capture type requires a network connection.
bool CaptureTypeRequiresNetworkConnection(PerformCaptureType capture_type) {
  switch (capture_type) {
    case PerformCaptureType::kCapture:
    case PerformCaptureType::kTextDetection:
      return false;
    case PerformCaptureType::kSearch:
    case PerformCaptureType::kScanner:
    case PerformCaptureType::kSunfish:
      return true;
  }
}

// Returns the target panel bounds in screen coordinates.
gfx::Rect CalculateSearchResultPanelScreenBounds(
    const gfx::Rect& work_area_in_screen,
    const gfx::Rect& captured_region_in_screen) {
  // Attempt to place the panel on the left by default.
  gfx::Rect bounds(
      work_area_in_screen.x() + capture_mode::kPanelWorkAreaSpacing,
      work_area_in_screen.bottom() -
          capture_mode::kSearchResultsPanelTotalHeight -
          capture_mode::kPanelWorkAreaSpacing,
      capture_mode::kSearchResultsPanelTotalWidth,
      capture_mode::kSearchResultsPanelTotalHeight);

  // If the region would then intersect with the panel, attempt to place the
  // panel on the right.
  if (bounds.Intersects(captured_region_in_screen)) {
    bounds.set_x(work_area_in_screen.right() -
                 capture_mode::kSearchResultsPanelTotalWidth -
                 capture_mode::kPanelWorkAreaSpacing);

    // If the region would still intersect with the panel, choose the side with
    // the least intersection.
    if (bounds.Intersects(captured_region_in_screen)) {
      // Calculate the horizontal distance from the centerpoint of the work area
      // to the left and right edges of the capture region. The panel will be
      // placed on the side with the smaller distance (more space for the
      // panel).
      const int center_x = work_area_in_screen.CenterPoint().x();
      const int left_dist = center_x - captured_region_in_screen.x();
      const int right_dist = captured_region_in_screen.right() - center_x;
      if (left_dist < right_dist) {
        bounds.set_x(work_area_in_screen.x() +
                     capture_mode::kPanelWorkAreaSpacing);
      }
    }
  }

  return bounds;
}

bool IsIncognitoWindow(aura::Window* window) {
  return !Shell::Get()->saved_desk_delegate()->IsWindowPersistable(window);
}

bool IsIncognitoWindowOpen() {
  auto windows =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
  for (aura::Window* window : windows) {
    if (IsIncognitoWindow(window) && !WindowState::Get(window)->IsMinimized()) {
      return true;
    }
  }

  return false;
}

void ShowSunfishIncognitoNudge() {
  AnchoredNudgeData nudge_data(
      kSunfishIncognitoNudgeId, NudgeCatalogName::kSunfishIncognitoNudge,
      l10n_util::GetStringUTF16(IDS_ASH_SUNFISH_INCOGNITO_NUDGE_LABEL));
  AnchoredNudgeManager::Get()->Show(nudge_data);
}

}  // namespace

CaptureModeController::CaptureModeController(
    std::unique_ptr<CaptureModeDelegate> delegate)
    : delegate_(std::move(delegate)),
      camera_controller_(
          std::make_unique<CaptureModeCameraController>(delegate_.get())),
      blocking_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          // A task priority of BEST_EFFORT is good enough for this runner,
          // since it's used for blocking file IO such as saving the
          // screenshots.
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})),
      num_consecutive_screenshots_scheduler_(
          FROM_HERE,
          kConsecutiveScreenshotThreshold,
          this,
          &CaptureModeController::RecordAndResetConsecutiveScreenshots),
      education_controller_(
          std::make_unique<CaptureModeEducationController>()) {
  DCHECK_EQ(g_instance, nullptr);
  g_instance = this;

  // Schedule recording of the number of screenshots taken per day.
  num_screenshots_taken_in_last_day_scheduler_.Start(
      FROM_HERE, base::Days(1),
      base::BindRepeating(
          &CaptureModeController::RecordAndResetScreenshotsTakenInLastDay,
          weak_ptr_factory_.GetWeakPtr()));

  // Schedule recording of the number of screenshots taken per week.
  num_screenshots_taken_in_last_week_scheduler_.Start(
      FROM_HERE, base::Days(7),
      base::BindRepeating(
          &CaptureModeController::RecordAndResetScreenshotsTakenInLastWeek,
          weak_ptr_factory_.GetWeakPtr()));

  DCHECK(!MessageViewFactory::HasCustomNotificationViewFactory(
      kScreenShotNotificationType));
  DCHECK(!MessageViewFactory::HasCustomNotificationViewFactory(
      kScreenRecordingNotificationType));

  MessageViewFactory::SetCustomNotificationViewFactory(
      kScreenShotNotificationType,
      base::BindRepeating(&CaptureModeAshNotificationView::CreateForImage));
  MessageViewFactory::SetCustomNotificationViewFactory(
      kScreenRecordingNotificationType,
      base::BindRepeating(&CaptureModeAshNotificationView::CreateForVideo));

  Shell::Get()->session_controller()->AddObserver(this);
  chromeos::PowerManagerClient::Get()->AddObserver(this);
  shell_observation_.Observe(Shell::Get());
}

CaptureModeController::~CaptureModeController() {
  if (IsActive()) {
    // If for some reason a session was started after `OnChromeTerminating()`
    // was called (see https://crbug.com/1350711), we must explicitly shut it
    // down, so that it can stop observing the things it observes.
    Stop();
  }

  chromeos::PowerManagerClient::Get()->RemoveObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
  // Remove the custom notification view factories.
  MessageViewFactory::ClearCustomNotificationViewFactory(
      kScreenShotNotificationType);
  MessageViewFactory::ClearCustomNotificationViewFactory(
      kScreenRecordingNotificationType);

  if (features::IsVideoConferenceEnabled()) {
    delegate_->UnregisterVideoConferenceManagerClient(vc_client_id_);
  }

  DCHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

// static
bool CaptureModeController::HasInstance() {
  if (g_instance) {
    return true;
  }

  return false;
}

// static
CaptureModeController* CaptureModeController::Get() {
  DCHECK(g_instance);
  return g_instance;
}

// static
void CaptureModeController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterFilePathPref(kCustomCapturePathPrefName,
                                 /*default_value=*/base::FilePath());
  registry->RegisterBooleanPref(kUsesDefaultCapturePathPrefName,
                                /*default_value=*/false);
  // TODO: crbug.com/388287849 - Clear this pref.
  registry->RegisterBooleanPref(kCanShowDemoToolsNudge,
                                /*default_value=*/true);
  registry->RegisterBooleanPref(prefs::kScannerConsentDisclaimerAccepted,
                                /*default_value=*/false);
  registry->RegisterBooleanPref(kCanShowSunfishRegionNudge,
                                /*default_value=*/true);
}

SearchResultsPanel* CaptureModeController::GetSearchResultsPanel() const {
  return search_results_panel_widget_
             ? views::AsViewClass<SearchResultsPanel>(
                   search_results_panel_widget_->GetContentsView())
             : nullptr;
}

void CaptureModeController::ShowSearchResultsPanel() {
  // We should not use `CanShowSunfishUi` here, as that could change between
  // sending the region and receiving a URL (for example, if the Sunfish policy
  // changes).
  DCHECK(features::IsSunfishFeatureEnabled());
  const bool is_active = IsActive();
  const bool should_end_session =
      is_active && capture_mode_session_->active_behavior()
                       ->ShouldEndSessionOnShowingSearchResults();
  if (!search_results_panel_widget_) {
    // A session must be active when the panel is first loaded, because it is
    // used to determine the panel bounds. If the user ends the session before
    // the panel loads, it will not be shown.
    if (!is_active) {
      return;
    }

    search_results_panel_widget_ = SearchResultsPanel::CreateWidget(
        capture_mode_session_->current_root(), is_active);
    RecordSearchResultsPanelEntryType(capture_mode_session_->active_behavior());

    // Let the session (and the focus cycler) know that the panel has been
    // created so it can be observed for focus changes. We only need to notify
    // the session if it isn't about to end.
    if (!should_end_session) {
      capture_mode_session_->OnSearchResultsPanelCreated(
          search_results_panel_widget_.get());
    }
  }

  // If the panel was not visible beforehand (either the panel was not created
  // yet or the panel was hidden from making a new selection), emit a metric.
  if (!search_results_panel_widget_->IsVisible()) {
    // Each time we make a new request, we should show the loading animation.
    GetSearchResultsPanel()->ShowLoadingAnimation();

    search_results_panel_widget_->Show();
    RecordSearchResultsPanelShown();
    // Setting or updating the bounds here only accounts for newly selected
    // regions. We also have to update the bounds elsewhere when the region is
    // adjusted or the display metrics change. We don't want the panel to update
    // its bounds when we make a multimodal search, as it would reset the panel
    // back to its default position each time.
    MaybeUpdateSearchResultsPanelBounds();
  }

  // Note at this point the session may no longer be active.
  if (should_end_session) {
    Stop();
  }
}

void CaptureModeController::CloseSearchResultsPanel() {
  search_results_panel_widget_.reset();
}

void CaptureModeController::MaybeUpdateSearchResultsPanelBounds() {
  // It only makes sense to update the panel bounds here if capture mode session
  // is currently active as we will use the current session's root to determine
  // the panel bounds. If the panel is alive outside the session, it will update
  // its own bounds on display or metric changes.
  if (!search_results_panel_widget_ || !IsActive()) {
    return;
  }

  // We should not use `CanShowSunfishUi` here, as that could change between
  // sending the region and receiving a URL (for example, if the Sunfish policy
  // changes).
  CHECK(features::IsSunfishFeatureEnabled());

  aura::Window* current_root = capture_mode_session_->current_root();
  // Update the panel root before recalculating its bounds.
  RefreshSearchResultsPanel(current_root);

  const gfx::Rect work_area_in_screen =
      search_results_panel_widget_->GetWorkAreaBoundsInScreen();

  gfx::Rect captured_region_in_screen(user_capture_region_);
  wm::ConvertRectToScreen(current_root, &captured_region_in_screen);

  gfx::Rect panel_bounds_in_screen = CalculateSearchResultPanelScreenBounds(
      work_area_in_screen, captured_region_in_screen);

  search_results_panel_widget_->SetBounds(panel_bounds_in_screen);
}

void CaptureModeController::OnLocatedEventDragged() {
  if (IsSearchResultsPanelVisible()) {
    search_results_panel_widget_->Hide();
  }
}

void CaptureModeController::RefreshSearchResultsPanel(
    aura::Window* current_root) {
  // Note we re-stack the panel even if it's not currently visible.
  if (auto* panel = GetSearchResultsPanel();
      panel &&
      panel->GetWidget()->GetNativeWindow()->GetRootWindow() != current_root) {
    panel->RefreshStackingOrder(current_root);
  }
}

bool CaptureModeController::IsActive() const {
  return capture_mode_session_ && !capture_mode_session_->is_shutting_down();
}

AudioRecordingMode CaptureModeController::GetEffectiveAudioRecordingMode()
    const {
  return IsAudioCaptureDisabledByPolicy() ? AudioRecordingMode::kOff
                                          : audio_recording_mode_;
}

bool CaptureModeController::IsAudioCaptureDisabledByPolicy() const {
  return delegate_->IsAudioCaptureDisabledByPolicy();
}

bool CaptureModeController::IsCustomFolderManagedByPolicy() const {
  return delegate_->GetPolicyCapturePath().enforcement ==
         CaptureModeDelegate::CapturePathEnforcement::kManaged;
}

bool CaptureModeController::IsSearchAllowedByPolicy() const {
  return delegate_->IsSearchAllowedByPolicy();
}

bool CaptureModeController::IsAudioRecordingInProgress() const {
  return video_recording_watcher_ &&
         !video_recording_watcher_->is_shutting_down() &&
         video_recording_watcher_->is_recording_audio();
}

bool CaptureModeController::IsShowingCameraPreview() const {
  return !!camera_controller_->camera_preview_widget();
}

bool CaptureModeController::IsEventOnSearchResultsPanel(
    const ui::LocatedEvent& event,
    const gfx::Point& screen_location) {
  // We check if the panel contains the event location, not just as the event
  // target, because the panel may not be the target of certain events (e.g.
  // right clicks), and lose focus, after which the panel will no longer be able
  // to be targeted (b/377019438).
  return IsSearchResultsPanelVisible() &&
         (search_results_panel_widget_->GetWindowBoundsInScreen().Contains(
              screen_location) ||
          capture_mode_util::IsEventTargetedOnWidget(
              event, search_results_panel_widget_.get()));
}

bool CaptureModeController::IsSearchResultsPanelVisible() const {
  return search_results_panel_widget_ &&
         search_results_panel_widget_->IsVisible();
}

bool CaptureModeController::IsNetworkConnectionOffline() const {
  return delegate_->IsNetworkConnectionOffline();
}

bool CaptureModeController::SupportsBehaviorChange(
    CaptureModeEntryType new_entry_type) const {
  // If no active session is running, we always support a new behavior type.
  if (!IsActive()) {
    return true;
  }
  // We only allow switching between sunfish and non-sunfish behaviors.
  return capture_mode_session_->active_behavior()->behavior_type() ==
             BehaviorType::kSunfish ||
         new_entry_type == CaptureModeEntryType::kSunfish;
}

void CaptureModeController::SetSource(CaptureModeSource source) {
  if (source == source_)
    return;

  source_ = source;
  if (IsActive()) {
    capture_mode_session_->OnCaptureSourceChanged(source_);
  }
}

void CaptureModeController::SetType(CaptureModeType type) {
  if (!can_start_new_recording() && type == CaptureModeType::kVideo) {
    // Overwrite video capture types to image, as we can't have more than one
    // recording at a time.
    type = CaptureModeType::kImage;
  }

  if (type == type_)
    return;

  type_ = type;
  if (IsActive()) {
    capture_mode_session_->OnCaptureTypeChanged(type_);
  }
}

void CaptureModeController::SetRecordingType(RecordingType recording_type) {
  if (recording_type == recording_type_)
    return;

  recording_type_ = recording_type;
  if (IsActive()) {
    capture_mode_session_->OnRecordingTypeChanged();
  }
}

void CaptureModeController::SetAudioRecordingMode(AudioRecordingMode mode) {
  audio_recording_mode_ = mode;

  if (IsActive()) {
    capture_mode_session_->OnAudioRecordingModeChanged();
  }
}

void CaptureModeController::EnableDemoTools(bool enable) {
  enable_demo_tools_ = enable;

  if (IsActive()) {
    capture_mode_session_->OnDemoToolsSettingsChanged();
  }
}

void CaptureModeController::Start(CaptureModeEntryType entry_type,
                                  OnSessionStartAttemptCallback callback) {
  StartInternal(SessionType::kReal, entry_type, std::move(callback));
}

void CaptureModeController::StartForGameDashboard(aura::Window* game_window) {
  CHECK(GameDashboardController::IsGameWindow(game_window));
  CaptureModeBehavior* behavior = GetBehavior(BehaviorType::kGameDashboard);
  behavior->SetPreSelectedWindow(game_window);
  StartInternal(SessionType::kReal, CaptureModeEntryType::kGameDashboard);
}

void CaptureModeController::StartRecordingInstantlyForGameDashboard(
    aura::Window* game_window) {
  CHECK(GameDashboardController::IsGameWindow(game_window));
  CaptureModeBehavior* behavior = GetBehavior(BehaviorType::kGameDashboard);
  behavior->SetPreSelectedWindow(game_window);
  StartInternal(SessionType::kNull, CaptureModeEntryType::kGameDashboard,
                base::BindOnce([](bool success) {
                  if (success) {
                    // Session initialization was successful.
                    CaptureModeController::Get()->PerformCapture();
                  }
                }));
}

void CaptureModeController::StartSunfishSession() {
  RecordScannerFeatureUserState(
      ScannerFeatureUserState::kSunfishScreenEnteredViaShortcut);
  CHECK(CanShowSunfishOrScannerUi());
  // Close the launcher nudge if it is still visible.
  AnchoredNudgeManager::Get()->Cancel(capture_mode::kSunfishLauncherNudgeId);
  StartInternal(
      SessionType::kReal, CaptureModeEntryType::kSunfish,
      base::BindOnce(
          &CaptureModeController::MaybeShowScannerDisclaimerOnSunfishStartup,
          weak_ptr_factory_.GetWeakPtr()));
}

void CaptureModeController::Stop() {
  CHECK(IsActive());
  capture_mode_session_->ReportSessionHistograms();
  capture_mode_session_->Shutdown();
  capture_mode_session_.reset();
  RefreshSearchResultsPanel(/*current_root=*/nullptr);

  delegate_->OnSessionStateChanged(/*started=*/false);
}

void CaptureModeController::NotifyRecordingStartAborted() {
  for (auto& observer : observers_) {
    observer.OnRecordingStartAborted();
  }
}

void CaptureModeController::SetUserCaptureRegion(const gfx::Rect& region,
                                                 bool by_user) {
  user_capture_region_ = region;
  if (!user_capture_region_.IsEmpty() && by_user)
    last_capture_region_update_time_ = base::TimeTicks::Now();

  if (!is_recording_in_progress() && source_ == CaptureModeSource::kRegion)
    camera_controller_->MaybeReparentPreviewWidget();
}

bool CaptureModeController::CanShowSunfishRegionNudge() const {
  // The nudge applies to both Sunfish and Scanner, if neither can be shown then
  // we don't want to show the nudge either.
  if (!CanShowSunfishOrScannerUi()) {
    return false;
  }

  auto* session_controller = Shell::Get()->session_controller();
  DCHECK(session_controller->IsActiveUserSessionStarted());

  std::optional<user_manager::UserType> user_type =
      session_controller->GetUserType();
  // This can only be called while a user is logged in, so `user_type` should
  // never be empty.
  DCHECK(user_type);
  switch (*user_type) {
    case user_manager::UserType::kRegular:
    case user_manager::UserType::kChild:
      // We only allow regular and child accounts to see the nudge.
      break;
    case user_manager::UserType::kGuest:
    case user_manager::UserType::kPublicAccount:
    case user_manager::UserType::kKioskChromeApp:
    case user_manager::UserType::kKioskWebApp:
    case user_manager::UserType::kKioskIWA:
    case user_manager::UserType::kKioskArcvmApp:
      return false;
  }

  auto* pref_service = session_controller->GetActivePrefService();
  DCHECK(pref_service);
  return pref_service->GetBoolean(kCanShowSunfishRegionNudge);
}

void CaptureModeController::DisableSunfishRegionNudgeForever() {
  capture_mode_util::GetActiveUserPrefService()->SetBoolean(
      kCanShowSunfishRegionNudge, false);
}

void CaptureModeController::SetUsesDefaultCaptureFolder(bool value) {
  DCHECK(!IsCustomFolderManagedByPolicy());
  capture_mode_util::GetActiveUserPrefService()->SetBoolean(
      kUsesDefaultCapturePathPrefName, value);

  if (IsActive())
    capture_mode_session_->OnDefaultCaptureFolderSelectionChanged();
}

void CaptureModeController::SetCustomCaptureFolder(const base::FilePath& path) {
  DCHECK(!IsCustomFolderManagedByPolicy());
  auto* pref_service = capture_mode_util::GetActiveUserPrefService();
  pref_service->SetFilePath(kCustomCapturePathPrefName, path);

  // When this function is called, it means the user is switching back to the
  // custom capture folder, and we need to reset the setting to force using the
  // default downloads folder.
  pref_service->SetBoolean(kUsesDefaultCapturePathPrefName, false);

  if (IsActive())
    capture_mode_session_->OnCaptureFolderMayHaveChanged();
}

base::FilePath CaptureModeController::GetCustomCaptureFolder() const {
  base::FilePath custom_path =
      capture_mode_util::GetActiveUserPrefService()->GetFilePath(
          kCustomCapturePathPrefName);
  const auto policy_path = delegate_->GetPolicyCapturePath();
  // If admin forced or recommended and there is no user chosen value - use it.
  if (policy_path.enforcement ==
          CaptureModeDelegate::CapturePathEnforcement::kManaged ||
      (custom_path.empty() &&
       policy_path.enforcement ==
           CaptureModeDelegate::CapturePathEnforcement::kRecommended)) {
    custom_path = policy_path.path;
  }
  return custom_path != delegate_->GetUserDefaultDownloadsFolder()
             ? custom_path
             : base::FilePath();
}

CaptureModeController::CaptureFolder
CaptureModeController::GetCurrentCaptureFolder() const {
  auto* session_controller = Shell::Get()->session_controller();
  if (!session_controller->IsActiveUserSessionStarted())
    return {GetTempDir(), /*is_default_downloads_folder=*/false};

  auto* pref_service = session_controller->GetActivePrefService();
  const auto default_downloads_folder =
      delegate_->GetUserDefaultDownloadsFolder();
  const auto policy_path = delegate_->GetPolicyCapturePath();
  // If admin forced - use it.
  if (policy_path.enforcement ==
      CaptureModeDelegate::CapturePathEnforcement::kManaged) {
    return {policy_path.path,
            /*is_default_downloads_folder=*/policy_path.path ==
                default_downloads_folder};
  }
  // Otherwise use user chosen custom one, if present.
  if (pref_service &&
      !pref_service->GetBoolean(kUsesDefaultCapturePathPrefName)) {
    const auto custom_path =
        pref_service->GetFilePath(kCustomCapturePathPrefName);
    if (!custom_path.empty()) {
      return {custom_path,
              /*is_default_downloads_folder=*/custom_path ==
                  default_downloads_folder};
    }
  }
  // Otherwise use the recommended by admin.
  if (policy_path.enforcement ==
      CaptureModeDelegate::CapturePathEnforcement::kRecommended) {
    return {policy_path.path,
            /*is_default_downloads_folder=*/policy_path.path ==
                default_downloads_folder};
  }

  // By default - downloads folder.
  return {default_downloads_folder,
          /*is_default_downloads_folder=*/true};
}

void CaptureModeController::CaptureScreenshotsOfAllDisplays() {
  CaptureInstantScreenshot(
      CaptureModeEntryType::kCaptureAllDisplays, CaptureModeSource::kFullscreen,
      base::BindOnce(&CaptureModeController::PerformScreenshotsOfAllDisplays,
                     weak_ptr_factory_.GetWeakPtr(), BehaviorType::kDefault),
      BehaviorType::kDefault);
}

void CaptureModeController::CaptureScreenshotOfGivenWindow(
    aura::Window* given_window) {
  CaptureInstantScreenshot(
      CaptureModeEntryType::kCaptureGivenWindow, CaptureModeSource::kWindow,
      base::BindOnce(&CaptureModeController::PerformScreenshotOfGivenWindow,
                     weak_ptr_factory_.GetWeakPtr(), given_window,
                     BehaviorType::kGameDashboard),
      BehaviorType::kGameDashboard);
}

void CaptureModeController::PerformCapture(PerformCaptureType capture_type) {
  DCHECK(IsActive());

  if (CaptureTypeRequiresNetworkConnection(capture_type) &&
      delegate_->IsNetworkConnectionOffline()) {
    capture_mode_session_->ShowActionContainerError(l10n_util::GetStringUTF16(
        IDS_ASH_SCREEN_CAPTURE_ACTION_ATTEMPTED_OFFLINE_ERROR));
    return;
  }

  if (pending_dlp_check_)
    return;

  const std::optional<CaptureParams> capture_params = GetCaptureParams();
  if (!capture_params)
    return;

  // If we are performing an image search and an Incognito window is open,
  // return and let the user know it must be closed before making an image
  // search.
  if (CaptureTypeIsImageSearch(capture_type) && IsIncognitoWindowOpen()) {
    Stop();
    ShowSunfishIncognitoNudge();
    return;
  }

  DCHECK(!pending_dlp_check_);
  pending_dlp_check_ = true;
  capture_mode_session_->OnWaitingForDlpConfirmationStarted();
  delegate_->CheckCaptureOperationRestrictionByDlp(
      capture_params->window, capture_params->bounds,
      base::BindOnce(
          &CaptureModeController::OnDlpRestrictionCheckedAtPerformingCapture,
          weak_ptr_factory_.GetWeakPtr(), capture_type));
}

void CaptureModeController::PerformImageSearch(
    PerformCaptureType capture_type) {
  if (!IsActive()) {
    // This function gets called asynchronously, and until it gets called, the
    // session could end due to e.g. locking the screen, suspending, or
    // switching users.
    return;
  }

  DCHECK(delegate_->IsCaptureAllowedByPolicy());

  const std::optional<CaptureParams> capture_params = GetCaptureParams();
  CHECK(capture_params);

  base::WeakPtr<BaseCaptureModeSession> image_search_token =
      capture_mode_session_->GetImageSearchToken();
  if (!image_search_token) {
    VLOG(1) << "Image search token invalid before capturing image.";
    // In theory, this should only be possible if the capture mode session is
    // the null session, which should not be able to perform image searches.
    return;
  }

  const bool was_cursor_originally_blocked = MaybeLockCursor();
  capture_mode_session_->OnPerformCaptureForSearchStarting(capture_type);

  // Capture the image for search. We use JPEG bytes for low file size and fast
  // compression speed.
  ui::GrabWindowSnapshotAsJPEG(
      capture_params->window, capture_params->bounds,
      base::BindOnce(&CaptureModeController::OnImageCapturedForSearch,
                     weak_ptr_factory_.GetWeakPtr(), capture_type,
                     was_cursor_originally_blocked, image_search_token));

  delegate_->OnCaptureImageAttempted(capture_params->window,
                                     capture_params->bounds);
}

void CaptureModeController::EndVideoRecording(EndRecordingReason reason) {
  if (!is_recording_in_progress()) {
    // A user may click on the stop recording button multiple times while still
    // in the process of hiding. See http://b/270625738.
    return;
  }

  RecordEndRecordingReason(reason);
  recording_service_remote_->StopRecording();
  TerminateRecordingUiElements();
}

void CaptureModeController::CheckFolderAvailability(
    const base::FilePath& folder,
    base::OnceCallback<void(bool available)> callback) {
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&base::PathExists, folder),
      std::move(callback));
}

void CaptureModeController::SetWindowProtectionMask(aura::Window* window,
                                                    uint32_t protection_mask) {
  if (protection_mask == display::CONTENT_PROTECTION_METHOD_NONE)
    protected_windows_.erase(window);
  else
    protected_windows_[window] = protection_mask;

  RefreshContentProtection();
}

void CaptureModeController::RefreshContentProtection() {
  if (!is_recording_in_progress())
    return;

  DCHECK(video_recording_watcher_);
  if (ShouldBlockRecordingForContentProtection(
          video_recording_watcher_->window_being_recorded())) {
    // HDCP violation is also considered a failure, and we're going to terminate
    // the service immediately so as not to record any further frames.
    RecordEndRecordingReason(EndRecordingReason::kHdcpInterruption);
    FinalizeRecording(/*success=*/false, gfx::ImageSkia());
    ShowVideoRecordingStoppedByHdcpNotification();
  }
}

bool CaptureModeController::IsRootDriveFsPath(
    const base::FilePath& path) const {
  base::FilePath mounted_path;
  if (delegate_->GetDriveFsMountPointPath(&mounted_path)) {
    if (path == mounted_path.Append("root"))
      return true;
  }
  return false;
}

bool CaptureModeController::IsAndroidFilesPath(
    const base::FilePath& path) const {
  return path == delegate_->GetAndroidFilesPath();
}

bool CaptureModeController::IsLinuxFilesPath(const base::FilePath& path) const {
  return path == delegate_->GetLinuxFilesPath();
}

bool CaptureModeController::IsRootOneDriveFilesPath(
    const base::FilePath& path) const {
  return path == delegate_->GetOneDriveVirtualPath();
}

std::unique_ptr<AshWebView> CaptureModeController::CreateSearchResultsView()
    const {
  return delegate_->CreateSearchResultsView();
}

aura::Window* CaptureModeController::GetOnCaptureSurfaceWidgetParentWindow()
    const {
  // Trying to get camera preview's parent from `video_recording_watcher_` first
  // if a video recording is in progress. As a capture session can be started
  // with `kImage` type while recording, and we should get the parent of the
  // camera preview with the settings inside VideoRecordingWatcher in this case,
  // e.g, CaptureModeSource for taking the video.
  if (is_recording_in_progress())
    return video_recording_watcher_->GetOnCaptureSurfaceWidgetParentWindow();

  if (IsActive())
    return capture_mode_session_->GetOnCaptureSurfaceWidgetParentWindow();

  return nullptr;
}

gfx::Rect CaptureModeController::GetCaptureSurfaceConfineBounds() const {
  // Getting the bounds from `video_recording_watcher_` first if a video
  // recording is in progress. As a capture session can be started with `kImage`
  // type while recording, and we should get the bounds with the settings inside
  // VideoRecordingWatcher in this case, e.g, user-selected region.
  if (is_recording_in_progress())
    return video_recording_watcher_->GetCaptureSurfaceConfineBounds();

  if (IsActive())
    return capture_mode_session_->GetCaptureSurfaceConfineBounds();

  return gfx::Rect();
}

std::vector<aura::Window*>
CaptureModeController::GetWindowsForCollisionAvoidance() const {
  std::vector<aura::Window*> windows_to_be_avoided;
  if (IsActive()) {
    const auto* capture_bar_widget =
        capture_mode_session_->GetCaptureModeBarWidget();
    CHECK(capture_bar_widget);
    auto* capture_bar_window = capture_bar_widget->GetNativeWindow();
    windows_to_be_avoided.push_back(capture_bar_window);
  }

  auto* camera_preview_widget = camera_controller_->camera_preview_widget();
  if (camera_preview_widget && camera_preview_widget->IsVisible()) {
    windows_to_be_avoided.push_back(camera_preview_widget->GetNativeView());
  }

  if (video_recording_watcher_ &&
      !video_recording_watcher_->is_shutting_down() &&
      video_recording_watcher_->recording_source() !=
          CaptureModeSource::kWindow) {
    if (auto* key_combo_widget =
            video_recording_watcher_->GetKeyComboWidgetIfVisible()) {
      windows_to_be_avoided.push_back(key_combo_widget->GetNativeWindow());
    }
  }

  return windows_to_be_avoided;
}

void CaptureModeController::MaybeUpdateVcPanel() {
  if (!features::IsVideoConferenceEnabled()) {
    return;
  }

  const bool is_camera_used = IsShowingCameraPreview();
  const bool is_recording_audio = IsAudioRecordingInProgress();
  const bool has_media_app = is_camera_used || is_recording_audio;

  delegate_->UpdateVideoConferenceManager(
      crosapi::mojom::VideoConferenceMediaUsageStatus::New(
          /*client_id=*/vc_client_id_,
          /*has_media_app=*/has_media_app,
          /*has_camera_permission=*/has_media_app,
          /*has_microphone_permission=*/has_media_app,
          /*is_capturing_camera=*/is_camera_used,
          /*is_capturing_microphone=*/is_recording_audio,
          /*is_capturing_screen=*/false));

  // If the camera is being recorded while disabled (e.g. privacy switch is
  // turned on), or the microphone is being recorded while mic input is muted,
  // we need to notify the user through the video conference manager.
  if (is_camera_used && is_camera_muted_) {
    delegate_->NotifyDeviceUsedWhileDisabled(
        crosapi::mojom::VideoConferenceMediaDevice::kCamera);
  }

  if (is_recording_audio && is_microphone_muted_) {
    delegate_->NotifyDeviceUsedWhileDisabled(
        crosapi::mojom::VideoConferenceMediaDevice::kMicrophone);
  }
}

void CaptureModeController::CheckScreenCaptureDlpRestrictions(
    bool shutting_down,
    OnCaptureModeDlpRestrictionChecked callback) {
  delegate_->CheckCaptureModeInitRestrictionByDlp(shutting_down,
                                                  std::move(callback));
}

bool CaptureModeController::ShouldAllowAnnotating() const {
  return is_recording_in_progress() && IsAnnotatingSupported();
}

bool CaptureModeController::IsAnnotatingSupported() const {
  return video_recording_watcher_ &&
         video_recording_watcher_->active_behavior()
             ->ShouldCreateAnnotationsOverlayController();
}

bool CaptureModeController::ActiveUserDefaultSearchProviderIsGoogle() const {
  return delegate_->ActiveUserDefaultSearchProviderIsGoogle();
}

void CaptureModeController::OnRecordingEnded(
    recording::mojom::RecordingStatus status,
    const gfx::ImageSkia& thumbnail) {
  low_disk_space_threshold_reached_ =
      status == recording::mojom::RecordingStatus::kLowDiskSpace ||
      status == recording::mojom::RecordingStatus::kLowDriveFsQuota;
  EmitServiceRecordingStatus(status);
  FinalizeRecording(status == recording::mojom::RecordingStatus::kSuccess,
                    thumbnail);
}

void CaptureModeController::GetDriveFsFreeSpaceBytes(
    GetDriveFsFreeSpaceBytesCallback callback) {
  delegate_->GetDriveFsFreeSpaceBytes(std::move(callback));
}

void CaptureModeController::OnActiveUserSessionChanged(
    const AccountId& account_id) {
  EndSessionOrRecording(EndRecordingReason::kActiveUserChange);

  camera_controller_->OnActiveUserSessionChanged();

  // Remove the previous notification when switching to another user.
  auto* message_center = message_center::MessageCenter::Get();
  message_center->RemoveNotification(kScreenCaptureNotificationId,
                                     /*by_user=*/false);
}

void CaptureModeController::OnFirstSessionStarted() {
  if (features::IsVideoConferenceEnabled()) {
    auto* vc_tray_controller = VideoConferenceTrayController::Get();
    is_camera_muted_ = vc_tray_controller->GetCameraMuted();
    is_microphone_muted_ = vc_tray_controller->GetMicrophoneMuted();
    delegate_->RegisterVideoConferenceManagerClient(this, vc_client_id_);
  }
}

void CaptureModeController::OnSessionStateChanged(
    session_manager::SessionState state) {
  if (Shell::Get()->session_controller()->IsUserSessionBlocked())
    EndSessionOrRecording(EndRecordingReason::kSessionBlocked);
}

void CaptureModeController::OnChromeTerminating() {
  // Order here matters. We may shutdown while a session with a camera is active
  // before recording starts, we need to inform the camera controller first to
  // destroy the camera preview first.
  camera_controller_->OnShuttingDown();
  EndSessionOrRecording(EndRecordingReason::kShuttingDown);
}

void CaptureModeController::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  EndSessionOrRecording(EndRecordingReason::kImminentSuspend);
}

void CaptureModeController::GetMediaApps(GetMediaAppsCallback callback) {
  std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> apps;

  if (is_recording_in_progress()) {
    apps.push_back(crosapi::mojom::VideoConferenceMediaAppInfo::New(
        /*id=*/capture_mode_media_app_id_,
        /*last_activity_time=*/base::Time::Now(),
        /*is_capturing_camera=*/IsShowingCameraPreview(),
        /*is_capturing_microphone=*/IsAudioRecordingInProgress(),
        /*is_capturing_screen=*/false,
        /*title=*/
        l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE),
        /*url=*/std::nullopt,
        /*app_type=*/crosapi::mojom::VideoConferenceAppType::kAshCaptureMode));
  }

  std::move(callback).Run(std::move(apps));
}

void CaptureModeController::ReturnToApp(const base::UnguessableToken& token,
                                        ReturnToAppCallback callback) {
  // The return-to-app feature is only available when recording an app window
  // (rather than the fullscreen or region). In this case, it simply "returns"
  // to that window by activating it.
  bool success = false;
  if (video_recording_watcher_ &&
      !video_recording_watcher_->is_shutting_down() &&
      video_recording_watcher_->recording_source() ==
          CaptureModeSource::kWindow) {
    wm::ActivateWindow(video_recording_watcher_->window_being_recorded());
    success = true;
  }
  std::move(callback).Run(success);
}

void CaptureModeController::SetSystemMediaDeviceStatus(
    crosapi::mojom::VideoConferenceMediaDevice device,
    bool enabled,
    SetSystemMediaDeviceStatusCallback callback) {
  switch (device) {
    case crosapi::mojom::VideoConferenceMediaDevice::kCamera:
      is_camera_muted_ = !enabled;
      std::move(callback).Run(true);
      return;
    case crosapi::mojom::VideoConferenceMediaDevice::kMicrophone:
      is_microphone_muted_ = !enabled;
      std::move(callback).Run(true);
      return;
    case crosapi::mojom::VideoConferenceMediaDevice::kUnusedDefault:
      std::move(callback).Run(false);
      return;
  }
}

void CaptureModeController::StopAllScreenShare() {
  // Our screen recordings are not considered screen shares, and we already have
  // the stop recording button, so this does nothing.
}

void CaptureModeController::OnPinnedStateChanged(aura::Window* pinned_window) {
  // TODO: crbug.com/404941151 - Remove this method and use
  // `SunfishScannerFeatureWatcher` instead.
  if (!Shell::Get()->screen_pinning_controller()->IsPinned()) {
    return;
  }

  if (IsActive() && capture_mode_session_->active_behavior()->behavior_type() ==
                        BehaviorType::kSunfish) {
    Stop();
  }
  CloseSearchResultsPanel();
}

void CaptureModeController::StartVideoRecordingImmediatelyForTesting() {
  DCHECK(IsActive());
  DCHECK_EQ(type_, CaptureModeType::kVideo);
  OnVideoRecordCountDownFinished();
}

void CaptureModeController::AddObserver(CaptureModeObserver* observer) {
  observers_.AddObserver(observer);
}

void CaptureModeController::RemoveObserver(CaptureModeObserver* observer) {
  observers_.RemoveObserver(observer);
}

void CaptureModeController::StartInternal(
    SessionType session_type,
    CaptureModeEntryType entry_type,
    OnSessionStartAttemptCallback callback) {
  // To be invoked at the exit of this function or
  // `OnDlpRestrictionCheckedAtSessionInit()`.
  base::ScopedClosureRunner deferred_runner(base::BindOnce(
      [](base::WeakPtr<CaptureModeController> controller,
         OnSessionStartAttemptCallback callback, bool was_active) {
        std::move(callback).Run(!was_active && controller &&
                                controller->IsActive());
      },
      weak_ptr_factory_.GetWeakPtr(), std::move(callback), IsActive()));

  education_controller_->CloseAllEducationNudgesAndTutorials();

  if (pending_dlp_check_) {
    return;
  }

  if (capture_mode_session_) {
    if (capture_mode_session_->is_shutting_down()) {
      return;
    }

    // If the active behavior type has not changed, no need to shutdown and
    // restart.
    if (capture_mode_session_->active_behavior()->behavior_type() ==
        ToBehaviorType(entry_type)) {
      return;
    }

    // Else if the behavior type has changed, shut down and restart with the new
    // behavior type.
    Stop();
  }

  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    return;
  }

  // If we are attempting to start a standalone Sunfish session and an Incognito
  // window is open, return and let the user know it must be closed before
  // making an image search.
  if (entry_type == CaptureModeEntryType::kSunfish && IsIncognitoWindowOpen()) {
    ShowSunfishIncognitoNudge();
    return;
  }

  pending_dlp_check_ = true;
  delegate_->CheckCaptureModeInitRestrictionByDlp(
      /*shutting_down=*/false,
      base::BindOnce(
          &CaptureModeController::OnDlpRestrictionCheckedAtSessionInit,
          weak_ptr_factory_.GetWeakPtr(), session_type, entry_type,
          deferred_runner.Release()));
}

void CaptureModeController::PushNewRootSizeToRecordingService(
    const gfx::Size& root_size,
    float device_scale_factor) {
  DCHECK(is_recording_in_progress());
  DCHECK(video_recording_watcher_);
  DCHECK(recording_service_remote_);

  recording_service_remote_->OnFrameSinkSizeChanged(root_size,
                                                    device_scale_factor);
}

void CaptureModeController::OnRecordedWindowChangingRoot(
    aura::Window* window,
    aura::Window* new_root) {
  DCHECK(is_recording_in_progress());
  DCHECK(video_recording_watcher_);
  DCHECK_EQ(window, video_recording_watcher_->window_being_recorded());
  DCHECK(recording_service_remote_);
  DCHECK(new_root);

  // When a window being recorded changes displays either due to a display
  // getting disconnected, or moved by the user, the stop-recording button
  // should follow that window to that display.
  capture_mode_util::SetStopRecordingButtonVisibility(window->GetRootWindow(),
                                                      false);
  capture_mode_util::SetStopRecordingButtonVisibility(new_root, true);

  for (auto& observer : observers_) {
    observer.OnRecordedWindowChangingRoot(new_root);
  }

  recording_service_remote_->OnRecordedWindowChangingRoot(
      new_root->GetFrameSinkId(), new_root->GetBoundsInRootWindow().size(),
      new_root->GetHost()->device_scale_factor());
}

void CaptureModeController::OnRecordedWindowSizeChanged(
    const gfx::Size& new_size) {
  DCHECK(is_recording_in_progress());
  DCHECK(video_recording_watcher_);
  DCHECK(recording_service_remote_);

  recording_service_remote_->OnRecordedWindowSizeChanged(new_size);
}

bool CaptureModeController::ShouldBlockRecordingForContentProtection(
    aura::Window* window_being_recorded) const {
  DCHECK(window_being_recorded);

  // The protected window can be a descendant of the window being recorded, for
  // examples:
  //   - When recording a fullscreen or partial region of it, the
  //     |window_being_recorded| in this case is the root window, and a
  //     protected window on this root will be a descendant.
  //   - When recording a browser window showing a page with protected content,
  //     the |window_being_recorded| in this case is the BrowserWidget, while
  //     the protected window will be the RenderWidgetHostViewAura, which is
  //     also a descendant.
  for (const auto& iter : protected_windows_) {
    if (window_being_recorded->Contains(iter.first))
      return true;
  }

  return false;
}

void CaptureModeController::EndSessionOrRecording(EndRecordingReason reason) {
  if (IsActive()) {
    // Suspend or user session changes can happen while the capture mode session
    // is active or after the three-second countdown had started but not
    // finished yet.
    Stop();
  }
  CloseSearchResultsPanel();

  if (!is_recording_in_progress())
    return;

  if (reason == EndRecordingReason::kImminentSuspend ||
      reason == EndRecordingReason::kShuttingDown) {
    // If suspend or shutdown happen while recording is in progress, we consider
    // this a failure, and cut the recording immediately. The recording service
    // will flush any remaining buffered chunks in the muxer before it
    // terminates.
    RecordEndRecordingReason(reason);
    FinalizeRecording(/*success=*/false, gfx::ImageSkia());
    return;
  }

  EndVideoRecording(reason);
}

std::optional<CaptureModeController::CaptureParams>
CaptureModeController::GetCaptureParams() const {
  DCHECK(IsActive());

  aura::Window* window = nullptr;
  gfx::Rect bounds;
  switch (source_) {
    case CaptureModeSource::kFullscreen:
      window = capture_mode_session_->current_root();
      DCHECK(window);
      DCHECK(window->IsRootWindow());
      bounds = window->bounds();
      break;

    case CaptureModeSource::kWindow:
      window = capture_mode_session_->GetSelectedWindow();
      if (!window) {
        // TODO(afakhry): Consider showing a toast or a notification that no
        // window was selected.
        return std::nullopt;
      }
      // window->bounds() are in root coordinates, but we want to get the
      // capture area in |window|'s coordinates.
      bounds = gfx::Rect(window->bounds().size());
      break;

    case CaptureModeSource::kRegion:
      window = capture_mode_session_->current_root();
      DCHECK(window);
      DCHECK(window->IsRootWindow());
      if (user_capture_region_.IsEmpty()) {
        // TODO(afakhry): Consider showing a toast or a notification that no
        // region was selected.
        return std::nullopt;
      }
      // TODO(afakhry): Consider any special handling of display scale changes
      // while video recording is in progress.
      bounds = user_capture_region_;
      break;
  }

  DCHECK(window);

  return CaptureParams{window, bounds};
}

void CaptureModeController::LaunchRecordingServiceAndStartRecording(
    const CaptureParams& capture_params,
    mojo::PendingReceiver<viz::mojom::FrameSinkVideoCaptureOverlay>
        cursor_overlay,
    AudioRecordingMode effective_audio_mode) {
  DCHECK(!recording_service_remote_.is_bound())
      << "Should not launch a new recording service while one is already "
         "running.";

  recording_service_remote_.reset();
  recording_service_client_receiver_.reset();
  drive_fs_quota_delegate_receiver_.reset();

  recording_service_remote_ = delegate_->LaunchRecordingService();
  recording_service_remote_.set_disconnect_handler(
      base::BindOnce(&CaptureModeController::OnRecordingServiceDisconnected,
                     weak_ptr_factory_.GetWeakPtr()));

  // Prepare the pending remotes of the client, the video capturer, and the
  // audio stream factory.
  mojo::PendingRemote<recording::mojom::RecordingServiceClient> client =
      recording_service_client_receiver_.BindNewPipeAndPassRemote();
  mojo::Remote<viz::mojom::FrameSinkVideoCapturer> video_capturer_remote;
  aura::Env::GetInstance()
      ->context_factory()
      ->GetHostFrameSinkManager()
      ->CreateVideoCapturer(video_capturer_remote.BindNewPipeAndPassReceiver());

  // The overlay is to be rendered on top of the video frames.
  constexpr int kStackingIndex = 1;
  video_capturer_remote->CreateOverlay(kStackingIndex,
                                       std::move(cursor_overlay));

  // We bind the microphone and/or system audio stream factories only if their
  // corresponding audio recording modes are enabled. This is ok since the
  // `microphone_stream_factory` and `system_audio_stream_factory` parameters in
  // the recording service APIs are optional, and can be not bound.
  mojo::PendingRemote<media::mojom::AudioStreamFactory>
      microphone_stream_factory;
  if (effective_audio_mode == AudioRecordingMode::kMicrophone ||
      effective_audio_mode == AudioRecordingMode::kSystemAndMicrophone) {
    delegate_->BindAudioStreamFactory(
        microphone_stream_factory.InitWithNewPipeAndPassReceiver());
  }
  mojo::PendingRemote<media::mojom::AudioStreamFactory>
      system_audio_stream_factory;
  if (effective_audio_mode == AudioRecordingMode::kSystem ||
      effective_audio_mode == AudioRecordingMode::kSystemAndMicrophone) {
    delegate_->BindAudioStreamFactory(
        system_audio_stream_factory.InitWithNewPipeAndPassReceiver());
  }

  if (microphone_stream_factory || system_audio_stream_factory) {
    capture_mode_util::MaybeUpdateCaptureModePrivacyIndicators();
    MaybeUpdateVcPanel();
  }

  // Only act as a `DriveFsQuotaDelegate` for the recording service if the video
  // file will be saved to a location in DriveFS.
  mojo::PendingRemote<recording::mojom::DriveFsQuotaDelegate>
      drive_fs_quota_delegate;
  const auto file_location = GetSaveToOption(current_video_file_path_);
  if (file_location == CaptureModeSaveToLocation::kDrive ||
      file_location == CaptureModeSaveToLocation::kDriveFolder) {
    drive_fs_quota_delegate =
        drive_fs_quota_delegate_receiver_.BindNewPipeAndPassRemote();
  }

  auto* root_window = capture_params.window->GetRootWindow();
  const auto frame_sink_id = root_window->GetFrameSinkId();
  DCHECK(frame_sink_id.is_valid());
  const float device_scale_factor =
      root_window->GetHost()->device_scale_factor();
  const gfx::Size frame_sink_size_dip = root_window->bounds().size();

  const auto bounds = capture_params.bounds;
  switch (source_) {
    case CaptureModeSource::kFullscreen:
      recording_service_remote_->RecordFullscreen(
          std::move(client), video_capturer_remote.Unbind(),
          std::move(microphone_stream_factory),
          std::move(system_audio_stream_factory),
          std::move(drive_fs_quota_delegate), current_video_file_path_,
          frame_sink_id, frame_sink_size_dip, device_scale_factor);
      break;

    case CaptureModeSource::kWindow:
      // Non-root window are not capturable by the |FrameSinkVideoCapturer|
      // unless its layer tree is identified by a |viz::SubtreeCaptureId|.
      // The |VideoRecordingWatcher| that we create while recording is in
      // progress creates a request to mark that window as capturable.
      // See https://crbug.com/1143930 for more details.
      DCHECK(!capture_params.window->IsRootWindow());
      DCHECK(capture_params.window->subtree_capture_id().is_valid());

      recording_service_remote_->RecordWindow(
          std::move(client), video_capturer_remote.Unbind(),
          std::move(microphone_stream_factory),
          std::move(system_audio_stream_factory),
          std::move(drive_fs_quota_delegate), current_video_file_path_,
          frame_sink_id, frame_sink_size_dip, device_scale_factor,
          capture_params.window->subtree_capture_id(), bounds.size());
      break;

    case CaptureModeSource::kRegion:
      recording_service_remote_->RecordRegion(
          std::move(client), video_capturer_remote.Unbind(),
          std::move(microphone_stream_factory),
          std::move(system_audio_stream_factory),
          std::move(drive_fs_quota_delegate), current_video_file_path_,
          frame_sink_id, frame_sink_size_dip, device_scale_factor, bounds);
      break;
  }
}

void CaptureModeController::OnRecordingServiceDisconnected() {
  // TODO(afakhry): Consider what to do if the service crashes during an ongoing
  // video recording. Do we try to resume recording, or notify with failure?
  // For now, just end the recording.
  // Note that the service could disconnect between the time we ask it to
  // StopRecording(), and it calling us back with OnRecordingEnded(), so we call
  // FinalizeRecording() in all cases.
  RecordEndRecordingReason(EndRecordingReason::kRecordingServiceDisconnected);
  FinalizeRecording(/*success=*/false, gfx::ImageSkia());
}

void CaptureModeController::FinalizeRecording(bool success,
                                              const gfx::ImageSkia& thumbnail) {
  // If |success| is false, then recording has been force-terminated due to a
  // failure on the service side, or a disconnection to it. We need to terminate
  // the recording-related UI elements.
  if (!success) {
    // TODO(afakhry): Show user a failure message.
    TerminateRecordingUiElements();
  }

  // Resetting the service remote would terminate its process.
  recording_service_remote_.reset();
  delegate_->OnServiceRemoteReset();
  recording_service_client_receiver_.reset();
  drive_fs_quota_delegate_receiver_.reset();
  const CaptureModeBehavior* behavior =
      video_recording_watcher_->active_behavior();
  video_recording_watcher_.reset();
  capture_mode_util::MaybeUpdateCaptureModePrivacyIndicators();
  MaybeUpdateVcPanel();

  delegate_->StopObservingRestrictedContent(base::BindOnce(
      &CaptureModeController::OnDlpRestrictionCheckedAtVideoEnd,
      weak_ptr_factory_.GetWeakPtr(), thumbnail, success, behavior));
}

void CaptureModeController::TerminateRecordingUiElements() {
  if (!is_recording_in_progress())
    return;

  capture_mode_util::SetStopRecordingButtonVisibility(
      video_recording_watcher_->window_being_recorded()->GetRootWindow(),
      false);

  capture_mode_util::TriggerAccessibilityAlert(
      IDS_ASH_SCREEN_CAPTURE_ALERT_RECORDING_STOPPED);

  // Reset the camera selection if it was auto-selected in the
  // client-initiated capture mode session after video recording is completed
  // to avoid the camera selection settings of the normal capture mode session
  // being overridden by the client-initiated capture mode session.
  camera_controller_->MaybeRevertAutoCameraSelection();

  video_recording_watcher_->ShutDown();

  for (auto& observer : observers_) {
    observer.OnRecordingEnded();
  }

  // GIF files take a while to finalize and fully get written to disk. Therefore
  // we show a notification to the user to let them know that the file will be
  // ready shortly.
  if (current_video_file_path_.MatchesExtension(".gif")) {
    ShowGifProgressNotification();
  }
}

void CaptureModeController::CaptureImage(const CaptureParams& capture_params,
                                         const base::FilePath& path,
                                         const CaptureModeBehavior* behavior) {
  // Note that |type_| may not necessarily be |kImage| here, since this may be
  // called to take an instant fullscreen screenshot for the keyboard shortcut,
  // which doesn't go through the capture mode UI, and doesn't change |type_|.
  CHECK(delegate_->IsCaptureAllowedByPolicy());

  // A screenshot can be requested via the fullscreen screenshot keyboard
  // shortcut (which uses the default `behavior`) even though an active capture
  // mode session belongs to a different `behavior` kind (e.g. Projector or
  // Game Dashboard). In this case, the assumption is that the user wants to
  // take a screenshot of the screen in its current state (i.e. while keeping
  // the session active). Therefore, we don't stop the session in this case.
  // See http://b/353908198 for more details.
  if (IsActive() && behavior == capture_mode_session_->active_behavior()) {
    // Other than the above mentioned case, we stop the session now, so the
    // capture UIs don't end up in the screenshot.
    Stop();
  }

  CHECK(!capture_params.bounds.IsEmpty());

  const bool was_cursor_originally_blocked = MaybeLockCursor();

  // Attempt the capture image. Note the callback `OnImageCaptured()` will only
  // be invoked if an image was successfully captured.
  ui::GrabWindowSnapshotAsPNG(
      capture_params.window, capture_params.bounds,
      base::BindOnce(&CaptureModeController::OnImageCaptured,
                     weak_ptr_factory_.GetWeakPtr(), path,
                     was_cursor_originally_blocked, behavior));

  ++num_screenshots_taken_in_last_day_;
  ++num_screenshots_taken_in_last_week_;

  ++num_consecutive_screenshots_;
  num_consecutive_screenshots_scheduler_.Reset();

  capture_mode_util::TriggerAccessibilityAlert(
      IDS_ASH_SCREEN_CAPTURE_ALERT_SCREENSHOT_CAPTURED);

  // Notifies DLP that taking a screenshot was attempted so that it may report
  // the event or show a warning if restricted content was captured.
  delegate_->OnCaptureImageAttempted(capture_params.window,
                                     capture_params.bounds);
}

void CaptureModeController::CaptureVideo(const CaptureParams& capture_params) {
  DCHECK_EQ(CaptureModeType::kVideo, type_);
  DCHECK(delegate_->IsCaptureAllowedByPolicy());

  if (skip_count_down_ui_) {
    OnVideoRecordCountDownFinished();
    return;
  }

  capture_mode_session_->StartCountDown(
      base::BindOnce(&CaptureModeController::OnVideoRecordCountDownFinished,
                     weak_ptr_factory_.GetWeakPtr()));

  capture_mode_util::TriggerAccessibilityAlert(
      IDS_ASH_SCREEN_CAPTURE_ALERT_RECORDING_STARTING);
}

void CaptureModeController::OnImageCaptured(
    const base::FilePath& path,
    bool was_cursor_originally_blocked,
    const CaptureModeBehavior* behavior,
    scoped_refptr<base::RefCountedMemory> png_bytes) {
  MaybeUnlockCursor(was_cursor_originally_blocked);

  if (!png_bytes || !png_bytes->size()) {
    LOG(ERROR) << "Failed to capture image.";
    ShowFailureNotification();
    return;
  }
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&SaveFile, png_bytes, delegate_->RedirectFilePath(path),
                     GetFallbackFilePathFromFile(path)),
      base::BindOnce(&CaptureModeController::OnImageFileSaved,
                     weak_ptr_factory_.GetWeakPtr(), png_bytes, behavior));
}

void CaptureModeController::OnImageCapturedForSearch(
    PerformCaptureType capture_type,
    bool was_cursor_originally_blocked,
    base::WeakPtr<BaseCaptureModeSession> image_search_token,
    scoped_refptr<base::RefCountedMemory> jpeg_bytes) {
  absl::Cleanup run_test_callback_on_return = [this, capture_type] {
    if (on_image_captured_for_search_callback_for_test_) {
      std::move(on_image_captured_for_search_callback_for_test_)
          .Run(capture_type);
    }
  };
  // From here on, no matter where the function exits, the cursor must be
  // unlocked and re-shown.
  MaybeUnlockCursor(was_cursor_originally_blocked);
  // The capture parameters / region / session may have changed before
  // `jpeg_bytes` were received.
  if (!image_search_token) {
    VLOG(1) << "Image search token invalid after capturing image.";
    return;
  }
  capture_mode_session_->OnPerformCaptureForSearchEnded(capture_type);

  const SkBitmap bitmap = gfx::JPEGCodec::Decode(*jpeg_bytes);
  if (ShouldPerformTextDetection(capture_type)) {
    delegate_->DetectTextInImage(
        bitmap, base::BindOnce(&CaptureModeController::OnTextDetectionComplete,
                               weak_ptr_factory_.GetWeakPtr(),
                               image_search_token, base::TimeTicks::Now()));
  }

  if (ShouldFetchScannerActions(capture_type)) {
    bool actions_fetched = false;
    if (ScannerController* scanner_controller =
            Shell::Get()->scanner_controller()) {
      // Note that `OnScannerActionsFetched` is always called, even if
      // `actions_fetched` is false. This is intentional, as
      // `OnScannerActionsFetched` stops the glow started by
      // `ShouldShowGlowWhileProcessingCaptureType` in `DefaultBehavior`
      // (guarded on whether `scanner_controller()` is non-null on `Shell`), and
      // in `SunfishBehavior` (always true).
      actions_fetched = scanner_controller->FetchActionsForImage(
          jpeg_bytes,
          base::BindOnce(&CaptureModeController::OnScannerActionsFetched,
                         weak_ptr_factory_.GetWeakPtr(), image_search_token));
    }

    if (capture_type == PerformCaptureType::kSunfish) {
      RecordScannerFeatureUserState(
          actions_fetched
              ? ScannerFeatureUserState::
                    kSunfishSessionImageCapturedAndActionsFetchStarted
              : ScannerFeatureUserState::
                    kSunfishSessionImageCapturedAndActionsNotFetched);
    }
    if (capture_type == PerformCaptureType::kScanner) {
      RecordScannerFeatureUserState(
          actions_fetched
              ? ScannerFeatureUserState::
                    kSmartActionsButtonImageCapturedAndActionsFetchStarted
              : ScannerFeatureUserState::
                    kSmartActionsButtonImageCapturedAndActionsNotFetched);
    }
  }

  if (!ShouldSendRegionSearch(capture_type)) {
    return;
  }

    const gfx::Image image = gfx::Image::CreateFrom1xBitmap(bitmap);
    const bool is_standalone_session =
        capture_mode_session_->active_behavior()->behavior_type() ==
        BehaviorType::kSunfish;
    delegate_->SendLensWebRegionSearch(
        image, is_standalone_session,
        base::BindRepeating(&CaptureModeController::OnSearchUrlFetched,
                            weak_ptr_factory_.GetWeakPtr(),
                            user_capture_region_, gfx::ImageSkia()),
        base::BindRepeating(&CaptureModeController::OnLensTextDetectionComplete,
                            weak_ptr_factory_.GetWeakPtr(), image_search_token),
        base::BindRepeating(&CaptureModeController::OnLensWebError,
                            weak_ptr_factory_.GetWeakPtr(),
                            image_search_token));

    // Immediately show the search results panel, with a loading animation in
    // place of the web contents. We will replace it once we receive the URL
    // from the server.
    ShowSearchResultsPanel();
}

void CaptureModeController::OnTextDetectionComplete(
    base::WeakPtr<BaseCaptureModeSession> image_search_token,
    base::TimeTicks ocr_attempt_start_time,
    std::optional<std::string> detected_text) {
  RecordOnDeviceOcrTimerCompleted(ocr_attempt_start_time);
  if (!image_search_token || !detected_text.has_value()) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::
            kSmartActionsButtonNotShownDueToTextDetectionCancelled);
    return;
  }

  if (detected_text->empty()) {
    RecordScannerFeatureUserState(
        ScannerFeatureUserState::
            kSmartActionsButtonNotShownDueToNoTextDetected);
    return;
  }

  AddCopyTextButton(*detected_text);
  capture_mode_session_->AddSmartActionsButton();
}

void CaptureModeController::OnLensTextDetectionComplete(
    base::WeakPtr<BaseCaptureModeSession> image_search_token,
    std::optional<std::string> detected_text) {
  bool text_present = detected_text.has_value() && !detected_text->empty();
  RecordCaptureModeTextDetectionResult(
      text_present ? CaptureModeTextDetectionResult::kSuccessTextPresent
                   : CaptureModeTextDetectionResult::kSuccessNoTextPresent);

  if (!image_search_token) {
    VLOG(1) << "Image search token invalid after text detection completed.";
    return;
  }

  if (!text_present) {
    return;
  }

  // Only use lens to automatically add a Copy Text button if we are in a
  // sunfish session.
  if (capture_mode_session_->active_behavior()->behavior_type() ==
      BehaviorType::kSunfish) {
    AddCopyTextButton(*detected_text);
  }
}

void CaptureModeController::AddCopyTextButton(std::string_view detected_text) {
  CHECK(!detected_text.empty());

  capture_mode_util::AddActionButton(
      base::BindOnce(&CaptureModeController::OnCopyTextButtonClicked,
                     weak_ptr_factory_.GetWeakPtr(),
                     base::UTF8ToUTF16(detected_text)),
      l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_COPY_TEXT_BUTTON_LABEL),
      &vector_icons::kContentCopyIcon,
      ActionButtonRank{ActionButtonType::kCopyText, /*weight=*/0},
      ActionButtonViewID::kCopyTextButton);
}

void CaptureModeController::OnCopyTextButtonClicked(
    const std::u16string& text) {
  CopyTextToClipboard(text);
  ShowTextCopiedToast();
  Stop();
}

void CaptureModeController::MaybeShowScannerDisclaimerOnSunfishStartup(
    bool startup_success) {
  if (!startup_success ||
      // Below conditions imply scanner is disabled in some way.
      // Hence we should skip showing the disclaimer.
      !ScannerController::CanShowUiForShell()) {
    if (!CanShowSunfishUi() && IsActive()) {
      // Should stop because if both scanner and sunfish are disabled, then
      // there is nothing you can do in the session.
      Stop();
    }
    return;
  }
  // Since this is at the end of startup internal, the capture_mode_session
  // should exist.
  CHECK(capture_mode_session_);

  // If declined, we should completely stop the sunfish session if only scanner
  // is enabled. If both scanner consent is declined and sunfish is disabled,
  // then there is nothing you can do in the session.
  // Otherwise, allow the session to continue (DoNothing) since sunfish can run
  // without scanner.
  base::RepeatingClosure decline_callback =
      CanShowSunfishUi() ? base::DoNothing()
                         : base::BindRepeating(&CaptureModeController::Stop,
                                               weak_ptr_factory_.GetWeakPtr());
  capture_mode_session_->MaybeShowScannerDisclaimer(
      ScannerEntryPoint::kSunfishSession,
      /*accept_callback=*/base::BindRepeating([]() {
        // Start a session after the disclaimer to ensure that it is started
        // correctly if the user has just consented.
        if (auto* scanner_controller = Shell::Get()->scanner_controller()) {
          scanner_controller->StartNewSession();
        }
      }),
      decline_callback);
}

void CaptureModeController::OnScannerActionsFetched(
    base::WeakPtr<BaseCaptureModeSession> image_search_token,
    ScannerSession::FetchActionsResponse actions_response) {
  if (!image_search_token) {
    return;
  }
  capture_mode_session_->OnScannerActionsFetched(std::move(actions_response));
}

void CaptureModeController::OnSearchUrlFetched(const gfx::Rect& captured_region,
                                               const gfx::ImageSkia& image,
                                               GURL url) {
  RecordCaptureModeImageSearchResult(CaptureModeImageSearchResult::kSuccess);
  if (captured_region == user_capture_region_) {
    NavigateSearchResultsPanel(url);
  }
}

void CaptureModeController::OnLensWebError(
    base::WeakPtr<BaseCaptureModeSession> image_search_token,
    CaptureModeImageSearchResult image_result,
    CaptureModeTextDetectionResult text_result) {
  // TODO: crbug.com/446249623 - Add separate error handling for text
  // detection.
  // If image search goes wrong, close the panel. Otherwise, even if there is
  // a text error, don't close the panel. Record metrics except for successes,
  // as those are recorded separately upon completion of the desired task.
  if (image_result != CaptureModeImageSearchResult::kSuccess) {
    RecordCaptureModeImageSearchResult(image_result);
    CloseSearchResultsPanel();
  }
  if (text_result != CaptureModeTextDetectionResult::kSuccessNoTextPresent &&
      text_result != CaptureModeTextDetectionResult::kSuccessTextPresent) {
    RecordCaptureModeTextDetectionResult(text_result);
  }

  // TODO: crbug.com/406072681 - Show an error message if the session is no
  // longer active, such as in the case of clicking the Search with Lens button
  // in a regular session.
  if (!image_search_token) {
    return;
  }

  CHECK(IsActive());
  Stop();
}

void CaptureModeController::OnSearchResultClicked() {
  // End capture mode session when a result link is clicked, as the assumed user
  // intention is to interact with the newly opened link.
  if (IsActive() && capture_mode_session_->active_behavior()
                        ->ShouldEndSessionOnSearchResultClicked()) {
    Stop();
  }

  RecordSearchResultClicked();
}

void CaptureModeController::OnImageFileSaved(
    scoped_refptr<base::RefCountedMemory> png_bytes,
    const CaptureModeBehavior* behavior,
    const base::FilePath& file_saved_path) {
  if (file_saved_path.empty()) {
    OnImageFileFinalized(/*image=*/gfx::Image(), behavior, /*success=*/false,
                         file_saved_path);
    return;
  }
  const auto image = gfx::Image::CreateFrom1xPNGBytes(png_bytes);
  delegate_->FinalizeSavedFile(
      base::BindOnce(&CaptureModeController::OnImageFileFinalized,
                     weak_ptr_factory_.GetWeakPtr(), image, behavior),
      file_saved_path, image, /*for_video=*/false);
}

void CaptureModeController::OnImageFileFinalized(
    const gfx::Image& image,
    const CaptureModeBehavior* behavior,
    bool success,
    const base::FilePath& file_saved_path) {
  if (!success) {
    ShowFailureNotification();
    return;
  }
  if (on_file_saved_callback_for_test_) {
    std::move(on_file_saved_callback_for_test_).Run(file_saved_path);
  }

  DCHECK(!image.IsEmpty());
  CopyImageToClipboard(image);
  ShowPreviewNotification(file_saved_path, image, CaptureModeType::kImage,
                          behavior);
  if (Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    RecordSaveToLocation(GetSaveToOption(file_saved_path), behavior);
  }

  // NOTE: Holding space `client` may be `nullptr` in tests.
  if (auto* client = HoldingSpaceController::Get()->client()) {
    client->AddItemOfType(HoldingSpaceItem::Type::kScreenshot, file_saved_path);
  }
}

void CaptureModeController::OnVideoFileSaved(
    const gfx::ImageSkia& video_thumbnail,
    const CaptureModeBehavior* behavior,
    bool success,
    const base::FilePath& saved_video_file_path) {
  DCHECK(base::CurrentUIThread::IsSet());

  if (!success) {
    ShowFailureNotification();
  } else {
    const bool is_gif = saved_video_file_path.MatchesExtension(".gif");
    if (behavior->ShouldShowPreviewNotification()) {
      ShowPreviewNotification(saved_video_file_path,
                              gfx::Image(video_thumbnail),
                              CaptureModeType::kVideo, behavior);
      // NOTE: Holding space `client` may be `nullptr` in tests.
      if (auto* client = HoldingSpaceController::Get()->client()) {
        client->AddItemOfType(is_gif
                                  ? HoldingSpaceItem::Type::kScreenRecordingGif
                                  : HoldingSpaceItem::Type::kScreenRecording,
                              saved_video_file_path);
      }

      // `behavior` could dangle here after the reply is received. Get the
      // client metric component now, instead of after the reply is received,
      // to prevent this.
      auto reply = base::BindOnce(&RecordVideoFileSizeKB, is_gif,
                                  behavior->GetClientMetricComponent());
      if (on_file_saved_callback_for_test_) {
        reply = std::move(reply).Then(
            base::BindOnce(std::move(on_file_saved_callback_for_test_),
                           saved_video_file_path));
      }

      // We only record the file size histogram if the recording is not saved on
      // DriveFs.
      blocking_task_runner_->PostTaskAndReplyWithResult(
          FROM_HERE, base::BindOnce(&GetFileSizeInKB, saved_video_file_path),
          std::move(reply));
    }

    CHECK(!recording_start_time_.is_null());
    RecordCaptureModeRecordingDuration(
        (base::TimeTicks::Now() - recording_start_time_), behavior, is_gif);
  }
  if (Shell::Get()->session_controller()->IsActiveUserSessionStarted()) {
    RecordSaveToLocation(GetSaveToOption(saved_video_file_path), behavior);
  }

  // If `on_file_saved_callback_for_test_` is not empty, it means that it hasn't
  // been consumed yet since file size metric will not be recorded if saved on
  // DriveFs for example the projector-initiated capture mode. In this case, we
  // need to explicitly run the callback to let the running wait runloop quit on
  // file saved.
  if (on_file_saved_callback_for_test_) {
    std::move(on_file_saved_callback_for_test_).Run(saved_video_file_path);
  }
}

void CaptureModeController::ShowPreviewNotification(
    const base::FilePath& screen_capture_path,
    const gfx::Image& preview_image,
    const CaptureModeType type,
    const CaptureModeBehavior* behavior) {
  const bool for_video = type == CaptureModeType::kVideo;
  const int title_id = GetNotificationTitleIdForFile(screen_capture_path);
  const int message_id = for_video && low_disk_space_threshold_reached_
                             ? IDS_ASH_SCREEN_CAPTURE_LOW_STORAGE_SPACE_MESSAGE
                             : kNoMessage;

  message_center::RichNotificationData optional_fields;
  optional_fields.buttons = behavior->GetNotificationButtonsInfo(for_video);

  optional_fields.image = preview_image;
  optional_fields.image_path = screen_capture_path;

  ShowNotification(
      kScreenCaptureNotificationId, title_id, message_id, optional_fields,
      base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
          base::BindRepeating(&CaptureModeController::HandleNotificationClicked,
                              weak_ptr_factory_.GetWeakPtr(),
                              screen_capture_path, type,
                              behavior->behavior_type())),
      message_center::SystemNotificationWarningLevel::NORMAL, kCaptureModeIcon,
      for_video);
}

void CaptureModeController::HandleNotificationClicked(
    const base::FilePath& screen_capture_path,
    const CaptureModeType type,
    const BehaviorType behavior_type,
    std::optional<int> button_index) {
  if (!button_index.has_value()) {
    // Open the item with the default handler.
    delegate_->OpenScreenCaptureItem(screen_capture_path);
    RecordScreenshotNotificationQuickAction(CaptureQuickAction::kOpenDefault);
  } else {
    const int button_index_value = button_index.value();
    if (type == CaptureModeType::kVideo) {
      if (behavior_type == BehaviorType::kGameDashboard) {
        switch (button_index_value) {
          case GameDashboardVideoNotificationButtonIndex::
              kButtonShareToYoutube:
            OnShareToYouTubeButtonPressed();
            break;
          case GameDashboardVideoNotificationButtonIndex::
              kButtonDeleteGameVideo:
            DeleteFileAsync(screen_capture_path);
            break;
          default:
            NOTREACHED();
        }
      } else {
        CHECK_EQ(VideoNotificationButtonIndex::kButtonDeleteVideo,
                 button_index_value);
        DeleteFileAsync(screen_capture_path);
      }
    } else {
      CHECK_EQ(type, CaptureModeType::kImage);
      switch (button_index_value) {
        case ScreenshotNotificationButtonIndex::kButtonEdit:
          delegate_->OpenScreenshotInImageEditor(screen_capture_path);
          RecordScreenshotNotificationQuickAction(
              CaptureQuickAction::kBacklight);
          break;
        case ScreenshotNotificationButtonIndex::kButtonDelete:
          DeleteFileAsync(screen_capture_path);
          RecordScreenshotNotificationQuickAction(CaptureQuickAction::kDelete);
          break;
        default:
          NOTREACHED();
      }
    }
  }

  // This has to be done at the end to avoid a use-after-free crash, since
  // removing the notification will delete its delegate, which owns the callback
  // to this function. The callback's state owns any passed-by-ref arguments,
  // such as |screen_capture_path| which we use in this function.
  message_center::MessageCenter::Get()->RemoveNotification(
      kScreenCaptureNotificationId, /*by_user=*/false);
}

base::FilePath CaptureModeController::BuildImagePath() const {
  return BuildPathNoExtension(kScreenshotFileName, base::Time::Now())
      .AddExtension("png");
}

base::FilePath CaptureModeController::BuildVideoPath() const {
  return BuildPathNoExtension(kVideoFileName, base::Time::Now())
      .AddExtension(GetVideoExtension(recording_type_, source_));
}

base::FilePath CaptureModeController::BuildImagePathForDisplay(
    int display_index) const {
  auto path_str =
      BuildPathNoExtension(kScreenshotFileName, base::Time::Now()).value();
  auto full_path = base::StringPrintf("%s - Display %d.png", path_str.c_str(),
                                      display_index);
  return base::FilePath(full_path);
}

base::FilePath CaptureModeController::BuildPathNoExtension(
    std::string_view base_name,
    base::Time timestamp) const {
  return GetCurrentCaptureFolder().path.AppendASCII(base::StrCat(
      {base_name, base::UnlocalizedTimeFormatWithPattern(timestamp, " y-MM-dd"),
       base::UnlocalizedTimeFormatWithPattern(
           timestamp,
           delegate_->Uses24HourFormat() ? " HH.mm.ss" : " h.mm.ss a")}));
}

base::FilePath CaptureModeController::GetFallbackFilePathFromFile(
    const base::FilePath& path) {
  auto* session_controller = Shell::Get()->session_controller();
  const auto fallback_dir = session_controller->IsActiveUserSessionStarted()
                                ? delegate_->GetUserDefaultDownloadsFolder()
                                : GetTempDir();
  return fallback_dir.Append(path.BaseName());
}

void CaptureModeController::RecordAndResetScreenshotsTakenInLastDay() {
  RecordNumberOfScreenshotsTakenInLastDay(num_screenshots_taken_in_last_day_);
  num_screenshots_taken_in_last_day_ = 0;
}

void CaptureModeController::RecordAndResetScreenshotsTakenInLastWeek() {
  RecordNumberOfScreenshotsTakenInLastWeek(num_screenshots_taken_in_last_week_);
  num_screenshots_taken_in_last_week_ = 0;
}

void CaptureModeController::RecordAndResetConsecutiveScreenshots() {
  RecordNumberOfConsecutiveScreenshots(num_consecutive_screenshots_);
  num_consecutive_screenshots_ = 0;
}

void CaptureModeController::OnVideoRecordCountDownFinished() {
  // Ensure `on_countdown_finished_callback_for_test_` is run after this
  // function.
  base::ScopedClosureRunner scoped_closure(
      std::move(on_countdown_finished_callback_for_test_));

  // If this event is dispatched after the capture session was cancelled or
  // destroyed, this should be a no-op.
  if (!IsActive())
    return;

  const std::optional<CaptureParams> capture_params = GetCaptureParams();
  if (!capture_params) {
    // There's nothing to capture, so we'll stop the session and skip the rest.
    Stop();
    return;
  }

  // During the 3-second count down, screen content might have changed. We must
  // check again the DLP restrictions.
  DCHECK(!pending_dlp_check_);
  pending_dlp_check_ = true;
  capture_mode_session_->OnWaitingForDlpConfirmationStarted();
  delegate_->CheckCaptureOperationRestrictionByDlp(
      capture_params->window, capture_params->bounds,
      base::BindOnce(
          &CaptureModeController::OnDlpRestrictionCheckedAtCountDownFinished,
          weak_ptr_factory_.GetWeakPtr()));
}

void CaptureModeController::OnCaptureFolderCreated(
    const CaptureParams& capture_params,
    const base::FilePath& capture_file_full_path) {
  if (!IsActive()) {
    // This function gets called asynchronously, and until it gets called, the
    // session could end due e.g. locking the screen, suspending, or switching
    // users.
    return;
  }

  // An empty path is sent to indicate an error.
  if (capture_file_full_path.empty()) {
    Stop();
    return;
  }

  BeginVideoRecording(capture_params, capture_file_full_path);
}

void CaptureModeController::BeginVideoRecording(
    const CaptureParams& capture_params,
    const base::FilePath& video_file_path) {
  CHECK(!video_file_path.empty());
  CHECK(IsVideoFileExtensionSupported(video_file_path));
  CHECK(can_start_new_recording());

  if (!IsActive()) {
    // This function gets called asynchronously, and until it gets called, the
    // session could end due to e.g. locking the screen, suspending, or
    // switching users.
    return;
  }

  base::AutoReset<bool> initializing_resetter(&is_initializing_recording_,
                                              true);

  // Do not trigger an alert when exiting the session, since we end the session
  // to start recording.
  capture_mode_session_->set_a11y_alert_on_session_exit(false);

  // Acquire the session's layer in order to potentially reuse it for painting
  // a highlight around the region being recorded.
  std::unique_ptr<ui::Layer> session_layer =
      capture_mode_session_->ReleaseLayer();
  session_layer->set_delegate(nullptr);

  // At this point, recording is guaranteed to start, and cannot be prevented by
  // DLP or user cancellation.
  capture_mode_session_->set_is_stopping_to_start_video_recording(true);

  // Cache the active behavior of the capture session to be passed to video
  // recording watcher after stopping the capture mode session.
  CaptureModeBehavior* active_behavior =
      capture_mode_session_->active_behavior();

  // Stop the capture session now, so the bar doesn't show up in the captured
  // video.
  Stop();

  // Use the `video_file_path` instead of `recording_type_` to determine if the
  // recording format supports audio recording. This is because the actual
  // format can be different, since GIF for example is only supported when the
  // recording `source_` is `kRegion`.
  const AudioRecordingMode effective_audio_mode =
      SupportsAudioRecording(video_file_path) ? GetEffectiveAudioRecordingMode()
                                              : AudioRecordingMode::kOff;
  const bool should_record_audio =
      effective_audio_mode != AudioRecordingMode::kOff;
  mojo::PendingRemote<viz::mojom::FrameSinkVideoCaptureOverlay>
      cursor_capture_overlay;
  auto cursor_overlay_receiver =
      cursor_capture_overlay.InitWithNewPipeAndPassReceiver();
  video_recording_watcher_ = std::make_unique<VideoRecordingWatcher>(
      this, active_behavior, capture_params.window,
      std::move(cursor_capture_overlay), should_record_audio);

  aura::Window* root_window = capture_params.window->GetRootWindow();
  for (auto& observer : observers_) {
    observer.OnRecordingStarted(root_window);
  }

  // We only paint the recorded area highlight for window and region captures.
  if (source_ != CaptureModeSource::kFullscreen)
    video_recording_watcher_->Reset(std::move(session_layer));

  DCHECK(current_video_file_path_.empty());
  recording_start_time_ = base::TimeTicks::Now();
  current_video_file_path_ = video_file_path;

  LaunchRecordingServiceAndStartRecording(
      capture_params, std::move(cursor_overlay_receiver), effective_audio_mode);

  // Intentionally record the metrics before `DetachFromSession` as
  // `enable_demo_tools_` may be overwritten otherwise.
  RecordRecordingStartsWithDemoTools(enable_demo_tools_, active_behavior);

  // Restore the cached capture mode configs when the capture mode session ends
  // to start video recording in case another default capture mode session
  // starts while video recording in progress.
  active_behavior->DetachFromSession();

  capture_mode_util::SetStopRecordingButtonVisibility(root_window, true);

  delegate_->StartObservingRestrictedContent(
      capture_params.window, capture_params.bounds,
      base::BindOnce(&CaptureModeController::InterruptVideoRecording,
                     weak_ptr_factory_.GetWeakPtr()));

  if (on_video_recording_started_callback_for_test_) {
    std::move(on_video_recording_started_callback_for_test_).Run();
  }
}

void CaptureModeController::InterruptVideoRecording() {
  EndVideoRecording(EndRecordingReason::kDlpInterruption);
}

void CaptureModeController::OnDlpRestrictionCheckedAtPerformingCapture(
    PerformCaptureType capture_type,
    bool proceed) {
  pending_dlp_check_ = false;

  if (!IsActive()) {
    // This function gets called asynchronously, and until it gets called, the
    // session could end due to e.g. locking the screen, suspending, or
    // switching users.
    return;
  }

  // We don't need to bring capture mode UIs back if `proceed` is false or if
  // the session is about to shutdown. See also
  // `CaptureModeBehavior::ShouldReShowUisAtPerformingCapture`.
  auto* active_behavior = capture_mode_session_->active_behavior();
  capture_mode_session_->OnWaitingForDlpConfirmationEnded(
      /*reshow_uis=*/proceed &&
      active_behavior->ShouldReShowUisAtPerformingCapture(capture_type));

  if (!proceed) {
    Stop();
    return;
  }

  const std::optional<CaptureParams> capture_params = GetCaptureParams();
  CHECK(capture_params);

  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    Stop();
    return;
  }

  if (type_ == CaptureModeType::kImage) {
    if (capture_type == PerformCaptureType::kCapture) {
      CaptureImage(*capture_params, BuildImagePath(),
                   capture_mode_session_->active_behavior());
    } else {
      PerformImageSearch(capture_type);
    }
  } else {
    // HDCP affects only video recording.
    if (ShouldBlockRecordingForContentProtection(capture_params->window)) {
      ShowDisabledNotification(CaptureAllowance::kDisallowedByHdcp);
      Stop();
      return;
    }

    CaptureVideo(*capture_params);
  }
}

void CaptureModeController::OnDlpRestrictionCheckedAtCountDownFinished(
    bool proceed) {
  pending_dlp_check_ = false;

  if (!IsActive()) {
    // This function gets called asynchronously, and until it gets called, the
    // session could end due to e.g. locking the screen, suspending, or
    // switching users.
    return;
  }

  // We don't need to bring back capture mode UIs on 3-second count down
  // finished, since the session is about to shutdown anyways for starting the
  // video recording.
  capture_mode_session_->OnWaitingForDlpConfirmationEnded(/*reshow_uis=*/false);

  if (!proceed) {
    Stop();
    return;
  }

  const std::optional<CaptureParams> capture_params = GetCaptureParams();
  if (!capture_params) {
    Stop();
    return;
  }

  // Now that we're done with DLP restrictions checks, we can perform the policy
  // and HDCP checks, which may have changed during the 3-second count down and
  // during the time the DLP warning dialog was shown.
  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    Stop();
    return;
  }

  if (ShouldBlockRecordingForContentProtection(capture_params->window)) {
    Stop();
    ShowDisabledNotification(CaptureAllowance::kDisallowedByHdcp);
    return;
  }

  // The creation of the required capture folder that will host the video is
  // asynchronous. We don't want the user to be able to bail out of the
  // session at this point, since we don't want to create that folder in vain.
  capture_mode_session_->set_can_exit_on_escape(false);

  CaptureModeBehavior* active_behavior =
      capture_mode_session_->active_behavior();
  if (!active_behavior->SupportsAudioRecordingMode(
          GetEffectiveAudioRecordingMode())) {
    // Before asking the client to create a folder to host the video file, we
    // check if they require audio recording to be enabled, but it can't be
    // allowed due to admin policy. In this case we just abort the recording by
    // stopping the capture mode session without starting any recording. This
    // will eventually call `CaptureModeObserver::OnRecordingStartAborted()`
    // which should let clients do any necessary clean ups.
    Stop();
    return;
  }

  if (active_behavior->RequiresCaptureFolderCreation()) {
    active_behavior->CreateCaptureFolder(
        base::BindOnce(&CaptureModeController::OnCaptureFolderCreated,
                       weak_ptr_factory_.GetWeakPtr(), *capture_params));
    return;
  }

  const base::FilePath current_path = BuildVideoPath();

  // If the current capture folder is not the default `Downloads` folder, we
  // need to validate the current folder first before starting the video
  // recording.
  if (!GetCurrentCaptureFolder().is_default_downloads_folder) {
    blocking_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE,
        base::BindOnce(&SelectFilePathForCapturedFile,
                       delegate_->RedirectFilePath(current_path),
                       GetFallbackFilePathFromFile(current_path)),
        base::BindOnce(&CaptureModeController::BeginVideoRecording,
                       weak_ptr_factory_.GetWeakPtr(), *capture_params));
    return;
  }

  BeginVideoRecording(*capture_params, current_path);
}

void CaptureModeController::OnDlpRestrictionCheckedAtSessionInit(
    SessionType session_type,
    CaptureModeEntryType entry_type,
    base::OnceClosure at_exit_closure,
    bool proceed) {
  base::ScopedClosureRunner deferred_runner(std::move(at_exit_closure));

  pending_dlp_check_ = false;

  if (!proceed) {
    return;
  }

  CHECK(!capture_mode_session_);

  // Check policy again even though we checked in Start(), but due to the DLP
  // warning dialog can be accepted after a long wait, maybe something changed
  // in the middle.
  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    return;
  }

  // Close any previously opened panel to ensure a clean slate.
  // TODO(b/377370403): Revisit this decision.
  CloseSearchResultsPanel();

  // Before we start the session, if video recording is in progress, we need to
  // set the current type to image (except if the new behavior type is sunfish),
  // as we can't have more than one recording at a time. The video toggle button
  // in the capture mode bar will be disabled.
  if (!can_start_new_recording()) {
    SetType(CaptureModeType::kImage);
  } else if (entry_type == CaptureModeEntryType::kProjector) {
    CHECK(!delegate_->IsAudioCaptureDisabledByPolicy())
        << "A projector session should not be allowed to begin if audio "
           "capture is disabled by policy.";
  }
  const BehaviorType behavior_type = ToBehaviorType(entry_type);

  RecordCaptureModeEntryType(entry_type);
  if (ShouldClearCaptureRegion(behavior_type)) {
    SetUserCaptureRegion(gfx::Rect(), /*by_user=*/false);
  }

  delegate_->OnSessionStateChanged(/*started=*/true);

  capture_mode_session_ =
      CreateSession(session_type, this, GetBehavior(behavior_type));
  capture_mode_session_->Initialize();
  camera_controller_->OnCaptureSessionStarted();
}

void CaptureModeController::OnDlpRestrictionCheckedAtVideoEnd(
    const gfx::ImageSkia& video_thumbnail,
    bool success,
    const CaptureModeBehavior* behavior,
    bool proceed) {
  const bool should_delete_file = !proceed;
  const auto video_file_path = current_video_file_path_;
  current_video_file_path_.clear();

  if (should_delete_file) {
    // Remove any lingering notification, e.g. the GIF progress notification,
    // before proceeding, since it no longer makes sense as the file will be
    // deleted.
    message_center::MessageCenter::Get()->RemoveNotification(
        kScreenCaptureNotificationId, /*by_user=*/false);

    DeleteFileAsync(video_file_path);
    OnVideoFileFinalized(/*should_delete_file=*/true, video_thumbnail);
  } else {
    if (!success) {
      OnVideoFileSaved(video_thumbnail, behavior, success, video_file_path);
      OnVideoFileFinalized(/*should_delete_file=*/false, video_thumbnail);
      return;
    }
    delegate_->FinalizeSavedFile(
        base::BindOnce(&CaptureModeController::OnVideoFileSaved,
                       weak_ptr_factory_.GetWeakPtr(), video_thumbnail,
                       behavior)
            .Then(base::BindOnce(&CaptureModeController::OnVideoFileFinalized,
                                 weak_ptr_factory_.GetWeakPtr(),
                                 /*should_delete_file=*/false,
                                 video_thumbnail)),
        video_file_path, gfx::Image(video_thumbnail), /*for_video=*/true);
  }
}

void CaptureModeController::OnVideoFileFinalized(
    bool should_delete_file,
    const gfx::ImageSkia& video_thumbnail) {
  low_disk_space_threshold_reached_ = false;
  recording_start_time_ = base::TimeTicks();

  for (auto& observer : observers_) {
    observer.OnVideoFileFinalized(should_delete_file, video_thumbnail);
  }
}

void CaptureModeController::CaptureInstantScreenshot(
    CaptureModeEntryType entry_type,
    CaptureModeSource source,
    base::OnceClosure instant_screenshot_callback,
    BehaviorType behavior_type) {
  if (pending_dlp_check_) {
    return;
  }

  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    return;
  }

  pending_dlp_check_ = true;
  delegate_->CheckCaptureModeInitRestrictionByDlp(
      /*shutting_down=*/false,
      base::BindOnce(
          &CaptureModeController::OnDlpRestrictionCheckedAtCaptureScreenshot,
          weak_ptr_factory_.GetWeakPtr(), entry_type, source,
          std::move(instant_screenshot_callback), behavior_type));
}

void CaptureModeController::OnDlpRestrictionCheckedAtCaptureScreenshot(
    CaptureModeEntryType entry_type,
    CaptureModeSource source,
    base::OnceClosure instant_screenshot_callback,
    BehaviorType behavior_type,
    bool proceed) {
  pending_dlp_check_ = false;
  if (!proceed) {
    return;
  }

  // Due to the fact that the DLP warning dialog may take a while, check the
  // enterprise policy again even though we checked it in
  // `CaptureInstantScreenshot()`.
  if (!delegate_->IsCaptureAllowedByPolicy()) {
    ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
    return;
  }

  std::move(instant_screenshot_callback).Run();

  // Since this doesn't create a capture mode session, log metrics here.
  RecordCaptureModeEntryType(entry_type);
  RecordCaptureModeConfiguration(
      CaptureModeType::kImage, source,
      // The values of `recording_type_` and `GetEffectiveAudioRecordingMode()`
      // will be ignored, since the type is `kImage`.
      recording_type_, GetEffectiveAudioRecordingMode(),
      GetBehavior(behavior_type));
}

void CaptureModeController::PerformScreenshotsOfAllDisplays(
    BehaviorType behavior_type) {
  // Get a vector of RootWindowControllers with primary root window at first.
  const std::vector<RootWindowController*> controllers =
      RootWindowController::root_window_controllers();
  // Capture screenshot for each individual display.
  int display_index = 1;
  for (RootWindowController* controller : controllers) {
    // TODO(shidi): Check with UX what notification should show if
    // some (but not all) of the displays have restricted content and
    // whether we should localize the display name.
    const CaptureParams capture_params{controller->GetRootWindow(),
                                       controller->GetRootWindow()->bounds()};
    CaptureImage(capture_params,
                 controllers.size() == 1
                     ? BuildImagePath()
                     : BuildImagePathForDisplay(display_index),
                 GetBehavior(behavior_type));
    ++display_index;
  }
}

void CaptureModeController::PerformScreenshotOfGivenWindow(
    aura::Window* given_window,
    BehaviorType behavior_type) {
  const CaptureParams capture_params{given_window,
                                     gfx::Rect(given_window->bounds().size())};
  CaptureImage(capture_params, BuildImagePath(), GetBehavior(behavior_type));
}

bool CaptureModeController::ShouldClearCaptureRegion(
    BehaviorType behavior_type) const {
  // Reset the user capture region if enough time has passed as it can be
  // annoying to still have the old capture region from the previous session
  // long time ago, or if the active behavior is sunfish behavior.
  return !user_capture_region_.IsEmpty() &&
         (base::TimeTicks::Now() - last_capture_region_update_time_ >
              kResetCaptureRegionDuration ||
          behavior_type == BehaviorType::kSunfish);
}

CaptureModeSaveToLocation CaptureModeController::GetSaveToOption(
    const base::FilePath& path) {
  DCHECK(Shell::Get()->session_controller()->IsActiveUserSessionStarted());
  const auto dir_path = path.DirName();
  if (dir_path == delegate_->GetUserDefaultDownloadsFolder())
    return CaptureModeSaveToLocation::kDefault;
  base::FilePath mounted_path;
  if (delegate_->GetDriveFsMountPointPath(&mounted_path)) {
    const auto drive_root_path = mounted_path.Append("root");
    if (dir_path == drive_root_path)
      return CaptureModeSaveToLocation::kDrive;

    if (drive_root_path.IsParent(dir_path))
      return CaptureModeSaveToLocation::kDriveFolder;
  }
  base::FilePath one_drive_mount_path = delegate_->GetOneDriveVirtualPath();
  if (!one_drive_mount_path.empty()) {
    if (dir_path == one_drive_mount_path) {
      return CaptureModeSaveToLocation::kOneDrive;
    }
    if (one_drive_mount_path.IsParent(dir_path)) {
      return CaptureModeSaveToLocation::kOneDriveFolder;
    }
  }
  return CaptureModeSaveToLocation::kCustomizedFolder;
}

CaptureModeBehavior* CaptureModeController::GetBehavior(
    BehaviorType behavior_type) {
  auto& behavior = behaviors_map_[behavior_type];
  if (!behavior) {
    behavior = CaptureModeBehavior::Create(behavior_type);
  }

  return behavior.get();
}

void CaptureModeController::DeleteFileAsync(const base::FilePath& path) {
  OnFileDeletedCallback callback =
      on_file_deleted_callback_for_test_
          ? std::move(on_file_deleted_callback_for_test_)
          : base::BindOnce([](const base::FilePath& path, bool success) {
              // TODO(afakhry): Show toast?
              if (!success) {
                LOG(ERROR) << "Failed to delete the file: " << path;
              }
            });
  const base::FilePath onedrive_path = delegate_->GetOneDriveMountPointPath();
  if (onedrive_path.IsParent(path)) {
    delegate_->DeleteRemoteFile(path,
                                base::BindOnce(std::move(callback), path));
    return;
  }
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&base::DeleteFile, path),
      base::BindOnce(std::move(callback), path));
}

void CaptureModeController::NavigateSearchResultsPanel(const GURL& url) {
  if (auto* panel = GetSearchResultsPanel()) {
    capture_mode_util::TriggerAccessibilityAlert(
        IDS_ASH_SUNFISH_RESULTS_LOADED_ACCESSIBLE_NAME);
    panel->Navigate(url);
  }
}

}  // namespace ash