1// Copyright (c) 2012 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 "chrome/browser/ui/metro_pin_tab_helper_win.h"
6
7#include <set>
8
9#include "base/base_paths.h"
10#include "base/bind.h"
11#include "base/files/file_path.h"
12#include "base/files/file_util.h"
13#include "base/logging.h"
14#include "base/memory/ref_counted.h"
15#include "base/memory/ref_counted_memory.h"
16#include "base/metrics/histogram.h"
17#include "base/path_service.h"
18#include "base/strings/string_number_conversions.h"
19#include "base/strings/utf_string_conversions.h"
20#include "base/win/metro.h"
21#include "chrome/browser/favicon/favicon_tab_helper.h"
22#include "chrome/common/chrome_paths.h"
23#include "content/public/browser/browser_thread.h"
24#include "content/public/browser/web_contents.h"
25#include "crypto/sha2.h"
26#include "third_party/skia/include/core/SkCanvas.h"
27#include "third_party/skia/include/core/SkColor.h"
28#include "ui/gfx/canvas.h"
29#include "ui/gfx/codec/png_codec.h"
30#include "ui/gfx/color_analysis.h"
31#include "ui/gfx/color_utils.h"
32#include "ui/gfx/image/image.h"
33#include "ui/gfx/rect.h"
34#include "ui/gfx/size.h"
35
36DEFINE_WEB_CONTENTS_USER_DATA_KEY(MetroPinTabHelper);
37
38namespace {
39
40// Histogram name for site-specific tile pinning metrics.
41const char kMetroPinMetric[] = "Metro.SecondaryTilePin";
42
43// Generate an ID for the tile based on |url_str|. The ID is simply a hash of
44// the URL.
45base::string16 GenerateTileId(const base::string16& url_str) {
46  uint8 hash[crypto::kSHA256Length];
47  crypto::SHA256HashString(base::UTF16ToUTF8(url_str), hash, sizeof(hash));
48  std::string hash_str = base::HexEncode(hash, sizeof(hash));
49  return base::UTF8ToUTF16(hash_str);
50}
51
52// Get the path of the directory to store the tile logos in.
53base::FilePath GetTileImagesDir() {
54  base::FilePath tile_images_dir;
55  if (!PathService::Get(chrome::DIR_USER_DATA, &tile_images_dir))
56    return base::FilePath();
57
58  tile_images_dir = tile_images_dir.Append(L"TileImages");
59  if (!base::DirectoryExists(tile_images_dir) &&
60      !base::CreateDirectory(tile_images_dir))
61    return base::FilePath();
62
63  return tile_images_dir;
64}
65
66// For the given |image| and |tile_id|, try to create a site specific logo in
67// |logo_dir|. The path of any created logo is returned in |logo_path|. Return
68// value indicates whether a site specific logo was created.
69bool CreateSiteSpecificLogo(const SkBitmap& bitmap,
70                            const base::string16& tile_id,
71                            const base::FilePath& logo_dir,
72                            base::FilePath* logo_path) {
73  const int kLogoWidth = 120;
74  const int kLogoHeight = 120;
75  const int kBoxWidth = 40;
76  const int kBoxHeight = 40;
77  const int kCaptionHeight = 20;
78  const double kBoxFade = 0.75;
79
80  if (bitmap.isNull())
81    return false;
82
83  // Fill the tile logo with the dominant color of the favicon bitmap.
84  SkColor dominant_color = color_utils::CalculateKMeanColorOfBitmap(bitmap);
85  SkPaint paint;
86  paint.setColor(dominant_color);
87  gfx::Canvas canvas(gfx::Size(kLogoWidth, kLogoHeight), 1.0f,
88                     true);
89  canvas.DrawRect(gfx::Rect(0, 0, kLogoWidth, kLogoHeight), paint);
90
91  // Now paint a faded square for the favicon to go in.
92  color_utils::HSL shift = {-1, -1, kBoxFade};
93  paint.setColor(color_utils::HSLShift(dominant_color, shift));
94  int box_left = (kLogoWidth - kBoxWidth) / 2;
95  int box_top = (kLogoHeight - kCaptionHeight - kBoxHeight) / 2;
96  canvas.DrawRect(gfx::Rect(box_left, box_top, kBoxWidth, kBoxHeight), paint);
97
98  // Now paint the favicon into the tile, leaving some room at the bottom for
99  // the caption.
100  int left = (kLogoWidth - bitmap.width()) / 2;
101  int top = (kLogoHeight - kCaptionHeight - bitmap.height()) / 2;
102  canvas.DrawImageInt(gfx::ImageSkia::CreateFrom1xBitmap(bitmap), left, top);
103
104  SkBitmap logo_bitmap = canvas.ExtractImageRep().sk_bitmap();
105  std::vector<unsigned char> logo_png;
106  if (!gfx::PNGCodec::EncodeBGRASkBitmap(logo_bitmap, true, &logo_png))
107    return false;
108
109  *logo_path = logo_dir.Append(tile_id).ReplaceExtension(L".png");
110  return base::WriteFile(*logo_path,
111                         reinterpret_cast<char*>(&logo_png[0]),
112                         logo_png.size()) > 0;
113}
114
115// Get the path to the backup logo. If the backup logo already exists in
116// |logo_dir|, it will be used, otherwise it will be copied out of the install
117// folder. (The version in the install folder is not used as it may disappear
118// after an upgrade, causing tiles to lose their images if Windows rebuilds
119// its tile image cache.)
120// The path to the logo is returned in |logo_path|, with the return value
121// indicating success.
122bool GetPathToBackupLogo(const base::FilePath& logo_dir,
123                         base::FilePath* logo_path) {
124  const wchar_t kDefaultLogoFileName[] = L"SecondaryTile.png";
125  *logo_path = logo_dir.Append(kDefaultLogoFileName);
126  if (base::PathExists(*logo_path))
127    return true;
128
129  base::FilePath default_logo_path;
130  if (!PathService::Get(base::DIR_MODULE, &default_logo_path))
131    return false;
132
133  default_logo_path = default_logo_path.Append(kDefaultLogoFileName);
134  return base::CopyFile(default_logo_path, *logo_path);
135}
136
137// UMA reporting callback for site-specific secondary tile creation.
138void PinPageReportUmaCallback(
139    base::win::MetroSecondaryTilePinUmaResult result) {
140  UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric,
141                            result,
142                            base::win::METRO_PIN_STATE_LIMIT);
143}
144
145// The PinPageTaskRunner class performs the necessary FILE thread actions to
146// pin a page, such as generating or copying the tile image file. When it
147// has performed these actions it will send the tile creation request to the
148// metro driver.
149class PinPageTaskRunner : public base::RefCountedThreadSafe<PinPageTaskRunner> {
150 public:
151  // Creates a task runner for the pinning operation with the given details.
152  // |favicon| can be a null image (i.e. favicon.isNull() can be true), in
153  // which case the backup tile image will be used.
154  PinPageTaskRunner(const base::string16& title,
155                    const base::string16& url,
156                    const SkBitmap& favicon);
157
158  void Run();
159  void RunOnFileThread();
160
161 private:
162  ~PinPageTaskRunner() {}
163
164  // Details of the page being pinned.
165  const base::string16 title_;
166  const base::string16 url_;
167  SkBitmap favicon_;
168
169  friend class base::RefCountedThreadSafe<PinPageTaskRunner>;
170  DISALLOW_COPY_AND_ASSIGN(PinPageTaskRunner);
171};
172
173PinPageTaskRunner::PinPageTaskRunner(const base::string16& title,
174                                     const base::string16& url,
175                                     const SkBitmap& favicon)
176    : title_(title),
177      url_(url),
178      favicon_(favicon) {}
179
180void PinPageTaskRunner::Run() {
181  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
182
183  content::BrowserThread::PostTask(
184      content::BrowserThread::FILE,
185      FROM_HERE,
186      base::Bind(&PinPageTaskRunner::RunOnFileThread, this));
187}
188
189void PinPageTaskRunner::RunOnFileThread() {
190  DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
191
192  base::string16 tile_id = GenerateTileId(url_);
193  base::FilePath logo_dir = GetTileImagesDir();
194  if (logo_dir.empty()) {
195    LOG(ERROR) << "Could not create directory to store tile image.";
196    return;
197  }
198
199  base::FilePath logo_path;
200  if (!CreateSiteSpecificLogo(favicon_, tile_id, logo_dir, &logo_path) &&
201      !GetPathToBackupLogo(logo_dir, &logo_path)) {
202    LOG(ERROR) << "Count not get path to logo tile.";
203    return;
204  }
205
206  UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric,
207                            base::win::METRO_PIN_LOGO_READY,
208                            base::win::METRO_PIN_STATE_LIMIT);
209
210  HMODULE metro_module = base::win::GetMetroModule();
211  if (!metro_module)
212    return;
213
214  base::win::MetroPinToStartScreen metro_pin_to_start_screen =
215      reinterpret_cast<base::win::MetroPinToStartScreen>(
216          ::GetProcAddress(metro_module, "MetroPinToStartScreen"));
217  if (!metro_pin_to_start_screen) {
218    NOTREACHED();
219    return;
220  }
221
222  metro_pin_to_start_screen(tile_id,
223                            title_,
224                            url_,
225                            logo_path,
226                            base::Bind(&PinPageReportUmaCallback));
227}
228
229}  // namespace
230
231class MetroPinTabHelper::FaviconChooser {
232 public:
233  FaviconChooser(MetroPinTabHelper* helper,
234                 const base::string16& title,
235                 const base::string16& url,
236                 const SkBitmap& history_bitmap);
237
238  ~FaviconChooser() {}
239
240  // Pin the page on the FILE thread using the current |best_candidate_| and
241  // delete the FaviconChooser.
242  void UseChosenCandidate();
243
244  // Update the |best_candidate_| with the newly downloaded favicons provided.
245  void UpdateCandidate(int id,
246                       const GURL& image_url,
247                       const std::vector<SkBitmap>& bitmaps);
248
249  void AddPendingRequest(int request_id);
250
251 private:
252  // The tab helper that this chooser is operating for.
253  MetroPinTabHelper* helper_;
254
255  // Title and URL of the page being pinned.
256  const base::string16 title_;
257  const base::string16 url_;
258
259  // The best candidate we have so far for the current pin operation.
260  SkBitmap best_candidate_;
261
262  // Outstanding favicon download requests.
263  std::set<int> in_progress_requests_;
264
265  DISALLOW_COPY_AND_ASSIGN(FaviconChooser);
266};
267
268MetroPinTabHelper::FaviconChooser::FaviconChooser(
269    MetroPinTabHelper* helper,
270    const base::string16& title,
271    const base::string16& url,
272    const SkBitmap& history_bitmap)
273        : helper_(helper),
274          title_(title),
275          url_(url),
276          best_candidate_(history_bitmap) {}
277
278void MetroPinTabHelper::FaviconChooser::UseChosenCandidate() {
279  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
280  scoped_refptr<PinPageTaskRunner> runner(
281      new PinPageTaskRunner(title_, url_, best_candidate_));
282  runner->Run();
283  helper_->FaviconDownloaderFinished();
284}
285
286void MetroPinTabHelper::FaviconChooser::UpdateCandidate(
287    int id,
288    const GURL& image_url,
289    const std::vector<SkBitmap>& bitmaps) {
290  const int kMaxIconSize = 32;
291
292  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
293
294  std::set<int>::iterator iter = in_progress_requests_.find(id);
295  // Check that this request is one of ours.
296  if (iter == in_progress_requests_.end())
297    return;
298
299  in_progress_requests_.erase(iter);
300
301  // Process the bitmaps, keeping the one that is best so far.
302  for (std::vector<SkBitmap>::const_iterator iter = bitmaps.begin();
303       iter != bitmaps.end();
304       ++iter) {
305
306    // If the new bitmap is too big, ignore it.
307    if (iter->height() > kMaxIconSize || iter->width() > kMaxIconSize)
308      continue;
309
310    // If we don't have a best candidate yet, this is better so just grab it.
311    if (best_candidate_.isNull()) {
312      best_candidate_ = *iter;
313      continue;
314    }
315
316    // If it is smaller than our best one so far, ignore it.
317    if (iter->height() <= best_candidate_.height() ||
318        iter->width() <= best_candidate_.width()) {
319      continue;
320    }
321
322    // Othewise it is our new best candidate.
323    best_candidate_ = *iter;
324  }
325
326  // If there are no more outstanding requests, pin the page on the FILE thread.
327  // Once this happens this downloader has done its job, so delete it.
328  if (in_progress_requests_.empty())
329    UseChosenCandidate();
330}
331
332void MetroPinTabHelper::FaviconChooser::AddPendingRequest(int request_id) {
333  in_progress_requests_.insert(request_id);
334}
335
336MetroPinTabHelper::MetroPinTabHelper(content::WebContents* web_contents)
337    : content::WebContentsObserver(web_contents) {
338}
339
340MetroPinTabHelper::~MetroPinTabHelper() {}
341
342bool MetroPinTabHelper::IsPinned() const {
343  HMODULE metro_module = base::win::GetMetroModule();
344  if (!metro_module)
345    return false;
346
347  typedef BOOL (*MetroIsPinnedToStartScreen)(const base::string16&);
348  MetroIsPinnedToStartScreen metro_is_pinned_to_start_screen =
349      reinterpret_cast<MetroIsPinnedToStartScreen>(
350          ::GetProcAddress(metro_module, "MetroIsPinnedToStartScreen"));
351  if (!metro_is_pinned_to_start_screen) {
352    NOTREACHED();
353    return false;
354  }
355
356  GURL url = web_contents()->GetURL();
357  base::string16 tile_id = GenerateTileId(base::UTF8ToUTF16(url.spec()));
358  return metro_is_pinned_to_start_screen(tile_id) != 0;
359}
360
361void MetroPinTabHelper::TogglePinnedToStartScreen() {
362  if (IsPinned()) {
363    UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric,
364                              base::win::METRO_UNPIN_INITIATED,
365                              base::win::METRO_PIN_STATE_LIMIT);
366    UnPinPageFromStartScreen();
367    return;
368  }
369
370  UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric,
371                            base::win::METRO_PIN_INITIATED,
372                            base::win::METRO_PIN_STATE_LIMIT);
373  GURL url = web_contents()->GetURL();
374  base::string16 url_str = base::UTF8ToUTF16(url.spec());
375  base::string16 title = web_contents()->GetTitle();
376  // TODO(oshima): Use scoped_ptr::Pass to pass it to other thread.
377  SkBitmap favicon;
378  FaviconTabHelper* favicon_tab_helper = FaviconTabHelper::FromWebContents(
379      web_contents());
380  if (favicon_tab_helper->FaviconIsValid()) {
381    // Only the 1x bitmap data is needed.
382    favicon = favicon_tab_helper->GetFavicon().AsImageSkia().GetRepresentation(
383        1.0f).sk_bitmap();
384  }
385
386  favicon_chooser_.reset(new FaviconChooser(this, title, url_str, favicon));
387
388  if (favicon_url_candidates_.empty()) {
389    favicon_chooser_->UseChosenCandidate();
390    return;
391  }
392
393  // Request all the candidates.
394  int max_image_size = 0;  // Do not resize images.
395  for (std::vector<content::FaviconURL>::const_iterator iter =
396           favicon_url_candidates_.begin();
397       iter != favicon_url_candidates_.end();
398       ++iter) {
399    favicon_chooser_->AddPendingRequest(
400        web_contents()->DownloadImage(iter->icon_url,
401            true,
402            max_image_size,
403            base::Bind(&MetroPinTabHelper::DidDownloadFavicon,
404                       base::Unretained(this))));
405  }
406
407}
408
409void MetroPinTabHelper::DidNavigateMainFrame(
410    const content::LoadCommittedDetails& /*details*/,
411    const content::FrameNavigateParams& /*params*/) {
412  // Cancel any outstanding pin operations once the user navigates away from
413  // the page.
414  if (favicon_chooser_.get())
415    favicon_chooser_.reset();
416  // Any candidate favicons we have are now out of date so clear them.
417  favicon_url_candidates_.clear();
418}
419
420void MetroPinTabHelper::DidUpdateFaviconURL(
421    const std::vector<content::FaviconURL>& candidates) {
422  favicon_url_candidates_ = candidates;
423}
424
425void MetroPinTabHelper::DidDownloadFavicon(
426    int id,
427    int http_status_code,
428    const GURL& image_url,
429    const std::vector<SkBitmap>& bitmaps,
430    const std::vector<gfx::Size>& original_bitmap_sizes) {
431  if (favicon_chooser_.get()) {
432    favicon_chooser_->UpdateCandidate(id, image_url, bitmaps);
433  }
434}
435
436void MetroPinTabHelper::UnPinPageFromStartScreen() {
437  HMODULE metro_module = base::win::GetMetroModule();
438  if (!metro_module)
439    return;
440
441  base::win::MetroUnPinFromStartScreen metro_un_pin_from_start_screen =
442      reinterpret_cast<base::win::MetroUnPinFromStartScreen>(
443          ::GetProcAddress(metro_module, "MetroUnPinFromStartScreen"));
444  if (!metro_un_pin_from_start_screen) {
445    NOTREACHED();
446    return;
447  }
448
449  GURL url = web_contents()->GetURL();
450  base::string16 tile_id = GenerateTileId(base::UTF8ToUTF16(url.spec()));
451  metro_un_pin_from_start_screen(tile_id,
452                                 base::Bind(&PinPageReportUmaCallback));
453}
454
455void MetroPinTabHelper::FaviconDownloaderFinished() {
456  favicon_chooser_.reset();
457}
458