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 string16 GenerateTileId(const 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 !file_util::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 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), ui::SCALE_FACTOR_100P, 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 string16& title, 156 const 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 string16 title_; 167 const string16 url_; 168 SkBitmap favicon_; 169 170 friend class base::RefCountedThreadSafe<PinPageTaskRunner>; 171 DISALLOW_COPY_AND_ASSIGN(PinPageTaskRunner); 172 }; 173 174 PinPageTaskRunner::PinPageTaskRunner(const string16& title, 175 const 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 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 string16& title, 236 const 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 int requested_size, 249 const std::vector<SkBitmap>& bitmaps); 250 251 void AddPendingRequest(int request_id); 252 253 private: 254 // The tab helper that this chooser is operating for. 255 MetroPinTabHelper* helper_; 256 257 // Title and URL of the page being pinned. 258 const string16 title_; 259 const string16 url_; 260 261 // The best candidate we have so far for the current pin operation. 262 SkBitmap best_candidate_; 263 264 // Outstanding favicon download requests. 265 std::set<int> in_progress_requests_; 266 267 DISALLOW_COPY_AND_ASSIGN(FaviconChooser); 268 }; 269 270 MetroPinTabHelper::FaviconChooser::FaviconChooser( 271 MetroPinTabHelper* helper, 272 const string16& title, 273 const string16& url, 274 const SkBitmap& history_bitmap) 275 : helper_(helper), 276 title_(title), 277 url_(url), 278 best_candidate_(history_bitmap) {} 279 280 void MetroPinTabHelper::FaviconChooser::UseChosenCandidate() { 281 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 282 scoped_refptr<PinPageTaskRunner> runner( 283 new PinPageTaskRunner(title_, url_, best_candidate_)); 284 runner->Run(); 285 helper_->FaviconDownloaderFinished(); 286 } 287 288 void MetroPinTabHelper::FaviconChooser::UpdateCandidate( 289 int id, 290 const GURL& image_url, 291 int requested_size, 292 const std::vector<SkBitmap>& bitmaps) { 293 const int kMaxIconSize = 32; 294 295 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 296 297 std::set<int>::iterator iter = in_progress_requests_.find(id); 298 // Check that this request is one of ours. 299 if (iter == in_progress_requests_.end()) 300 return; 301 302 in_progress_requests_.erase(iter); 303 304 // Process the bitmaps, keeping the one that is best so far. 305 for (std::vector<SkBitmap>::const_iterator iter = bitmaps.begin(); 306 iter != bitmaps.end(); 307 ++iter) { 308 309 // If the new bitmap is too big, ignore it. 310 if (iter->height() > kMaxIconSize || iter->width() > kMaxIconSize) 311 continue; 312 313 // If we don't have a best candidate yet, this is better so just grab it. 314 if (best_candidate_.isNull()) { 315 best_candidate_ = *iter; 316 continue; 317 } 318 319 // If it is smaller than our best one so far, ignore it. 320 if (iter->height() <= best_candidate_.height() || 321 iter->width() <= best_candidate_.width()) { 322 continue; 323 } 324 325 // Othewise it is our new best candidate. 326 best_candidate_ = *iter; 327 } 328 329 // If there are no more outstanding requests, pin the page on the FILE thread. 330 // Once this happens this downloader has done its job, so delete it. 331 if (in_progress_requests_.empty()) 332 UseChosenCandidate(); 333 } 334 335 void MetroPinTabHelper::FaviconChooser::AddPendingRequest(int request_id) { 336 in_progress_requests_.insert(request_id); 337 } 338 339 MetroPinTabHelper::MetroPinTabHelper(content::WebContents* web_contents) 340 : content::WebContentsObserver(web_contents) { 341 } 342 343 MetroPinTabHelper::~MetroPinTabHelper() {} 344 345 bool MetroPinTabHelper::IsPinned() const { 346 HMODULE metro_module = base::win::GetMetroModule(); 347 if (!metro_module) 348 return false; 349 350 typedef BOOL (*MetroIsPinnedToStartScreen)(const string16&); 351 MetroIsPinnedToStartScreen metro_is_pinned_to_start_screen = 352 reinterpret_cast<MetroIsPinnedToStartScreen>( 353 ::GetProcAddress(metro_module, "MetroIsPinnedToStartScreen")); 354 if (!metro_is_pinned_to_start_screen) { 355 NOTREACHED(); 356 return false; 357 } 358 359 GURL url = web_contents()->GetURL(); 360 string16 tile_id = GenerateTileId(UTF8ToUTF16(url.spec())); 361 return metro_is_pinned_to_start_screen(tile_id) != 0; 362 } 363 364 void MetroPinTabHelper::TogglePinnedToStartScreen() { 365 if (IsPinned()) { 366 UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, 367 base::win::METRO_UNPIN_INITIATED, 368 base::win::METRO_PIN_STATE_LIMIT); 369 UnPinPageFromStartScreen(); 370 return; 371 } 372 373 UMA_HISTOGRAM_ENUMERATION(kMetroPinMetric, 374 base::win::METRO_PIN_INITIATED, 375 base::win::METRO_PIN_STATE_LIMIT); 376 GURL url = web_contents()->GetURL(); 377 string16 url_str = UTF8ToUTF16(url.spec()); 378 string16 title = web_contents()->GetTitle(); 379 // TODO(oshima): Use scoped_ptr::Pass to pass it to other thread. 380 SkBitmap favicon; 381 FaviconTabHelper* favicon_tab_helper = FaviconTabHelper::FromWebContents( 382 web_contents()); 383 if (favicon_tab_helper->FaviconIsValid()) { 384 // Only the 1x bitmap data is needed. 385 favicon = favicon_tab_helper->GetFavicon().AsImageSkia().GetRepresentation( 386 ui::SCALE_FACTOR_100P).sk_bitmap(); 387 } 388 389 favicon_chooser_.reset(new FaviconChooser(this, title, url_str, favicon)); 390 391 if (favicon_url_candidates_.empty()) { 392 favicon_chooser_->UseChosenCandidate(); 393 return; 394 } 395 396 // Request all the candidates. 397 int preferred_image_size = 0; // Request the first image. 398 int max_image_size = 0; // Do not resize images. 399 for (std::vector<content::FaviconURL>::const_iterator iter = 400 favicon_url_candidates_.begin(); 401 iter != favicon_url_candidates_.end(); 402 ++iter) { 403 favicon_chooser_->AddPendingRequest( 404 web_contents()->DownloadImage(iter->icon_url, 405 true, 406 preferred_image_size, 407 max_image_size, 408 base::Bind(&MetroPinTabHelper::DidDownloadFavicon, 409 base::Unretained(this)))); 410 } 411 412 } 413 414 void MetroPinTabHelper::DidNavigateMainFrame( 415 const content::LoadCommittedDetails& /*details*/, 416 const content::FrameNavigateParams& /*params*/) { 417 // Cancel any outstanding pin operations once the user navigates away from 418 // the page. 419 if (favicon_chooser_.get()) 420 favicon_chooser_.reset(); 421 // Any candidate favicons we have are now out of date so clear them. 422 favicon_url_candidates_.clear(); 423 } 424 425 void MetroPinTabHelper::DidUpdateFaviconURL( 426 int32 page_id, 427 const std::vector<content::FaviconURL>& candidates) { 428 favicon_url_candidates_ = candidates; 429 } 430 431 void MetroPinTabHelper::DidDownloadFavicon( 432 int id, 433 int http_status_code, 434 const GURL& image_url, 435 int requested_size, 436 const std::vector<SkBitmap>& bitmaps) { 437 if (favicon_chooser_.get()) { 438 favicon_chooser_->UpdateCandidate(id, image_url, requested_size, bitmaps); 439 } 440 } 441 442 void MetroPinTabHelper::UnPinPageFromStartScreen() { 443 HMODULE metro_module = base::win::GetMetroModule(); 444 if (!metro_module) 445 return; 446 447 base::win::MetroUnPinFromStartScreen metro_un_pin_from_start_screen = 448 reinterpret_cast<base::win::MetroUnPinFromStartScreen>( 449 ::GetProcAddress(metro_module, "MetroUnPinFromStartScreen")); 450 if (!metro_un_pin_from_start_screen) { 451 NOTREACHED(); 452 return; 453 } 454 455 GURL url = web_contents()->GetURL(); 456 string16 tile_id = GenerateTileId(UTF8ToUTF16(url.spec())); 457 metro_un_pin_from_start_screen(tile_id, 458 base::Bind(&PinPageReportUmaCallback)); 459 } 460 461 void MetroPinTabHelper::FaviconDownloaderFinished() { 462 favicon_chooser_.reset(); 463 } 464