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