Home | History | Annotate | Download | only in web_applications
      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