blob: f103ba596d2b06f0edec4295e7162defc9fb2478 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/clipboard/clipboard_history_controller_impl.h"
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <vector>
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/clipboard/clipboard_history_controller_delegate.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_menu_model_adapter.h"
#include "ash/clipboard/clipboard_history_resource_manager.h"
#include "ash/clipboard/clipboard_history_url_title_fetcher.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/clipboard_nudge_constants.h"
#include "ash/clipboard/clipboard_nudge_controller.h"
#include "ash/clipboard/scoped_clipboard_history_pause_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/display_util.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/color_util.h"
#include "ash/wm/window_util.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "base/one_shot_event.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/clipboard/clipboard_util.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/color/color_provider_source.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/menu/menu_controller.h"
#if BUILDFLAG(USE_XKBCOMMON)
#include "ui/events/keycodes/xkb_keysym.h"
#include "ui/events/ozone/layout/keyboard_layout_engine_manager.h"
#include "ui/events/ozone/layout/xkb/xkb_keyboard_layout_engine.h"
#endif
namespace ash {
namespace {
// Encodes `bitmap` and maps the corresponding ClipboardHistoryItem ID, `id, to
// the resulting PNG in `encoded_pngs`. This function should run on a background
// thread.
void EncodeBitmapToPNG(
base::OnceClosure barrier_callback,
std::map<base::UnguessableToken, std::vector<uint8_t>>* const encoded_pngs,
base::UnguessableToken id,
SkBitmap bitmap) {
auto png = ui::clipboard_util::EncodeBitmapToPng(bitmap);
// Don't acquire the lock until after the image encoding has finished.
static base::NoDestructor<base::Lock> map_lock;
base::AutoLock lock(*map_lock);
encoded_pngs->emplace(id, std::move(png));
std::move(barrier_callback).Run();
}
// Returns the clipboard instance for the current thread.
ui::ClipboardNonBacked* GetClipboard() {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
DCHECK(clipboard);
return clipboard;
}
// Returns the last active user pref service or `nullptr` if one does not exist.
PrefService* GetLastActiveUserPrefService() {
return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
}
// Returns the time when the menu was last shown for the user associated with
// the last active user pref service, or `std::nullopt` if the menu was not
// previously marked as having been shown.
std::optional<base::Time> GetMenuLastTimeShown() {
if (auto* prefs = GetLastActiveUserPrefService()) {
if (auto* pref = prefs->FindPreference(prefs::kMultipasteMenuLastTimeShown);
pref && !pref->IsDefaultValue()) {
return base::ValueToTime(pref->GetValue());
}
}
return std::nullopt;
}
// Marks the time when the menu was last shown for the user associated with the
// last active user pref service.
void MarkMenuLastTimeShown() {
if (auto* prefs = GetLastActiveUserPrefService()) {
prefs->SetTime(prefs::kMultipasteMenuLastTimeShown, base::Time::Now());
}
}
// Emits a user action indicating that the clipboard history item at menu index
// `command_id` was pasted.
void RecordMenuIndexPastedUserAction(int command_id) {
// Per guidance in user_metrics.h, use string literals for action names.
switch (command_id) {
case 1:
base::RecordAction(
base::UserMetricsAction("Ash_ClipboardHistory_PastedItem1"));
break;
case 2:
base::RecordAction(
base::UserMetricsAction("Ash_ClipboardHistory_PastedItem2"));
break;
case 3:
base::RecordAction(
base::UserMetricsAction("Ash_ClipboardHistory_PastedItem3"));
break;
case 4:
base::RecordAction(
base::UserMetricsAction("Ash_ClipboardHistory_PastedItem4"));
break;
case 5:
base::RecordAction(
base::UserMetricsAction("Ash_ClipboardHistory_PastedItem5"));
break;
default:
NOTREACHED();
}
}
void RecordPasteItemIndex(int index) {
CHECK_GE(index, clipboard_history_util::kFirstItemCommandId);
CHECK_LT(index, clipboard_history_util::kCommandIdBoundary);
// Record the paste item's history list index in a histogram to get a
// distribution of where in the list users paste from.
base::UmaHistogramExactLinear(
"Ash.ClipboardHistory.ContextMenu.MenuOptionSelected", index,
/*exclusive_max=*/clipboard_history_util::kCommandIdBoundary);
// Record the paste item's history list index as a user action to analyze
// usage patterns, e.g., how frequently the same index is pasted multiple
// times in a row.
RecordMenuIndexPastedUserAction(index);
}
#if BUILDFLAG(USE_XKBCOMMON)
// Looks up the DomCode assigned to the keysym. In some edge cases,
// such as Dvorak layout, the original DomCode may be different
// from US standard layout.
ui::DomCode LookUpXkbDomCode(int keysym) {
if (!base::SysInfo::IsRunningOnChromeOS()) {
// On linux-chromeos, stub layout engine is used.
return ui::DomCode::NONE;
}
auto* layout_engine =
ui::KeyboardLayoutEngineManager::GetKeyboardLayoutEngine();
if (!layout_engine) {
return ui::DomCode::NONE;
}
return static_cast<ui::XkbKeyboardLayoutEngine*>(layout_engine)
->GetDomCodeByKeysym(keysym, /*modifiers=*/std::nullopt);
}
#endif
ui::KeyEvent SyntheticCtrlV(ui::EventType type) {
ui::DomCode dom_code = ui::DomCode::NONE;
#if BUILDFLAG(USE_XKBCOMMON)
dom_code = LookUpXkbDomCode(XKB_KEY_v);
#endif
return dom_code == ui::DomCode::NONE
? ui::KeyEvent(type, ui::VKEY_V, ui::EF_CONTROL_DOWN)
: ui::KeyEvent(type, ui::VKEY_V, dom_code, ui::EF_CONTROL_DOWN);
}
ui::KeyEvent SyntheticCtrl(ui::EventType type) {
int flags = type == ui::ET_KEY_PRESSED ? ui::EF_CONTROL_DOWN : ui::EF_NONE;
ui::DomCode dom_code = ui::DomCode::NONE;
#if BUILDFLAG(USE_XKBCOMMON)
dom_code = LookUpXkbDomCode(XKB_KEY_Control_L);
#endif
return dom_code == ui::DomCode::NONE
? ui::KeyEvent(type, ui::VKEY_CONTROL, flags)
: ui::KeyEvent(type, ui::VKEY_CONTROL, dom_code, flags);
}
void SyntheticPaste(
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) {
auto* host = GetWindowTreeHostForDisplay(
display::Screen::GetScreen()->GetDisplayForNewWindows().id());
CHECK(host);
// Because we do not require the user to release Ctrl+V before selecting a
// clipboard history item to paste, the Ctrl+V event we synthesize below may
// be discarded as a perceived continuation of the long press. Preempt this
// scenario by issuing a Ctrl+V release to ensure that the press and release
// below are handled as an independent paste.
// TODO(http://b/283533126): Replace this workaround with a long-term fix.
if (paste_source == crosapi::mojom::ClipboardHistoryControllerShowSource::
kControlVLongpress) {
ui::KeyEvent v_release = SyntheticCtrlV(ui::ET_KEY_RELEASED);
host->DeliverEventToSink(&v_release);
ui::KeyEvent ctrl_release = SyntheticCtrl(ui::ET_KEY_RELEASED);
host->DeliverEventToSink(&ctrl_release);
}
ui::KeyEvent ctrl_press = SyntheticCtrl(ui::ET_KEY_PRESSED);
host->DeliverEventToSink(&ctrl_press);
ui::KeyEvent v_press = SyntheticCtrlV(ui::ET_KEY_PRESSED);
host->DeliverEventToSink(&v_press);
ui::KeyEvent v_release = SyntheticCtrlV(ui::ET_KEY_RELEASED);
host->DeliverEventToSink(&v_release);
ui::KeyEvent ctrl_release = SyntheticCtrl(ui::ET_KEY_RELEASED);
host->DeliverEventToSink(&ctrl_release);
}
using ClipboardHistoryPasteType =
ClipboardHistoryControllerImpl::ClipboardHistoryPasteType;
bool IsPlainTextPaste(ClipboardHistoryPasteType paste_type) {
switch (paste_type) {
case ClipboardHistoryPasteType::kPlainTextAccelerator:
case ClipboardHistoryPasteType::kPlainTextKeystroke:
case ClipboardHistoryPasteType::kPlainTextMouse:
case ClipboardHistoryPasteType::kPlainTextTouch:
case ClipboardHistoryPasteType::kPlainTextVirtualKeyboard:
case ClipboardHistoryPasteType::kPlainTextCtrlV:
return true;
case ClipboardHistoryPasteType::kRichTextAccelerator:
case ClipboardHistoryPasteType::kRichTextKeystroke:
case ClipboardHistoryPasteType::kRichTextMouse:
case ClipboardHistoryPasteType::kRichTextTouch:
case ClipboardHistoryPasteType::kRichTextVirtualKeyboard:
case ClipboardHistoryPasteType::kRichTextCtrlV:
return false;
}
}
ClipboardHistoryPasteType CalculatePasteType(
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source,
int event_flags) {
// There are no specific flags that indicate a paste triggered by a
// keystroke, so assume by default that keystroke was the event source
// and then check for the other known possibilities. This assumption may
// cause pastes from unknown sources to be incorrectly captured as
// keystroke pastes, but we do not expect such cases to significantly
// alter metrics.
const bool paste_plain_text = event_flags & ui::EF_SHIFT_DOWN;
if (paste_source ==
crosapi::mojom::ClipboardHistoryControllerShowSource::kVirtualKeyboard) {
return paste_plain_text
? ClipboardHistoryPasteType::kPlainTextVirtualKeyboard
: ClipboardHistoryPasteType::kRichTextVirtualKeyboard;
}
ClipboardHistoryPasteType paste_type =
paste_plain_text ? ClipboardHistoryPasteType::kPlainTextKeystroke
: ClipboardHistoryPasteType::kRichTextKeystroke;
if (event_flags & ui::EF_MOUSE_BUTTON) {
paste_type = paste_plain_text ? ClipboardHistoryPasteType::kPlainTextMouse
: ClipboardHistoryPasteType::kRichTextMouse;
} else if (event_flags & ui::EF_FROM_TOUCH) {
paste_type = paste_plain_text ? ClipboardHistoryPasteType::kPlainTextTouch
: ClipboardHistoryPasteType::kRichTextTouch;
}
return paste_type;
}
} // namespace
// ClipboardHistoryControllerImpl::AcceleratorTarget ---------------------------
class ClipboardHistoryControllerImpl::AcceleratorTarget
: public ui::AcceleratorTarget {
public:
explicit AcceleratorTarget(ClipboardHistoryControllerImpl* controller)
: controller_(controller),
delete_selected_(ui::Accelerator(
/*key_code=*/ui::VKEY_BACK,
/*modifiers=*/ui::EF_NONE,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
tab_navigation_(ui::Accelerator(
/*key_code=*/ui::VKEY_TAB,
/*modifiers=*/ui::EF_NONE,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
shift_tab_navigation_(ui::Accelerator(
/*key_code=*/ui::VKEY_TAB,
/*modifiers=*/ui::EF_SHIFT_DOWN,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
paste_first_item_(ui::Accelerator(
/*key_code=*/ui::VKEY_V,
/*modifiers=*/ui::EF_CONTROL_DOWN,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
paste_first_item_plaintext_(ui::Accelerator(
/*key_code=*/ui::VKEY_V,
/*modifiers=*/ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)) {}
AcceleratorTarget(const AcceleratorTarget&) = delete;
AcceleratorTarget& operator=(const AcceleratorTarget&) = delete;
~AcceleratorTarget() override = default;
void OnMenuShown() {
Shell::Get()->accelerator_controller()->Register(
{delete_selected_, tab_navigation_, shift_tab_navigation_,
paste_first_item_, paste_first_item_plaintext_},
/*target=*/this);
}
void OnMenuClosed() {
Shell::Get()->accelerator_controller()->UnregisterAll(/*target=*/this);
}
private:
// ui::AcceleratorTarget:
bool AcceleratorPressed(const ui::Accelerator& accelerator) override {
CHECK(controller_->IsMenuShowing());
if (accelerator == delete_selected_) {
HandleDeleteSelected();
} else if (accelerator == tab_navigation_) {
HandleTab();
} else if (accelerator == shift_tab_navigation_) {
HandleShiftTab();
} else if (accelerator == paste_first_item_) {
HandlePasteFirstItem(ClipboardHistoryPasteType::kRichTextCtrlV);
} else if (accelerator == paste_first_item_plaintext_) {
HandlePasteFirstItem(ClipboardHistoryPasteType::kPlainTextCtrlV);
} else {
NOTREACHED();
return false;
}
return true;
}
bool CanHandleAccelerators() const override {
return controller_->IsMenuShowing() ||
controller_->HasAvailableHistoryItems();
}
void HandleDeleteSelected() { controller_->DeleteSelectedMenuItemIfAny(); }
void HandleTab() { controller_->AdvancePseudoFocus(/*reverse=*/false); }
void HandleShiftTab() { controller_->AdvancePseudoFocus(/*reverse=*/true); }
void HandlePasteFirstItem(ClipboardHistoryPasteType paste_type) {
const auto first_item_command_id =
controller_->context_menu_->GetFirstMenuItemCommand();
CHECK(first_item_command_id);
controller_->PasteClipboardItemByCommandId(*first_item_command_id,
paste_type);
}
// The controller responsible for showing the clipboard history menu.
const raw_ptr<ClipboardHistoryControllerImpl> controller_;
// Deletes the selected menu item.
const ui::Accelerator delete_selected_;
// Moves the pseudo focus forward.
const ui::Accelerator tab_navigation_;
// Moves the pseudo focus backward.
const ui::Accelerator shift_tab_navigation_;
// Pastes the first item in the clipboard history menu.
const ui::Accelerator paste_first_item_;
// Pastes the plain text data of the first item in the clipboard history menu.
const ui::Accelerator paste_first_item_plaintext_;
};
// ClipboardHistoryControllerImpl::MenuDelegate --------------------------------
class ClipboardHistoryControllerImpl::MenuDelegate
: public ui::SimpleMenuModel::Delegate {
public:
explicit MenuDelegate(ClipboardHistoryControllerImpl* controller)
: controller_(controller) {}
MenuDelegate(const MenuDelegate&) = delete;
MenuDelegate& operator=(const MenuDelegate&) = delete;
// ui::SimpleMenuModel::Delegate:
void ExecuteCommand(int command_id, int event_flags) override {
controller_->ExecuteCommand(command_id, event_flags);
}
private:
// The controller responsible for showing the Clipboard History menu.
const raw_ptr<ClipboardHistoryControllerImpl> controller_;
};
// ClipboardHistoryControllerImpl ----------------------------------------------
ClipboardHistoryControllerImpl::ClipboardHistoryControllerImpl(
std::unique_ptr<ClipboardHistoryControllerDelegate> delegate)
: delegate_(std::move(delegate)),
image_model_factory_(delegate_->CreateImageModelFactory()),
url_title_fetcher_(delegate_->CreateUrlTitleFetcher()),
clipboard_history_(std::make_unique<ClipboardHistory>()),
resource_manager_(std::make_unique<ClipboardHistoryResourceManager>(
clipboard_history_.get())),
accelerator_target_(std::make_unique<AcceleratorTarget>(this)),
nudge_controller_(
std::make_unique<ClipboardNudgeController>(clipboard_history_.get())),
menu_delegate_(std::make_unique<MenuDelegate>(this)) {
if (!image_model_factory_ || !url_title_fetcher_) {
CHECK_IS_TEST();
}
clipboard_history_->AddObserver(this);
resource_manager_->AddObserver(this);
SessionController::Get()->AddObserver(this);
}
ClipboardHistoryControllerImpl::~ClipboardHistoryControllerImpl() {
SessionController::Get()->RemoveObserver(this);
resource_manager_->RemoveObserver(this);
clipboard_history_->RemoveObserver(this);
}
// static
void ClipboardHistoryControllerImpl::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
ClipboardNudgeController::RegisterProfilePrefs(registry);
registry->RegisterTimePref(prefs::kMultipasteMenuLastTimeShown, base::Time());
}
void ClipboardHistoryControllerImpl::Shutdown() {
if (IsMenuShowing()) {
context_menu_->Cancel(/*will_paste_item=*/false);
}
nudge_controller_.reset();
}
bool ClipboardHistoryControllerImpl::IsMenuShowing() const {
return context_menu_ && context_menu_->IsRunning();
}
void ClipboardHistoryControllerImpl::ToggleMenuShownByAccelerator(
bool is_plain_text_paste) {
if (IsMenuShowing()) {
// Before hiding the menu, paste the selected menu item, or the first item
// if none is selected.
PasteClipboardItemByCommandId(
context_menu_->GetSelectedMenuItemCommand().value_or(
clipboard_history_util::kFirstItemCommandId),
is_plain_text_paste ? ClipboardHistoryPasteType::kPlainTextAccelerator
: ClipboardHistoryPasteType::kRichTextAccelerator);
return;
}
// Do not allow the plain text shortcut to open the menu.
if (is_plain_text_paste) {
return;
}
if (clipboard_history_util::IsEnabledInCurrentMode() && IsEmpty()) {
nudge_controller_->ShowNudge(ClipboardNudgeType::kZeroStateNudge);
return;
}
ShowMenu(CalculateAnchorRect(), ui::MENU_SOURCE_KEYBOARD,
crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator);
}
void ClipboardHistoryControllerImpl::AddObserver(
ClipboardHistoryController::Observer* observer) {
observers_.AddObserver(observer);
}
void ClipboardHistoryControllerImpl::RemoveObserver(
ClipboardHistoryController::Observer* observer) {
observers_.RemoveObserver(observer);
}
bool ClipboardHistoryControllerImpl::ShowMenu(
const gfx::Rect& anchor_rect,
ui::MenuSourceType source_type,
crosapi::mojom::ClipboardHistoryControllerShowSource show_source) {
return ShowMenu(anchor_rect, source_type, show_source,
OnMenuClosingCallback());
}
bool ClipboardHistoryControllerImpl::ShowMenu(
const gfx::Rect& anchor_rect,
ui::MenuSourceType source_type,
crosapi::mojom::ClipboardHistoryControllerShowSource show_source,
OnMenuClosingCallback callback) {
if (IsMenuShowing() || !HasAvailableHistoryItems()) {
return false;
}
// Close the running context menu, if any, before showing the clipboard
// history menu.
if (auto* active_menu_instance = views::MenuController::GetActiveInstance()) {
active_menu_instance->Cancel(views::MenuController::ExitType::kAll);
}
last_menu_source_ = show_source;
// `Unretained()` is safe because `this` owns `context_menu_`.
context_menu_ = ClipboardHistoryMenuModelAdapter::Create(
menu_delegate_.get(), std::move(callback),
base::BindRepeating(&ClipboardHistoryControllerImpl::OnMenuClosed,
base::Unretained(this)),
clipboard_history_.get());
context_menu_->Run(anchor_rect, source_type, show_source,
GetMenuLastTimeShown(),
nudge_controller_->GetNudgeLastTimeShown());
CHECK(IsMenuShowing());
accelerator_target_->OnMenuShown();
// The first menu item should be selected by default after the clipboard
// history menu shows. Note that the menu item is selected asynchronously
// to avoid the interference from synthesized mouse events.
menu_task_timer_.Start(
FROM_HERE, base::TimeDelta(),
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>&
controller_weak_ptr) {
if (!controller_weak_ptr) {
return;
}
controller_weak_ptr->context_menu_->SelectMenuItemWithCommandId(
clipboard_history_util::kFirstItemCommandId);
if (controller_weak_ptr->initial_item_selected_callback_for_test_) {
controller_weak_ptr->initial_item_selected_callback_for_test_
.Run();
}
},
weak_ptr_factory_.GetWeakPtr()));
MarkMenuLastTimeShown();
base::UmaHistogramEnumeration("Ash.ClipboardHistory.ContextMenu.ShowMenu",
show_source);
for (auto& observer : observers_) {
observer.OnClipboardHistoryMenuShown(show_source);
}
return true;
}
bool ClipboardHistoryControllerImpl::IsEmpty() const {
return clipboard_history_->IsEmpty();
}
void ClipboardHistoryControllerImpl::FireItemUpdateNotificationTimerForTest() {
item_update_notification_timer_.FireNow();
}
void ClipboardHistoryControllerImpl::GetHistoryValues(
GetHistoryValuesCallback callback) const {
// Map of `ClipboardHistoryItem` IDs to their corresponding bitmaps.
std::map<base::UnguessableToken, SkBitmap> bitmaps_to_be_encoded;
for (auto& item : clipboard_history_->GetItems()) {
if (item.display_format() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kPng) {
const auto& maybe_png = item.data().maybe_png();
if (!maybe_png.has_value()) {
// The clipboard contains an image which has not yet been encoded to a
// PNG.
auto maybe_bitmap = item.data().GetBitmapIfPngNotEncoded();
DCHECK(maybe_bitmap.has_value());
bitmaps_to_be_encoded.emplace(item.id(),
std::move(maybe_bitmap.value()));
}
}
}
// Map of `ClipboardHistoryItem` IDs to their encoded PNGs.
auto encoded_pngs = std::make_unique<
std::map<base::UnguessableToken, std::vector<uint8_t>>>();
auto* encoded_pngs_ptr = encoded_pngs.get();
// Post back to this sequence once all images have been encoded.
base::RepeatingClosure barrier = base::BarrierClosure(
bitmaps_to_be_encoded.size(),
base::BindPostTaskToCurrentDefault(base::BindOnce(
&ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs,
weak_ptr_factory_.GetMutableWeakPtr(), std::move(callback),
std::move(encoded_pngs))));
// Encode images on background threads.
for (auto id_and_bitmap : bitmaps_to_be_encoded) {
base::ThreadPool::PostTask(
FROM_HERE, base::BindOnce(&EncodeBitmapToPNG, barrier, encoded_pngs_ptr,
std::move(id_and_bitmap.first),
std::move(id_and_bitmap.second)));
}
if (!new_bitmap_to_write_while_encoding_for_test_.isNull()) {
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteImage(new_bitmap_to_write_while_encoding_for_test_);
new_bitmap_to_write_while_encoding_for_test_.reset();
}
}
gfx::Rect ClipboardHistoryControllerImpl::GetMenuBoundsInScreenForTest() const {
return context_menu_->GetMenuBoundsInScreenForTest(); // IN-TEST
}
void ClipboardHistoryControllerImpl::BlockGetHistoryValuesForTest() {
get_history_values_blocker_for_test_.reset();
get_history_values_blocker_for_test_ = std::make_unique<base::OneShotEvent>();
}
void ClipboardHistoryControllerImpl::ResumeGetHistoryValuesForTest() {
DCHECK(get_history_values_blocker_for_test_);
get_history_values_blocker_for_test_->Signal();
}
void ClipboardHistoryControllerImpl::OnScreenshotNotificationCreated() {
nudge_controller_->MarkScreenshotNotificationShown();
}
bool ClipboardHistoryControllerImpl::HasAvailableHistoryItems() const {
return clipboard_history_util::IsEnabledInCurrentMode() && !IsEmpty();
}
std::unique_ptr<ScopedClipboardHistoryPause>
ClipboardHistoryControllerImpl::CreateScopedPause() {
return std::make_unique<ScopedClipboardHistoryPauseImpl>(
clipboard_history_.get());
}
void ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs(
GetHistoryValuesCallback callback,
std::unique_ptr<std::map<base::UnguessableToken, std::vector<uint8_t>>>
encoded_pngs) {
// If a test is performing some work that must be done before history values
// are returned, wait to run this function until that work is finished.
if (get_history_values_blocker_for_test_ &&
!get_history_values_blocker_for_test_->is_signaled()) {
get_history_values_blocker_for_test_->Post(
FROM_HERE,
base::BindOnce(
&ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
std::move(encoded_pngs)));
return;
}
std::vector<ClipboardHistoryItem> item_results;
// Check after asynchronous PNG encoding finishes to make sure we have not
// entered a state where clipboard history is disabled, e.g., a locked screen.
if (!clipboard_history_util::IsEnabledInCurrentMode()) {
std::move(callback).Run(std::move(item_results));
return;
}
bool all_images_encoded = true;
for (auto& item : clipboard_history_->GetItems()) {
if (item.display_format() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kPng &&
!item.data().maybe_png().has_value()) {
// The clipboard contains an image which has not yet been encoded to a
// PNG. Hopefully we just finished encoding and the PNG can be found
// in `encoded_pngs`; otherwise this item was added while other PNGs
// were being encoded.
auto png_it = encoded_pngs->find(item.id());
if (png_it == encoded_pngs->end()) {
// Can't find the encoded PNG. We'll need to restart
// `GetHistoryValues()` from the top, but allow this for loop to finish
// to let PNGs we've already encoded get set to their appropriate
// clipboards, to avoid re-encoding.
all_images_encoded = false;
} else {
item.data().SetPngDataAfterEncoding(std::move(png_it->second));
}
}
item_results.emplace_back(item);
}
if (!all_images_encoded) {
GetHistoryValues(std::move(callback));
return;
}
std::move(callback).Run(std::move(item_results));
}
std::vector<std::string> ClipboardHistoryControllerImpl::GetHistoryItemIds()
const {
std::vector<std::string> item_ids;
if (HasAvailableHistoryItems()) {
for (const auto& item : history()->GetItems()) {
item_ids.push_back(item.id().ToString());
}
}
return item_ids;
}
bool ClipboardHistoryControllerImpl::PasteClipboardItemById(
const std::string& item_id,
int event_flags,
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) {
const std::list<ClipboardHistoryItem>& history_items = history()->GetItems();
auto iter_by_id = std::find_if(history_items.cbegin(), history_items.cend(),
[&item_id](const ClipboardHistoryItem& item) {
return item.id().ToString() == item_id;
});
if (iter_by_id == history_items.cend()) {
return false;
}
RecordPasteItemIndex(std::distance(history_items.cbegin(), iter_by_id) +
clipboard_history_util::kFirstItemCommandId);
MaybePostPasteTask(*iter_by_id, CalculatePasteType(paste_source, event_flags),
paste_source);
return true;
}
bool ClipboardHistoryControllerImpl::DeleteClipboardItemById(
const std::string& item_id) {
for (const auto& item : history()->GetItems()) {
if (item.id().ToString() == item_id) {
DeleteClipboardHistoryItem(item);
return true;
}
}
return false;
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item,
bool is_duplicate) {
PostItemUpdateNotificationTask();
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryItemRemoved(
const ClipboardHistoryItem& item) {
PostItemUpdateNotificationTask();
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryCleared() {
// Prevent clipboard contents from being restored if the clipboard history is
// cleared shortly after pasting an item.
weak_ptr_factory_.InvalidateWeakPtrs();
// Notify observers of the history being cleared after invalidating weak
// pointers.
PostItemUpdateNotificationTask();
// Make sure the menu is closed now that there are no items to show.
if (IsMenuShowing()) {
context_menu_->Cancel(/*will_paste_item=*/false);
}
}
void ClipboardHistoryControllerImpl::OnOperationConfirmed(bool copy) {
static int confirmed_paste_count = 0;
// Here we assume that a paste operation from the clipboard history menu never
// interleaves with a user-initiated copy or paste operation from another
// source, such as pressing the ctrl-v accelerator or clicking a context menu
// option. In other words, when `pastes_to_be_confirmed_` is positive, the
// next confirmed operation is expected to be a paste from clipboard history.
// This assumption should hold in most cases given that the clipboard history
// menu is always closed after one paste, and it usually takes a relatively
// long time for a user to perform the next copy or paste. For this metric, we
// tolerate a small margin of error.
if (pastes_to_be_confirmed_ > 0 && !copy) {
++confirmed_paste_count;
--pastes_to_be_confirmed_;
} else {
// Note that both copies and pastes from the standard clipboard cause the
// clipboard history consecutive paste count to be emitted and reset.
if (confirmed_paste_count > 0) {
base::UmaHistogramCounts100("Ash.ClipboardHistory.ConsecutivePastes",
confirmed_paste_count);
confirmed_paste_count = 0;
}
if (copy) {
// Record copy actions once they are confirmed, rather than when clipboard
// data first changes, to allow multiple data changes to be debounced into
// a single copy operation. This ensures that each user-initiated copy is
// recorded only once. See `ClipboardHistory::OnDataChanged()` for further
// explanation.
base::RecordAction(base::UserMetricsAction("Ash_Clipboard_CopiedItem"));
} else {
// Pastes from clipboard history are already recorded in
// `PasteMenuItemData()`. Here, we record just pastes from the standard
// clipboard, to see how standard clipboard pastes interleave with
// clipboard history pastes.
base::RecordAction(base::UserMetricsAction("Ash_Clipboard_PastedItem"));
}
// Verify that this operation did not interleave with a clipboard history
// paste.
DCHECK_EQ(pastes_to_be_confirmed_, 0);
// Whether or not the non-interleaving assumption has held, always reset
// `pastes_to_be_confirmed_` to prevent standard clipboard pastes from
// possibly being counted as clipboard history pastes, which could
// significantly affect the clipboard history consecutive pastes metric.
pastes_to_be_confirmed_ = 0;
}
// Callback will be run after clipboard data restoration.
if (confirmed_operation_callback_for_test_ && !clipboard_data_replaced_) {
confirmed_operation_callback_for_test_.Run(/*success=*/true);
}
}
void ClipboardHistoryControllerImpl::OnCachedImageModelUpdated(
const std::vector<base::UnguessableToken>& menu_item_ids) {
PostItemUpdateNotificationTask();
}
void ClipboardHistoryControllerImpl::OnSessionStateChanged(
session_manager::SessionState state) {
PostItemUpdateNotificationTask();
}
void ClipboardHistoryControllerImpl::OnLoginStatusChanged(
LoginStatus login_status) {
PostItemUpdateNotificationTask();
}
void ClipboardHistoryControllerImpl::PostItemUpdateNotificationTask() {
// Uses the async task to debounce multiple clipboard history changes in
// short duration. Restart the timer if it is running.
// This is done to avoid notifying observers multiple times if there are
// multiple clipboard history changes in a short period. For example, if the
// clipboard history reaches the cache limit and a new clipboard history item
// arrives at the same time, there would be two clipboard history changes: the
// addition of the new item and the removal of an obsolete item. In this case,
// this class should only notify observers only once.
item_update_notification_timer_.Start(
FROM_HERE, base::TimeDelta(),
base::BindOnce(
&ClipboardHistoryControllerImpl::MaybeNotifyObserversOfItemUpdate,
weak_ptr_factory_.GetWeakPtr()));
}
void ClipboardHistoryControllerImpl::MaybeNotifyObserversOfItemUpdate() {
const bool has_available_items = HasAvailableHistoryItems();
if (!has_available_items && !has_available_items_in_last_update_) {
// There are no available items, and there were none in the last
// notification either. Nothing has changed, so return early.
return;
}
for (auto& observer : observers_) {
observer.OnClipboardHistoryItemsUpdated();
}
has_available_items_in_last_update_ = has_available_items;
}
void ClipboardHistoryControllerImpl::ExecuteCommand(int command_id,
int event_flags) {
DCHECK(context_menu_);
DCHECK_GE(command_id, clipboard_history_util::kFirstItemCommandId);
DCHECK_LE(command_id, clipboard_history_util::kMaxItemCommandId);
using Action = clipboard_history_util::Action;
Action action = context_menu_->GetActionForCommandId(command_id);
switch (action) {
case Action::kPaste:
PasteClipboardItemByCommandId(
command_id, CalculatePasteType(last_menu_source_, event_flags));
return;
case Action::kDelete:
DeleteItemWithCommandId(command_id);
return;
case Action::kSelect:
context_menu_->SelectMenuItemWithCommandId(command_id);
return;
case Action::kSelectItemHoveredByMouse:
context_menu_->SelectMenuItemHoveredByMouse();
return;
case Action::kEmpty:
DUMP_WILL_BE_NOTREACHED_NORETURN();
return;
}
}
void ClipboardHistoryControllerImpl::PasteClipboardItemByCommandId(
int command_id,
ClipboardHistoryPasteType paste_type) {
// Force close the context menu. Failure to do so before dispatching our
// synthetic key event will result in the context menu consuming the event.
// When closing the menu, indicate that the menu is closing because of an
// imminent paste. Note that in some cases, this will indicate paste intent
// for pastes that ultimately fail. For now, this is an acceptable inaccuracy.
CHECK(context_menu_);
context_menu_->Cancel(/*will_paste_item=*/true);
// `command_id` should match the pasted item's index in `context_menu_`.
RecordPasteItemIndex(command_id);
MaybePostPasteTask(context_menu_->GetItemFromCommandId(command_id),
paste_type, last_menu_source_);
}
void ClipboardHistoryControllerImpl::MaybePostPasteTask(
const ClipboardHistoryItem& item,
ClipboardHistoryPasteType paste_type,
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) {
// Deactivate ClipboardImageModelFactory prior to pasting to ensure that any
// modifications to the clipboard for HTML rendering purposes are reversed.
// This factory may be nullptr in tests.
if (auto* clipboard_image_factory = ClipboardImageModelFactory::Get()) {
clipboard_image_factory->Deactivate();
}
if (auto* active_window = window_util::GetActiveWindow()) {
// Paste asynchronously to ensure ARC windows handle paste events correctly.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&ClipboardHistoryControllerImpl::PasteClipboardHistoryItem,
weak_ptr_factory_.GetWeakPtr(), active_window, item, paste_type,
paste_source));
}
}
void ClipboardHistoryControllerImpl::PasteClipboardHistoryItem(
aura::Window* intended_window,
ClipboardHistoryItem item,
ClipboardHistoryPasteType paste_type,
crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) {
// Return early if any of these conditions occur:
// 1. The original clipboard data has been replaced by an in-progress
// clipboard history paste.
// 2. The active window has changed.
// 3. The clipboard history feature is disabled under the current mode.
if (clipboard_data_replaced_ || !intended_window ||
intended_window != window_util::GetActiveWindow() ||
!clipboard_history_util::IsEnabledInCurrentMode()) {
if (confirmed_operation_callback_for_test_)
confirmed_operation_callback_for_test_.Run(/*success=*/false);
return;
}
// Get information about the data to be pasted.
bool paste_plain_text = IsPlainTextPaste(paste_type);
auto* clipboard = GetClipboard();
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
const auto* current_clipboard_data = clipboard->GetClipboardData(&data_dst);
// Clipboard history pastes are performed by temporarily writing data to the
// system clipboard, if necessary, and then issuing a standard paste.
// Determine the data we should temporarily write to the clipboard, if any, so
// that we can paste the selected history item.
std::unique_ptr<ui::ClipboardData> data_to_paste;
if (paste_plain_text) {
data_to_paste = std::make_unique<ui::ClipboardData>();
data_to_paste->set_commit_time(item.data().commit_time());
data_to_paste->set_text(item.data().text());
auto data_src = item.data().source();
if (data_src) {
data_to_paste->set_source(data_src);
}
} else if (!current_clipboard_data ||
*current_clipboard_data != item.data()) {
data_to_paste = std::make_unique<ui::ClipboardData>(item.data());
}
// Pause changes to clipboard history while manipulating the clipboard.
std::unique_ptr<ui::ClipboardData> replaced_data;
// If necessary, replace the clipboard's current data before issuing a paste.
if (data_to_paste) {
ScopedClipboardHistoryPauseImpl scoped_pause(
clipboard_history_.get(),
clipboard_history_util::PauseBehavior::kDefault);
replaced_data =
GetClipboard()->WriteClipboardData(std::move(data_to_paste));
clipboard_data_replaced_ = !!replaced_data;
}
++pastes_to_be_confirmed_;
// Use synthetic pastes as a fallback solution.
if (!delegate_->Paste()) {
SyntheticPaste(paste_source);
}
clipboard_history_util::RecordClipboardHistoryItemPasted(item);
base::UmaHistogramEnumeration("Ash.ClipboardHistory.PasteType", paste_type);
base::UmaHistogramEnumeration("Ash.ClipboardHistory.PasteSource",
paste_source);
for (auto& observer : observers_) {
observer.OnClipboardHistoryPasted();
}
// If the clipboard was not changed--i.e., we pasted the full data on the
// clipboard--then we are done modifying the clipboard buffer.
if (!replaced_data) {
return;
}
// Restore the clipboard data asynchronously. Some apps take a long time to
// receive the paste event, and some apps will read from the clipboard
// multiple times per paste. Wait a bit before writing `data_to_restore` back
// to the clipboard.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>& weak_ptr,
std::unique_ptr<ui::ClipboardData> data_to_restore) {
std::unique_ptr<ScopedClipboardHistoryPauseImpl> scoped_pause;
if (weak_ptr) {
// When restoring the original clipboard content, pause clipboard
// history to avoid committing data already at the top of the
// clipboard history list.
scoped_pause = std::make_unique<ScopedClipboardHistoryPauseImpl>(
weak_ptr->clipboard_history_.get(),
clipboard_history_util::PauseBehavior::kDefault);
}
GetClipboard()->WriteClipboardData(std::move(data_to_restore));
if (weak_ptr) {
weak_ptr->clipboard_data_replaced_ = false;
// Confirm the operation after data restoration if needed.
if (auto& callback_for_test =
weak_ptr->confirmed_operation_callback_for_test_;
callback_for_test && !weak_ptr->pastes_to_be_confirmed_) {
callback_for_test.Run(/*success=*/true);
}
}
},
weak_ptr_factory_.GetWeakPtr(), std::move(replaced_data)),
buffer_restoration_delay_for_test_.value_or(base::Milliseconds(200)));
}
void ClipboardHistoryControllerImpl::DeleteSelectedMenuItemIfAny() {
DCHECK(context_menu_);
auto selected_command = context_menu_->GetSelectedMenuItemCommand();
// Return early if no item is selected.
if (!selected_command.has_value())
return;
DeleteItemWithCommandId(*selected_command);
}
void ClipboardHistoryControllerImpl::DeleteItemWithCommandId(int command_id) {
DCHECK(context_menu_);
// Pressing VKEY_DELETE is handled here via AcceleratorTarget because the
// contextual menu consumes the key event. Record the "pressing the delete
// button" histogram here because this action does the same thing as
// activating the button directly via click/tap. There is no special handling
// for pasting an item via VKEY_RETURN because in that case the menu does not
// process the key event.
const auto& to_be_deleted_item =
context_menu_->GetItemFromCommandId(command_id);
DeleteClipboardHistoryItem(to_be_deleted_item);
// If the item to be deleted is the last one, close the whole menu.
if (context_menu_->GetMenuItemsCount() == 1) {
context_menu_->Cancel(/*will_paste_item=*/false);
return;
}
context_menu_->RemoveMenuItemWithCommandId(command_id);
}
void ClipboardHistoryControllerImpl::DeleteClipboardHistoryItem(
const ClipboardHistoryItem& item) {
clipboard_history_util::RecordClipboardHistoryItemDeleted(item);
clipboard_history_->RemoveItemForId(item.id());
}
void ClipboardHistoryControllerImpl::AdvancePseudoFocus(bool reverse) {
DCHECK(context_menu_);
context_menu_->AdvancePseudoFocus(reverse);
}
gfx::Rect ClipboardHistoryControllerImpl::CalculateAnchorRect() const {
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto* host = GetWindowTreeHostForDisplay(display.id());
// Some web apps render the caret in an IFrame, and we will not get the
// bounds in that case.
// TODO(https://crbug.com/1099930): Show the menu in the middle of the
// webview if the bounds are empty.
ui::TextInputClient* text_input_client =
host->GetInputMethod()->GetTextInputClient();
// `text_input_client` may be null. For example, in clamshell mode and without
// any window open.
const gfx::Rect textfield_bounds =
text_input_client ? text_input_client->GetCaretBounds() : gfx::Rect();
// Note that the width of caret's bounds may be zero in some views (such as
// the search bar of Google search web page). So we cannot use
// gfx::Size::IsEmpty() here. In addition, the applications using IFrame may
// provide unreliable `textfield_bounds` which are not fully contained by the
// display bounds.
const bool textfield_bounds_are_valid =
textfield_bounds.size() != gfx::Size() &&
IsRectContainedByAnyDisplay(textfield_bounds);
if (textfield_bounds_are_valid)
return textfield_bounds;
return gfx::Rect(display::Screen::GetScreen()->GetCursorScreenPoint(),
gfx::Size());
}
void ClipboardHistoryControllerImpl::OnMenuClosed() {
accelerator_target_->OnMenuClosed();
// Reset `context_menu_` in the asynchronous way. Because the menu may be
// accessed after `OnMenuClosed()` is called.
menu_task_timer_.Start(
FROM_HERE, base::TimeDelta(),
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>&
controller_weak_ptr) {
if (controller_weak_ptr)
controller_weak_ptr->context_menu_.reset();
},
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace ash