1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/clipboard/clipboard_history_resource_manager.h"
6 
7 #include <string>
8 
9 #include "ash/clipboard/clipboard_history_util.h"
10 #include "ash/public/cpp/clipboard_image_model_factory.h"
11 #include "ash/resources/vector_icons/vector_icons.h"
12 #include "base/bind.h"
13 #include "base/notreached.h"
14 #include "base/stl_util.h"
15 #include "base/strings/escape.h"
16 #include "base/strings/string_split.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "ui/base/clipboard/clipboard_data.h"
20 #include "ui/base/clipboard/custom_data_helper.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/gfx/canvas.h"
23 #include "ui/gfx/color_palette.h"
24 #include "ui/gfx/image/canvas_image_source.h"
25 #include "ui/gfx/paint_vector_icon.h"
26 #include "ui/strings/grit/ui_strings.h"
27 
28 namespace ash {
29 
30 namespace {
31 
32 constexpr int kPlaceholderImageEdgePadding = 5;
33 constexpr int kPlaceholderImageWidth = 234;
34 constexpr int kPlaceholderImageHeight = 74;
35 constexpr int kPlaceholderImageOutlineCornerRadius = 8;
36 constexpr int kPlaceholderImageSVGSize = 32;
37 
38 // Used to draw the UnrenderedHTMLPlaceholderImage, which is shown while HTML is
39 // rendering. Drawn in order to turn the square and single colored SVG into a
40 // multicolored rectangle image.
41 class UnrenderedHTMLPlaceholderImage : public gfx::CanvasImageSource {
42  public:
UnrenderedHTMLPlaceholderImage()43   UnrenderedHTMLPlaceholderImage()
44       : gfx::CanvasImageSource(
45             gfx::Size(kPlaceholderImageWidth, kPlaceholderImageHeight)) {}
46   UnrenderedHTMLPlaceholderImage(const UnrenderedHTMLPlaceholderImage&) =
47       delete;
48   UnrenderedHTMLPlaceholderImage& operator=(
49       const UnrenderedHTMLPlaceholderImage&) = delete;
50   ~UnrenderedHTMLPlaceholderImage() override = default;
51 
52   // gfx::CanvasImageSource:
Draw(gfx::Canvas * canvas)53   void Draw(gfx::Canvas* canvas) override {
54     cc::PaintFlags flags;
55     flags.setStyle(cc::PaintFlags::kFill_Style);
56     flags.setAntiAlias(true);
57     flags.setColor(gfx::kGoogleGrey100);
58     canvas->DrawRoundRect(
59         {kPlaceholderImageEdgePadding, kPlaceholderImageEdgePadding,
60          kPlaceholderImageWidth - 2 * kPlaceholderImageEdgePadding,
61          kPlaceholderImageHeight - 2 * kPlaceholderImageEdgePadding},
62         kPlaceholderImageOutlineCornerRadius, flags);
63 
64     flags = cc::PaintFlags();
65     flags.setStyle(cc::PaintFlags::kFill_Style);
66     flags.setAntiAlias(true);
67     const gfx::ImageSkia center_image =
68         gfx::CreateVectorIcon(kUnrenderedHtmlPlaceholderIcon,
69                               kPlaceholderImageSVGSize, gfx::kGoogleGrey600);
70     canvas->DrawImageInt(
71         center_image, (size().width() - center_image.size().width()) / 2,
72         (size().height() - center_image.size().height()) / 2, flags);
73   }
74 };
75 
76 // Helpers ---------------------------------------------------------------------
77 
78 // Returns the localized string for the specified |resource_id|.
GetLocalizedString(int resource_id)79 base::string16 GetLocalizedString(int resource_id) {
80   return ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
81       resource_id);
82 }
83 
84 // Returns the label to display for the custom data contained within |data|.
GetLabelForCustomData(const ui::ClipboardData & data)85 base::string16 GetLabelForCustomData(const ui::ClipboardData& data) {
86   // Currently the only supported type of custom data is file system data. This
87   // code should not be reached if `data` does not contain file system data.
88   base::string16 sources = ClipboardHistoryUtil::GetFileSystemSources(data);
89   if (sources.empty()) {
90     NOTREACHED();
91     return base::string16();
92   }
93 
94   // Split sources into a list.
95   std::vector<base::StringPiece16> source_list =
96       base::SplitStringPiece(sources, base::UTF8ToUTF16("\n"),
97                              base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
98 
99   // Strip path information, so all that's left are file names.
100   for (auto it = source_list.begin(); it != source_list.end(); ++it)
101     *it = it->substr(it->find_last_of(base::UTF8ToUTF16("/")) + 1);
102 
103   // Join file names, unescaping encoded character sequences for display. This
104   // ensures that "My%20File.txt" will display as "My File.txt".
105   return base::UTF8ToUTF16(base::UnescapeURLComponent(
106       base::UTF16ToUTF8(base::JoinString(source_list, base::UTF8ToUTF16(", "))),
107       base::UnescapeRule::SPACES));
108 }
109 
110 }  // namespace
111 
112 // ClipboardHistoryResourceManager ---------------------------------------------
113 
ClipboardHistoryResourceManager(const ClipboardHistory * clipboard_history)114 ClipboardHistoryResourceManager::ClipboardHistoryResourceManager(
115     const ClipboardHistory* clipboard_history)
116     : clipboard_history_(clipboard_history),
117       placeholder_image_model_(
118           ui::ImageModel::FromImageSkia(gfx::CanvasImageSource::MakeImageSkia<
119                                         UnrenderedHTMLPlaceholderImage>())) {
120   clipboard_history_->AddObserver(this);
121 }
122 
~ClipboardHistoryResourceManager()123 ClipboardHistoryResourceManager::~ClipboardHistoryResourceManager() {
124   clipboard_history_->RemoveObserver(this);
125   if (ClipboardImageModelFactory::Get())
126     ClipboardImageModelFactory::Get()->OnShutdown();
127 }
128 
GetImageModel(const ClipboardHistoryItem & item) const129 ui::ImageModel ClipboardHistoryResourceManager::GetImageModel(
130     const ClipboardHistoryItem& item) const {
131   // Use a cached image model when possible.
132   auto cached_image_model = FindCachedImageModelForItem(item);
133   if (cached_image_model == cached_image_models_.end() ||
134       cached_image_model->image_model.IsEmpty()) {
135     return placeholder_image_model_;
136   }
137   return cached_image_model->image_model;
138 }
139 
GetLabel(const ClipboardHistoryItem & item) const140 base::string16 ClipboardHistoryResourceManager::GetLabel(
141     const ClipboardHistoryItem& item) const {
142   const ui::ClipboardData& data = item.data();
143   switch (ClipboardHistoryUtil::CalculateMainFormat(data).value()) {
144     case ui::ClipboardInternalFormat::kBitmap:
145       return GetLocalizedString(IDS_CLIPBOARD_MENU_IMAGE);
146     case ui::ClipboardInternalFormat::kText:
147       return base::UTF8ToUTF16(data.text());
148     case ui::ClipboardInternalFormat::kHtml:
149       return base::UTF8ToUTF16(data.markup_data());
150     case ui::ClipboardInternalFormat::kSvg:
151       return base::UTF8ToUTF16(data.svg_data());
152     case ui::ClipboardInternalFormat::kRtf:
153       return GetLocalizedString(IDS_CLIPBOARD_MENU_RTF_CONTENT);
154     case ui::ClipboardInternalFormat::kBookmark:
155       return base::UTF8ToUTF16(data.bookmark_title());
156     case ui::ClipboardInternalFormat::kWeb:
157       return GetLocalizedString(IDS_CLIPBOARD_MENU_WEB_SMART_PASTE);
158     case ui::ClipboardInternalFormat::kCustom:
159       return GetLabelForCustomData(data);
160   }
161 }
162 
AddObserver(Observer * observer) const163 void ClipboardHistoryResourceManager::AddObserver(Observer* observer) const {
164   observers_.AddObserver(observer);
165 }
166 
RemoveObserver(Observer * observer) const167 void ClipboardHistoryResourceManager::RemoveObserver(Observer* observer) const {
168   observers_.RemoveObserver(observer);
169 }
170 
171 ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel() = default;
172 
173 ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel(
174     const CachedImageModel& other) = default;
175 
176 ClipboardHistoryResourceManager::CachedImageModel&
177 ClipboardHistoryResourceManager::CachedImageModel::operator=(
178     const CachedImageModel&) = default;
179 
180 ClipboardHistoryResourceManager::CachedImageModel::~CachedImageModel() =
181     default;
182 
CacheImageModel(const base::UnguessableToken & id,ui::ImageModel image_model)183 void ClipboardHistoryResourceManager::CacheImageModel(
184     const base::UnguessableToken& id,
185     ui::ImageModel image_model) {
186   auto cached_image_model = base::ConstCastIterator(
187       cached_image_models_, FindCachedImageModelForId(id));
188   if (cached_image_model == cached_image_models_.end())
189     return;
190 
191   cached_image_model->image_model = std::move(image_model);
192 
193   for (auto& observer : observers_) {
194     observer.OnCachedImageModelUpdated(
195         cached_image_model->clipboard_history_item_ids);
196   }
197 }
198 
199 std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForId(const base::UnguessableToken & id) const200 ClipboardHistoryResourceManager::FindCachedImageModelForId(
201     const base::UnguessableToken& id) const {
202   return std::find_if(cached_image_models_.cbegin(),
203                       cached_image_models_.cend(),
204                       [&](const auto& cached_image_model) {
205                         return cached_image_model.id == id;
206                       });
207 }
208 
209 std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForItem(const ClipboardHistoryItem & item) const210 ClipboardHistoryResourceManager::FindCachedImageModelForItem(
211     const ClipboardHistoryItem& item) const {
212   return std::find_if(
213       cached_image_models_.cbegin(), cached_image_models_.cend(),
214       [&](const auto& cached_image_model) {
215         return base::Contains(cached_image_model.clipboard_history_item_ids,
216                               item.id());
217       });
218 }
219 
CancelUnfinishedRequests()220 void ClipboardHistoryResourceManager::CancelUnfinishedRequests() {
221   for (const auto& cached_image_model : cached_image_models_) {
222     if (cached_image_model.image_model.IsEmpty())
223       ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model.id);
224   }
225 }
226 
OnClipboardHistoryItemAdded(const ClipboardHistoryItem & item)227 void ClipboardHistoryResourceManager::OnClipboardHistoryItemAdded(
228     const ClipboardHistoryItem& item) {
229   // For items that will be represented by their rendered HTML, we need to do
230   // some prep work to pre-render and cache an image model.
231   if (ClipboardHistoryUtil::CalculateMainFormat(item.data()) !=
232       ui::ClipboardInternalFormat::kHtml) {
233     return;
234   }
235 
236   const auto& items = clipboard_history_->GetItems();
237 
238   // See if we have an |existing| item that will render the same as |item|.
239   auto it = std::find_if(items.begin(), items.end(), [&](const auto& existing) {
240     return &existing != &item && existing.data().bitmap().isNull() &&
241            existing.data().markup_data() == item.data().markup_data();
242   });
243 
244   // If we don't have an existing image model in the cache, create one and
245   // instruct ClipboardImageModelFactory to render it. Note that the factory may
246   // or may not start rendering immediately depending on its activation status.
247   if (it == items.end()) {
248     base::UnguessableToken id = base::UnguessableToken::Create();
249     CachedImageModel cached_image_model;
250     cached_image_model.id = id;
251     cached_image_model.clipboard_history_item_ids.push_back(item.id());
252     cached_image_models_.push_back(std::move(cached_image_model));
253 
254     ClipboardImageModelFactory::Get()->Render(
255         id, item.data().markup_data(),
256         base::BindOnce(&ClipboardHistoryResourceManager::CacheImageModel,
257                        weak_factory_.GetWeakPtr(), id));
258     return;
259   }
260   // If we do have an existing model, we need only to update its usages.
261   auto cached_image_model = base::ConstCastIterator(
262       cached_image_models_, FindCachedImageModelForItem(*it));
263   DCHECK(cached_image_model != cached_image_models_.end());
264   cached_image_model->clipboard_history_item_ids.push_back(item.id());
265 }
266 
OnClipboardHistoryItemRemoved(const ClipboardHistoryItem & item)267 void ClipboardHistoryResourceManager::OnClipboardHistoryItemRemoved(
268     const ClipboardHistoryItem& item) {
269   // For items that will not be represented by their rendered HTML, do nothing.
270   if (ClipboardHistoryUtil::CalculateMainFormat(item.data()) !=
271       ui::ClipboardInternalFormat::kHtml) {
272     return;
273   }
274 
275   // We should have an image model in the cache.
276   auto cached_image_model = base::ConstCastIterator(
277       cached_image_models_, FindCachedImageModelForItem(item));
278 
279   DCHECK(cached_image_model != cached_image_models_.end());
280 
281   // Update usages.
282   base::Erase(cached_image_model->clipboard_history_item_ids, item.id());
283   if (!cached_image_model->clipboard_history_item_ids.empty())
284     return;
285 
286   // If the ImageModel was never rendered, cancel the request.
287   if (cached_image_model->image_model.IsEmpty())
288     ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model->id);
289 
290   // If the cached image model is no longer in use, it can be erased.
291   cached_image_models_.erase(cached_image_model);
292 }
293 
OnClipboardHistoryCleared()294 void ClipboardHistoryResourceManager::OnClipboardHistoryCleared() {
295   CancelUnfinishedRequests();
296   cached_image_models_ = std::vector<CachedImageModel>();
297 }
298 
299 }  // namespace ash
300