Skip to content

Commit

Permalink
capture_mode: Show an image thumbnail of the video in the notification
Browse files Browse the repository at this point in the history
With CL, the recording service will be able to provide an
RGB image to the client, which can be used as a thumbnail
representation of the video.

This CL also has the necessary changes to show a "play"
icon overlay on top of the video thumbnail in the
notification image.

BUG=1176327
TEST=Manual. Added unit test.

Change-Id: Ifc080d055da36cc8b73f11b21de99c7f94fb25dd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2753760
Commit-Queue: Ahmed Fakhry <afakhry@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: James Cook <jamescook@chromium.org>
Cr-Commit-Position: refs/heads/master@{#862899}
  • Loading branch information
Ahmed Fakhry authored and Chromium LUCI CQ committed Mar 15, 2021
1 parent 7cb0e44 commit 13cf4cb
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 56 deletions.
60 changes: 41 additions & 19 deletions ash/capture_mode/capture_mode_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ constexpr char kScreenCaptureNotificationId[] = "capture_mode_notification";
constexpr char kScreenCaptureStoppedNotificationId[] =
"capture_mode_stopped_notification";
constexpr char kScreenCaptureNotifierId[] = "ash.capture_mode_controller";
constexpr char kScreenCaptureNotificationType[] =
"capture_mode_notification_type";
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
Expand Down Expand Up @@ -162,6 +163,8 @@ void DeleteFileAsync(scoped_refptr<base::SequencedTaskRunner> task_runner,
}

// 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,
Expand All @@ -170,7 +173,8 @@ void ShowNotification(
scoped_refptr<message_center::NotificationDelegate> delegate,
message_center::SystemNotificationWarningLevel warning_level =
message_center::SystemNotificationWarningLevel::NORMAL,
const gfx::VectorIcon& notification_icon = kCaptureModeIcon) {
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;
Expand All @@ -184,8 +188,11 @@ void ShowNotification(
message_center::NotifierType::SYSTEM_COMPONENT,
kScreenCaptureNotifierId),
optional_fields, delegate, notification_icon, warning_level);
if (type == message_center::NOTIFICATION_TYPE_CUSTOM)
notification->set_custom_view_type(kScreenCaptureNotificationType);
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.
Expand Down Expand Up @@ -304,10 +311,15 @@ CaptureModeController::CaptureModeController(
weak_ptr_factory_.GetWeakPtr()));

DCHECK(!message_center::MessageViewFactory::HasCustomNotificationViewFactory(
kScreenCaptureNotificationType));
kScreenShotNotificationType));
DCHECK(!message_center::MessageViewFactory::HasCustomNotificationViewFactory(
kScreenRecordingNotificationType));
message_center::MessageViewFactory::SetCustomNotificationViewFactory(
kScreenShotNotificationType,
base::BindRepeating(&CaptureModeNotificationView::CreateForImage));
message_center::MessageViewFactory::SetCustomNotificationViewFactory(
kScreenCaptureNotificationType,
base::BindRepeating(&CaptureModeNotificationView::Create));
kScreenRecordingNotificationType,
base::BindRepeating(&CaptureModeNotificationView::CreateForVideo));

Shell::Get()->session_controller()->AddObserver(this);
chromeos::PowerManagerClient::Get()->AddObserver(this);
Expand All @@ -316,9 +328,11 @@ CaptureModeController::CaptureModeController(
CaptureModeController::~CaptureModeController() {
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
Shell::Get()->session_controller()->RemoveObserver(this);
// Remove the custom notification view factory.
// Remove the custom notification view factories.
message_center::MessageViewFactory::ClearCustomNotificationViewFactory(
kScreenCaptureNotificationType);
kScreenShotNotificationType);
message_center::MessageViewFactory::ClearCustomNotificationViewFactory(
kScreenRecordingNotificationType);

DCHECK_EQ(g_instance, this);
g_instance = nullptr;
Expand Down Expand Up @@ -504,7 +518,7 @@ void CaptureModeController::RefreshContentProtection() {
// HDCP violation is also considered a failure, and we're not going to wait
// for any buffered frames in the recording service.
RecordEndRecordingReason(EndRecordingReason::kHdcpInterruption);
OnRecordingEnded(/*success=*/false);
OnRecordingEnded(/*success=*/false, gfx::ImageSkia());
ShowVideoRecordingStoppedNotification(/*for_hdcp=*/true);
}
}
Expand All @@ -516,7 +530,8 @@ void CaptureModeController::OnMuxerOutput(const std::string& chunk) {
.Then(on_video_file_status_);
}

void CaptureModeController::OnRecordingEnded(bool success) {
void CaptureModeController::OnRecordingEnded(bool success,
const gfx::ImageSkia& thumbnail) {
delegate_->StopObservingRestrictedContent();

// If |success| is false, then recording has been force-terminated due to a
Expand All @@ -534,7 +549,7 @@ void CaptureModeController::OnRecordingEnded(bool success) {
DCHECK(video_file_handler_);
video_file_handler_.AsyncCall(&VideoFileHandler::FlushBufferedChunks)
.Then(base::BindOnce(&CaptureModeController::OnVideoFileSaved,
weak_ptr_factory_.GetWeakPtr()));
weak_ptr_factory_.GetWeakPtr(), thumbnail));
}

void CaptureModeController::OnActiveUserSessionChanged(
Expand Down Expand Up @@ -644,7 +659,7 @@ void CaptureModeController::EndSessionOrRecording(EndRecordingReason reason) {
// block the suspend until all chunks have been received, and then we can
// resume it.
RecordEndRecordingReason(EndRecordingReason::kImminentSuspend);
OnRecordingEnded(/*success=*/false);
OnRecordingEnded(/*success=*/false, gfx::ImageSkia());
return;
}

Expand Down Expand Up @@ -786,7 +801,7 @@ void CaptureModeController::OnRecordingServiceDisconnected() {
// StopRecording(), and it calling us back with OnRecordingEnded(), so we call
// OnRecordingEnded() in all cases.
RecordEndRecordingReason(EndRecordingReason::kRecordingServiceDisconnected);
OnRecordingEnded(/*success=*/false);
OnRecordingEnded(/*success=*/false, gfx::ImageSkia());
}

CaptureAllowance CaptureModeController::IsCaptureAllowedByEnterprisePolicies(
Expand Down Expand Up @@ -819,7 +834,9 @@ void CaptureModeController::TerminateRecordingUiElements() {

void CaptureModeController::CaptureImage(const CaptureParams& capture_params,
const base::FilePath& path) {
DCHECK_EQ(CaptureModeType::kImage, type_);
// 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_EQ(CaptureAllowance::kAllowed,
IsCaptureAllowedByEnterprisePolicies(capture_params));

Expand Down Expand Up @@ -909,14 +926,17 @@ void CaptureModeController::OnVideoFileStatus(bool success) {
EndVideoRecording(EndRecordingReason::kFileIoError);
}

void CaptureModeController::OnVideoFileSaved(bool success) {
void CaptureModeController::OnVideoFileSaved(
const gfx::ImageSkia& video_thumbnail,
bool success) {
DCHECK(base::CurrentUIThread::IsSet());
DCHECK(video_file_handler_);

if (!success) {
ShowFailureNotification();
} else {
ShowPreviewNotification(current_video_file_path_, gfx::Image(),
ShowPreviewNotification(current_video_file_path_,
gfx::Image(video_thumbnail),
CaptureModeType::kVideo);
DCHECK(!recording_start_time_.is_null());
RecordCaptureModeRecordTime(
Expand Down Expand Up @@ -965,7 +985,9 @@ void CaptureModeController::ShowPreviewNotification(
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(&CaptureModeController::HandleNotificationClicked,
weak_ptr_factory_.GetWeakPtr(),
screen_capture_path, type)));
screen_capture_path, type)),
message_center::SystemNotificationWarningLevel::NORMAL, kCaptureModeIcon,
for_video);
}

void CaptureModeController::HandleNotificationClicked(
Expand Down
8 changes: 5 additions & 3 deletions ash/capture_mode/capture_mode_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class ASH_EXPORT CaptureModeController

// recording::mojom::RecordingServiceClient:
void OnMuxerOutput(const std::string& chunk) override;
void OnRecordingEnded(bool success) override;
void OnRecordingEnded(bool success, const gfx::ImageSkia& thumbnail) override;

// SessionObserver:
void OnActiveUserSessionChanged(const AccountId& account_id) override;
Expand Down Expand Up @@ -237,8 +237,10 @@ class ASH_EXPORT CaptureModeController
void OnVideoFileStatus(bool success);

// Called back when the |video_file_handler_| flushes the remaining cached
// video chunks in its buffer. Called on the UI thread.
void OnVideoFileSaved(bool success);
// video chunks in its buffer. Called on the UI thread. |video_thumbnail| is
// an RGB image provided by the recording service that can be used as a
// thumbnail of the video in the notification.
void OnVideoFileSaved(const gfx::ImageSkia& video_thumbnail, bool success);

// Shows a preview notification of the newly taken screenshot or screen
// recording.
Expand Down
85 changes: 65 additions & 20 deletions ash/capture_mode/capture_mode_notification_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "ash/style/ash_color_provider.h"
#include "ash/style/scoped_light_mode_as_default.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
Expand All @@ -17,15 +18,20 @@ namespace ash {

namespace {

// Constants related to the banner view on the image capture notification.
// Constants related to the banner view on the image capture notifications.
constexpr int kBannerHeightDip = 36;
constexpr int kBannerHorizontalInsetDip = 12;
constexpr int kBannerVerticalInsetDip = 8;
constexpr int kBannerIconTextSpacingDip = 8;
constexpr int kBannerIconSizeDip = 20;

// Constants related to the play icon view for video capture notifications.
constexpr int kPlayIconSizeDip = 24;
constexpr int kPlayIconBackgroundCornerRadiusDip = 20;
constexpr gfx::Size kPlayIconViewSize{40, 40};

// Creates the banner view that will show on top of the notification image.
std::unique_ptr<views::View> CreateBannerViewImpl() {
std::unique_ptr<views::View> CreateBannerView() {
std::unique_ptr<views::View> banner_view = std::make_unique<views::View>();

// Use the light mode as default as notification is still using light
Expand Down Expand Up @@ -61,15 +67,34 @@ std::unique_ptr<views::View> CreateBannerViewImpl() {
return banner_view;
}

// Creates the play icon view which shows on top of the video thumbnail in the
// notification.
std::unique_ptr<views::View> CreatePlayIconView() {
auto play_view = std::make_unique<views::ImageView>();
auto* color_provider = AshColorProvider::Get();
const SkColor icon_color = color_provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary);
play_view->SetImage(gfx::CreateVectorIcon(kCaptureModePlayIcon,
kPlayIconSizeDip, icon_color));
play_view->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
play_view->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
const SkColor background_color = color_provider->GetBaseLayerColor(
AshColorProvider::BaseLayerType::kTransparent80);
play_view->SetBackground(views::CreateRoundedRectBackground(
background_color, kPlayIconBackgroundCornerRadiusDip));
return play_view;
}

} // namespace

CaptureModeNotificationView::CaptureModeNotificationView(
const message_center::Notification& notification)
: message_center::NotificationViewMD(notification) {
// Create the banner view if notification image is not empty. The banner
// will show on top of the notification image.
const message_center::Notification& notification,
CaptureModeType capture_type)
: message_center::NotificationViewMD(notification),
capture_type_(capture_type) {
// Creates the extra view which will depend on the type of the notification.
if (!notification.image().IsEmpty())
CreateBannerView();
CreateExtraView();

// We need to observe this view as |this| view will be re-used for
// notifications for with/without image scenarios if |this| is not destroyed
Expand All @@ -81,45 +106,65 @@ CaptureModeNotificationView::~CaptureModeNotificationView() = default;

// static
std::unique_ptr<message_center::MessageView>
CaptureModeNotificationView::Create(
CaptureModeNotificationView::CreateForImage(
const message_center::Notification& notification) {
return std::make_unique<CaptureModeNotificationView>(notification);
return std::make_unique<CaptureModeNotificationView>(notification,
CaptureModeType::kImage);
}

// static
std::unique_ptr<message_center::MessageView>
CaptureModeNotificationView::CreateForVideo(
const message_center::Notification& notification) {
return std::make_unique<CaptureModeNotificationView>(notification,
CaptureModeType::kVideo);
}

void CaptureModeNotificationView::Layout() {
message_center::NotificationViewMD::Layout();
if (!banner_view_)
if (!extra_view_)
return;

// Calculate the banner view's desired bounds.
gfx::Rect banner_bounds = image_container_view()->GetContentsBounds();
banner_bounds.set_y(banner_bounds.bottom() - kBannerHeightDip);
banner_bounds.set_height(kBannerHeightDip);
banner_view_->SetBoundsRect(banner_bounds);
gfx::Rect extra_view_bounds = image_container_view()->GetContentsBounds();

if (capture_type_ == CaptureModeType::kImage) {
// The extra view in this case is a banner laid out at the bottom of the
// image container.
extra_view_bounds.set_y(extra_view_bounds.bottom() - kBannerHeightDip);
extra_view_bounds.set_height(kBannerHeightDip);
} else {
DCHECK_EQ(capture_type_, CaptureModeType::kVideo);
// The extra view in this case is a play icon centered in the view.
extra_view_bounds.ClampToCenteredSize(kPlayIconViewSize);
}

extra_view_->SetBoundsRect(extra_view_bounds);
}

void CaptureModeNotificationView::OnChildViewAdded(views::View* observed_view,
views::View* child) {
if (observed_view == this && child == image_container_view())
CreateBannerView();
CreateExtraView();
}

void CaptureModeNotificationView::OnChildViewRemoved(views::View* observed_view,
views::View* child) {
if (observed_view == this && child == image_container_view())
banner_view_ = nullptr;
extra_view_ = nullptr;
}

void CaptureModeNotificationView::OnViewIsDeleting(View* observed_view) {
DCHECK_EQ(observed_view, this);
views::View::RemoveObserver(this);
}

void CaptureModeNotificationView::CreateBannerView() {
void CaptureModeNotificationView::CreateExtraView() {
DCHECK(image_container_view());
DCHECK(!image_container_view()->children().empty());
DCHECK(!banner_view_);
banner_view_ = image_container_view()->AddChildView(CreateBannerViewImpl());
DCHECK(!extra_view_);
extra_view_ = image_container_view()->AddChildView(
capture_type_ == CaptureModeType::kImage ? CreateBannerView()
: CreatePlayIconView());
}

} // namespace ash
Loading

0 comments on commit 13cf4cb

Please sign in to comment.