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