123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719 |
- // Copyright 2020 The Chromium Authors. All rights reserved.
- // 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 <utility>
- #include "ash/capture_mode/capture_mode_ash_notification_view.h"
- #include "ash/capture_mode/capture_mode_camera_controller.h"
- #include "ash/capture_mode/capture_mode_metrics.h"
- #include "ash/capture_mode/capture_mode_notification_view.h"
- #include "ash/capture_mode/capture_mode_session.h"
- #include "ash/capture_mode/capture_mode_util.h"
- #include "ash/constants/ash_features.h"
- #include "ash/constants/notifier_catalogs.h"
- #include "ash/projector/projector_controller_impl.h"
- #include "ash/public/cpp/capture_mode/recording_overlay_view.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/notification_utils.h"
- #include "ash/resources/vector_icons/vector_icons.h"
- #include "ash/root_window_controller.h"
- #include "ash/session/session_controller_impl.h"
- #include "ash/shell.h"
- #include "ash/strings/grit/ash_strings.h"
- #include "ash/system/message_center/message_view_factory.h"
- #include "ash/wm/tablet_mode/tablet_mode_controller.h"
- #include "base/auto_reset.h"
- #include "base/bind.h"
- #include "base/callback_helpers.h"
- #include "base/check.h"
- #include "base/check_op.h"
- #include "base/files/file_path.h"
- #include "base/files/file_util.h"
- #include "base/location.h"
- #include "base/memory/ref_counted_memory.h"
- #include "base/memory/scoped_refptr.h"
- #include "base/notreached.h"
- #include "base/strings/stringprintf.h"
- #include "base/task/current_thread.h"
- #include "base/task/task_traits.h"
- #include "base/task/thread_pool.h"
- #include "base/time/time.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 "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/types/display_constants.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"
- 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 format strings of the file names of captured images.
- // TODO(afakhry): Discuss with UX localizing "Screenshot" and "Screen
- // recording".
- constexpr char kScreenshotFileNameFmtStr[] = "Screenshot %s %s";
- constexpr char kVideoFileNameFmtStr[] = "Screen recording %s %s";
- constexpr char kDateFmtStr[] = "%d-%02d-%02d";
- constexpr char k24HourTimeFmtStr[] = "%02d.%02d.%02d";
- constexpr char kAmPmTimeFmtStr[] = "%d.%02d.%02d";
- // 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";
- // The name of a boolean pref that determines whether we can show the selfie
- // camera 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 kCanShowCameraNudge[] = "ash.capture_mode.can_show_camera_nudge";
- // The screenshot notification button index.
- enum ScreenshotNotificationButtonIndex {
- BUTTON_EDIT = 0,
- BUTTON_DELETE,
- };
- // The video notification button index.
- enum VideoNotificationButtonIndex {
- BUTTON_DELETE_VIDEO = 0,
- };
- // Returns the date extracted from |timestamp| as a string to be part of
- // captured file names. Note that naturally formatted dates includes slashes
- // (e.g. 2020/09/02), which will cause problems when used in file names since
- // slash is a path separator.
- std::string GetDateStr(const base::Time::Exploded& timestamp) {
- return base::StringPrintf(kDateFmtStr, timestamp.year, timestamp.month,
- timestamp.day_of_month);
- }
- // Returns the time extracted from |timestamp| as a string to be part of
- // captured file names. Also note that naturally formatted times include colons
- // (e.g. 11:20 AM), which is restricted in file names in most file systems.
- // https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations.
- std::string GetTimeStr(const base::Time::Exploded& timestamp,
- bool use_24_hour) {
- if (use_24_hour) {
- return base::StringPrintf(k24HourTimeFmtStr, timestamp.hour,
- timestamp.minute, timestamp.second);
- }
- int hour = timestamp.hour % 12;
- if (hour <= 0)
- hour += 12;
- std::string time = base::StringPrintf(kAmPmTimeFmtStr, hour, timestamp.minute,
- timestamp.second);
- return time.append(timestamp.hour >= 12 ? " PM" : " AM");
- }
- // 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);
- const int size = static_cast<int>(data->size());
- DCHECK(size);
- if (size != base::WriteFile(
- path, reinterpret_cast<const char*>(data->front()), size)) {
- 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));
- }
- void DeleteFileAsync(scoped_refptr<base::SequencedTaskRunner> task_runner,
- const base::FilePath& path,
- OnFileDeletedCallback callback) {
- task_runner->PostTaskAndReplyWithResult(
- FROM_HERE, base::BindOnce(&base::DeleteFile, path),
- callback ? base::BindOnce(std::move(callback), path)
- : base::BindOnce(
- [](const base::FilePath& path, bool success) {
- // TODO(afakhry): Show toast?
- if (!success)
- LOG(ERROR) << "Failed to delete the file: " << path;
- },
- path));
- }
- // 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;
- std::unique_ptr<message_center::Notification> notification =
- CreateSystemNotification(
- type, notification_id, l10n_util::GetStringUTF16(title_id),
- l10n_util::GetStringUTF16(message_id),
- 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);
- }
- // Remove the previous notification before showing the new one if there is
- // any.
- auto* message_center = message_center::MessageCenter::Get();
- message_center->RemoveNotification(notification_id,
- /*by_user=*/false);
- message_center->AddNotification(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);
- }
- // 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();
- return IDS_ASH_SCREEN_CAPTURE_POLICY_DISABLED_MESSAGE;
- }
- }
- // 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);
- }
- // 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());
- }
- // 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;
- }
- }
- PrefService* GetActiveUserPrefService() {
- DCHECK(Shell::Get()->session_controller()->IsActiveUserSessionStarted());
- auto* pref_service =
- Shell::Get()->session_controller()->GetActivePrefService();
- DCHECK(pref_service);
- return pref_service;
- }
- base::FilePath GetTempDir() {
- base::FilePath temp_dir;
- if (!base::GetTempDir(&temp_dir))
- LOG(ERROR) << "Failed to find the temporary directory.";
- return temp_dir;
- }
- } // 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
- // or the successive webm video chunks received from the recording
- // service.
- {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
- base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})),
- num_consecutive_screenshots_scheduler_(
- FROM_HERE,
- kConsecutiveScreenshotThreshold,
- this,
- &CaptureModeController::RecordAndResetConsecutiveScreenshots) {
- 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));
- if (features::IsNotificationsRefreshEnabled()) {
- MessageViewFactory::SetCustomNotificationViewFactory(
- kScreenShotNotificationType,
- base::BindRepeating(&CaptureModeAshNotificationView::CreateForImage));
- MessageViewFactory::SetCustomNotificationViewFactory(
- kScreenRecordingNotificationType,
- base::BindRepeating(&CaptureModeAshNotificationView::CreateForVideo));
- } else {
- MessageViewFactory::SetCustomNotificationViewFactory(
- kScreenShotNotificationType,
- base::BindRepeating(&CaptureModeNotificationView::CreateForImage));
- MessageViewFactory::SetCustomNotificationViewFactory(
- kScreenRecordingNotificationType,
- base::BindRepeating(&CaptureModeNotificationView::CreateForVideo));
- }
- Shell::Get()->session_controller()->AddObserver(this);
- chromeos::PowerManagerClient::Get()->AddObserver(this);
- }
- 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);
- DCHECK_EQ(g_instance, this);
- g_instance = nullptr;
- }
- // 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);
- registry->RegisterBooleanPref(kCanShowCameraNudge, /*default_value=*/true);
- }
- bool CaptureModeController::IsActive() const {
- return capture_mode_session_ && !capture_mode_session_->is_shutting_down();
- }
- void CaptureModeController::SetSource(CaptureModeSource source) {
- if (source == source_)
- return;
- source_ = source;
- if (capture_mode_session_)
- capture_mode_session_->OnCaptureSourceChanged(source_);
- }
- void CaptureModeController::SetType(CaptureModeType type) {
- if (is_recording_in_progress() && 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 (capture_mode_session_)
- capture_mode_session_->OnCaptureTypeChanged(type_);
- }
- void CaptureModeController::EnableAudioRecording(bool enable_audio_recording) {
- if (enable_audio_recording_ == enable_audio_recording)
- return;
- enable_audio_recording_ = enable_audio_recording;
- }
- void CaptureModeController::Start(CaptureModeEntryType entry_type) {
- if (capture_mode_session_ || pending_dlp_check_)
- return;
- if (!delegate_->IsCaptureAllowedByPolicy()) {
- ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
- return;
- }
- pending_dlp_check_ = true;
- delegate_->CheckCaptureModeInitRestrictionByDlp(base::BindOnce(
- &CaptureModeController::OnDlpRestrictionCheckedAtSessionInit,
- weak_ptr_factory_.GetWeakPtr(), entry_type));
- }
- void CaptureModeController::Stop() {
- DCHECK(IsActive());
- capture_mode_session_->ReportSessionHistograms();
- capture_mode_session_->Shutdown();
- capture_mode_session_.reset();
- delegate_->OnSessionStateChanged(/*started=*/false);
- }
- 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::CanShowUserNudge() const {
- auto* session_controller = Shell::Get()->session_controller();
- DCHECK(session_controller->IsActiveUserSessionStarted());
- absl::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::USER_TYPE_REGULAR:
- case user_manager::USER_TYPE_CHILD:
- // We only allow regular and child accounts to see the nudge.
- break;
- case user_manager::USER_TYPE_GUEST:
- case user_manager::USER_TYPE_PUBLIC_ACCOUNT:
- case user_manager::USER_TYPE_KIOSK_APP:
- case user_manager::USER_TYPE_ARC_KIOSK_APP:
- case user_manager::USER_TYPE_WEB_KIOSK_APP:
- case user_manager::USER_TYPE_ACTIVE_DIRECTORY:
- case user_manager::NUM_USER_TYPES:
- return false;
- }
- auto* pref_service = session_controller->GetActivePrefService();
- DCHECK(pref_service);
- return pref_service->GetBoolean(kCanShowCameraNudge);
- }
- void CaptureModeController::DisableUserNudgeForever() {
- GetActiveUserPrefService()->SetBoolean(kCanShowCameraNudge, false);
- }
- void CaptureModeController::SetUsesDefaultCaptureFolder(bool value) {
- GetActiveUserPrefService()->SetBoolean(kUsesDefaultCapturePathPrefName,
- value);
- if (IsActive())
- capture_mode_session_->OnDefaultCaptureFolderSelectionChanged();
- }
- void CaptureModeController::SetCustomCaptureFolder(const base::FilePath& path) {
- auto* pref_service = 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 {
- const auto custom_path =
- GetActiveUserPrefService()->GetFilePath(kCustomCapturePathPrefName);
- 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();
- 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};
- }
- }
- return {default_downloads_folder,
- /*is_default_downloads_folder=*/true};
- }
- void CaptureModeController::CaptureScreenshotsOfAllDisplays() {
- if (pending_dlp_check_)
- return;
- if (!delegate_->IsCaptureAllowedByPolicy()) {
- ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
- return;
- }
- pending_dlp_check_ = true;
- delegate_->CheckCaptureModeInitRestrictionByDlp(base::BindOnce(
- &CaptureModeController::
- OnDlpRestrictionCheckedAtCaptureScreenshotsOfAllDisplays,
- weak_ptr_factory_.GetWeakPtr()));
- }
- void CaptureModeController::PerformCapture() {
- DCHECK(IsActive());
- if (pending_dlp_check_)
- return;
- const absl::optional<CaptureParams> capture_params = GetCaptureParams();
- if (!capture_params)
- 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()));
- }
- void CaptureModeController::EndVideoRecording(EndRecordingReason reason) {
- 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();
- }
- }
- void CaptureModeController::ToggleRecordingOverlayEnabled() {
- DCHECK(is_recording_in_progress());
- DCHECK(video_recording_watcher_->is_in_projector_mode());
- video_recording_watcher_->ToggleRecordingOverlayEnabled();
- }
- std::unique_ptr<RecordingOverlayView>
- CaptureModeController::CreateRecordingOverlayView() {
- return delegate_->CreateRecordingOverlayView();
- }
- 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();
- }
- aura::Window* CaptureModeController::GetCameraPreviewParentWindow() 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_->GetCameraPreviewParentWindow();
- if (IsActive())
- return capture_mode_session_->GetCameraPreviewParentWindow();
- return nullptr;
- }
- gfx::Rect CaptureModeController::GetCameraPreviewConfineBounds() 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_->GetCameraPreviewConfineBounds();
- if (IsActive())
- return capture_mode_session_->GetCameraPreviewConfineBounds();
- return gfx::Rect();
- }
- 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::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::StartVideoRecordingImmediatelyForTesting() {
- DCHECK(IsActive());
- DCHECK_EQ(type_, CaptureModeType::kVideo);
- OnVideoRecordCountDownFinished();
- }
- 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);
- 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 BrowserFrame, 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();
- }
- 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);
- }
- absl::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 absl::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 absl::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) {
- 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,
- base::Unretained(this)));
- // 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 audio stream factory only if audio recording is enabled. This
- // is ok since the |audio_stream_factory| parameter in the recording service
- // APIs is optional, and can be not bound.
- mojo::PendingRemote<media::mojom::AudioStreamFactory> audio_stream_factory;
- if (enable_audio_recording_) {
- delegate_->BindAudioStreamFactory(
- audio_stream_factory.InitWithNewPipeAndPassReceiver());
- }
- // 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(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(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(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 bool was_in_projector_mode =
- video_recording_watcher_->is_in_projector_mode();
- video_recording_watcher_.reset();
- delegate_->StopObservingRestrictedContent(
- base::BindOnce(&CaptureModeController::OnDlpRestrictionCheckedAtVideoEnd,
- weak_ptr_factory_.GetWeakPtr(), thumbnail, success,
- was_in_projector_mode));
- }
- 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);
- video_recording_watcher_->ShutDown();
- }
- void CaptureModeController::CaptureImage(const CaptureParams& capture_params,
- const base::FilePath& path) {
- // 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_|.
- DCHECK(delegate_->IsCaptureAllowedByPolicy());
- // Stop the capture session now, so as not to take a screenshot of the capture
- // bar.
- if (IsActive())
- Stop();
- DCHECK(!capture_params.bounds.IsEmpty());
- 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();
- }
- ui::GrabWindowSnapshotAsyncPNG(
- capture_params.window, capture_params.bounds,
- base::BindOnce(&CaptureModeController::OnImageCaptured,
- weak_ptr_factory_.GetWeakPtr(), path,
- was_cursor_originally_blocked));
- ++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,
- scoped_refptr<base::RefCountedMemory> png_bytes) {
- if (!was_cursor_originally_blocked) {
- auto* shell = Shell::Get();
- auto* cursor_manager = shell->cursor_manager();
- if (!shell->tablet_mode_controller()->InTabletMode())
- cursor_manager->ShowCursor();
- cursor_manager->UnlockCursor();
- }
- 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, path,
- GetFallbackFilePathFromFile(path)),
- base::BindOnce(&CaptureModeController::OnImageFileSaved,
- weak_ptr_factory_.GetWeakPtr(), png_bytes));
- }
- void CaptureModeController::OnImageFileSaved(
- scoped_refptr<base::RefCountedMemory> png_bytes,
- const base::FilePath& file_saved_path) {
- if (file_saved_path.empty()) {
- ShowFailureNotification();
- return;
- }
- if (on_file_saved_callback_for_test_)
- std::move(on_file_saved_callback_for_test_).Run(file_saved_path);
- DCHECK(png_bytes && png_bytes->size());
- const auto image = gfx::Image::CreateFrom1xPNGBytes(png_bytes);
- CopyImageToClipboard(image);
- ShowPreviewNotification(file_saved_path, image, CaptureModeType::kImage);
- if (Shell::Get()->session_controller()->IsActiveUserSessionStarted())
- RecordSaveToLocation(GetSaveToOption(file_saved_path));
- HoldingSpaceClient* client = HoldingSpaceController::Get()->client();
- if (client) // May be `nullptr` in tests.
- client->AddScreenshot(file_saved_path);
- }
- void CaptureModeController::OnVideoFileSaved(
- const base::FilePath& saved_video_file_path,
- const gfx::ImageSkia& video_thumbnail,
- bool success,
- bool in_projector_mode) {
- DCHECK(base::CurrentUIThread::IsSet());
- if (!success) {
- ShowFailureNotification();
- } else {
- if (!in_projector_mode) {
- ShowPreviewNotification(saved_video_file_path,
- gfx::Image(video_thumbnail),
- CaptureModeType::kVideo);
- HoldingSpaceClient* client = HoldingSpaceController::Get()->client();
- if (client) // May be `nullptr` in tests.
- client->AddScreenRecording(saved_video_file_path);
- }
- DCHECK(!recording_start_time_.is_null());
- RecordCaptureModeRecordTime(
- (base::TimeTicks::Now() - recording_start_time_).InSeconds(),
- in_projector_mode);
- }
- if (Shell::Get()->session_controller()->IsActiveUserSessionStarted())
- RecordSaveToLocation(GetSaveToOption(saved_video_file_path));
- 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 bool for_video = type == CaptureModeType::kVideo;
- const int title_id = for_video ? IDS_ASH_SCREEN_CAPTURE_RECORDING_TITLE
- : IDS_ASH_SCREEN_CAPTURE_SCREENSHOT_TITLE;
- const int message_id = for_video && low_disk_space_threshold_reached_
- ? IDS_ASH_SCREEN_CAPTURE_LOW_STORAGE_SPACE_MESSAGE
- : IDS_ASH_SCREEN_CAPTURE_MESSAGE;
- message_center::RichNotificationData optional_fields;
- message_center::ButtonInfo edit_button(
- l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_EDIT));
- if (!for_video && !Shell::Get()->session_controller()->IsUserSessionBlocked())
- optional_fields.buttons.push_back(edit_button);
- message_center::ButtonInfo delete_button(
- l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_DELETE));
- optional_fields.buttons.push_back(delete_button);
- optional_fields.image = preview_image;
- 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)),
- message_center::SystemNotificationWarningLevel::NORMAL, kCaptureModeIcon,
- for_video);
- }
- void CaptureModeController::HandleNotificationClicked(
- const base::FilePath& screen_capture_path,
- const CaptureModeType type,
- absl::optional<int> button_index) {
- if (!button_index.has_value()) {
- // Show the item in the folder.
- delegate_->ShowScreenCaptureItemInFolder(screen_capture_path);
- RecordScreenshotNotificationQuickAction(CaptureQuickAction::kFiles);
- } else {
- const int button_index_value = button_index.value();
- if (type == CaptureModeType::kVideo) {
- DCHECK_EQ(button_index_value,
- VideoNotificationButtonIndex::BUTTON_DELETE_VIDEO);
- DeleteFileAsync(blocking_task_runner_, screen_capture_path,
- std::move(on_file_deleted_callback_for_test_));
- } else {
- DCHECK_EQ(type, CaptureModeType::kImage);
- switch (button_index_value) {
- case ScreenshotNotificationButtonIndex::BUTTON_EDIT:
- delegate_->OpenScreenshotInImageEditor(screen_capture_path);
- RecordScreenshotNotificationQuickAction(
- CaptureQuickAction::kBacklight);
- break;
- case ScreenshotNotificationButtonIndex::BUTTON_DELETE:
- DeleteFileAsync(blocking_task_runner_, screen_capture_path,
- std::move(on_file_deleted_callback_for_test_));
- RecordScreenshotNotificationQuickAction(CaptureQuickAction::kDelete);
- break;
- default:
- NOTREACHED();
- break;
- }
- }
- }
- // 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(kScreenshotFileNameFmtStr, base::Time::Now())
- .AddExtension("png");
- }
- base::FilePath CaptureModeController::BuildVideoPath() const {
- return BuildPathNoExtension(kVideoFileNameFmtStr, base::Time::Now())
- .AddExtension("webm");
- }
- base::FilePath CaptureModeController::BuildImagePathForDisplay(
- int display_index) const {
- auto path_str =
- BuildPathNoExtension(kScreenshotFileNameFmtStr, 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(
- const char* const format_string,
- base::Time timestamp) const {
- base::Time::Exploded exploded_time;
- timestamp.LocalExplode(&exploded_time);
- return GetCurrentCaptureFolder().path.AppendASCII(base::StringPrintf(
- format_string, GetDateStr(exploded_time).c_str(),
- GetTimeStr(exploded_time, delegate_->Uses24HourFormat()).c_str()));
- }
- 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 absl::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::OnProjectorContainerFolderCreated(
- const CaptureParams& capture_params,
- const base::FilePath& file_path_no_extension) {
- 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 (file_path_no_extension.empty()) {
- Stop();
- return;
- }
- BeginVideoRecording(capture_params, /*for_projector=*/true,
- file_path_no_extension.AddExtension("webm"));
- }
- void CaptureModeController::BeginVideoRecording(
- const CaptureParams& capture_params,
- bool for_projector,
- const base::FilePath& video_file_path) {
- DCHECK_EQ(capture_mode_session_->is_in_projector_mode(), for_projector);
- DCHECK(!video_file_path.empty());
- DCHECK(video_file_path.MatchesExtension(".webm"));
- 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);
- // Stop the capture session now, so the bar doesn't show up in the captured
- // video.
- Stop();
- mojo::PendingRemote<viz::mojom::FrameSinkVideoCaptureOverlay>
- cursor_capture_overlay;
- auto cursor_overlay_receiver =
- cursor_capture_overlay.InitWithNewPipeAndPassReceiver();
- video_recording_watcher_ = std::make_unique<VideoRecordingWatcher>(
- this, capture_params.window, std::move(cursor_capture_overlay),
- for_projector);
- // 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));
- capture_mode_util::SetStopRecordingButtonVisibility(
- capture_params.window->GetRootWindow(), true);
- delegate_->StartObservingRestrictedContent(
- capture_params.window, capture_params.bounds,
- base::BindOnce(&CaptureModeController::InterruptVideoRecording,
- weak_ptr_factory_.GetWeakPtr()));
- }
- void CaptureModeController::InterruptVideoRecording() {
- EndVideoRecording(EndRecordingReason::kDlpInterruption);
- }
- void CaptureModeController::OnDlpRestrictionCheckedAtPerformingCapture(
- 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
- // `type_` is `CaptureModeType::kImage`, since the session is about to
- // shutdown anyways at these use cases, so it's better to avoid any wasted
- // effort. In the case of video recording, we need to reshow the UIs so that
- // we can start the 3-second count down animation.
- capture_mode_session_->OnWaitingForDlpConfirmationEnded(
- /*reshow_uis=*/proceed && type_ != CaptureModeType::kImage);
- if (!proceed) {
- Stop();
- return;
- }
- const absl::optional<CaptureParams> capture_params = GetCaptureParams();
- DCHECK(capture_params);
- if (!delegate_->IsCaptureAllowedByPolicy()) {
- ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
- Stop();
- return;
- }
- if (type_ == CaptureModeType::kImage) {
- CaptureImage(*capture_params, BuildImagePath());
- } 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 absl::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;
- }
- // In Projector mode, the creation of the DriveFS 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);
- if (capture_mode_session_->is_in_projector_mode()) {
- ProjectorControllerImpl::Get()->CreateScreencastContainerFolder(
- base::BindOnce(
- &CaptureModeController::OnProjectorContainerFolderCreated,
- 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, current_path,
- GetFallbackFilePathFromFile(current_path)),
- base::BindOnce(&CaptureModeController::BeginVideoRecording,
- weak_ptr_factory_.GetWeakPtr(), *capture_params,
- /*for_projector=*/false));
- return;
- }
- BeginVideoRecording(*capture_params, /*for_projector=*/false, current_path);
- }
- void CaptureModeController::OnDlpRestrictionCheckedAtSessionInit(
- CaptureModeEntryType entry_type,
- bool proceed) {
- pending_dlp_check_ = false;
- if (!proceed)
- return;
- DCHECK(!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;
- }
- // Starting capture mode from the Projector app will put it in a special mode
- // where only video recording is allowed, with audio recording enabled.
- bool for_projector = false;
- // Before we start the session, if video recording is in progress, we need to
- // set the current type to image, 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 (is_recording_in_progress()) {
- SetType(CaptureModeType::kImage);
- } else if (entry_type == CaptureModeEntryType::kProjector) {
- DCHECK(features::IsProjectorEnabled());
- for_projector = true;
- // TODO(afakhry): Discuss with PM whether we want this to affect the audio
- // settings of future generic capture mode sessions.
- enable_audio_recording_ = true;
- SetType(CaptureModeType::kVideo);
- }
- RecordCaptureModeEntryType(entry_type);
- // 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.
- if (!user_capture_region_.IsEmpty() &&
- base::TimeTicks::Now() - last_capture_region_update_time_ >
- kResetCaptureRegionDuration) {
- SetUserCaptureRegion(gfx::Rect(), /*by_user=*/false);
- }
- delegate_->OnSessionStateChanged(/*started=*/true);
- capture_mode_session_ =
- std::make_unique<CaptureModeSession>(this, for_projector);
- capture_mode_session_->Initialize();
- camera_controller_->OnCaptureSessionStarted();
- }
- void CaptureModeController::OnDlpRestrictionCheckedAtVideoEnd(
- const gfx::ImageSkia& video_thumbnail,
- bool success,
- bool in_projector_mode,
- 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) {
- DeleteFileAsync(blocking_task_runner_, video_file_path,
- std::move(on_file_deleted_callback_for_test_));
- } else {
- OnVideoFileSaved(video_file_path, video_thumbnail, success,
- in_projector_mode);
- }
- if (features::IsProjectorEnabled()) {
- ProjectorControllerImpl::Get()->OnDlpRestrictionCheckedAtVideoEnd(
- in_projector_mode, should_delete_file, video_thumbnail);
- }
- low_disk_space_threshold_reached_ = false;
- recording_start_time_ = base::TimeTicks();
- }
- void CaptureModeController::
- OnDlpRestrictionCheckedAtCaptureScreenshotsOfAllDisplays(bool proceed) {
- pending_dlp_check_ = false;
- if (!proceed)
- return;
- // Due to fact that the DLP warning dialog may take a while, check policy
- // again even though we checked in CaptureScreenshotsOfAllDisplays().
- if (!delegate_->IsCaptureAllowedByPolicy()) {
- ShowDisabledNotification(CaptureAllowance::kDisallowedByPolicy);
- return;
- }
- // 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));
- ++display_index;
- }
- // Since this doesn't create a capture mode session, log metrics here.
- RecordCaptureModeEntryType(CaptureModeEntryType::kCaptureAllDisplays);
- RecordCaptureModeConfiguration(
- CaptureModeType::kImage, CaptureModeSource::kFullscreen,
- /*audio_on=*/false, /*is_in_projector_mode=*/false);
- }
- 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;
- }
- return CaptureModeSaveToLocation::kCustomizedFolder;
- }
- } // namespace ash
|