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/web_applications/web_app_ui.h" 6 7 #include "base/bind.h" 8 #include "base/bind_helpers.h" 9 #include "base/file_util.h" 10 #include "base/path_service.h" 11 #include "base/prefs/pref_service.h" 12 #include "base/strings/string16.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/extensions/image_loader.h" 16 #include "chrome/browser/extensions/tab_helper.h" 17 #include "chrome/browser/favicon/favicon_tab_helper.h" 18 #include "chrome/browser/history/select_favicon_frames.h" 19 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/web_applications/web_app.h" 21 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" 22 #include "chrome/common/extensions/manifest_handlers/icons_handler.h" 23 #include "chrome/common/pref_names.h" 24 #include "content/public/browser/browser_thread.h" 25 #include "content/public/browser/notification_details.h" 26 #include "content/public/browser/notification_registrar.h" 27 #include "content/public/browser/notification_source.h" 28 #include "content/public/browser/web_contents.h" 29 #include "extensions/common/extension.h" 30 #include "grit/theme_resources.h" 31 #include "skia/ext/image_operations.h" 32 #include "third_party/skia/include/core/SkBitmap.h" 33 #include "ui/base/resource/resource_bundle.h" 34 #include "ui/gfx/image/image.h" 35 #include "ui/gfx/image/image_family.h" 36 #include "ui/gfx/image/image_skia.h" 37 #include "url/gurl.h" 38 39 #if defined(OS_POSIX) && !defined(OS_MACOSX) 40 #include "base/environment.h" 41 #endif 42 43 #if defined(OS_WIN) 44 #include "base/win/shortcut.h" 45 #include "base/win/windows_version.h" 46 #include "chrome/browser/web_applications/web_app_win.h" 47 #include "ui/gfx/icon_util.h" 48 #endif 49 50 using content::BrowserThread; 51 using content::NavigationController; 52 using content::WebContents; 53 54 namespace { 55 56 #if defined(OS_MACOSX) 57 const int kDesiredSizes[] = {16, 32, 128, 256, 512}; 58 const size_t kNumDesiredSizes = arraysize(kDesiredSizes); 59 #elif defined(OS_LINUX) 60 // Linux supports icons of any size. FreeDesktop Icon Theme Specification states 61 // that "Minimally you should install a 48x48 icon in the hicolor theme." 62 const int kDesiredSizes[] = {16, 32, 48, 128, 256, 512}; 63 const size_t kNumDesiredSizes = arraysize(kDesiredSizes); 64 #elif defined(OS_WIN) 65 const int* kDesiredSizes = IconUtil::kIconDimensions; 66 const size_t kNumDesiredSizes = IconUtil::kNumIconDimensions; 67 #else 68 const int kDesiredSizes[] = {32}; 69 const size_t kNumDesiredSizes = arraysize(kDesiredSizes); 70 #endif 71 72 #if defined(OS_WIN) 73 // UpdateShortcutWorker holds all context data needed for update shortcut. 74 // It schedules a pre-update check to find all shortcuts that needs to be 75 // updated. If there are such shortcuts, it schedules icon download and 76 // update them when icons are downloaded. It observes TAB_CLOSING notification 77 // and cancels all the work when the underlying tab is closing. 78 class UpdateShortcutWorker : public content::NotificationObserver { 79 public: 80 explicit UpdateShortcutWorker(WebContents* web_contents); 81 82 void Run(); 83 84 private: 85 // Overridden from content::NotificationObserver: 86 virtual void Observe(int type, 87 const content::NotificationSource& source, 88 const content::NotificationDetails& details); 89 90 // Downloads icon via the FaviconTabHelper. 91 void DownloadIcon(); 92 93 // Favicon download callback. 94 void DidDownloadFavicon( 95 int requested_size, 96 int id, 97 int http_status_code, 98 const GURL& image_url, 99 const std::vector<SkBitmap>& bitmaps, 100 const std::vector<gfx::Size>& original_bitmap_sizes); 101 102 // Checks if shortcuts exists on desktop, start menu and quick launch. 103 void CheckExistingShortcuts(); 104 105 // Update shortcut files and icons. 106 void UpdateShortcuts(); 107 void UpdateShortcutsOnFileThread(); 108 109 // Callback after shortcuts are updated. 110 void OnShortcutsUpdated(bool); 111 112 // Deletes the worker on UI thread where it gets created. 113 void DeleteMe(); 114 void DeleteMeOnUIThread(); 115 116 content::NotificationRegistrar registrar_; 117 118 // Underlying WebContents whose shortcuts will be updated. 119 WebContents* web_contents_; 120 121 // Icons info from web_contents_'s web app data. 122 web_app::IconInfoList unprocessed_icons_; 123 124 // Cached shortcut data from the web_contents_. 125 ShellIntegration::ShortcutInfo shortcut_info_; 126 127 // Our copy of profile path. 128 base::FilePath profile_path_; 129 130 // File name of shortcut/ico file based on app title. 131 base::FilePath file_name_; 132 133 // Existing shortcuts. 134 std::vector<base::FilePath> shortcut_files_; 135 136 DISALLOW_COPY_AND_ASSIGN(UpdateShortcutWorker); 137 }; 138 139 UpdateShortcutWorker::UpdateShortcutWorker(WebContents* web_contents) 140 : web_contents_(web_contents), 141 profile_path_(Profile::FromBrowserContext( 142 web_contents->GetBrowserContext())->GetPath()) { 143 extensions::TabHelper* extensions_tab_helper = 144 extensions::TabHelper::FromWebContents(web_contents); 145 web_app::GetShortcutInfoForTab(web_contents_, &shortcut_info_); 146 web_app::GetIconsInfo(extensions_tab_helper->web_app_info(), 147 &unprocessed_icons_); 148 file_name_ = web_app::internals::GetSanitizedFileName(shortcut_info_.title); 149 150 registrar_.Add( 151 this, 152 chrome::NOTIFICATION_TAB_CLOSING, 153 content::Source<NavigationController>(&web_contents->GetController())); 154 } 155 156 void UpdateShortcutWorker::Run() { 157 // Starting by downloading app icon. 158 DownloadIcon(); 159 } 160 161 void UpdateShortcutWorker::Observe( 162 int type, 163 const content::NotificationSource& source, 164 const content::NotificationDetails& details) { 165 if (type == chrome::NOTIFICATION_TAB_CLOSING && 166 content::Source<NavigationController>(source).ptr() == 167 &web_contents_->GetController()) { 168 // Underlying tab is closing. 169 web_contents_ = NULL; 170 } 171 } 172 173 void UpdateShortcutWorker::DownloadIcon() { 174 // FetchIcon must run on UI thread because it relies on WebContents 175 // to download the icon. 176 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 177 178 if (web_contents_ == NULL) { 179 DeleteMe(); // We are done if underlying WebContents is gone. 180 return; 181 } 182 183 if (unprocessed_icons_.empty()) { 184 // No app icon. Just use the favicon from WebContents. 185 UpdateShortcuts(); 186 return; 187 } 188 189 int preferred_size = std::max(unprocessed_icons_.back().width, 190 unprocessed_icons_.back().height); 191 web_contents_->DownloadImage( 192 unprocessed_icons_.back().url, 193 true, // favicon 194 0, // no maximum size 195 base::Bind(&UpdateShortcutWorker::DidDownloadFavicon, 196 base::Unretained(this), 197 preferred_size)); 198 unprocessed_icons_.pop_back(); 199 } 200 201 void UpdateShortcutWorker::DidDownloadFavicon( 202 int requested_size, 203 int id, 204 int http_status_code, 205 const GURL& image_url, 206 const std::vector<SkBitmap>& bitmaps, 207 const std::vector<gfx::Size>& original_sizes) { 208 std::vector<ui::ScaleFactor> scale_factors; 209 scale_factors.push_back(ui::SCALE_FACTOR_100P); 210 211 std::vector<size_t> closest_indices; 212 SelectFaviconFrameIndices(original_sizes, 213 scale_factors, 214 requested_size, 215 &closest_indices, 216 NULL); 217 size_t closest_index = closest_indices[0]; 218 219 if (!bitmaps.empty() && !bitmaps[closest_index].isNull()) { 220 // Update icon with download image and update shortcut. 221 shortcut_info_.favicon.Add( 222 gfx::Image::CreateFrom1xBitmap(bitmaps[closest_index])); 223 extensions::TabHelper* extensions_tab_helper = 224 extensions::TabHelper::FromWebContents(web_contents_); 225 extensions_tab_helper->SetAppIcon(bitmaps[closest_index]); 226 UpdateShortcuts(); 227 } else { 228 // Try the next icon otherwise. 229 DownloadIcon(); 230 } 231 } 232 233 void UpdateShortcutWorker::CheckExistingShortcuts() { 234 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 235 236 // Locations to check to shortcut_paths. 237 struct { 238 int location_id; 239 const wchar_t* sub_dir; 240 } locations[] = { 241 { 242 base::DIR_USER_DESKTOP, 243 NULL 244 }, { 245 base::DIR_START_MENU, 246 NULL 247 }, { 248 // For Win7, create_in_quick_launch_bar means pinning to taskbar. 249 base::DIR_APP_DATA, 250 (base::win::GetVersion() >= base::win::VERSION_WIN7) ? 251 L"Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar" : 252 L"Microsoft\\Internet Explorer\\Quick Launch" 253 } 254 }; 255 256 for (int i = 0; i < arraysize(locations); ++i) { 257 base::FilePath path; 258 if (!PathService::Get(locations[i].location_id, &path)) { 259 NOTREACHED(); 260 continue; 261 } 262 263 if (locations[i].sub_dir != NULL) 264 path = path.Append(locations[i].sub_dir); 265 266 base::FilePath shortcut_file = path.Append(file_name_). 267 ReplaceExtension(FILE_PATH_LITERAL(".lnk")); 268 if (base::PathExists(shortcut_file)) { 269 shortcut_files_.push_back(shortcut_file); 270 } 271 } 272 } 273 274 void UpdateShortcutWorker::UpdateShortcuts() { 275 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, 276 base::Bind(&UpdateShortcutWorker::UpdateShortcutsOnFileThread, 277 base::Unretained(this))); 278 } 279 280 void UpdateShortcutWorker::UpdateShortcutsOnFileThread() { 281 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); 282 283 base::FilePath web_app_path = web_app::GetWebAppDataDirectory( 284 profile_path_, shortcut_info_.extension_id, shortcut_info_.url); 285 286 // Ensure web_app_path exists. web_app_path could be missing for a legacy 287 // shortcut created by Gears. 288 if (!base::PathExists(web_app_path) && 289 !base::CreateDirectory(web_app_path)) { 290 NOTREACHED(); 291 return; 292 } 293 294 base::FilePath icon_file = web_app_path.Append(file_name_).ReplaceExtension( 295 FILE_PATH_LITERAL(".ico")); 296 web_app::internals::CheckAndSaveIcon(icon_file, shortcut_info_.favicon); 297 298 // Update existing shortcuts' description, icon and app id. 299 CheckExistingShortcuts(); 300 if (!shortcut_files_.empty()) { 301 // Generates app id from web app url and profile path. 302 base::string16 app_id = ShellIntegration::GetAppModelIdForProfile( 303 UTF8ToWide(web_app::GenerateApplicationNameFromURL(shortcut_info_.url)), 304 profile_path_); 305 306 // Sanitize description 307 if (shortcut_info_.description.length() >= MAX_PATH) 308 shortcut_info_.description.resize(MAX_PATH - 1); 309 310 for (size_t i = 0; i < shortcut_files_.size(); ++i) { 311 base::win::ShortcutProperties shortcut_properties; 312 shortcut_properties.set_target(shortcut_files_[i]); 313 shortcut_properties.set_description(shortcut_info_.description); 314 shortcut_properties.set_icon(icon_file, 0); 315 shortcut_properties.set_app_id(app_id); 316 base::win::CreateOrUpdateShortcutLink( 317 shortcut_files_[i], shortcut_properties, 318 base::win::SHORTCUT_UPDATE_EXISTING); 319 } 320 } 321 322 OnShortcutsUpdated(true); 323 } 324 325 void UpdateShortcutWorker::OnShortcutsUpdated(bool) { 326 DeleteMe(); // We are done. 327 } 328 329 void UpdateShortcutWorker::DeleteMe() { 330 if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { 331 DeleteMeOnUIThread(); 332 } else { 333 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 334 base::Bind(&UpdateShortcutWorker::DeleteMeOnUIThread, 335 base::Unretained(this))); 336 } 337 } 338 339 void UpdateShortcutWorker::DeleteMeOnUIThread() { 340 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 341 delete this; 342 } 343 #endif // defined(OS_WIN) 344 345 void OnImageLoaded(ShellIntegration::ShortcutInfo shortcut_info, 346 web_app::ShortcutInfoCallback callback, 347 const gfx::Image& image) { 348 // If the image failed to load (e.g. if the resource being loaded was empty) 349 // use the standard application icon. 350 if (image.IsEmpty()) { 351 gfx::Image default_icon = 352 ResourceBundle::GetSharedInstance().GetImageNamed(IDR_APP_DEFAULT_ICON); 353 int size = kDesiredSizes[kNumDesiredSizes - 1]; 354 SkBitmap bmp = skia::ImageOperations::Resize( 355 *default_icon.ToSkBitmap(), skia::ImageOperations::RESIZE_BEST, 356 size, size); 357 gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bmp); 358 // We are on the UI thread, and this image is needed from the FILE thread, 359 // for creating shortcut icon files. 360 image_skia.MakeThreadSafe(); 361 shortcut_info.favicon.Add(gfx::Image(image_skia)); 362 } else { 363 // As described in UpdateShortcutInfoAndIconForApp, image contains all of 364 // the icons, hackily put into a single ImageSkia. Separate them out into 365 // individual ImageSkias and insert them into the icon family. 366 const gfx::ImageSkia& multires_image_skia = image.AsImageSkia(); 367 // NOTE: We do not call ImageSkia::EnsureRepsForSupportedScales here. 368 // The image reps here are not really for different scale factors (ImageSkia 369 // is just being used as a handy container for multiple images). 370 std::vector<gfx::ImageSkiaRep> image_reps = 371 multires_image_skia.image_reps(); 372 for (std::vector<gfx::ImageSkiaRep>::const_iterator it = image_reps.begin(); 373 it != image_reps.end(); ++it) { 374 gfx::ImageSkia image_skia(*it); 375 image_skia.MakeThreadSafe(); 376 shortcut_info.favicon.Add(image_skia); 377 } 378 } 379 380 callback.Run(shortcut_info); 381 } 382 383 } // namespace 384 385 namespace web_app { 386 387 ShellIntegration::ShortcutInfo ShortcutInfoForExtensionAndProfile( 388 const extensions::Extension* extension, Profile* profile) { 389 ShellIntegration::ShortcutInfo shortcut_info; 390 web_app::UpdateShortcutInfoForApp(*extension, profile, &shortcut_info); 391 return shortcut_info; 392 } 393 394 void GetShortcutInfoForTab(WebContents* web_contents, 395 ShellIntegration::ShortcutInfo* info) { 396 DCHECK(info); // Must provide a valid info. 397 398 const FaviconTabHelper* favicon_tab_helper = 399 FaviconTabHelper::FromWebContents(web_contents); 400 const extensions::TabHelper* extensions_tab_helper = 401 extensions::TabHelper::FromWebContents(web_contents); 402 const WebApplicationInfo& app_info = extensions_tab_helper->web_app_info(); 403 404 info->url = app_info.app_url.is_empty() ? web_contents->GetURL() : 405 app_info.app_url; 406 info->title = app_info.title.empty() ? 407 (web_contents->GetTitle().empty() ? UTF8ToUTF16(info->url.spec()) : 408 web_contents->GetTitle()) : 409 app_info.title; 410 info->description = app_info.description; 411 info->favicon.Add(favicon_tab_helper->GetFavicon()); 412 413 Profile* profile = 414 Profile::FromBrowserContext(web_contents->GetBrowserContext()); 415 info->profile_path = profile->GetPath(); 416 } 417 418 void UpdateShortcutForTabContents(WebContents* web_contents) { 419 #if defined(OS_WIN) 420 // UpdateShortcutWorker will delete itself when it's done. 421 UpdateShortcutWorker* worker = new UpdateShortcutWorker(web_contents); 422 worker->Run(); 423 #endif // defined(OS_WIN) 424 } 425 426 void UpdateShortcutInfoForApp(const extensions::Extension& app, 427 Profile* profile, 428 ShellIntegration::ShortcutInfo* shortcut_info) { 429 shortcut_info->extension_id = app.id(); 430 shortcut_info->is_platform_app = app.is_platform_app(); 431 shortcut_info->url = extensions::AppLaunchInfo::GetLaunchWebURL(&app); 432 shortcut_info->title = UTF8ToUTF16(app.name()); 433 shortcut_info->description = UTF8ToUTF16(app.description()); 434 shortcut_info->extension_path = app.path(); 435 shortcut_info->profile_path = profile->GetPath(); 436 shortcut_info->profile_name = 437 profile->GetPrefs()->GetString(prefs::kProfileName); 438 } 439 440 void UpdateShortcutInfoAndIconForApp( 441 const extensions::Extension& extension, 442 Profile* profile, 443 const web_app::ShortcutInfoCallback& callback) { 444 ShellIntegration::ShortcutInfo shortcut_info = 445 ShortcutInfoForExtensionAndProfile(&extension, profile); 446 447 // We want to load each icon into a separate ImageSkia to insert into an 448 // ImageFamily, but LoadImagesAsync currently only builds a single ImageSkia. 449 // Hack around this by loading all images into the ImageSkia as 100% 450 // representations, and later (in OnImageLoaded), pulling them out and 451 // individually inserting them into an ImageFamily. 452 // TODO(mgiuca): Have ImageLoader build the ImageFamily directly 453 // (http://crbug.com/230184). 454 std::vector<extensions::ImageLoader::ImageRepresentation> info_list; 455 for (size_t i = 0; i < kNumDesiredSizes; ++i) { 456 int size = kDesiredSizes[i]; 457 extensions::ExtensionResource resource = 458 extensions::IconsInfo::GetIconResource( 459 &extension, size, ExtensionIconSet::MATCH_EXACTLY); 460 if (!resource.empty()) { 461 info_list.push_back(extensions::ImageLoader::ImageRepresentation( 462 resource, 463 extensions::ImageLoader::ImageRepresentation::RESIZE_WHEN_LARGER, 464 gfx::Size(size, size), 465 ui::SCALE_FACTOR_100P)); 466 } 467 } 468 469 if (info_list.empty()) { 470 size_t i = kNumDesiredSizes - 1; 471 int size = kDesiredSizes[i]; 472 473 // If there is no icon at the desired sizes, we will resize what we can get. 474 // Making a large icon smaller is preferred to making a small icon larger, 475 // so look for a larger icon first: 476 extensions::ExtensionResource resource = 477 extensions::IconsInfo::GetIconResource( 478 &extension, size, ExtensionIconSet::MATCH_BIGGER); 479 if (resource.empty()) { 480 resource = extensions::IconsInfo::GetIconResource( 481 &extension, size, ExtensionIconSet::MATCH_SMALLER); 482 } 483 info_list.push_back(extensions::ImageLoader::ImageRepresentation( 484 resource, 485 extensions::ImageLoader::ImageRepresentation::RESIZE_WHEN_LARGER, 486 gfx::Size(size, size), 487 ui::SCALE_FACTOR_100P)); 488 } 489 490 // |info_list| may still be empty at this point, in which case LoadImage 491 // will call the OnImageLoaded callback with an empty image and exit 492 // immediately. 493 extensions::ImageLoader::Get(profile)->LoadImagesAsync(&extension, info_list, 494 base::Bind(&OnImageLoaded, shortcut_info, callback)); 495 } 496 497 } // namespace web_app 498