blob: ad9205566d1c197750c73662b9e7d23d39dadc4c [file] [log] [blame]
// 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_metrics.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/video_recording_watcher.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/capture_mode_delegate.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/status_area_widget.h"
#include "base/bind.h"
#include "base/bind_post_task.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/metrics/histogram_functions.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/threading/thread_task_runner_handle.h"
#include "base/time/time.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_data.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/l10n/l10n_util.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::TimeDelta::FromSeconds(5);
constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification";
constexpr char kScreenCaptureStoppedNotificationId[] =
"capture_mode_stopped_notification";
constexpr char kScreenCaptureNotifierId[] = "ash.capture_mode_controller";
// 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::TimeDelta::FromMinutes(8);
// 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");
}
// Writes the given |data| in a file with |path|. Returns true if saving
// succeeded, or false otherwise.
bool SaveFile(scoped_refptr<base::RefCountedMemory> data,
const base::FilePath& path) {
DCHECK(data);
const int size = static_cast<int>(data->size());
DCHECK(size);
DCHECK(!base::CurrentUIThread::IsSet());
DCHECK(!path.empty());
if (!base::PathExists(path.DirName())) {
LOG(ERROR) << "File path doesn't exist: " << path.DirName();
return false;
}
if (size != base::WriteFile(
path, reinterpret_cast<const char*>(data->front()), size)) {
LOG(ERROR) << "Failed to save file: " << path;
return false;
}
return true;
}
void DeleteFileAsync(scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::FilePath& path) {
task_runner->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&base::DeleteFile, 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.
void ShowNotification(
const base::string16& title,
const base::string16& message,
const message_center::RichNotificationData& optional_fields,
scoped_refptr<message_center::NotificationDelegate> delegate) {
const auto type = optional_fields.image.IsEmpty()
? message_center::NOTIFICATION_TYPE_SIMPLE
: message_center::NOTIFICATION_TYPE_IMAGE;
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotification(
type, kScreenCaptureNotificationId, title, message,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE),
GURL(),
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kScreenCaptureNotifierId),
optional_fields, delegate, kCaptureModeIcon,
message_center::SystemNotificationWarningLevel::NORMAL);
// Remove the previous notification before showing the new one if there is
// any.
auto* message_center = message_center::MessageCenter::Get();
message_center->RemoveNotification(kScreenCaptureNotificationId,
/*by_user=*/false);
message_center->AddNotification(std::move(notification));
}
// Shows a notification informing the user that Capture Mode operations are
// currently disabled.
void ShowDisabledNotification() {
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotification(
message_center::NOTIFICATION_TYPE_SIMPLE,
kScreenCaptureNotificationId,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISABLED_TITLE),
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISABLED_MESSAGE),
/*display_source=*/base::string16(), GURL(),
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kScreenCaptureNotifierId),
/*optional_fields=*/{}, /*delegate=*/nullptr,
vector_icons::kBusinessIcon,
message_center::SystemNotificationWarningLevel::CRITICAL_WARNING);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
// Shows a notification informing the user that a Capture Mode operation has
// failed.
void ShowFailureNotification() {
ShowNotification(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_FAILURE_TITLE),
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_FAILURE_MESSAGE),
/*optional_fields=*/{}, /*delegate=*/nullptr);
}
// Shows a notification informing the user that video recording was stopped.
void ShowVideoRecordingStoppedNotification() {
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotification(
message_center::NOTIFICATION_TYPE_SIMPLE,
kScreenCaptureStoppedNotificationId,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_STOPPED_TITLE),
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_STOPPED_MESSAGE),
/*display_source=*/base::string16(), GURL(),
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kScreenCaptureNotifierId),
/*optional_fields=*/{}, /*delegate=*/nullptr,
vector_icons::kBusinessIcon,
message_center::SystemNotificationWarningLevel::CRITICAL_WARNING);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
// Copies the bitmap representation of the given |image| to the clipboard.
void CopyImageToClipboard(const gfx::Image& image) {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
DCHECK(clipboard);
auto clipboard_data = std::make_unique<ui::ClipboardData>();
clipboard_data->SetBitmapData(image.AsBitmap());
clipboard->WriteClipboardData(std::move(clipboard_data));
}
} // namespace
CaptureModeController::CaptureModeController(
std::unique_ptr<CaptureModeDelegate> delegate)
: delegate_(std::move(delegate)),
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})),
recording_service_client_receiver_(this),
num_consecutive_screenshots_scheduler_(
FROM_HERE,
kConsecutiveScreenshotThreshold,
this,
&CaptureModeController::RecordAndResetConsecutiveScreenshots) {
DCHECK_EQ(g_instance, nullptr);
g_instance = this;
on_video_file_status_ =
base::BindRepeating(&CaptureModeController::OnVideoFileStatus,
weak_ptr_factory_.GetWeakPtr());
// Schedule recording of the number of screenshots taken per day.
num_screenshots_taken_in_last_day_scheduler_.Start(
FROM_HERE, base::TimeDelta::FromDays(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::TimeDelta::FromDays(7),
base::BindRepeating(
&CaptureModeController::RecordAndResetScreenshotsTakenInLastWeek,
weak_ptr_factory_.GetWeakPtr()));
Shell::Get()->session_controller()->AddObserver(this);
chromeos::PowerManagerClient::Get()->AddObserver(this);
}
CaptureModeController::~CaptureModeController() {
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
Shell::Get()->session_controller()->RemoveObserver(this);
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
CaptureModeController* CaptureModeController::Get() {
DCHECK(g_instance);
return g_instance;
}
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 (type == type_)
return;
type_ = type;
if (capture_mode_session_)
capture_mode_session_->OnCaptureTypeChanged(type_);
}
void CaptureModeController::Start(CaptureModeEntryType entry_type) {
if (capture_mode_session_)
return;
if (delegate_->IsCaptureModeInitRestricted()) {
ShowDisabledNotification();
return;
}
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);
}
capture_mode_session_ = std::make_unique<CaptureModeSession>(this);
}
void CaptureModeController::Stop() {
DCHECK(IsActive());
capture_mode_session_.reset();
}
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();
}
void CaptureModeController::CaptureScreenshotsOfAllDisplays() {
if (delegate_->IsCaptureModeInitRestricted()) {
ShowDisabledNotification();
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;
}
}
void CaptureModeController::PerformCapture() {
DCHECK(IsActive());
const base::Optional<CaptureParams> capture_params = GetCaptureParams();
if (!capture_params)
return;
if (!IsCaptureAllowed(*capture_params)) {
ShowDisabledNotification();
Stop();
return;
}
DCHECK(capture_mode_session_);
capture_mode_session_->ReportSessionHistograms();
if (type_ == CaptureModeType::kImage) {
CaptureImage(*capture_params, BuildImagePath());
}
else
CaptureVideo(*capture_params);
}
void CaptureModeController::EndVideoRecording(EndRecordingReason reason) {
RecordEndRecordingReason(reason);
recording_service_remote_->StopRecording();
TerminateRecordingUiElements();
}
void CaptureModeController::OpenFeedbackDialog() {
delegate_->OpenFeedbackDialog();
}
void CaptureModeController::OnMuxerOutput(const std::string& chunk) {
DCHECK(video_file_handler_);
video_file_handler_.AsyncCall(&VideoFileHandler::AppendChunk)
.WithArgs(const_cast<std::string&>(chunk))
.Then(on_video_file_status_);
}
void CaptureModeController::OnRecordingEnded(bool success) {
delegate_->StopObservingRestrictedContent();
window_frame_sink_.reset();
// 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();
recording_service_client_receiver_.reset();
DCHECK(video_file_handler_);
video_file_handler_.AsyncCall(&VideoFileHandler::FlushBufferedChunks)
.Then(base::BindOnce(&CaptureModeController::OnVideoFileSaved,
weak_ptr_factory_.GetWeakPtr()));
}
void CaptureModeController::OnActiveUserSessionChanged(
const AccountId& account_id) {
EndSessionOrRecording(EndRecordingReason::kActiveUserChange);
}
void CaptureModeController::OnSessionStateChanged(
session_manager::SessionState state) {
if (Shell::Get()->session_controller()->IsUserSessionBlocked())
EndSessionOrRecording(EndRecordingReason::kSessionBlocked);
}
void CaptureModeController::OnChromeTerminating() {
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::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();
return;
}
if (!is_recording_in_progress_)
return;
if (reason == EndRecordingReason::kImminentSuspend) {
// If suspend happens while recording is in progress, we consider this a
// failure, and cut the recording immediately. The recording service may
// have some buffered chunks that will never be received, and as a result,
// the a few seconds at the end of the recording may get lost.
// TODO(afakhry): Think whether this is what we want. We might be able to
// end the recording normally by asking the service to StopRecording(), and
// block the suspend until all chunks have been received, and then we can
// resume it.
RecordEndRecordingReason(EndRecordingReason::kImminentSuspend);
OnRecordingEnded(/*success=*/false);
return;
}
EndVideoRecording(reason);
}
base::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 base::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 base::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) {
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();
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::PendingRemote<viz::mojom::FrameSinkVideoCapturer> video_capturer;
aura::Env::GetInstance()
->context_factory()
->GetHostFrameSinkManager()
->CreateVideoCapturer(video_capturer.InitWithNewPipeAndPassReceiver());
// 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<audio::mojom::StreamFactory> audio_stream_factory;
if (enable_audio_recording_) {
delegate_->BindAudioStreamFactory(
audio_stream_factory.InitWithNewPipeAndPassReceiver());
}
auto frame_sink_id = capture_params.window->GetFrameSinkId();
if (!frame_sink_id.is_valid()) {
window_frame_sink_ = capture_params.window->CreateLayerTreeFrameSink();
frame_sink_id = capture_params.window->GetFrameSinkId();
DCHECK(frame_sink_id.is_valid());
}
const auto bounds = capture_params.bounds;
switch (source_) {
case CaptureModeSource::kFullscreen:
recording_service_remote_->RecordFullscreen(
std::move(client), std::move(video_capturer),
std::move(audio_stream_factory), frame_sink_id, bounds.size());
break;
case CaptureModeSource::kWindow:
// TODO(crbug.com/1143930): Window recording doesn't produce any frames at
// the moment.
recording_service_remote_->RecordWindow(
std::move(client), std::move(video_capturer),
std::move(audio_stream_factory), frame_sink_id, bounds.size(),
capture_params.window->GetRootWindow()
->GetBoundsInRootWindow()
.size());
break;
case CaptureModeSource::kRegion:
recording_service_remote_->RecordRegion(
std::move(client), std::move(video_capturer),
std::move(audio_stream_factory), frame_sink_id,
capture_params.window->GetRootWindow()
->GetBoundsInRootWindow()
.size(),
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
// OnRecordingEnded() in all cases.
RecordEndRecordingReason(EndRecordingReason::kRecordingServiceDisconnected);
OnRecordingEnded(/*success=*/false);
}
bool CaptureModeController::IsCaptureAllowed(
const CaptureParams& capture_params) const {
return delegate_->IsCaptureAllowed(
capture_params.window, capture_params.bounds,
/*for_video=*/type_ == CaptureModeType::kVideo);
}
void CaptureModeController::TerminateRecordingUiElements() {
if (!is_recording_in_progress_)
return;
is_recording_in_progress_ = false;
Shell::Get()->UpdateCursorCompositingEnabled();
capture_mode_util::SetStopRecordingButtonVisibility(
video_recording_watcher_->window_being_recorded()->GetRootWindow(),
false);
video_recording_watcher_.reset();
}
void CaptureModeController::CaptureImage(const CaptureParams& capture_params,
const base::FilePath& path) {
DCHECK_EQ(CaptureModeType::kImage, type_);
DCHECK(IsCaptureAllowed(capture_params));
// Stop the capture session now, so as not to take a screenshot of the capture
// bar.
if (IsActive())
Stop();
DCHECK(!capture_params.bounds.IsEmpty());
ui::GrabWindowSnapshotAsyncPNG(
capture_params.window, capture_params.bounds,
base::BindOnce(&CaptureModeController::OnImageCaptured,
weak_ptr_factory_.GetWeakPtr(), path));
++num_screenshots_taken_in_last_day_;
++num_screenshots_taken_in_last_week_;
++num_consecutive_screenshots_;
num_consecutive_screenshots_scheduler_.Reset();
}
void CaptureModeController::CaptureVideo(const CaptureParams& capture_params) {
DCHECK_EQ(CaptureModeType::kVideo, type_);
DCHECK(IsCaptureAllowed(capture_params));
if (skip_count_down_ui_) {
OnVideoRecordCountDownFinished();
return;
}
capture_mode_session_->StartCountDown(
base::BindOnce(&CaptureModeController::OnVideoRecordCountDownFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CaptureModeController::OnImageCaptured(
const base::FilePath& path,
scoped_refptr<base::RefCountedMemory> png_bytes) {
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),
base::BindOnce(&CaptureModeController::OnImageFileSaved,
weak_ptr_factory_.GetWeakPtr(), png_bytes, path));
}
void CaptureModeController::OnImageFileSaved(
scoped_refptr<base::RefCountedMemory> png_bytes,
const base::FilePath& path,
bool success) {
if (!success) {
ShowFailureNotification();
return;
}
if (!on_file_saved_callback_.is_null())
std::move(on_file_saved_callback_).Run(path);
DCHECK(png_bytes && png_bytes->size());
const auto image = gfx::Image::CreateFrom1xPNGBytes(png_bytes);
CopyImageToClipboard(image);
ShowPreviewNotification(path, image, CaptureModeType::kImage);
if (features::IsTemporaryHoldingSpaceEnabled()) {
HoldingSpaceClient* client = HoldingSpaceController::Get()->client();
if (client) // May be `nullptr` in tests.
client->AddScreenshot(path);
}
}
void CaptureModeController::OnVideoFileStatus(bool success) {
if (success)
return;
// TODO(afakhry): Show the user a message about IO failure.
EndVideoRecording(EndRecordingReason::kFileIoError);
}
void CaptureModeController::OnVideoFileSaved(bool success) {
DCHECK(base::CurrentUIThread::IsSet());
DCHECK(video_file_handler_);
if (!success) {
ShowFailureNotification();
} else {
ShowPreviewNotification(current_video_file_path_, gfx::Image(),
CaptureModeType::kVideo);
DCHECK(!recording_start_time_.is_null());
RecordCaptureModeRecordTime(
(base::TimeTicks::Now() - recording_start_time_).InSeconds());
if (features::IsTemporaryHoldingSpaceEnabled()) {
HoldingSpaceClient* client = HoldingSpaceController::Get()->client();
if (client) // May be `nullptr` in tests.
client->AddScreenRecording(current_video_file_path_);
}
}
if (!on_file_saved_callback_.is_null())
std::move(on_file_saved_callback_).Run(current_video_file_path_);
low_disk_space_threshold_reached_ = false;
recording_start_time_ = base::TimeTicks();
current_video_file_path_.clear();
video_file_handler_.Reset();
}
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 base::string16 title = l10n_util::GetStringUTF16(
for_video ? IDS_ASH_SCREEN_CAPTURE_RECORDING_TITLE
: IDS_ASH_SCREEN_CAPTURE_SCREENSHOT_TITLE);
const base::string16 message =
for_video && low_disk_space_threshold_reached_
? l10n_util::GetStringUTF16(
IDS_ASH_SCREEN_CAPTURE_LOW_DISK_SPACE_MESSAGE)
: l10n_util::GetStringUTF16(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)
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(
title, message, optional_fields,
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(&CaptureModeController::HandleNotificationClicked,
weak_ptr_factory_.GetWeakPtr(),
screen_capture_path, type)));
}
void CaptureModeController::HandleNotificationClicked(
const base::FilePath& screen_capture_path,
const CaptureModeType type,
base::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);
} 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);
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 {
const base::FilePath path = delegate_->GetActiveUserDownloadsDir();
base::Time::Exploded exploded_time;
timestamp.LocalExplode(&exploded_time);
return path.AppendASCII(base::StringPrintf(
format_string, GetDateStr(exploded_time).c_str(),
GetTimeStr(exploded_time, delegate_->Uses24HourFormat()).c_str()));
}
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() {
// If this event is dispatched after the capture session was cancelled or
// destroyed, this should be a no-op.
if (!IsActive())
return;
const base::Optional<CaptureParams> capture_params = GetCaptureParams();
// Stop the capture session now, so the bar doesn't show up in the captured
// video.
Stop();
if (!capture_params)
return;
// We enable the software-composited cursor, in order for the video capturer
// to be able to record it.
is_recording_in_progress_ = true;
Shell::Get()->UpdateCursorCompositingEnabled();
video_recording_watcher_ =
std::make_unique<VideoRecordingWatcher>(this, capture_params->window);
constexpr size_t kVideoBufferCapacityBytes = 512 * 1024;
// We use a threshold of 512 MB to end the video recording due to low disk
// space, which is the same threshold as that used by the low disk space
// notification (See low_disk_notification.cc).
constexpr size_t kLowDiskSpaceThresholdInBytes = 512 * 1024 * 1024;
// The |video_file_handler_| performs all its tasks on the
// |blocking_task_runner_|. However, we want the low disk space callback to be
// run on the UI thread.
base::OnceClosure on_low_disk_space_callback =
base::BindPostTask(base::ThreadTaskRunnerHandle::Get(),
base::BindOnce(&CaptureModeController::OnLowDiskSpace,
weak_ptr_factory_.GetWeakPtr()));
DCHECK(current_video_file_path_.empty());
recording_start_time_ = base::TimeTicks::Now();
current_video_file_path_ = BuildVideoPath();
video_file_handler_ = VideoFileHandler::Create(
blocking_task_runner_, current_video_file_path_,
kVideoBufferCapacityBytes, kLowDiskSpaceThresholdInBytes,
std::move(on_low_disk_space_callback));
video_file_handler_.AsyncCall(&VideoFileHandler::Initialize)
.Then(on_video_file_status_);
LaunchRecordingServiceAndStartRecording(*capture_params);
delegate_->StartObservingRestrictedContent(
capture_params->window, capture_params->bounds,
base::BindOnce(&CaptureModeController::InterruptVideoRecording,
weak_ptr_factory_.GetWeakPtr()));
capture_mode_util::SetStopRecordingButtonVisibility(
capture_params->window->GetRootWindow(), true);
}
void CaptureModeController::InterruptVideoRecording() {
ShowVideoRecordingStoppedNotification();
EndVideoRecording(EndRecordingReason::kDlpInterruption);
}
void CaptureModeController::OnLowDiskSpace() {
DCHECK(base::CurrentUIThread::IsSet());
low_disk_space_threshold_reached_ = true;
// We end the video recording normally (i.e. we don't consider this to be a
// failure). The low disk space threashold was chosen to be big enough to
// allow the remaining chunks to be saved normally. However,
// |low_disk_space_threshold_reached_| will be used to display a different
// message in the notification.
EndVideoRecording(EndRecordingReason::kLowDiskSpace);
}
} // namespace ash