Commit 7fa67e3f authored by Ahmed Fakhry's avatar Ahmed Fakhry Committed by Commit Bot

capture_mode: Add logic to capture images

This CL implements the image capture part of this feature,
from all the possible sources, and handles saving the image
files and showing preview notifications as well as error
notifications.

Later CLs will implement logic to disable capture mode
operations such as policy or Data Leak Prevention (DLP).

BUG=1121699

Change-Id: I6dfba6f57adb0c48270b4d5ebb71e7d28fe489df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2376476
Commit-Queue: Ahmed Fakhry <afakhry@chromium.org>
Reviewed-by: default avatarJames Cook <jamescook@chromium.org>
Cr-Commit-Position: refs/heads/master@{#804036}
parent 11d2a7ec
......@@ -2691,6 +2691,18 @@ Here are some things you can try to get started.
</message>
<!-- Capture Mode -->
<message name="IDS_ASH_SCREEN_CAPTURE_DISABLED_TITLE" desc="The title of the notification when capture mode is disabled.">
Screen capture disabled
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_FAILURE_TITLE" desc="The title of the notification when capture mode fails.">
An error occurred
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_DISABLED_MESSAGE" desc="The text of the notification when capture mode is disabled.">
The ability to screen capture has been disabled by your administrator.
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_FAILURE_MESSAGE" desc="The text of the notification when capture mode fails.">
Failed to capture screen
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE" desc="The name that is the display source of the screen capture notification.">
Screen capture
</message>
......
c4ac976289a1db5f5e982fdd4148d219848331ac
\ No newline at end of file
c4ac976289a1db5f5e982fdd4148d219848331ac
\ No newline at end of file
79ed560659475d7632f6a9266546b4a7f7bc5de7
\ No newline at end of file
79ed560659475d7632f6a9266546b4a7f7bc5de7
\ No newline at end of file
......@@ -5,6 +5,7 @@
#include "ash/capture_mode/capture_mode_controller.h"
#include <memory>
#include <string>
#include <utility>
#include "ash/capture_mode/capture_mode_session.h"
......@@ -16,11 +17,20 @@
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.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_service.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 {
......@@ -28,8 +38,15 @@ namespace {
CaptureModeController* g_instance = nullptr;
const char kScreenCaptureNotificationId[] = "capture_mode_notification";
const char kScreenCaptureNotifierId[] = "ash.capture_mode_controller";
constexpr char kScreenCaptureNotificationId[] = "capture_mode_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".
constexpr char kScreenshotFileNameFmtStr[] = "Screenshot %s %s.png";
constexpr char kDateFmtStr[] = "%d-%02d-%02d";
constexpr char k24HourTimeFmtStr[] = "%02d.%02d.%02d";
constexpr char kAmPmTimeFmtStr[] = "%d.%02d.%02d";
// The notification button index.
enum NotificationButtonIndex {
......@@ -37,6 +54,120 @@ enum NotificationButtonIndex {
BUTTON_DELETE,
};
// 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(const base::FilePath& path) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
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() {
ShowNotification(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISABLED_TITLE),
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISABLED_MESSAGE),
/*optional_fields=*/{}, /*delegate=*/nullptr);
}
// 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);
}
} // namespace
CaptureModeController::CaptureModeController(
......@@ -91,16 +222,128 @@ void CaptureModeController::Stop() {
void CaptureModeController::PerformCapture() {
DCHECK(IsActive());
if (!IsCaptureAllowed()) {
ShowDisabledNotification();
Stop();
return;
}
if (type_ == CaptureModeType::kImage) {
CaptureImage();
} else {
CaptureVideo();
}
// The above capture functions should have ended the session.
DCHECK(!IsActive());
}
void CaptureModeController::EndVideoRecording() {
// TODO(afakhry): Fill in here.
}
bool CaptureModeController::IsCaptureAllowed() const {
// TODO(afakhry): Fill in here.
return true;
}
void CaptureModeController::CaptureImage() {
DCHECK_EQ(CaptureModeType::kImage, type_);
DCHECK(IsCaptureAllowed());
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.
Stop();
return;
}
// 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.
Stop();
return;
}
bounds = user_capture_region_;
break;
}
DCHECK(window);
DCHECK(!bounds.IsEmpty());
// Stop the capture session now, so as not to take a screenshot of the capture
// bar.
Stop();
ui::GrabWindowSnapshotAsyncPNG(
window, bounds,
base::BindOnce(&CaptureModeController::OnImageCaptured,
weak_ptr_factory_.GetWeakPtr(), base::Time::Now()));
}
void CaptureModeController::EndVideoRecording() {
void CaptureModeController::CaptureVideo() {
DCHECK(IsCaptureAllowed());
// TODO(afakhry): Fill in here.
Stop();
}
void CaptureModeController::OnImageCaptured(
base::Time timestamp,
scoped_refptr<base::RefCountedMemory> png_bytes) {
if (!png_bytes || !png_bytes->size()) {
LOG(ERROR) << "Failed to capture image.";
ShowFailureNotification();
return;
}
const base::FilePath path = BuildImagePath(timestamp);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
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;
}
DCHECK(png_bytes && png_bytes->size());
// TODO(afakhry): Save image to clipboard.
ShowPreviewNotification(path, gfx::Image::CreateFrom1xPNGBytes(png_bytes));
}
void CaptureModeController::ShowNotification(
const base::FilePath& screen_capture_path) {
void CaptureModeController::ShowPreviewNotification(
const base::FilePath& screen_capture_path,
const gfx::Image& preview_image) {
const base::string16 title =
l10n_util::GetStringUTF16(type_ == CaptureModeType::kImage
? IDS_ASH_SCREEN_CAPTURE_SCREENSHOT_TITLE
......@@ -108,41 +351,22 @@ void CaptureModeController::ShowNotification(
const base::string16 message =
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_MESSAGE);
message_center::RichNotificationData optional_field;
message_center::RichNotificationData optional_fields;
message_center::ButtonInfo edit_button(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_EDIT));
optional_field.buttons.push_back(edit_button);
optional_fields.buttons.push_back(edit_button);
message_center::ButtonInfo delete_button(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_BUTTON_DELETE));
optional_field.buttons.push_back(delete_button);
// TODO: Assign image for screenshot or screenrecording preview. For now it's
// an empty image.
optional_field.image = gfx::Image();
optional_fields.buttons.push_back(delete_button);
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotification(
message_center::NOTIFICATION_TYPE_IMAGE, kScreenCaptureNotificationId,
title, message,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_DISPLAY_SOURCE),
GURL(),
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kScreenCaptureNotifierId),
optional_field,
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(
&CaptureModeController::HandleNotificationClicked,
weak_ptr_factory_.GetWeakPtr(), screen_capture_path)),
kCaptureModeIcon,
message_center::SystemNotificationWarningLevel::NORMAL);
optional_fields.image = preview_image;
// Remove the previous notification before showing the new one if there is
// one.
message_center::MessageCenter::Get()->RemoveNotification(
kScreenCaptureNotificationId, /*by_user=*/false);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
ShowNotification(
title, message, optional_fields,
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(&CaptureModeController::HandleNotificationClicked,
weak_ptr_factory_.GetWeakPtr(),
screen_capture_path)));
}
void CaptureModeController::HandleNotificationClicked(
......@@ -151,18 +375,30 @@ void CaptureModeController::HandleNotificationClicked(
if (!button_index.has_value()) {
// Show the item in the folder.
delegate_->ShowScreenCaptureItemInFolder(screen_capture_path);
message_center::MessageCenter::Get()->RemoveNotification(
kScreenCaptureNotificationId, /*by_user=*/false);
return;
} else {
// TODO: fill in here.
switch (button_index.value()) {
case NotificationButtonIndex::BUTTON_EDIT:
break;
case NotificationButtonIndex::BUTTON_DELETE:
DeleteFileAsync(screen_capture_path);
break;
}
}
// TODO: fill in here.
switch (button_index.value()) {
case NotificationButtonIndex::BUTTON_EDIT:
break;
case NotificationButtonIndex::BUTTON_DELETE:
break;
}
message_center::MessageCenter::Get()->RemoveNotification(
kScreenCaptureNotificationId, /*by_user=*/false);
}
base::FilePath CaptureModeController::BuildImagePath(
base::Time timestamp) const {
const base::FilePath path = delegate_->GetActiveUserDownloadsDir();
base::Time::Exploded exploded_time;
timestamp.LocalExplode(&exploded_time);
return path.AppendASCII(base::StringPrintf(
kScreenshotFileNameFmtStr, GetDateStr(exploded_time).c_str(),
GetTimeStr(exploded_time, delegate_->Uses24HourFormat()).c_str()));
}
} // namespace ash
......@@ -10,8 +10,15 @@
#include "ash/ash_export.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/public/cpp/capture_mode_delegate.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/weak_ptr.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image.h"
namespace base {
class FilePath;
class Time;
} // namespace base
namespace ash {
......@@ -62,11 +69,43 @@ class ASH_EXPORT CaptureModeController {
void EndVideoRecording();
private:
// Show notification for the newly taken screenshot or screen recording.
void ShowNotification(const base::FilePath& screen_capture_path);
// Returns true if doing a screen capture is currently allowed, false
// otherwise.
bool IsCaptureAllowed() const;
// The below functions start the actual image/video capture. They expect that
// the capture session is still active when called, so they can retrieve the
// capture parameters they need. They will end the sessions themselves.
// They should never be called if IsCaptureAllowed() returns false.
void CaptureImage();
void CaptureVideo();
// Called back when an image has been captured to trigger an attempt to save
// the image as a file. |timestamp| is the time at which the capture was
// triggered, and |png_bytes| is the buffer containing the captured image in a
// PNG format.
void OnImageCaptured(base::Time timestamp,
scoped_refptr<base::RefCountedMemory> png_bytes);
// Called back when an attempt to save the image file has been completed, with
// |success| indicating whether the attempt succeeded for failed. |png_bytes|
// is the buffer containing the captured image in a PNG format, which will be
// used to show a preview of the image in a notification. If saving was
// successful, then the image was saved in |path|.
void OnImageFileSaved(scoped_refptr<base::RefCountedMemory> png_bytes,
const base::FilePath& path,
bool success);
// Shows a preview notification of the newly taken screenshot or screen
// recording.
void ShowPreviewNotification(const base::FilePath& screen_capture_path,
const gfx::Image& preview_image);
void HandleNotificationClicked(const base::FilePath& screen_capture_path,
base::Optional<int> button_index);
// Builds a path for an image screenshot file that was taken at |timestamp|.
base::FilePath BuildImagePath(base::Time timestamp) const;
std::unique_ptr<CaptureModeDelegate> delegate_;
CaptureModeType type_ = CaptureModeType::kImage;
......
......@@ -88,6 +88,15 @@ CaptureModeSession::~CaptureModeSession() {
SetMouseWarpEnabled(old_mouse_warp_status_);
}
aura::Window* CaptureModeSession::GetSelectedWindow() const {
// Note that the capture bar widget is activatable, so we can't use
// window_util::GetActiveWindow(). Instead, we use the MRU window tracker and
// get the top-most window if any.
auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
return mru_windows.empty() ? nullptr : mru_windows[0];
}
void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) {
capture_mode_bar_view_->OnCaptureSourceChanged(new_source);
SetMouseWarpEnabled(new_source != CaptureModeSource::kRegion);
......@@ -136,14 +145,8 @@ void CaptureModeSession::OnTouchEvent(ui::TouchEvent* event) {
}
gfx::Rect CaptureModeSession::GetSelectedWindowBounds() const {
// Note that the capture bar widget is activatable, so we can't use
// window_util::GetActiveWindow(). Instead, we use the MRU window tracker and
// get the top-most window if any.
auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
if (!mru_windows.empty())
return mru_windows[0]->bounds();
return gfx::Rect();
auto* window = GetSelectedWindow();
return window ? window->bounds() : gfx::Rect();
}
void CaptureModeSession::RefreshStackingOrder(aura::Window* parent_container) {
......
......@@ -38,10 +38,15 @@ class ASH_EXPORT CaptureModeSession : public ui::LayerOwner,
CaptureModeSession& operator=(const CaptureModeSession&) = delete;
~CaptureModeSession() override;
aura::Window* current_root() const { return current_root_; }
CaptureModeBarView* capture_mode_bar_view() const {
return capture_mode_bar_view_;
}
// Gets the current window selected for |kWindow| capture source. Returns
// nullptr if no window is available for selection.
aura::Window* GetSelectedWindow() const;
// Called when either the capture source or type changes.
void OnCaptureSourceChanged(CaptureModeSource new_source);
void OnCaptureTypeChanged(CaptureModeType new_type);
......@@ -57,7 +62,7 @@ class ASH_EXPORT CaptureModeSession : public ui::LayerOwner,
void OnTouchEvent(ui::TouchEvent* event) override;
private:
// Gets the current window selected for |kWindow| capture source.
// Gets the bounds of current window selected for |kWindow| capture source.
gfx::Rect GetSelectedWindowBounds() const;
// Ensures that the bar widget is on top of everything, and the overlay (which
......
......@@ -16,4 +16,8 @@ base::FilePath TestCaptureModeDelegate::GetActiveUserDownloadsDir() const {
void TestCaptureModeDelegate::ShowScreenCaptureItemInFolder(
const base::FilePath& file_path) {}
bool TestCaptureModeDelegate::Uses24HourFormat() const {
return false;
}
} // namespace ash
......@@ -19,6 +19,7 @@ class TestCaptureModeDelegate : public CaptureModeDelegate {
// CaptureModeDelegate:
base::FilePath GetActiveUserDownloadsDir() const override;
void ShowScreenCaptureItemInFolder(const base::FilePath& file_path) override;
bool Uses24HourFormat() const override;
};
} // namespace ash
......
......@@ -27,6 +27,11 @@ class ASH_PUBLIC_EXPORT CaptureModeDelegate {
// Shows the screenshot or screen recording item in the screen capture folder.
virtual void ShowScreenCaptureItemInFolder(
const base::FilePath& file_path) = 0;
// Returns true if the current user is using the 24-hour format (i.e. 14:00
// vs. 2:00 PM). This is used to build the file name of the captured image or
// video.
virtual bool Uses24HourFormat() const = 0;
};
} // namespace ash
......
......@@ -5,10 +5,13 @@
#include "chrome/browser/ui/ash/chrome_capture_mode_delegate.h"
#include "base/files/file_path.h"
#include "base/i18n/time_formatting.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "chromeos/login/login_state/login_state.h"
#include "components/prefs/pref_service.h"
ChromeCaptureModeDelegate::ChromeCaptureModeDelegate() = default;
......@@ -26,3 +29,12 @@ void ChromeCaptureModeDelegate::ShowScreenCaptureItemInFolder(
platform_util::ShowItemInFolder(ProfileManager::GetActiveUserProfile(),
file_path);
}
bool ChromeCaptureModeDelegate::Uses24HourFormat() const {
Profile* profile = ProfileManager::GetActiveUserProfile();
// TODO(afakhry): Consider moving |prefs::kUse24HourClock| to ash/public so
// we can do this entirely in ash.
if (profile)
return profile->GetPrefs()->GetBoolean(prefs::kUse24HourClock);
return base::GetHourClockType() == base::k24HourClock;
}
......@@ -20,6 +20,7 @@ class ChromeCaptureModeDelegate : public ash::CaptureModeDelegate {
// ash::CaptureModeDelegate:
base::FilePath GetActiveUserDownloadsDir() const override;
void ShowScreenCaptureItemInFolder(const base::FilePath& file_path) override;
bool Uses24HourFormat() const override;
};
#endif // CHROME_BROWSER_UI_ASH_CHROME_CAPTURE_MODE_DELEGATE_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment