| // 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 <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); | 
 |  | 
 |   // 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 = | 
 |       std::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 = std::ranges::find(pauses_, pause_id, &PauseInfo::pause_id); | 
 |   DCHECK(pause_it != pauses_.end()); | 
 |   pauses_.erase(pause_it); | 
 | } | 
 |  | 
 | }  // namespace ash |