#include "ash/clipboard/clipboard_history.h"
#include <algorithm>
#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/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 = std::ranges::find(history_list_, id, &ClipboardHistoryItem::id);
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;
}
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) {
commit_data_weak_factory_.InvalidateWeakPtrs();
Clear();
return;
}
commit_data_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::MaybeCommitData,
commit_data_weak_factory_.GetWeakPtr(), *clipboard_data,
!pauses_.empty() &&
pauses_.front().pause_behavior ==
PauseBehavior::kAllowReorderOnPaste));
if (pauses_.empty()) {
clipboard_histogram_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::OnClipboardOperation,
clipboard_histogram_weak_factory_.GetWeakPtr(),
true),
base::Milliseconds(100));
}
}
void ClipboardHistory::OnClipboardDataRead() {
if (!pauses_.empty())
return;
clipboard_histogram_weak_factory_.InvalidateWeakPtrs();
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::OnClipboardOperation,
clipboard_histogram_weak_factory_.GetWeakPtr(),
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 {
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() {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
if (!clipboard)
return;
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
const auto* clipboard_data = clipboard->GetClipboardData(&data_dst);
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 =
std::ranges::find(history_list_, data, &ClipboardHistoryItem::data);
bool is_duplicate = iter != history_list_.cend();
if (is_duplicate) {
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 = std::ranges::find(pauses_, pause_id, &PauseInfo::pause_id);
DCHECK(pause_it != pauses_.end());
pauses_.erase(pause_it);
}
}