blob: 796da02a05e167dc1e41363c869f324ba4efb22a [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/clipboard_nudge_controller.h"
#include "base/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/threading/sequenced_task_runner_handle.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 {
namespace {
// The different operations ClipboardHistory sees. These values are written to
// logs. New enum values can be added, but existing enums must never be
// renumbered, deleted, or reused. Keep this up to date with the
// ClipboardHistoryOperation enum in enums.xml.
enum class ClipboardHistoryOperation {
// Copy, initiated through any method which triggers the clipboard to be
// written to.
kCopy = 0,
// Paste, detected when the clipboard is read.
kPaste = 1,
// Insert new types above this line.
kMaxValue = kPaste
};
void RecordClipboardHistoryOperation(ClipboardHistoryOperation operation) {
base::UmaHistogramEnumeration("Ash.ClipboardHistory.Operation", operation);
}
} // namespace
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_;
}
void ClipboardHistory::Clear() {
history_list_ = std::list<ClipboardHistoryItem>();
for (auto& observer : observers_)
observer.OnClipboardHistoryCleared();
}
bool ClipboardHistory::IsEmpty() const {
return GetItems().empty();
}
void ClipboardHistory::RemoveItemForId(const base::UnguessableToken& id) {
auto iter = std::find_if(history_list_.cbegin(), history_list_.cend(),
[&id](const auto& item) { return item.id() == 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);
for (auto& observer : observers_)
observer.OnClipboardHistoryItemRemoved(removed);
}
void ClipboardHistory::OnClipboardDataChanged() {
if (!ClipboardHistoryUtil::IsEnabledInCurrentMode())
return;
if (num_pause_ > 0)
return;
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
// Clipboard may not exist in tests.
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 forms(Locked Fullscreen
// state). Clear ClipboardHistory.
commit_data_weak_factory_.InvalidateWeakPtrs();
Clear();
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::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::OnClipboardOperation,
clipboard_histogram_weak_factory_.GetWeakPtr(),
/*copy=*/true),
base::Milliseconds(100));
// We post 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::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ClipboardHistory::MaybeCommitData,
commit_data_weak_factory_.GetWeakPtr(), *clipboard_data));
}
void ClipboardHistory::OnClipboardDataRead() {
if (num_pause_ > 0)
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::SequencedTaskRunnerHandle::Get()->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);
if (copy) {
RecordClipboardHistoryOperation(ClipboardHistoryOperation::kCopy);
consecutive_copies_++;
if (consecutive_pastes_ > 0) {
base::UmaHistogramCounts100("Ash.Clipboard.ConsecutivePastes",
consecutive_pastes_);
consecutive_pastes_ = 0;
}
return;
}
consecutive_pastes_++;
// NOTE: this includes pastes by the ClipboardHistory menu.
RecordClipboardHistoryOperation(ClipboardHistoryOperation::kPaste);
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::MaybeCommitData(ui::ClipboardData data) {
if (!ClipboardHistoryUtil::IsSupported(data))
return;
auto iter =
std::find_if(history_list_.cbegin(), history_list_.cend(),
[&data](const auto& item) { return item.data() == data; });
bool is_duplicate = iter != history_list_.cend();
if (is_duplicate) {
// If |data| already exists in |history_list_| then move it to the front
// instead of creating a new one because creating a new one will result in a
// new unique identifier.
history_list_.splice(history_list_.begin(), history_list_, iter);
} else {
history_list_.emplace_front(std::move(data));
}
for (auto& observer : observers_)
observer.OnClipboardHistoryItemAdded(history_list_.front(), is_duplicate);
if (history_list_.size() > ClipboardHistoryUtil::kMaxClipboardItemsShared) {
auto removed = std::move(history_list_.back());
history_list_.pop_back();
for (auto& observer : observers_)
observer.OnClipboardHistoryItemRemoved(removed);
}
}
void ClipboardHistory::Pause() {
++num_pause_;
}
void ClipboardHistory::Resume() {
--num_pause_;
}
} // namespace ash