#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;
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";
constexpr char kScreenshotFileName[] = "Screenshot";
constexpr char kVideoFileName[] = "Screen recording";
constexpr base::TimeDelta kResetCaptureRegionDuration = base::Minutes(8);
constexpr char kCustomCapturePathPrefName[] =
"ash.capture_mode.custom_save_path";
constexpr char kUsesDefaultCapturePathPrefName[] =
"ash.capture_mode.uses_default_capture_path";
constexpr char kShareToYouTubeURL[] = "https://youtube.com/upload";
constexpr char kCanShowDemoToolsNudge[] =
"ash.capture_mode.can_show_demo_tools_nudge";
constexpr char kCanShowSunfishRegionNudge[] =
"ash.capture_mode.can_show_sunfish_region_nudge";
constexpr char kCaptureModeTextCopiedToastId[] = "capture_mode_text_copied";
constexpr char kSunfishIncognitoNudgeId[] = "kSunfishIncognitoNudge";
constexpr int kNoMessage = -1;
enum ScreenshotNotificationButtonIndex {
kButtonEdit = 0,
kButtonDelete,
};
enum GameDashboardVideoNotificationButtonIndex {
kButtonShareToYoutube = 0,
kButtonDeleteGameVideo,
};
enum VideoNotificationButtonIndex {
kButtonDeleteVideo = 0,
};
std::string GetVideoExtension(RecordingType recording_type,
CaptureModeSource source) {
switch (recording_type) {
case RecordingType::kGif:
return source == CaptureModeSource::kRegion ? "gif" : "webm";
case RecordingType::kWebM:
return "webm";
}
}
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;
}
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;
}
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;
}
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));
}
void OnShareToYouTubeButtonPressed() {
NewWindowDelegate::GetInstance()->OpenUrl(
GURL(kShareToYouTubeURL),
NewWindowDelegate::OpenUrlFrom::kUserInteraction,
NewWindowDelegate::Disposition::kNewForegroundTab);
}
void AddNotificationToMessageCenter(
std::unique_ptr<message_center::Notification> notification) {
auto* message_center = message_center::MessageCenter::Get();
message_center->RemoveNotification(notification->id(),
false);
message_center->AddNotification(std::move(notification));
}
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));
}
void ShowFailureNotification() {
ShowNotification(kScreenCaptureStoppedNotificationId,
IDS_ASH_SCREEN_CAPTURE_FAILURE_TITLE,
IDS_ASH_SCREEN_CAPTURE_FAILURE_MESSAGE,
{}, nullptr);
}
void ShowGifProgressNotification() {
message_center::RichNotificationData optional_fields;
optional_fields.progress = -1;
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, nullptr, kCaptureModeIcon,
message_center::SystemNotificationWarningLevel::NORMAL));
}
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();
}
}
void ShowDisabledNotification(CaptureAllowance allowance) {
DCHECK(allowance != CaptureAllowance::kAllowed);
ShowNotification(
kScreenCaptureNotificationId,
GetDisabledNotificationMessageId(allowance, true),
GetDisabledNotificationMessageId(allowance, false),
{}, nullptr,
message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
allowance == CaptureAllowance::kDisallowedByHdcp
? kCaptureModeIcon
: vector_icons::kBusinessIcon);
}
void ShowVideoRecordingStoppedByHdcpNotification() {
ShowNotification(
kScreenCaptureStoppedNotificationId,
IDS_ASH_SCREEN_CAPTURE_HDCP_STOPPED_TITLE,
IDS_ASH_SCREEN_CAPTURE_HDCP_BLOCKED_MESSAGE,
{}, nullptr,
message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
kCaptureModeIcon);
}
void ShowTextCopiedToast() {
ToastManager::Get()->Show(ToastData(
kCaptureModeTextCopiedToastId, ToastCatalogName::kCaptureModeTextCopied,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_TEXT_COPIED_TOAST)));
}
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);
}
void EmitServiceRecordingStatus(recording::mojom::RecordingStatus status) {
using recording::mojom::RecordingStatus;
switch (status) {
case RecordingStatus::kSuccess:
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;
}
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;
}
return size_in_bytes.value() / 1024;
}
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();
}
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;
}
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();
}
if (cursor_manager->IsCursorLocked()) {
cursor_manager->UnlockCursor();
}
}
}
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;
}
}
bool ShouldPerformTextDetection(PerformCaptureType capture_type) {
return features::IsCaptureModeOnDeviceOcrEnabled() &&
capture_type == PerformCaptureType::kTextDetection;
}
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;
}
bool ShouldSendRegionSearch(PerformCaptureType capture_type) {
return CanShowSunfishUi() && CaptureTypeIsImageSearch(capture_type);
}
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;
}
}
gfx::Rect CalculateSearchResultPanelScreenBounds(
const gfx::Rect& work_area_in_screen,
const gfx::Rect& captured_region_in_screen) {
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 (bounds.Intersects(captured_region_in_screen)) {
bounds.set_x(work_area_in_screen.right() -
capture_mode::kSearchResultsPanelTotalWidth -
capture_mode::kPanelWorkAreaSpacing);
if (bounds.Intersects(captured_region_in_screen)) {
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);
}
}
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(
{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;
num_screenshots_taken_in_last_day_scheduler_.Start(
FROM_HERE, base::Days(1),
base::BindRepeating(
&CaptureModeController::RecordAndResetScreenshotsTakenInLastDay,
weak_ptr_factory_.GetWeakPtr()));
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()) {
Stop();
}
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
Shell::Get()->session_controller()->RemoveObserver(this);
MessageViewFactory::ClearCustomNotificationViewFactory(
kScreenShotNotificationType);
MessageViewFactory::ClearCustomNotificationViewFactory(
kScreenRecordingNotificationType);
if (features::IsVideoConferenceEnabled()) {
delegate_->UnregisterVideoConferenceManagerClient(vc_client_id_);
}
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
bool CaptureModeController::HasInstance() {
if (g_instance) {
return true;
}
return false;
}
CaptureModeController* CaptureModeController::Get() {
DCHECK(g_instance);
return g_instance;
}
void CaptureModeController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterFilePathPref(kCustomCapturePathPrefName,
base::FilePath());
registry->RegisterBooleanPref(kUsesDefaultCapturePathPrefName,
false);
registry->RegisterBooleanPref(kCanShowDemoToolsNudge,
true);
registry->RegisterBooleanPref(prefs::kScannerConsentDisclaimerAccepted,
false);
registry->RegisterBooleanPref(kCanShowSunfishRegionNudge,
true);
}
SearchResultsPanel* CaptureModeController::GetSearchResultsPanel() const {
return search_results_panel_widget_
? views::AsViewClass<SearchResultsPanel>(
search_results_panel_widget_->GetContentsView())
: nullptr;
}
void CaptureModeController::ShowSearchResultsPanel() {
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_) {
if (!is_active) {
return;
}
search_results_panel_widget_ = SearchResultsPanel::CreateWidget(
capture_mode_session_->current_root(), is_active);
RecordSearchResultsPanelEntryType(capture_mode_session_->active_behavior());
if (!should_end_session) {
capture_mode_session_->OnSearchResultsPanelCreated(
search_results_panel_widget_.get());
}
}
if (!search_results_panel_widget_->IsVisible()) {
GetSearchResultsPanel()->ShowLoadingAnimation();
search_results_panel_widget_->Show();
RecordSearchResultsPanelShown();
MaybeUpdateSearchResultsPanelBounds();
}
if (should_end_session) {
Stop();
}
}
void CaptureModeController::CloseSearchResultsPanel() {
search_results_panel_widget_.reset();
}
void CaptureModeController::MaybeUpdateSearchResultsPanelBounds() {
if (!search_results_panel_widget_ || !IsActive()) {
return;
}
CHECK(features::IsSunfishFeatureEnabled());
aura::Window* current_root = capture_mode_session_->current_root();
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) {
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) {
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 (!IsActive()) {
return true;
}
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) {
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) {
CaptureModeController::Get()->PerformCapture();
}
}));
}
void CaptureModeController::StartSunfishSession() {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kSunfishScreenEnteredViaShortcut);
CHECK(CanShowSunfishOrScannerUi());
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(nullptr);
delegate_->OnSessionStateChanged(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 {
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();
DCHECK(user_type);
switch (*user_type) {
case user_manager::UserType::kRegular:
case user_manager::UserType::kChild:
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);
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 (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(), false};
auto* pref_service = session_controller->GetActivePrefService();
const auto default_downloads_folder =
delegate_->GetUserDefaultDownloadsFolder();
const auto policy_path = delegate_->GetPolicyCapturePath();
if (policy_path.enforcement ==
CaptureModeDelegate::CapturePathEnforcement::kManaged) {
return {policy_path.path,
policy_path.path ==
default_downloads_folder};
}
if (pref_service &&
!pref_service->GetBoolean(kUsesDefaultCapturePathPrefName)) {
const auto custom_path =
pref_service->GetFilePath(kCustomCapturePathPrefName);
if (!custom_path.empty()) {
return {custom_path,
custom_path ==
default_downloads_folder};
}
}
if (policy_path.enforcement ==
CaptureModeDelegate::CapturePathEnforcement::kRecommended) {
return {policy_path.path,
policy_path.path ==
default_downloads_folder};
}
return {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 (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()) {
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.";
return;
}
const bool was_cursor_originally_blocked = MaybeLockCursor();
capture_mode_session_->OnPerformCaptureForSearchStarting(capture_type);
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()) {
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())) {
RecordEndRecordingReason(EndRecordingReason::kHdcpInterruption);
FinalizeRecording(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 {
if (is_recording_in_progress())
return video_recording_watcher_->GetOnCaptureSurfaceWidgetParentWindow();
if (IsActive())
return capture_mode_session_->GetOnCaptureSurfaceWidgetParentWindow();
return nullptr;
}
gfx::Rect CaptureModeController::GetCaptureSurfaceConfineBounds() const {
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(
vc_client_id_,
has_media_app,
has_media_app,
has_media_app,
is_camera_used,
is_recording_audio,
false));
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();
auto* message_center = message_center::MessageCenter::Get();
message_center->RemoveNotification(kScreenCaptureNotificationId,
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() {
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(
capture_mode_media_app_id_,
base::Time::Now(),
IsShowingCameraPreview(),
IsAudioRecordingInProgress(),
false,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE),
std::nullopt,
crosapi::mojom::VideoConferenceAppType::kAshCaptureMode));
}
std::move(callback).Run(std::move(apps));
}
void CaptureModeController::ReturnToApp(const base::UnguessableToken& token,
ReturnToAppCallback callback) {
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() {
}
void CaptureModeController::OnPinnedStateChanged(aura::Window* pinned_window) {
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) {
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 (capture_mode_session_->active_behavior()->behavior_type() ==
ToBehaviorType(entry_type)) {
return;
}
Stop();
}
if (!delegate_->IsCaptureAllowedByPolicy()) {
ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
return;
}
if (entry_type == CaptureModeEntryType::kSunfish && IsIncognitoWindowOpen()) {
ShowSunfishIncognitoNudge();
return;
}
pending_dlp_check_ = true;
delegate_->CheckCaptureModeInitRestrictionByDlp(
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);
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);
for (const auto& iter : protected_windows_) {
if (window_being_recorded->Contains(iter.first))
return true;
}
return false;
}
void CaptureModeController::EndSessionOrRecording(EndRecordingReason reason) {
if (IsActive()) {
Stop();
}
CloseSearchResultsPanel();
if (!is_recording_in_progress())
return;
if (reason == EndRecordingReason::kImminentSuspend ||
reason == EndRecordingReason::kShuttingDown) {
RecordEndRecordingReason(reason);
FinalizeRecording(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) {
return std::nullopt;
}
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()) {
return std::nullopt;
}
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()));
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());
constexpr int kStackingIndex = 1;
video_capturer_remote->CreateOverlay(kStackingIndex,
std::move(cursor_overlay));
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();
}
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:
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() {
RecordEndRecordingReason(EndRecordingReason::kRecordingServiceDisconnected);
FinalizeRecording(false, gfx::ImageSkia());
}
void CaptureModeController::FinalizeRecording(bool success,
const gfx::ImageSkia& thumbnail) {
if (!success) {
TerminateRecordingUiElements();
}
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);
camera_controller_->MaybeRevertAutoCameraSelection();
video_recording_watcher_->ShutDown();
for (auto& observer : observers_) {
observer.OnRecordingEnded();
}
if (current_video_file_path_.MatchesExtension(".gif")) {
ShowGifProgressNotification();
}
}
void CaptureModeController::CaptureImage(const CaptureParams& capture_params,
const base::FilePath& path,
const CaptureModeBehavior* behavior) {
CHECK(delegate_->IsCaptureAllowedByPolicy());
if (IsActive() && behavior == capture_mode_session_->active_behavior()) {
Stop();
}
CHECK(!capture_params.bounds.IsEmpty());
const bool was_cursor_originally_blocked = MaybeLockCursor();
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);
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);
}
};
MaybeUnlockCursor(was_cursor_originally_blocked);
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()) {
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));
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;
}
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, 0},
ActionButtonViewID::kCopyTextButton);
}
void CaptureModeController::OnCopyTextButtonClicked(
const std::u16string& text) {
CopyTextToClipboard(text);
ShowTextCopiedToast();
Stop();
}
void CaptureModeController::MaybeShowScannerDisclaimerOnSunfishStartup(
bool startup_success) {
if (!startup_success ||
!ScannerController::CanShowUiForShell()) {
if (!CanShowSunfishUi() && IsActive()) {
Stop();
}
return;
}
CHECK(capture_mode_session_);
base::RepeatingClosure decline_callback =
CanShowSunfishUi() ? base::DoNothing()
: base::BindRepeating(&CaptureModeController::Stop,
weak_ptr_factory_.GetWeakPtr());
capture_mode_session_->MaybeShowScannerDisclaimer(
ScannerEntryPoint::kSunfishSession,
base::BindRepeating([]() {
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) {
if (image_result != CaptureModeImageSearchResult::kSuccess) {
RecordCaptureModeImageSearchResult(image_result);
CloseSearchResultsPanel();
}
if (text_result != CaptureModeTextDetectionResult::kSuccessNoTextPresent &&
text_result != CaptureModeTextDetectionResult::kSuccessTextPresent) {
RecordCaptureModeTextDetectionResult(text_result);
}
if (!image_search_token) {
return;
}
CHECK(IsActive());
Stop();
}
void CaptureModeController::OnSearchResultClicked() {
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(gfx::Image(), behavior, 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, 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);
}
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);
if (auto* client = HoldingSpaceController::Get()->client()) {
client->AddItemOfType(is_gif
? HoldingSpaceItem::Type::kScreenRecordingGif
: HoldingSpaceItem::Type::kScreenRecording,
saved_video_file_path);
}
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));
}
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_) {
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()) {
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();
}
}
}
message_center::MessageCenter::Get()->RemoveNotification(
kScreenCaptureNotificationId, 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() {
base::ScopedClosureRunner scoped_closure(
std::move(on_countdown_finished_callback_for_test_));
if (!IsActive())
return;
const std::optional<CaptureParams> capture_params = GetCaptureParams();
if (!capture_params) {
Stop();
return;
}
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()) {
return;
}
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()) {
return;
}
base::AutoReset<bool> initializing_resetter(&is_initializing_recording_,
true);
capture_mode_session_->set_a11y_alert_on_session_exit(false);
std::unique_ptr<ui::Layer> session_layer =
capture_mode_session_->ReleaseLayer();
session_layer->set_delegate(nullptr);
capture_mode_session_->set_is_stopping_to_start_video_recording(true);
CaptureModeBehavior* active_behavior =
capture_mode_session_->active_behavior();
Stop();
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);
}
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);
RecordRecordingStartsWithDemoTools(enable_demo_tools_, active_behavior);
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()) {
return;
}
auto* active_behavior = capture_mode_session_->active_behavior();
capture_mode_session_->OnWaitingForDlpConfirmationEnded(
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 {
if (ShouldBlockRecordingForContentProtection(capture_params->window)) {
ShowDisabledNotification(CaptureAllowance::kDisallowedByHdcp);
Stop();
return;
}
CaptureVideo(*capture_params);
}
}
void CaptureModeController::OnDlpRestrictionCheckedAtCountDownFinished(
bool proceed) {
pending_dlp_check_ = false;
if (!IsActive()) {
return;
}
capture_mode_session_->OnWaitingForDlpConfirmationEnded(false);
if (!proceed) {
Stop();
return;
}
const std::optional<CaptureParams> capture_params = GetCaptureParams();
if (!capture_params) {
Stop();
return;
}
if (!delegate_->IsCaptureAllowedByPolicy()) {
ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
Stop();
return;
}
if (ShouldBlockRecordingForContentProtection(capture_params->window)) {
Stop();
ShowDisabledNotification(CaptureAllowance::kDisallowedByHdcp);
return;
}
capture_mode_session_->set_can_exit_on_escape(false);
CaptureModeBehavior* active_behavior =
capture_mode_session_->active_behavior();
if (!active_behavior->SupportsAudioRecordingMode(
GetEffectiveAudioRecordingMode())) {
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 (!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_);
if (!delegate_->IsCaptureAllowedByPolicy()) {
ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
return;
}
CloseSearchResultsPanel();
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(), false);
}
delegate_->OnSessionStateChanged(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) {
message_center::MessageCenter::Get()->RemoveNotification(
kScreenCaptureNotificationId, false);
DeleteFileAsync(video_file_path);
OnVideoFileFinalized(true, video_thumbnail);
} else {
if (!success) {
OnVideoFileSaved(video_thumbnail, behavior, success, video_file_path);
OnVideoFileFinalized(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(),
false,
video_thumbnail)),
video_file_path, gfx::Image(video_thumbnail), 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(
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;
}
if (!delegate_->IsCaptureAllowedByPolicy()) {
ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
return;
}
std::move(instant_screenshot_callback).Run();
RecordCaptureModeEntryType(entry_type);
RecordCaptureModeConfiguration(
CaptureModeType::kImage, source,
recording_type_, GetEffectiveAudioRecordingMode(),
GetBehavior(behavior_type));
}
void CaptureModeController::PerformScreenshotsOfAllDisplays(
BehaviorType behavior_type) {
const std::vector<RootWindowController*> controllers =
RootWindowController::root_window_controllers();
int display_index = 1;
for (RootWindowController* controller : controllers) {
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 {
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) {
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);
}
}
}