// 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/web_applications/web_app_ui.h" #include "base/bind.h" #include "base/bind_helpers.h" #include "base/file_util.h" #include "base/path_service.h" #include "base/prefs/pref_service.h" #include "base/strings/string16.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/image_loader.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/favicon/favicon_tab_helper.h" #include "chrome/browser/history/select_favicon_frames.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/web_applications/web_app.h" #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" #include "chrome/common/extensions/manifest_handlers/icons_handler.h" #include "chrome/common/pref_names.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_registrar.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "extensions/common/extension.h" #include "grit/theme_resources.h" #include "skia/ext/image_operations.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_family.h" #include "ui/gfx/image/image_skia.h" #include "url/gurl.h" #if defined(OS_POSIX) && !defined(OS_MACOSX) #include "base/environment.h" #endif #if defined(OS_WIN) #include "base/win/shortcut.h" #include "base/win/windows_version.h" #include "chrome/browser/web_applications/web_app_win.h" #include "ui/gfx/icon_util.h" #endif using content::BrowserThread; using content::NavigationController; using content::WebContents; namespace { #if defined(OS_MACOSX) const int kDesiredSizes[] = {16, 32, 128, 256, 512}; const size_t kNumDesiredSizes = arraysize(kDesiredSizes); #elif defined(OS_LINUX) // Linux supports icons of any size. FreeDesktop Icon Theme Specification states // that "Minimally you should install a 48x48 icon in the hicolor theme." const int kDesiredSizes[] = {16, 32, 48, 128, 256, 512}; const size_t kNumDesiredSizes = arraysize(kDesiredSizes); #elif defined(OS_WIN) const int* kDesiredSizes = IconUtil::kIconDimensions; const size_t kNumDesiredSizes = IconUtil::kNumIconDimensions; #else const int kDesiredSizes[] = {32}; const size_t kNumDesiredSizes = arraysize(kDesiredSizes); #endif #if defined(OS_WIN) // UpdateShortcutWorker holds all context data needed for update shortcut. // It schedules a pre-update check to find all shortcuts that needs to be // updated. If there are such shortcuts, it schedules icon download and // update them when icons are downloaded. It observes TAB_CLOSING notification // and cancels all the work when the underlying tab is closing. class UpdateShortcutWorker : public content::NotificationObserver { public: explicit UpdateShortcutWorker(WebContents* web_contents); void Run(); private: // Overridden from content::NotificationObserver: virtual void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details); // Downloads icon via the FaviconTabHelper. void DownloadIcon(); // Favicon download callback. void DidDownloadFavicon( int requested_size, int id, int http_status_code, const GURL& image_url, const std::vector& bitmaps, const std::vector& original_bitmap_sizes); // Checks if shortcuts exists on desktop, start menu and quick launch. void CheckExistingShortcuts(); // Update shortcut files and icons. void UpdateShortcuts(); void UpdateShortcutsOnFileThread(); // Callback after shortcuts are updated. void OnShortcutsUpdated(bool); // Deletes the worker on UI thread where it gets created. void DeleteMe(); void DeleteMeOnUIThread(); content::NotificationRegistrar registrar_; // Underlying WebContents whose shortcuts will be updated. WebContents* web_contents_; // Icons info from web_contents_'s web app data. web_app::IconInfoList unprocessed_icons_; // Cached shortcut data from the web_contents_. ShellIntegration::ShortcutInfo shortcut_info_; // Our copy of profile path. base::FilePath profile_path_; // File name of shortcut/ico file based on app title. base::FilePath file_name_; // Existing shortcuts. std::vector shortcut_files_; DISALLOW_COPY_AND_ASSIGN(UpdateShortcutWorker); }; UpdateShortcutWorker::UpdateShortcutWorker(WebContents* web_contents) : web_contents_(web_contents), profile_path_(Profile::FromBrowserContext( web_contents->GetBrowserContext())->GetPath()) { extensions::TabHelper* extensions_tab_helper = extensions::TabHelper::FromWebContents(web_contents); web_app::GetShortcutInfoForTab(web_contents_, &shortcut_info_); web_app::GetIconsInfo(extensions_tab_helper->web_app_info(), &unprocessed_icons_); file_name_ = web_app::internals::GetSanitizedFileName(shortcut_info_.title); registrar_.Add( this, chrome::NOTIFICATION_TAB_CLOSING, content::Source(&web_contents->GetController())); } void UpdateShortcutWorker::Run() { // Starting by downloading app icon. DownloadIcon(); } void UpdateShortcutWorker::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { if (type == chrome::NOTIFICATION_TAB_CLOSING && content::Source(source).ptr() == &web_contents_->GetController()) { // Underlying tab is closing. web_contents_ = NULL; } } void UpdateShortcutWorker::DownloadIcon() { // FetchIcon must run on UI thread because it relies on WebContents // to download the icon. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (web_contents_ == NULL) { DeleteMe(); // We are done if underlying WebContents is gone. return; } if (unprocessed_icons_.empty()) { // No app icon. Just use the favicon from WebContents. UpdateShortcuts(); return; } int preferred_size = std::max(unprocessed_icons_.back().width, unprocessed_icons_.back().height); web_contents_->DownloadImage( unprocessed_icons_.back().url, true, // favicon 0, // no maximum size base::Bind(&UpdateShortcutWorker::DidDownloadFavicon, base::Unretained(this), preferred_size)); unprocessed_icons_.pop_back(); } void UpdateShortcutWorker::DidDownloadFavicon( int requested_size, int id, int http_status_code, const GURL& image_url, const std::vector& bitmaps, const std::vector& original_sizes) { std::vector scale_factors; scale_factors.push_back(ui::SCALE_FACTOR_100P); std::vector closest_indices; SelectFaviconFrameIndices(original_sizes, scale_factors, requested_size, &closest_indices, NULL); size_t closest_index = closest_indices[0]; if (!bitmaps.empty() && !bitmaps[closest_index].isNull()) { // Update icon with download image and update shortcut. shortcut_info_.favicon.Add( gfx::Image::CreateFrom1xBitmap(bitmaps[closest_index])); extensions::TabHelper* extensions_tab_helper = extensions::TabHelper::FromWebContents(web_contents_); extensions_tab_helper->SetAppIcon(bitmaps[closest_index]); UpdateShortcuts(); } else { // Try the next icon otherwise. DownloadIcon(); } } void UpdateShortcutWorker::CheckExistingShortcuts() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); // Locations to check to shortcut_paths. struct { int location_id; const wchar_t* sub_dir; } locations[] = { { base::DIR_USER_DESKTOP, NULL }, { base::DIR_START_MENU, NULL }, { // For Win7, create_in_quick_launch_bar means pinning to taskbar. base::DIR_APP_DATA, (base::win::GetVersion() >= base::win::VERSION_WIN7) ? L"Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar" : L"Microsoft\\Internet Explorer\\Quick Launch" } }; for (int i = 0; i < arraysize(locations); ++i) { base::FilePath path; if (!PathService::Get(locations[i].location_id, &path)) { NOTREACHED(); continue; } if (locations[i].sub_dir != NULL) path = path.Append(locations[i].sub_dir); base::FilePath shortcut_file = path.Append(file_name_). ReplaceExtension(FILE_PATH_LITERAL(".lnk")); if (base::PathExists(shortcut_file)) { shortcut_files_.push_back(shortcut_file); } } } void UpdateShortcutWorker::UpdateShortcuts() { BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, base::Bind(&UpdateShortcutWorker::UpdateShortcutsOnFileThread, base::Unretained(this))); } void UpdateShortcutWorker::UpdateShortcutsOnFileThread() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); base::FilePath web_app_path = web_app::GetWebAppDataDirectory( profile_path_, shortcut_info_.extension_id, shortcut_info_.url); // Ensure web_app_path exists. web_app_path could be missing for a legacy // shortcut created by Gears. if (!base::PathExists(web_app_path) && !base::CreateDirectory(web_app_path)) { NOTREACHED(); return; } base::FilePath icon_file = web_app_path.Append(file_name_).ReplaceExtension( FILE_PATH_LITERAL(".ico")); web_app::internals::CheckAndSaveIcon(icon_file, shortcut_info_.favicon); // Update existing shortcuts' description, icon and app id. CheckExistingShortcuts(); if (!shortcut_files_.empty()) { // Generates app id from web app url and profile path. base::string16 app_id = ShellIntegration::GetAppModelIdForProfile( base::UTF8ToWide( web_app::GenerateApplicationNameFromURL(shortcut_info_.url)), profile_path_); // Sanitize description if (shortcut_info_.description.length() >= MAX_PATH) shortcut_info_.description.resize(MAX_PATH - 1); for (size_t i = 0; i < shortcut_files_.size(); ++i) { base::win::ShortcutProperties shortcut_properties; shortcut_properties.set_target(shortcut_files_[i]); shortcut_properties.set_description(shortcut_info_.description); shortcut_properties.set_icon(icon_file, 0); shortcut_properties.set_app_id(app_id); base::win::CreateOrUpdateShortcutLink( shortcut_files_[i], shortcut_properties, base::win::SHORTCUT_UPDATE_EXISTING); } } OnShortcutsUpdated(true); } void UpdateShortcutWorker::OnShortcutsUpdated(bool) { DeleteMe(); // We are done. } void UpdateShortcutWorker::DeleteMe() { if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { DeleteMeOnUIThread(); } else { BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(&UpdateShortcutWorker::DeleteMeOnUIThread, base::Unretained(this))); } } void UpdateShortcutWorker::DeleteMeOnUIThread() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); delete this; } #endif // defined(OS_WIN) void OnImageLoaded(ShellIntegration::ShortcutInfo shortcut_info, web_app::ShortcutInfoCallback callback, const gfx::Image& image) { // If the image failed to load (e.g. if the resource being loaded was empty) // use the standard application icon. if (image.IsEmpty()) { gfx::Image default_icon = ResourceBundle::GetSharedInstance().GetImageNamed(IDR_APP_DEFAULT_ICON); int size = kDesiredSizes[kNumDesiredSizes - 1]; SkBitmap bmp = skia::ImageOperations::Resize( *default_icon.ToSkBitmap(), skia::ImageOperations::RESIZE_BEST, size, size); gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bmp); // We are on the UI thread, and this image is needed from the FILE thread, // for creating shortcut icon files. image_skia.MakeThreadSafe(); shortcut_info.favicon.Add(gfx::Image(image_skia)); } else { // As described in UpdateShortcutInfoAndIconForApp, image contains all of // the icons, hackily put into a single ImageSkia. Separate them out into // individual ImageSkias and insert them into the icon family. const gfx::ImageSkia& multires_image_skia = image.AsImageSkia(); // NOTE: We do not call ImageSkia::EnsureRepsForSupportedScales here. // The image reps here are not really for different scale factors (ImageSkia // is just being used as a handy container for multiple images). std::vector image_reps = multires_image_skia.image_reps(); for (std::vector::const_iterator it = image_reps.begin(); it != image_reps.end(); ++it) { gfx::ImageSkia image_skia(*it); image_skia.MakeThreadSafe(); shortcut_info.favicon.Add(image_skia); } } callback.Run(shortcut_info); } } // namespace namespace web_app { ShellIntegration::ShortcutInfo ShortcutInfoForExtensionAndProfile( const extensions::Extension* extension, Profile* profile) { ShellIntegration::ShortcutInfo shortcut_info; web_app::UpdateShortcutInfoForApp(*extension, profile, &shortcut_info); return shortcut_info; } void GetShortcutInfoForTab(WebContents* web_contents, ShellIntegration::ShortcutInfo* info) { DCHECK(info); // Must provide a valid info. const FaviconTabHelper* favicon_tab_helper = FaviconTabHelper::FromWebContents(web_contents); const extensions::TabHelper* extensions_tab_helper = extensions::TabHelper::FromWebContents(web_contents); const WebApplicationInfo& app_info = extensions_tab_helper->web_app_info(); info->url = app_info.app_url.is_empty() ? web_contents->GetURL() : app_info.app_url; info->title = app_info.title.empty() ? (web_contents->GetTitle().empty() ? base::UTF8ToUTF16(info->url.spec()) : web_contents->GetTitle()) : app_info.title; info->description = app_info.description; info->favicon.Add(favicon_tab_helper->GetFavicon()); Profile* profile = Profile::FromBrowserContext(web_contents->GetBrowserContext()); info->profile_path = profile->GetPath(); } void UpdateShortcutForTabContents(WebContents* web_contents) { #if defined(OS_WIN) // UpdateShortcutWorker will delete itself when it's done. UpdateShortcutWorker* worker = new UpdateShortcutWorker(web_contents); worker->Run(); #endif // defined(OS_WIN) } void UpdateShortcutInfoForApp(const extensions::Extension& app, Profile* profile, ShellIntegration::ShortcutInfo* shortcut_info) { shortcut_info->extension_id = app.id(); shortcut_info->is_platform_app = app.is_platform_app(); shortcut_info->url = extensions::AppLaunchInfo::GetLaunchWebURL(&app); shortcut_info->title = base::UTF8ToUTF16(app.name()); shortcut_info->description = base::UTF8ToUTF16(app.description()); shortcut_info->extension_path = app.path(); shortcut_info->profile_path = profile->GetPath(); shortcut_info->profile_name = profile->GetPrefs()->GetString(prefs::kProfileName); } void UpdateShortcutInfoAndIconForApp( const extensions::Extension& extension, Profile* profile, const web_app::ShortcutInfoCallback& callback) { ShellIntegration::ShortcutInfo shortcut_info = ShortcutInfoForExtensionAndProfile(&extension, profile); // We want to load each icon into a separate ImageSkia to insert into an // ImageFamily, but LoadImagesAsync currently only builds a single ImageSkia. // Hack around this by loading all images into the ImageSkia as 100% // representations, and later (in OnImageLoaded), pulling them out and // individually inserting them into an ImageFamily. // TODO(mgiuca): Have ImageLoader build the ImageFamily directly // (http://crbug.com/230184). std::vector info_list; for (size_t i = 0; i < kNumDesiredSizes; ++i) { int size = kDesiredSizes[i]; extensions::ExtensionResource resource = extensions::IconsInfo::GetIconResource( &extension, size, ExtensionIconSet::MATCH_EXACTLY); if (!resource.empty()) { info_list.push_back(extensions::ImageLoader::ImageRepresentation( resource, extensions::ImageLoader::ImageRepresentation::RESIZE_WHEN_LARGER, gfx::Size(size, size), ui::SCALE_FACTOR_100P)); } } if (info_list.empty()) { size_t i = kNumDesiredSizes - 1; int size = kDesiredSizes[i]; // If there is no icon at the desired sizes, we will resize what we can get. // Making a large icon smaller is preferred to making a small icon larger, // so look for a larger icon first: extensions::ExtensionResource resource = extensions::IconsInfo::GetIconResource( &extension, size, ExtensionIconSet::MATCH_BIGGER); if (resource.empty()) { resource = extensions::IconsInfo::GetIconResource( &extension, size, ExtensionIconSet::MATCH_SMALLER); } info_list.push_back(extensions::ImageLoader::ImageRepresentation( resource, extensions::ImageLoader::ImageRepresentation::RESIZE_WHEN_LARGER, gfx::Size(size, size), ui::SCALE_FACTOR_100P)); } // |info_list| may still be empty at this point, in which case LoadImage // will call the OnImageLoaded callback with an empty image and exit // immediately. extensions::ImageLoader::Get(profile)->LoadImagesAsync(&extension, info_list, base::Bind(&OnImageLoaded, shortcut_info, callback)); } } // namespace web_app