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.">
Cancel
</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.">
Pause
</message>

@ -0,0 +1 @@
78af3668f6d2945de8f98d57b602be639ddb81e8

@ -25,6 +25,12 @@
#include "chrome/browser/ui/ash/download_status/notification_display_client.h"
#include "chromeos/crosapi/mojom/download_controller.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 {
@ -134,6 +140,15 @@ std::optional<std::u16string> GetText(
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
// associated with `profile`.
void OpenFile(Profile* profile, const base::FilePath& file_path) {
@ -240,22 +255,38 @@ DisplayMetadata DisplayManager::CalculateDisplayMetadata(
&kResumeIcon, IDS_ASH_DOWNLOAD_COMMAND_TEXT_RESUME,
CommandType::kResume);
}
const base::FilePath& full_path = *download_status.full_path;
switch (download_status.state) {
case crosapi::mojom::DownloadState::kComplete:
// NOTE: `kOpenFile` is not shown so it doesn't require an icon/text_id.
command_infos.emplace_back(
base::BindRepeating(
&DisplayManager::PerformCommand, weak_ptr_factory_.GetWeakPtr(),
CommandType::kOpenFile, *download_status.full_path),
base::BindRepeating(&DisplayManager::PerformCommand,
weak_ptr_factory_.GetWeakPtr(),
CommandType::kOpenFile, full_path),
/*icon=*/nullptr, /*text_id=*/-1, CommandType::kOpenFile);
// NOTE: The `kShowInFolder` button does not have an icon.
command_infos.emplace_back(
base::BindRepeating(
&DisplayManager::PerformCommand, weak_ptr_factory_.GetWeakPtr(),
CommandType::kShowInFolder, *download_status.full_path),
base::BindRepeating(&DisplayManager::PerformCommand,
weak_ptr_factory_.GetWeakPtr(),
CommandType::kShowInFolder, full_path),
/*icon=*/nullptr, IDS_ASH_DOWNLOAD_COMMAND_TEXT_SHOW_IN_FOLDER,
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;
case crosapi::mojom::DownloadState::kInProgress:
// 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.file_path = *download_status.full_path;
display_metadata.file_path = full_path;
display_metadata.image = download_status.image;
display_metadata.progress = GetProgress(download_status);
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),
/*callback=*/base::DoNothing());
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:
OpenFile(profile_, std::get<base::FilePath>(param));
break;

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

@ -22,11 +22,12 @@ constexpr int64_t kUnknownTotalBytes = -1;
crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
Profile* profile,
std::string_view extension,
crosapi::mojom::DownloadState state,
crosapi::mojom::DownloadProgressPtr progress) {
crosapi::mojom::DownloadStatusPtr download_status =
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->progress = std::move(progress);
download_status->state = state;
@ -36,10 +37,11 @@ crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus(
Profile* profile,
std::string_view extension,
int64_t received_bytes,
const std::optional<int64_t>& total_bytes) {
return CreateDownloadStatus(
profile, crosapi::mojom::DownloadState::kInProgress,
profile, extension, crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false, received_bytes,
total_bytes.value_or(kUnknownTotalBytes), /*visible=*/true));

@ -6,6 +6,7 @@
#define CHROME_BROWSER_UI_ASH_DOWNLOAD_STATUS_DISPLAY_TEST_UTIL_H_
#include <optional>
#include <string_view>
#include "chromeos/crosapi/mojom/download_status_updater.mojom.h"
@ -13,10 +14,11 @@ class Profile;
namespace ash::download_status {
// Creates a download status associated with a file under the downloads
// directory of `profile`.
// Creates a download status associated with a file with the specified
// `extension` under the downloads directory of `profile`.
crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
Profile* profile,
std::string_view extension,
crosapi::mojom::DownloadState state,
crosapi::mojom::DownloadProgressPtr progress);
@ -24,6 +26,7 @@ crosapi::mojom::DownloadStatusPtr CreateDownloadStatus(
// with a file under the downloads directory of `profile`.
crosapi::mojom::DownloadStatusPtr CreateInProgressDownloadStatus(
Profile* profile,
std::string_view extension,
int64_t received_bytes,
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 <optional>
#include <utility>
#include <vector>
@ -28,14 +29,17 @@ namespace ash::download_status {
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
// `CommandType::kShowInBrowser` to `kOpenItem`, because `kOpenItem` is not
// accessible from a holding space chip's context menu.
HoldingSpaceCommandId ConvertCommandTypeToId(CommandType type) {
std::optional<HoldingSpaceCommandId> ConvertCommandTypeToId(CommandType type) {
switch (type) {
case CommandType::kCancel:
return HoldingSpaceCommandId::kCancelItem;
case CommandType::kCopyToClipboard:
return std::nullopt;
case CommandType::kOpenFile:
return HoldingSpaceCommandId::kOpenItem;
case CommandType::kPause:
@ -51,12 +55,16 @@ HoldingSpaceCommandId ConvertCommandTypeToId(CommandType type) {
}
}
// Returns the holding space item action corresponding to `type`.
holding_space_metrics::ItemAction ConvertCommandTypeToAction(CommandType type) {
// Returns the holding space item action corresponding to `type` if any. If
// there is no such action, returns `std::nullopt`.
std::optional<holding_space_metrics::ItemAction> ConvertCommandTypeToAction(
CommandType type) {
using ItemAction = holding_space_metrics::ItemAction;
switch (type) {
case CommandType::kCancel:
return ItemAction::kCancel;
case CommandType::kCopyToClipboard:
return std::nullopt;
case CommandType::kOpenFile:
return ItemAction::kLaunch;
case CommandType::kPause:
@ -121,23 +129,31 @@ void HoldingSpaceDisplayClient::AddOrUpdate(
// Generate in-progress commands from `display_metadata`.
std::vector<HoldingSpaceItem::InProgressCommand> in_progress_commands;
for (const auto& command_info : display_metadata.command_infos) {
if (const HoldingSpaceCommandId id =
ConvertCommandTypeToId(command_info.type);
holding_space_util::IsInProgressCommand(id)) {
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);
},
ConvertCommandTypeToAction(command_info.type),
command_info.command_callback));
const std::optional<HoldingSpaceCommandId> id =
ConvertCommandTypeToId(command_info.type);
const std::optional<holding_space_metrics::ItemAction> item_action =
ConvertCommandTypeToAction(command_info.type);
// Skip `command_info` if:
// 1. It does not have a corresponding ID; OR
// 2. Its corresponding ID is not for an in-progress command; OR
// 3. It does not have a corresponding item action.
if (!id || !holding_space_util::IsInProgressCommand(*id) || !item_action) {
continue;
}
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.

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

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

@ -53,6 +53,8 @@
#include "ui/aura/env_observer.h"
#include "ui/aura/test/find_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/gfx/image/image_unittest_util.h"
#include "ui/message_center/public/cpp/notification.h"
@ -75,7 +77,9 @@ using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Not;
@ -123,6 +127,8 @@ int GetCommandTextId(CommandType command_type) {
switch (command_type) {
case CommandType::kCancel:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_CANCEL;
case CommandType::kCopyToClipboard:
return IDS_ASH_DOWNLOAD_COMMAND_TEXT_COPY_TO_CLIPBOARD;
case CommandType::kOpenFile:
NOTREACHED_NORETURN();
case CommandType::kPause:
@ -251,6 +257,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CancelDownload) {
}));
crosapi::mojom::DownloadStatusPtr uncancellable_download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
uncancellable_download->cancellable = false;
@ -278,6 +285,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CancelDownload) {
}));
crosapi::mojom::DownloadStatusPtr cancellable_download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
cancellable_download->cancellable = true;
@ -335,13 +343,13 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
[&notification_id](const message_center::Notification& notification) {
notification_id = notification.id();
}));
crosapi::mojom::DownloadStatusPtr download =
CreateDownloadStatus(profile, crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false,
/*received_bytes=*/1024,
/*total_bytes=*/1024,
/*visible=*/false));
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile, /*extension=*/"txt", crosapi::mojom::DownloadState::kComplete,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false,
/*received_bytes=*/1024,
/*total_bytes=*/1024,
/*visible=*/false));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
@ -384,6 +392,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
@ -418,9 +427,10 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
// still show.
IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, CompleteDownload) {
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateDownloadStatus(profile, crosapi::mojom::DownloadState::kInProgress,
/*progress=*/nullptr);
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"txt", crosapi::mojom::DownloadState::kInProgress,
/*progress=*/nullptr);
EXPECT_FALSE(download->target_file_path);
std::string notification_id;
@ -544,6 +554,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
@ -580,12 +591,14 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ImageDownload) {
notification_id = notification.id();
}));
// Create a download.
// Create an image download.
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*received_bytes=*/0,
/*total_bytes=*/1024);
crosapi::mojom::DownloadStatusPtr download = CreateDownloadStatus(
profile,
/*extension=*/"png", crosapi::mojom::DownloadState::kInProgress,
crosapi::mojom::DownloadProgress::New(
/*loop=*/false, /*received_bytes=*/0,
/*total_bytes=*/1024, /*visible=*/true));
Update(download->Clone());
Mock::VerifyAndClearExpectations(&service_observer());
@ -614,6 +627,49 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ImageDownload) {
*large_image_view->original_image().bitmap(),
gfx::test::CreateBitmap(/*width=*/360,
/*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
@ -630,6 +686,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0);
Update(download->Clone());
@ -668,6 +725,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(ProfileManager::GetActiveUserProfile(),
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
@ -691,6 +749,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
download->pausable = true;
@ -803,6 +862,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest, ShowInFolder) {
}));
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
Update(download->Clone());
@ -860,6 +920,7 @@ IN_PROC_BROWSER_TEST_F(NotificationDisplayClientBrowserTest,
Profile* const profile = ProfileManager::GetActiveUserProfile();
crosapi::mojom::DownloadStatusPtr download =
CreateInProgressDownloadStatus(profile,
/*extension=*/"txt",
/*received_bytes=*/0,
/*total_bytes=*/1024);
download->cancellable = true;

@ -9512,6 +9512,15 @@ should be able to be added at any place in this file.
</description>
</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">
<owner>andrewxu@chromium.org</owner>
<owner>cros-system-ui-productivity-eng@google.com</owner>