| // Copyright (c) 2012 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 "chrome/browser/ui/metro_pin_tab_helper_win.h" |
| |
| #include <set> |
| |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/file_util.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/metrics/histogram.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/win/metro.h" |
| #include "chrome/browser/favicon/favicon_tab_helper.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "crypto/sha2.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/color_analysis.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/rect.h" |
| #include "ui/gfx/size.h" |
| |
| DEFINE_WEB_CONTENTS_USER_DATA_KEY(MetroPinTabHelper); |
| |
| namespace { |
| |
| // Histogram name for site-specific tile pinning metrics. |
| const char kMetroPinMetric[] = "Metro.SecondaryTilePin"; |
| |
| // Generate an ID for the tile based on |url_str|. The ID is simply a hash of |
| // the URL. |
| base::string16 GenerateTileId(const base::string16& url_str) { |
| uint8 hash[crypto::kSHA256Length]; |
| crypto::SHA256HashString(base::UTF16ToUTF8(url_str), hash, sizeof(hash)); |
| std::string hash_str = base::HexEncode(hash, sizeof(hash)); |
| return base::UTF8ToUTF16(hash_str); |
| } |
| |
| // Get the path of the directory to store the tile logos in. |
| base::FilePath GetTileImagesDir() { |
| base::FilePath tile_images_dir; |
| if (!PathService::Get(chrome::DIR_USER_DATA, &tile_images_dir)) |
| return base::FilePath(); |
| |
| tile_images_dir = tile_images_dir.Append(L"TileImages"); |
| if (!base::DirectoryExists(tile_images_dir) && |
| !base::CreateDirectory(tile_images_dir)) |
| return base::FilePath(); |
| |
| return tile_images_dir; |
| } |
| |
| // For the given |image| and |tile_id|, try to create a site specific logo in |
| // |logo_dir|. The path of any created logo is returned in |logo_path|. Return |
| // value indicates whether a site specific logo was created. |
| bool CreateSiteSpecificLogo(const SkBitmap& bitmap, |
| const base::string16& tile_id, |
| const base::FilePath& logo_dir, |
| base::FilePath* logo_path) { |
| const int kLogoWidth = 120; |
| const int kLogoHeight = 120; |
| const int kBoxWidth = 40; |
| const int kBoxHeight = 40; |
| const int kCaptionHeight = 20; |
| const double kBoxFade = 0.75; |
| |
| if (bitmap.isNull()) |
| return false; |
| |
| // Fill the tile logo with the dominant color of the favicon bitmap. |
| SkColor dominant_color = color_utils::CalculateKMeanColorOfBitmap(bitmap); |
| SkPaint paint; |
| paint.setColor(dominant_color); |
| gfx::Canvas canvas(gfx::Size(kLogoWidth, kLogoHeight), 1.0f, |
| true); |
| canvas.DrawRect(gfx::Rect(0, 0, kLogoWidth, kLogoHeight), paint); |
| |
| // Now paint a faded square for the favicon to go in. |
| color_utils::HSL shift = {-1, -1, kBoxFade}; |
| paint.setColor(color_utils::HSLShift(dominant_color, shift)); |
| int box_left = (kLogoWidth - kBoxWidth) / 2; |
| int box_top = (kLogoHeight - kCaptionHeight - kBoxHeight) / 2; |
| canvas.DrawRect(gfx::Rect(box_left, box_top, kBoxWidth, kBoxHeight), paint); |
| |
| // Now paint the favicon into the tile, leaving some room at the bottom for |
| // the caption. |
| int left = (kLogoWidth - bitmap.width()) / 2; |
| int top = (kLogoHeight - kCaptionHeight - bitmap.height()) / 2; |
| canvas.DrawImageInt(gfx::ImageSkia::CreateFrom1xBitmap(bitmap), left, top); |
| |
| SkBitmap logo_bitmap = canvas.ExtractImageRep().sk_bitmap(); |
| std::vector<unsigned char> logo_png; |
| if (!gfx::PNGCodec::EncodeBGRASkBitmap(logo_bitmap, true, &logo_png)) |
| return false; |
| |
| *logo_path = logo_dir.Append(tile_id).ReplaceExtension(L".png"); |
| return base::WriteFile(*logo_path, |
| reinterpret_cast<char*>(&logo_png[0]), |
| logo_png.size()) > 0; |
| } |
| |
| // Get the path to the backup logo. If the backup logo already exists in |
| // |logo_dir|, it will be used, otherwise it will be copied out of the install |
| // folder. (The version in the install folder is not used as it may disappear |
| // after an upgrade, causing tiles to lose their images if Windows rebuilds |
| // its tile image cache.) |
| // The path to the logo is returned in |logo_path|, with the return value |
| // indicating success. |
| bool GetPathToBackupLogo(const base::FilePath& logo_dir, |
| base::FilePath* logo_path) { |
| const wchar_t kDefaultLogoFileName[] = L"SecondaryTile.png"; |
| *logo_path = logo_dir.Append(kDefaultLogoFileName); |
| if (base::PathExists(*logo_path)) |
| return true; |
| |
| base::FilePath default_logo_path; |
| if (!PathService::Get(base::DIR_MODULE, &default_logo_path)) |
| return false; |
| |
| default_logo_path = default_logo_path.Append(kDefaultLogoFileName); |
| return base::CopyFile(default_logo_path, *logo_path); |
| } |
| |
| // UMA reporting callback for site-specific secondary tile creation. |
| void PinPageReportUmaCallback( |
| base::win::MetroSecondaryTilePinUmaResult result) { |
| UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, |
| result, |
| base::win::METRO_PIN_STATE_LIMIT); |
| } |
| |
| // The PinPageTaskRunner class performs the necessary FILE thread actions to |
| // pin a page, such as generating or copying the tile image file. When it |
| // has performed these actions it will send the tile creation request to the |
| // metro driver. |
| class PinPageTaskRunner : public base::RefCountedThreadSafe<PinPageTaskRunner> { |
| public: |
| // Creates a task runner for the pinning operation with the given details. |
| // |favicon| can be a null image (i.e. favicon.isNull() can be true), in |
| // which case the backup tile image will be used. |
| PinPageTaskRunner(const base::string16& title, |
| const base::string16& url, |
| const SkBitmap& favicon); |
| |
| void Run(); |
| void RunOnFileThread(); |
| |
| private: |
| ~PinPageTaskRunner() {} |
| |
| // Details of the page being pinned. |
| const base::string16 title_; |
| const base::string16 url_; |
| SkBitmap favicon_; |
| |
| friend class base::RefCountedThreadSafe<PinPageTaskRunner>; |
| DISALLOW_COPY_AND_ASSIGN(PinPageTaskRunner); |
| }; |
| |
| PinPageTaskRunner::PinPageTaskRunner(const base::string16& title, |
| const base::string16& url, |
| const SkBitmap& favicon) |
| : title_(title), |
| url_(url), |
| favicon_(favicon) {} |
| |
| void PinPageTaskRunner::Run() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::FILE, |
| FROM_HERE, |
| base::Bind(&PinPageTaskRunner::RunOnFileThread, this)); |
| } |
| |
| void PinPageTaskRunner::RunOnFileThread() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::FILE); |
| |
| base::string16 tile_id = GenerateTileId(url_); |
| base::FilePath logo_dir = GetTileImagesDir(); |
| if (logo_dir.empty()) { |
| LOG(ERROR) << "Could not create directory to store tile image."; |
| return; |
| } |
| |
| base::FilePath logo_path; |
| if (!CreateSiteSpecificLogo(favicon_, tile_id, logo_dir, &logo_path) && |
| !GetPathToBackupLogo(logo_dir, &logo_path)) { |
| LOG(ERROR) << "Count not get path to logo tile."; |
| return; |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, |
| base::win::METRO_PIN_LOGO_READY, |
| base::win::METRO_PIN_STATE_LIMIT); |
| |
| HMODULE metro_module = base::win::GetMetroModule(); |
| if (!metro_module) |
| return; |
| |
| base::win::MetroPinToStartScreen metro_pin_to_start_screen = |
| reinterpret_cast<base::win::MetroPinToStartScreen>( |
| ::GetProcAddress(metro_module, "MetroPinToStartScreen")); |
| if (!metro_pin_to_start_screen) { |
| NOTREACHED(); |
| return; |
| } |
| |
| metro_pin_to_start_screen(tile_id, |
| title_, |
| url_, |
| logo_path, |
| base::Bind(&PinPageReportUmaCallback)); |
| } |
| |
| } // namespace |
| |
| class MetroPinTabHelper::FaviconChooser { |
| public: |
| FaviconChooser(MetroPinTabHelper* helper, |
| const base::string16& title, |
| const base::string16& url, |
| const SkBitmap& history_bitmap); |
| |
| ~FaviconChooser() {} |
| |
| // Pin the page on the FILE thread using the current |best_candidate_| and |
| // delete the FaviconChooser. |
| void UseChosenCandidate(); |
| |
| // Update the |best_candidate_| with the newly downloaded favicons provided. |
| void UpdateCandidate(int id, |
| const GURL& image_url, |
| const std::vector<SkBitmap>& bitmaps); |
| |
| void AddPendingRequest(int request_id); |
| |
| private: |
| // The tab helper that this chooser is operating for. |
| MetroPinTabHelper* helper_; |
| |
| // Title and URL of the page being pinned. |
| const base::string16 title_; |
| const base::string16 url_; |
| |
| // The best candidate we have so far for the current pin operation. |
| SkBitmap best_candidate_; |
| |
| // Outstanding favicon download requests. |
| std::set<int> in_progress_requests_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FaviconChooser); |
| }; |
| |
| MetroPinTabHelper::FaviconChooser::FaviconChooser( |
| MetroPinTabHelper* helper, |
| const base::string16& title, |
| const base::string16& url, |
| const SkBitmap& history_bitmap) |
| : helper_(helper), |
| title_(title), |
| url_(url), |
| best_candidate_(history_bitmap) {} |
| |
| void MetroPinTabHelper::FaviconChooser::UseChosenCandidate() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| scoped_refptr<PinPageTaskRunner> runner( |
| new PinPageTaskRunner(title_, url_, best_candidate_)); |
| runner->Run(); |
| helper_->FaviconDownloaderFinished(); |
| } |
| |
| void MetroPinTabHelper::FaviconChooser::UpdateCandidate( |
| int id, |
| const GURL& image_url, |
| const std::vector<SkBitmap>& bitmaps) { |
| const int kMaxIconSize = 32; |
| |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| std::set<int>::iterator iter = in_progress_requests_.find(id); |
| // Check that this request is one of ours. |
| if (iter == in_progress_requests_.end()) |
| return; |
| |
| in_progress_requests_.erase(iter); |
| |
| // Process the bitmaps, keeping the one that is best so far. |
| for (std::vector<SkBitmap>::const_iterator iter = bitmaps.begin(); |
| iter != bitmaps.end(); |
| ++iter) { |
| |
| // If the new bitmap is too big, ignore it. |
| if (iter->height() > kMaxIconSize || iter->width() > kMaxIconSize) |
| continue; |
| |
| // If we don't have a best candidate yet, this is better so just grab it. |
| if (best_candidate_.isNull()) { |
| best_candidate_ = *iter; |
| continue; |
| } |
| |
| // If it is smaller than our best one so far, ignore it. |
| if (iter->height() <= best_candidate_.height() || |
| iter->width() <= best_candidate_.width()) { |
| continue; |
| } |
| |
| // Othewise it is our new best candidate. |
| best_candidate_ = *iter; |
| } |
| |
| // If there are no more outstanding requests, pin the page on the FILE thread. |
| // Once this happens this downloader has done its job, so delete it. |
| if (in_progress_requests_.empty()) |
| UseChosenCandidate(); |
| } |
| |
| void MetroPinTabHelper::FaviconChooser::AddPendingRequest(int request_id) { |
| in_progress_requests_.insert(request_id); |
| } |
| |
| MetroPinTabHelper::MetroPinTabHelper(content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents) { |
| } |
| |
| MetroPinTabHelper::~MetroPinTabHelper() {} |
| |
| bool MetroPinTabHelper::IsPinned() const { |
| HMODULE metro_module = base::win::GetMetroModule(); |
| if (!metro_module) |
| return false; |
| |
| typedef BOOL (*MetroIsPinnedToStartScreen)(const base::string16&); |
| MetroIsPinnedToStartScreen metro_is_pinned_to_start_screen = |
| reinterpret_cast<MetroIsPinnedToStartScreen>( |
| ::GetProcAddress(metro_module, "MetroIsPinnedToStartScreen")); |
| if (!metro_is_pinned_to_start_screen) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| GURL url = web_contents()->GetURL(); |
| base::string16 tile_id = GenerateTileId(base::UTF8ToUTF16(url.spec())); |
| return metro_is_pinned_to_start_screen(tile_id) != 0; |
| } |
| |
| void MetroPinTabHelper::TogglePinnedToStartScreen() { |
| if (IsPinned()) { |
| UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, |
| base::win::METRO_UNPIN_INITIATED, |
| base::win::METRO_PIN_STATE_LIMIT); |
| UnPinPageFromStartScreen(); |
| return; |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, |
| base::win::METRO_PIN_INITIATED, |
| base::win::METRO_PIN_STATE_LIMIT); |
| GURL url = web_contents()->GetURL(); |
| base::string16 url_str = base::UTF8ToUTF16(url.spec()); |
| base::string16 title = web_contents()->GetTitle(); |
| // TODO(oshima): Use scoped_ptr::Pass to pass it to other thread. |
| SkBitmap favicon; |
| FaviconTabHelper* favicon_tab_helper = FaviconTabHelper::FromWebContents( |
| web_contents()); |
| if (favicon_tab_helper->FaviconIsValid()) { |
| // Only the 1x bitmap data is needed. |
| favicon = favicon_tab_helper->GetFavicon().AsImageSkia().GetRepresentation( |
| 1.0f).sk_bitmap(); |
| } |
| |
| favicon_chooser_.reset(new FaviconChooser(this, title, url_str, favicon)); |
| |
| if (favicon_url_candidates_.empty()) { |
| favicon_chooser_->UseChosenCandidate(); |
| return; |
| } |
| |
| // Request all the candidates. |
| int max_image_size = 0; // Do not resize images. |
| for (std::vector<content::FaviconURL>::const_iterator iter = |
| favicon_url_candidates_.begin(); |
| iter != favicon_url_candidates_.end(); |
| ++iter) { |
| favicon_chooser_->AddPendingRequest( |
| web_contents()->DownloadImage(iter->icon_url, |
| true, |
| max_image_size, |
| base::Bind(&MetroPinTabHelper::DidDownloadFavicon, |
| base::Unretained(this)))); |
| } |
| |
| } |
| |
| void MetroPinTabHelper::DidNavigateMainFrame( |
| const content::LoadCommittedDetails& /*details*/, |
| const content::FrameNavigateParams& /*params*/) { |
| // Cancel any outstanding pin operations once the user navigates away from |
| // the page. |
| if (favicon_chooser_.get()) |
| favicon_chooser_.reset(); |
| // Any candidate favicons we have are now out of date so clear them. |
| favicon_url_candidates_.clear(); |
| } |
| |
| void MetroPinTabHelper::DidUpdateFaviconURL( |
| const std::vector<content::FaviconURL>& candidates) { |
| favicon_url_candidates_ = candidates; |
| } |
| |
| void MetroPinTabHelper::DidDownloadFavicon( |
| int id, |
| int http_status_code, |
| const GURL& image_url, |
| const std::vector<SkBitmap>& bitmaps, |
| const std::vector<gfx::Size>& original_bitmap_sizes) { |
| if (favicon_chooser_.get()) { |
| favicon_chooser_->UpdateCandidate(id, image_url, bitmaps); |
| } |
| } |
| |
| void MetroPinTabHelper::UnPinPageFromStartScreen() { |
| HMODULE metro_module = base::win::GetMetroModule(); |
| if (!metro_module) |
| return; |
| |
| base::win::MetroUnPinFromStartScreen metro_un_pin_from_start_screen = |
| reinterpret_cast<base::win::MetroUnPinFromStartScreen>( |
| ::GetProcAddress(metro_module, "MetroUnPinFromStartScreen")); |
| if (!metro_un_pin_from_start_screen) { |
| NOTREACHED(); |
| return; |
| } |
| |
| GURL url = web_contents()->GetURL(); |
| base::string16 tile_id = GenerateTileId(base::UTF8ToUTF16(url.spec())); |
| metro_un_pin_from_start_screen(tile_id, |
| base::Bind(&PinPageReportUmaCallback)); |
| } |
| |
| void MetroPinTabHelper::FaviconDownloaderFinished() { |
| favicon_chooser_.reset(); |
| } |