0

[SysUi download integration] Add "copy to clipboard" button

This CL adds a notification button to copy the download image to
clipboard if the image download has completed.

A demo has been attached to the issue.

Bug: b/326122967
Change-Id: I4d035757bdea75a00941cab1c16a9d5ca2eb435e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5310539
Commit-Queue: Andrew Xu <andrewxu@chromium.org>
Reviewed-by: David Black <dmblack@google.com>
Cr-Commit-Position: refs/heads/main@{#1265967}
This commit is contained in:
andrewxu
2024-02-27 19:18:10 +00:00
committed by Chromium LUCI CQ
parent 178307719b
commit f7c36899ec
11 changed files with 215 additions and 64 deletions

@@ -7680,6 +7680,9 @@ To shut down the device, press and hold the power button on the device again.
<message name="IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL" desc="Text of the command to cancel a download."> <message name="IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL" desc="Text of the command to cancel a download.">
Cancel Cancel
</message> </message>
<message name="IDS_ASH_DOWNLOAD_COMMAND_TEXT_COPY_TO_CLIPBOARD" desc="Text of the command to copy the download file to clipboard.">
Copy to clipboard
</message>
<message name="IDS_ASH_DOWNLOAD_COMMAND_TEXT_PAUSE" desc="Text of the command to pause a download."> <message name="IDS_ASH_DOWNLOAD_COMMAND_TEXT_PAUSE" desc="Text of the command to pause a download.">
Pause Pause
</message> </message>

@@ -0,0 +1 @@
78af3668f6d2945de8f98d57b602be639ddb81e8

@@ -25,6 +25,12 @@
#include "chrome/browser/ui/ash/download_status/notification_display_client.h" #include "chrome/browser/ui/ash/download_status/notification_display_client.h"
#include "chromeos/crosapi/mojom/download_controller.mojom.h" #include "chromeos/crosapi/mojom/download_controller.mojom.h"
#include "chromeos/crosapi/mojom/download_status_updater.mojom.h" #include "chromeos/crosapi/mojom/download_status_updater.mojom.h"
#include "net/base/mime_util.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/gfx/image/image_skia.h"
namespace ash::download_status { namespace ash::download_status {
@@ -134,6 +140,15 @@ std::optional<std::u16string> GetText(
return file_path.get().BaseName().LossyDisplayName(); return file_path.get().BaseName().LossyDisplayName();
} }
// Returns true if the file referred to by `file_path` is of an image MIME type.
bool HasSupportedImageMimeType(const base::FilePath& file_path) {
std::string mime_type;
if (net::GetMimeTypeFromFile(file_path, &mime_type)) {
return blink::IsSupportedImageMimeType(mime_type);
}
return false;
}
// Opens the download file specified by `file_path` under the file system // Opens the download file specified by `file_path` under the file system
// associated with `profile`. // associated with `profile`.
void OpenFile(Profile* profile, const base::FilePath& file_path) { void OpenFile(Profile* profile, const base::FilePath& file_path) {
@@ -240,22 +255,38 @@ DisplayMetadata DisplayManager::CalculateDisplayMetadata(
&kResumeIcon, IDS_ASH_DOWNLOAD_COMMAND_TEXT_RESUME, &kResumeIcon, IDS_ASH_DOWNLOAD_COMMAND_TEXT_RESUME,
CommandType::kResume); CommandType::kResume);
} }
const base::FilePath& full_path = *download_status.full_path;
switch (download_status.state) { switch (download_status.state) {
case crosapi::mojom::DownloadState::kComplete: case crosapi::mojom::DownloadState::kComplete:
// NOTE: `kOpenFile` is not shown so it doesn't require an icon/text_id. // NOTE: `kOpenFile` is not shown so it doesn't require an icon/text_id.
command_infos.emplace_back( command_infos.emplace_back(
base::BindRepeating( base::BindRepeating(&DisplayManager::PerformCommand,
&DisplayManager::PerformCommand, weak_ptr_factory_.GetWeakPtr(), weak_ptr_factory_.GetWeakPtr(),
CommandType::kOpenFile, *download_status.full_path), CommandType::kOpenFile, full_path),
/*icon=*/nullptr, /*text_id=*/-1, CommandType::kOpenFile); /*icon=*/nullptr, /*text_id=*/-1, CommandType::kOpenFile);
// NOTE: The `kShowInFolder` button does not have an icon. // NOTE: The `kShowInFolder` button does not have an icon.
command_infos.emplace_back( command_infos.emplace_back(
base::BindRepeating( base::BindRepeating(&DisplayManager::PerformCommand,
&DisplayManager::PerformCommand, weak_ptr_factory_.GetWeakPtr(), weak_ptr_factory_.GetWeakPtr(),
CommandType::kShowInFolder, *download_status.full_path), CommandType::kShowInFolder, full_path),
/*icon=*/nullptr, IDS_ASH_DOWNLOAD_COMMAND_TEXT_SHOW_IN_FOLDER, /*icon=*/nullptr, IDS_ASH_DOWNLOAD_COMMAND_TEXT_SHOW_IN_FOLDER,
CommandType::kShowInFolder); CommandType::kShowInFolder);
// Add a command to copy the download file to clipboard if:
// 1. `download_status` has a valid image; AND
// 2. The download file is an image.
// NOTE: The `kCopyToClipboard` button does not require an icon.
if (const gfx::ImageSkia& image = download_status.image;
!image.isNull() && !image.size().IsEmpty() &&
HasSupportedImageMimeType(full_path)) {
command_infos.emplace_back(
base::BindRepeating(&DisplayManager::PerformCommand,
weak_ptr_factory_.GetWeakPtr(),
CommandType::kCopyToClipboard, full_path),
/*icon=*/nullptr, IDS_ASH_DOWNLOAD_COMMAND_TEXT_COPY_TO_CLIPBOARD,
CommandType::kCopyToClipboard);
}
break; break;
case crosapi::mojom::DownloadState::kInProgress: case crosapi::mojom::DownloadState::kInProgress:
// NOTE: `kShowInBrowser` is not shown so doesn't require an icon/text_id. // NOTE: `kShowInBrowser` is not shown so doesn't require an icon/text_id.
@@ -284,7 +315,7 @@ DisplayMetadata DisplayManager::CalculateDisplayMetadata(
} }
display_metadata.command_infos = std::move(command_infos); display_metadata.command_infos = std::move(command_infos);
display_metadata.file_path = *download_status.full_path; display_metadata.file_path = full_path;
display_metadata.image = download_status.image; display_metadata.image = download_status.image;
display_metadata.progress = GetProgress(download_status); display_metadata.progress = GetProgress(download_status);
display_metadata.secondary_text = download_status.status_text; display_metadata.secondary_text = download_status.status_text;
@@ -301,6 +332,13 @@ void DisplayManager::PerformCommand(
download_status_updater_->Cancel(/*guid=*/std::get<std::string>(param), download_status_updater_->Cancel(/*guid=*/std::get<std::string>(param),
/*callback=*/base::DoNothing()); /*callback=*/base::DoNothing());
break; break;
case CommandType::kCopyToClipboard: {
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteFilenames(ui::FileInfosToURIList(
/*filenames=*/{ui::FileInfo(std::get<base::FilePath>(param),
/*display_name=*/base::FilePath())}));
break;
}
case CommandType::kOpenFile: case CommandType::kOpenFile:
OpenFile(profile_, std::get<base::FilePath>(param)); OpenFile(profile_, std::get<base::FilePath>(param));
break; break;

@@ -22,6 +22,7 @@ namespace ash::download_status {
// Lists the types of commands that can be performed on a displayed download. // Lists the types of commands that can be performed on a displayed download.
enum class CommandType { enum class CommandType {
kCancel, kCancel,
kCopyToClipboard,
kOpenFile, kOpenFile,
kPause, kPause,
kResume, kResume,

@@ -22,11 +22,12 @@ constexpr int64_t kUnknownTotalBytes = -1;
crosapi::mojom::DownloadStatusPtr CreateDownloadStatus( crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
Profile* profile, Profile* profile,
std::string_view extension,
crosapi::mojom::DownloadState state, crosapi::mojom::DownloadState state,
crosapi::mojom::DownloadProgressPtr progress) { crosapi::mojom::DownloadProgressPtr progress) {
crosapi::mojom::DownloadStatusPtr download_status = crosapi::mojom::DownloadStatusPtr download_status =
crosapi::mojom::DownloadStatus::New(); crosapi::mojom::DownloadStatus::New();
download_status->full_path = test::CreateFile(profile); download_status->full_path = test::CreateFile(profile, extension);
download_status->guid = base::UnguessableToken::Create().ToString(); download_status->guid = base::UnguessableToken::Create().ToString();
download_status->progress = std::move(progress); download_status->progress = std::move(progress);
download_status->state = state; download_status->state = state;
@@ -36,10 +37,11 @@ crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus( crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus(
Profile* profile, Profile* profile,
std::string_view extension,
int64_t received_bytes, int64_t received_bytes,
const std::optional<int64_t>& total_bytes) { const std::optional<int64_t>& total_bytes) {
return CreateDownloadStatus( return CreateDownloadStatus(
profile, crosapi::mojom::DownloadState::kInProgress, profile, extension, crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New( crosapi::mojom::DownloadProgress::New(
/*loop=*/false, received_bytes, /*loop=*/false, received_bytes,
total_bytes.value_or(kUnknownTotalBytes), /*visible=*/true)); total_bytes.value_or(kUnknownTotalBytes), /*visible=*/true));

@@ -6,6 +6,7 @@
#define CHROME_BROWSER_UI_ASH_DOWNLOAD_STATUS_DISPLAY_TEST_UTIL_H_ #define CHROME_BROWSER_UI_ASH_DOWNLOAD_STATUS_DISPLAY_TEST_UTIL_H_
#include <optional> #include <optional>
#include <string_view>
#include "chromeos/crosapi/mojom/download_status_updater.mojom.h" #include "chromeos/crosapi/mojom/download_status_updater.mojom.h"
@@ -13,10 +14,11 @@ class Profile;
namespace ash::download_status { namespace ash::download_status {
// Creates a download status associated with a file under the downloads // Creates a download status associated with a file with the specified
// directory of `profile`. // `extension` under the downloads directory of `profile`.
crosapi::mojom::DownloadStatusPtr CreateDownloadStatus( crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
Profile* profile, Profile* profile,
std::string_view extension,
crosapi::mojom::DownloadState state, crosapi::mojom::DownloadState state,
crosapi::mojom::DownloadProgressPtr progress); crosapi::mojom::DownloadProgressPtr progress);
@@ -24,6 +26,7 @@ crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
// with a file under the downloads directory of `profile`. // with a file under the downloads directory of `profile`.
crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus( crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus(
Profile* profile, Profile* profile,
std::string_view extension,
int64_t received_bytes, int64_t received_bytes,
const std::optional<int64_t>& total_bytes = std::nullopt); const std::optional<int64_t>& total_bytes = std::nullopt);

@@ -4,6 +4,7 @@
#include "chrome/browser/ui/ash/download_status/holding_space_display_client.h" #include "chrome/browser/ui/ash/download_status/holding_space_display_client.h"
#include <optional>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -28,14 +29,17 @@ namespace ash::download_status {
namespace { namespace {
// Returns the command ID corresponding to the given command type. // Returns the command ID corresponding to the given command type if any. If
// there is no such command ID, returns `std::nullopt`.
// NOTE: It is fine to map both `CommandType::kOpenFile` and // NOTE: It is fine to map both `CommandType::kOpenFile` and
// `CommandType::kShowInBrowser` to `kOpenItem`, because `kOpenItem` is not // `CommandType::kShowInBrowser` to `kOpenItem`, because `kOpenItem` is not
// accessible from a holding space chip's context menu. // accessible from a holding space chip's context menu.
HoldingSpaceCommandId ConvertCommandTypeToId(CommandType type) { std::optional<HoldingSpaceCommandId> ConvertCommandTypeToId(CommandType type) {
switch (type) { switch (type) {
case CommandType::kCancel: case CommandType::kCancel:
return HoldingSpaceCommandId::kCancelItem; return HoldingSpaceCommandId::kCancelItem;
case CommandType::kCopyToClipboard:
return std::nullopt;
case CommandType::kOpenFile: case CommandType::kOpenFile:
return HoldingSpaceCommandId::kOpenItem; return HoldingSpaceCommandId::kOpenItem;
case CommandType::kPause: case CommandType::kPause:
@@ -51,12 +55,16 @@ HoldingSpaceCommandId ConvertCommandTypeToId(CommandType type) {
} }
} }
// Returns the holding space item action corresponding to `type`. // Returns the holding space item action corresponding to `type` if any. If
holding_space_metrics::ItemAction ConvertCommandTypeToAction(CommandType type) { // there is no such action, returns `std::nullopt`.
std::optional<holding_space_metrics::ItemAction> ConvertCommandTypeToAction(
CommandType type) {
using ItemAction = holding_space_metrics::ItemAction; using ItemAction = holding_space_metrics::ItemAction;
switch (type) { switch (type) {
case CommandType::kCancel: case CommandType::kCancel:
return ItemAction::kCancel; return ItemAction::kCancel;
case CommandType::kCopyToClipboard:
return std::nullopt;
case CommandType::kOpenFile: case CommandType::kOpenFile:
return ItemAction::kLaunch; return ItemAction::kLaunch;
case CommandType::kPause: case CommandType::kPause:
@@ -121,23 +129,31 @@ void HoldingSpaceDisplayClient::AddOrUpdate(
// Generate in-progress commands from `display_metadata`. // Generate in-progress commands from `display_metadata`.
std::vector<HoldingSpaceItem::InProgressCommand> in_progress_commands; std::vector<HoldingSpaceItem::InProgressCommand> in_progress_commands;
for (const auto& command_info : display_metadata.command_infos) { for (const auto& command_info : display_metadata.command_infos) {
if (const HoldingSpaceCommandId id = const std::optional<HoldingSpaceCommandId> id =
ConvertCommandTypeToId(command_info.type); ConvertCommandTypeToId(command_info.type);
holding_space_util::IsInProgressCommand(id)) { const std::optional<holding_space_metrics::ItemAction> item_action =
in_progress_commands.emplace_back( ConvertCommandTypeToAction(command_info.type);
id, command_info.text_id, command_info.icon,
base::BindRepeating( // Skip `command_info` if:
[](holding_space_metrics::ItemAction action, // 1. It does not have a corresponding ID; OR
const base::RepeatingClosure& command_callback, // 2. Its corresponding ID is not for an in-progress command; OR
const HoldingSpaceItem* item, HoldingSpaceCommandId command_id, // 3. It does not have a corresponding item action.
holding_space_metrics::EventSource event_source) { if (!id || !holding_space_util::IsInProgressCommand(*id) || !item_action) {
command_callback.Run(); continue;
holding_space_metrics::RecordItemAction(
/*items=*/{item}, action, event_source);
},
ConvertCommandTypeToAction(command_info.type),
command_info.command_callback));
} }
in_progress_commands.emplace_back(
*id, command_info.text_id, command_info.icon,
base::BindRepeating(
[](holding_space_metrics::ItemAction action,
const base::RepeatingClosure& command_callback,
const HoldingSpaceItem* item, HoldingSpaceCommandId command_id,
holding_space_metrics::EventSource event_source) {
command_callback.Run();
holding_space_metrics::RecordItemAction(
/*items=*/{item}, action, event_source);
},
*item_action, command_info.command_callback));
} }
// Specify the backing file. // Specify the backing file.

@@ -137,17 +137,18 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr in_progress_download = crosapi::mojom::DownloadStatusPtr in_progress_download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
in_progress_download->cancellable = true; in_progress_download->cancellable = true;
Update(in_progress_download->Clone()); Update(in_progress_download->Clone());
crosapi::mojom::DownloadStatusPtr completed_download = crosapi::mojom::DownloadStatusPtr completed_download = CreateDownloadStatus(
CreateDownloadStatus(profile, crosapi::mojom::DownloadState::kComplete, profile, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New( crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*loop=*/false,
/*received_bytes=*/1024, /*received_bytes=*/1024,
/*total_bytes=*/1024, /*total_bytes=*/1024,
/*visible=*/false)); /*visible=*/false));
Update(completed_download->Clone()); Update(completed_download->Clone());
test_api().Show(); test_api().Show();
@@ -258,12 +259,13 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr in_progress_download = crosapi::mojom::DownloadStatusPtr in_progress_download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
in_progress_download->cancellable = true; in_progress_download->cancellable = true;
Update(in_progress_download->Clone()); Update(in_progress_download->Clone());
crosapi::mojom::DownloadStatusPtr completed_download = CreateDownloadStatus( crosapi::mojom::DownloadStatusPtr completed_download = CreateDownloadStatus(
profile, crosapi::mojom::DownloadState::kComplete, profile, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New( crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*loop=*/false,
/*received_bytes=*/1024, /*total_bytes=*/1024, /*visible=*/false)); /*received_bytes=*/1024, /*total_bytes=*/1024, /*visible=*/false));
@@ -367,14 +369,14 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
ClickCompletedDownloadChip) { ClickCompletedDownloadChip) {
// Add a completed download. // Add a completed download.
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
CreateDownloadStatus(ProfileManager::GetActiveUserProfile(), ProfileManager::GetActiveUserProfile(),
crosapi::mojom::DownloadState::kComplete, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New( crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*loop=*/false,
/*received_bytes=*/1024, /*received_bytes=*/1024,
/*total_bytes=*/1024, /*total_bytes=*/1024,
/*visible=*/false)); /*visible=*/false));
Update(download->Clone()); Update(download->Clone());
test_api().Show(); test_api().Show();
@@ -416,6 +418,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
// Add an in-progress download. // Add an in-progress download.
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -456,6 +459,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, CompleteDownload) {
Profile* const active_profile = ProfileManager::GetActiveUserProfile(); Profile* const active_profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(active_profile, CreateInProgressDownloadStatus(active_profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -535,6 +539,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, CompleteDownload) {
// Add a new in-progress download with the duplicate download guid. // Add a new in-progress download with the duplicate download guid.
crosapi::mojom::DownloadStatusPtr duplicate_download = crosapi::mojom::DownloadStatusPtr duplicate_download =
CreateInProgressDownloadStatus(active_profile, CreateInProgressDownloadStatus(active_profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
duplicate_download->guid = download->guid; duplicate_download->guid = download->guid;
@@ -548,8 +553,9 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, CompleteDownload) {
IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
IndeterminateDownload) { IndeterminateDownload) {
// Create a download with an unknown total bytes count. // Create a download with an unknown total bytes count.
crosapi::mojom::DownloadStatusPtr download = CreateInProgressDownloadStatus( crosapi::mojom::DownloadStatusPtr download =
ProfileManager::GetActiveUserProfile(), /*received_bytes=*/0); CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt", /*received_bytes=*/0);
Update(download->Clone()); Update(download->Clone());
test_api().Show(); test_api().Show();
@@ -568,6 +574,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
// could happen when a download is blocked. // could happen when a download is blocked.
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->progress->visible = false; download->progress->visible = false;
@@ -592,6 +599,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
InterruptDownload) { InterruptDownload) {
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -610,6 +618,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
PauseAndResumeDownloadViaContextMenu) { PauseAndResumeDownloadViaContextMenu) {
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->pausable = true; download->pausable = true;
@@ -689,6 +698,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
PauseAndResumeDownloadViaSecondaryAction) { PauseAndResumeDownloadViaSecondaryAction) {
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->pausable = true; download->pausable = true;
@@ -773,6 +783,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, SecondaryLabel) { IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest, SecondaryLabel) {
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -806,6 +817,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
ServiceSuspendedDuringDownload) { ServiceSuspendedDuringDownload) {
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -849,6 +861,7 @@ IN_PROC_BROWSER_TEST_F(HoldingSpaceDisplayClientBrowserTest,
// Create an in-progress download that can be canceled and paused. // Create an in-progress download that can be canceled and paused.
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->cancellable = true; download->cancellable = true;

@@ -144,6 +144,8 @@ const char* GetMetricString(CommandType command) {
switch (command) { switch (command) {
case CommandType::kCancel: case CommandType::kCancel:
return "DownloadNotificationV2.Button_Cancel"; return "DownloadNotificationV2.Button_Cancel";
case CommandType::kCopyToClipboard:
return "DownloadNotificationV2.Button_CopyToClipboard";
case CommandType::kOpenFile: case CommandType::kOpenFile:
return "DownloadNotificationV2.Click_Completed"; return "DownloadNotificationV2.Click_Completed";
case CommandType::kPause: case CommandType::kPause:
@@ -167,6 +169,7 @@ bool IsBodyClickCommandType(CommandType command) {
case CommandType::kShowInBrowser: case CommandType::kShowInBrowser:
return true; return true;
case CommandType::kCancel: case CommandType::kCancel:
case CommandType::kCopyToClipboard:
case CommandType::kPause: case CommandType::kPause:
case CommandType::kResume: case CommandType::kResume:
case CommandType::kShowInFolder: case CommandType::kShowInFolder:
@@ -180,6 +183,7 @@ bool IsBodyClickCommandType(CommandType command) {
bool IsButtonClickCommandType(CommandType command) { bool IsButtonClickCommandType(CommandType command) {
switch (command) { switch (command) {
case CommandType::kCancel: case CommandType::kCancel:
case CommandType::kCopyToClipboard:
case CommandType::kPause: case CommandType::kPause:
case CommandType::kResume: case CommandType::kResume:
case CommandType::kShowInFolder: case CommandType::kShowInFolder:

@@ -53,6 +53,8 @@
#include "ui/aura/env_observer.h" #include "ui/aura/env_observer.h"
#include "ui/aura/test/find_window.h" #include "ui/aura/test/find_window.h"
#include "ui/aura/window.h" #include "ui/aura/window.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_unittest_util.h" #include "ui/gfx/image/image_unittest_util.h"
#include "ui/message_center/public/cpp/notification.h" #include "ui/message_center/public/cpp/notification.h"
@@ -75,7 +77,9 @@ using ::testing::_;
using ::testing::AllOf; using ::testing::AllOf;
using ::testing::Contains; using ::testing::Contains;
using ::testing::Each; using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq; using ::testing::Eq;
using ::testing::Field;
using ::testing::Mock; using ::testing::Mock;
using ::testing::NiceMock; using ::testing::NiceMock;
using ::testing::Not; using ::testing::Not;
@@ -123,6 +127,8 @@ int GetCommandTextId(CommandType command_type) {
switch (command_type) { switch (command_type) {
case CommandType::kCancel: case CommandType::kCancel:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL; return IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL;
case CommandType::kCopyToClipboard:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_COPY_TO_CLIPBOARD;
case CommandType::kOpenFile: case CommandType::kOpenFile:
NOTREACHED_NORETURN(); NOTREACHED_NORETURN();
case CommandType::kPause: case CommandType::kPause:
@@ -251,6 +257,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CancelDownload) {
})); }));
crosapi::mojom::DownloadStatusPtr uncancellable_download = crosapi::mojom::DownloadStatusPtr uncancellable_download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
uncancellable_download->cancellable = false; uncancellable_download->cancellable = false;
@@ -278,6 +285,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CancelDownload) {
})); }));
crosapi::mojom::DownloadStatusPtr cancellable_download = crosapi::mojom::DownloadStatusPtr cancellable_download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
cancellable_download->cancellable = true; cancellable_download->cancellable = true;
@@ -335,13 +343,13 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
[&notification_id](const message_center::Notification& notification) { [&notification_id](const message_center::Notification& notification) {
notification_id = notification.id(); notification_id = notification.id();
})); }));
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
CreateDownloadStatus(profile, crosapi::mojom::DownloadState::kComplete, profile, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New( crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*loop=*/false,
/*received_bytes=*/1024, /*received_bytes=*/1024,
/*total_bytes=*/1024, /*total_bytes=*/1024,
/*visible=*/false)); /*visible=*/false));
Update(download->Clone()); Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer()); Mock::VerifyAndClearExpectations(&service_observer());
@@ -384,6 +392,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
})); }));
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -418,9 +427,10 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
// still show. // still show.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CompleteDownload) { IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CompleteDownload) {
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
CreateDownloadStatus(profile, crosapi::mojom::DownloadState::kInProgress, profile,
/*progress=*/nullptr); /*extension=*/"txt", crosapi::mojom::DownloadState::kInProgress,
/*progress=*/nullptr);
EXPECT_FALSE(download->target_file_path); EXPECT_FALSE(download->target_file_path);
std::string notification_id; std::string notification_id;
@@ -544,6 +554,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -580,12 +591,14 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ImageDownload) {
notification_id = notification.id(); notification_id = notification.id();
})); }));
// Create a download. // Create an image download.
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
CreateInProgressDownloadStatus(profile, profile,
/*received_bytes=*/0, /*extension=*/"png", crosapi::mojom::DownloadState::kInProgress,
/*total_bytes=*/1024); crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*received_bytes=*/0,
/*total_bytes=*/1024, /*visible=*/true));
Update(download->Clone()); Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer()); Mock::VerifyAndClearExpectations(&service_observer());
@@ -614,6 +627,49 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ImageDownload) {
*large_image_view->original_image().bitmap(), *large_image_view->original_image().bitmap(),
gfx::test::CreateBitmap(/*width=*/360, gfx::test::CreateBitmap(/*width=*/360,
/*height=*/240, image_color))); /*height=*/240, image_color)));
// An in-progress image download's notification should not have a 'Copy to
// clipboard' button.
const std::u16string copy_to_clipboard_button_text =
l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kCopyToClipboard));
EXPECT_THAT(
popup_view->GetActionButtonsForTest(),
Not(Contains(Pointee(Property(&views::LabelButton::GetText,
Eq(copy_to_clipboard_button_text))))));
// Complete `download`. Then check action buttons.
MarkDownloadStatusCompleted(*download);
Update(download->Clone());
const std::vector<raw_ptr<views::LabelButton, VectorExperimental>>
action_buttons = popup_view->GetActionButtonsForTest();
EXPECT_THAT(
action_buttons,
ElementsAre(
Pointee(Property(&views::LabelButton::GetText,
Eq(l10n_util::GetStringUTF16(
GetCommandTextId(CommandType::kShowInFolder))))),
Pointee(Property(&views::LabelButton::GetText,
Eq(copy_to_clipboard_button_text)))));
// Click the 'Copy to clipboard' button. Then verify the click is recorded.
base::UserActionTester tester;
auto copy_to_clipboard_button_iter =
base::ranges::find(action_buttons, copy_to_clipboard_button_text,
&views::LabelButton::GetText);
ASSERT_NE(copy_to_clipboard_button_iter, action_buttons.cend());
test::Click(*copy_to_clipboard_button_iter, ui::EF_NONE);
EXPECT_EQ(
tester.GetActionCount("DownloadNotificationV2.Button_CopyToClipboard"),
1);
// Verify the filename in the clipboard as expected.
base::test::TestFuture<std::vector<ui::FileInfo>> test_future;
ui::Clipboard::GetForCurrentThread()->ReadFilenames(
ui::ClipboardBuffer::kCopyPaste,
/*data_dst=*/nullptr, test_future.GetCallback());
EXPECT_THAT(test_future.Get(),
ElementsAre(Field(&ui::FileInfo::path, *download->full_path)));
} }
// Verifies that the notification of a download with an unknown total bytes // Verifies that the notification of a download with an unknown total bytes
@@ -630,6 +686,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0); /*received_bytes=*/0);
Update(download->Clone()); Update(download->Clone());
@@ -668,6 +725,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
})); }));
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(), CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -691,6 +749,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
})); }));
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->pausable = true; download->pausable = true;
@@ -803,6 +862,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ShowInFolder) {
})); }));
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
Update(download->Clone()); Update(download->Clone());
@@ -860,6 +920,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile(); Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download = crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile, CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0, /*received_bytes=*/0,
/*total_bytes=*/1024); /*total_bytes=*/1024);
download->cancellable = true; download->cancellable = true;

@@ -9512,6 +9512,15 @@ should be able to be added at any place in this file.
</description> </description>
</action> </action>
<action name="DownloadNotificationV2.Button_CopyToClipboard">
<owner>andrewxu@chromium.org</owner>
<owner>cros-system-ui-productivity-eng@google.com</owner>
<description>
User clicks &quot;Copy to clipboard&quot; button on a download notification
with the downloads integration V2 feature enabled.
</description>
</action>
<action name="DownloadNotificationV2.Button_Pause"> <action name="DownloadNotificationV2.Button_Pause">
<owner>andrewxu@chromium.org</owner> <owner>andrewxu@chromium.org</owner>
<owner>cros-system-ui-productivity-eng@google.com</owner> <owner>cros-system-ui-productivity-eng@google.com</owner>