| // 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 |