blob: 2aed8139e4f730e0f20bca1cc26672e8b7672534 [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.h"
#include <deque>
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/scoped_clipboard_history_pause_impl.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
#include "base/token.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_monitor.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
namespace ash {
using PauseBehavior = clipboard_history_util::PauseBehavior;
ClipboardHistory::ClipboardHistory() {
ui::ClipboardMonitor::GetInstance()->AddObserver(this);
}
ClipboardHistory::~ClipboardHistory() {
ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);
}
void ClipboardHistory::AddObserver(Observer* observer) const {
observers_.AddObserver(observer);
}
void ClipboardHistory::RemoveObserver(Observer* observer) const {
observers_.RemoveObserver(observer);
}
const std::list<ClipboardHistoryItem>& ClipboardHistory::GetItems() const {
return history_list_;
}
std::list<ClipboardHistoryItem>& ClipboardHistory::GetItems() {
return history_list_;
}
void ClipboardHistory::Clear() {
history_list_ = std::list<ClipboardHistoryItem>();
SyncClipboardToClipboardHistory();
for (auto& observer : observers_)
observer.OnClipboardHistoryCleared();
}
bool ClipboardHistory::IsEmpty() const {
return GetItems().empty();
}
void ClipboardHistory::RemoveItemForId(const base::UnguessableToken& id) {
auto iter = base::ranges::find(history_list_, id, &ClipboardHistoryItem::id);
// It is possible that the item specified by `id` has been removed. For
// example, `history_list_` has reached its maximum capacity. while the
// clipboard history menu is showing, a new item is added to `history_list_`.
// Then the user wants to delete the item which has already been removed due
// to overflow in `history_list_`.
if (iter == history_list_.cend())
return;
auto removed = std::move(*iter);
history_list_.erase(iter);
SyncClipboardToClipboardHistory();
for (auto& observer : observers_)
observer.OnClipboardHistoryItemRemoved(removed);
}
void ClipboardHistory::OnClipboardDataChanged() {
if (!clipboard_history_util::IsEnabledInCurrentMode())
return;
if (!pauses_.empty() &&
pauses_.front().pause_behavior == PauseBehavior::kDefault) {
return;
}
// The clipboard may not exist in tests.
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
if (!clipboard)
return;
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
const auto* clipboard_data = clipboard->GetClipboardData(&data_dst);
if (!clipboard_data) {
// `clipboard_data` is only empty when the clipboard is cleared. This is
// done to prevent data leakage into or from locked states (e.g., locked
// fullscreen). Clipboard history should also be cleared in this case.
commit_data_weak_factory_.InvalidateWeakPtrs();
Clear();
return;
}
// We post a task to commit `clipboard_data` at the end of the current task
// sequence to debounce the case where multiple copies are programmatically
// performed. Since only the most recent copy will be at the top of the
// clipboard, the user will likely be unaware of the intermediate copies that
// took place opaquely in the same task sequence and would be confused to see
// them in history. A real-world example would be copying the URL from the
// address bar in the browser. First a short form of the URL is copied,
// followed immediately by the long-form URL.
commit_data_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::MaybeCommitData,
commit_data_weak_factory_.GetWeakPtr(), *clipboard_data,
/*is_reorder_on_paste=*/!pauses_.empty() &&
pauses_.front().pause_behavior ==
PauseBehavior::kAllowReorderOnPaste));
// If clipboard history was paused with a contingency that allowed data to be
// committed, the operation that changed clipboard data was not a user's copy.
if (pauses_.empty()) {
// Debounce calls to `OnClipboardOperation()`. Certain surfaces
// (Omnibox) may read/write to the clipboard multiple times in one
// user-initiated operation. Add a delay because `PostTask()` is too fast to
// debounce multiple operations through the async web clipboard API. See
// https://crbug.com/1167403.
clipboard_histogram_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::OnClipboardOperation,
clipboard_histogram_weak_factory_.GetWeakPtr(),
/*copy=*/true),
base::Milliseconds(100));
}
}
void ClipboardHistory::OnClipboardDataRead() {
if (!pauses_.empty())
return;
// Debounce calls to `OnClipboardOperation()`. Certain surfaces
// (Omnibox) may read/write to the clipboard multiple times in one
// user-initiated operation. Add a delay because `PostTask()` is too fast to
// debounce multiple operations through the async web clipboard API. See
// https://crbug.com/1167403.
clipboard_histogram_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::OnClipboardOperation,
clipboard_histogram_weak_factory_.GetWeakPtr(),
/*copy=*/false),
base::Milliseconds(100));
}
void ClipboardHistory::OnClipboardOperation(bool copy) {
for (auto& observer : observers_)
observer.OnOperationConfirmed(copy);
using Operation = clipboard_history_util::Operation;
base::UmaHistogramEnumeration("Ash.ClipboardHistory.Operation",
copy ? Operation::kCopy : Operation::kPaste);
if (copy) {
consecutive_copies_++;
if (consecutive_pastes_ > 0) {
base::UmaHistogramCounts100("Ash.Clipboard.ConsecutivePastes",
consecutive_pastes_);
consecutive_pastes_ = 0;
}
} else {
// Note: This includes pastes by the clipboard history menu.
consecutive_pastes_++;
if (consecutive_copies_ > 0) {
base::UmaHistogramCounts100("Ash.Clipboard.ConsecutiveCopies",
consecutive_copies_);
consecutive_copies_ = 0;
}
}
}
base::WeakPtr<ClipboardHistory> ClipboardHistory::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void ClipboardHistory::SyncClipboardToClipboardHistory() {
// The clipboard may not exist in tests.
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
if (!clipboard)
return;
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
const auto* clipboard_data = clipboard->GetClipboardData(&data_dst);
// Only modify the clipboard if doing so would change its data, so as to avoid
// extraneous notifications to clipboard observers. If there is a change to
// make, pause clipboard history so that making the clipboard consistent with
// clipboard history does not cause clipboard history to update again.
ScopedClipboardHistoryPauseImpl scoped_pause(this);
if (history_list_.empty()) {
if (clipboard_data) {
static_cast<ui::Clipboard*>(clipboard)->Clear(
ui::ClipboardBuffer::kCopyPaste);
}
} else if (const auto& top_of_history_data = history_list_.front().data();
top_of_history_data != *clipboard_data) {
clipboard->WriteClipboardData(
std::make_unique<ui::ClipboardData>(top_of_history_data));
}
}
void ClipboardHistory::MaybeCommitData(ui::ClipboardData data,
bool is_reorder_on_paste) {
if (!clipboard_history_util::IsSupported(data))
return;
auto iter =
base::ranges::find(history_list_, data, &ClipboardHistoryItem::data);
bool is_duplicate = iter != history_list_.cend();
if (is_duplicate) {
// If `data` already exists in `history_list_` then move its corresponding
// item to the front of the list instead of creating a new item, because
// creating a new item will result in a new unique identifier. Replace the
// item's underlying clipboard data for consistency with the clipboard's
// current state.
iter->ReplaceEquivalentData(std::move(data));
history_list_.splice(history_list_.begin(), history_list_, iter);
using ReorderType = clipboard_history_util::ReorderType;
base::UmaHistogramEnumeration(
"Ash.ClipboardHistory.ReorderType",
is_reorder_on_paste ? ReorderType::kOnPaste : ReorderType::kOnCopy);
} else {
DCHECK(!is_reorder_on_paste);
history_list_.emplace_front(std::move(data));
}
for (auto& observer : observers_)
observer.OnClipboardHistoryItemAdded(history_list_.front(), is_duplicate);
if (history_list_.size() > clipboard_history_util::kMaxClipboardItems) {
auto removed = std::move(history_list_.back());
history_list_.pop_back();
for (auto& observer : observers_)
observer.OnClipboardHistoryItemRemoved(removed);
}
}
const base::Token& ClipboardHistory::Pause(PauseBehavior pause_behavior) {
pauses_.push_front({base::Token::CreateRandom(), pause_behavior});
return pauses_.front().pause_id;
}
void ClipboardHistory::Resume(const base::Token& pause_id) {
auto pause_it = base::ranges::find(pauses_, pause_id, &PauseInfo::pause_id);
DCHECK(pause_it != pauses_.end());
pauses_.erase(pause_it);
}
} // namespace ash