8a5bf2a0df6f0376e676cc1fe238247f83af9dca
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / web_applications / web_app_ui.cc
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         base::UTF8ToWide(
304             web_app::GenerateApplicationNameFromURL(shortcut_info_.url)),
305         profile_path_);
306
307     // Sanitize description
308     if (shortcut_info_.description.length() >= MAX_PATH)
309       shortcut_info_.description.resize(MAX_PATH - 1);
310
311     for (size_t i = 0; i < shortcut_files_.size(); ++i) {
312       base::win::ShortcutProperties shortcut_properties;
313       shortcut_properties.set_target(shortcut_files_[i]);
314       shortcut_properties.set_description(shortcut_info_.description);
315       shortcut_properties.set_icon(icon_file, 0);
316       shortcut_properties.set_app_id(app_id);
317       base::win::CreateOrUpdateShortcutLink(
318           shortcut_files_[i], shortcut_properties,
319           base::win::SHORTCUT_UPDATE_EXISTING);
320     }
321   }
322
323   OnShortcutsUpdated(true);
324 }
325
326 void UpdateShortcutWorker::OnShortcutsUpdated(bool) {
327   DeleteMe();  // We are done.
328 }
329
330 void UpdateShortcutWorker::DeleteMe() {
331   if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
332     DeleteMeOnUIThread();
333   } else {
334     BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
335       base::Bind(&UpdateShortcutWorker::DeleteMeOnUIThread,
336                  base::Unretained(this)));
337   }
338 }
339
340 void UpdateShortcutWorker::DeleteMeOnUIThread() {
341   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
342   delete this;
343 }
344 #endif  // defined(OS_WIN)
345
346 void OnImageLoaded(ShellIntegration::ShortcutInfo shortcut_info,
347                    web_app::ShortcutInfoCallback callback,
348                    const gfx::ImageFamily& image_family) {
349   // If the image failed to load (e.g. if the resource being loaded was empty)
350   // use the standard application icon.
351   if (image_family.empty()) {
352     gfx::Image default_icon =
353         ResourceBundle::GetSharedInstance().GetImageNamed(IDR_APP_DEFAULT_ICON);
354     int size = kDesiredSizes[kNumDesiredSizes - 1];
355     SkBitmap bmp = skia::ImageOperations::Resize(
356           *default_icon.ToSkBitmap(), skia::ImageOperations::RESIZE_BEST,
357           size, size);
358     gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bmp);
359     // We are on the UI thread, and this image is needed from the FILE thread,
360     // for creating shortcut icon files.
361     image_skia.MakeThreadSafe();
362     shortcut_info.favicon.Add(gfx::Image(image_skia));
363   } else {
364     shortcut_info.favicon = image_family;
365   }
366
367   callback.Run(shortcut_info);
368 }
369
370 }  // namespace
371
372 namespace web_app {
373
374 ShellIntegration::ShortcutInfo ShortcutInfoForExtensionAndProfile(
375     const extensions::Extension* extension, Profile* profile) {
376   ShellIntegration::ShortcutInfo shortcut_info;
377   web_app::UpdateShortcutInfoForApp(*extension, profile, &shortcut_info);
378   return shortcut_info;
379 }
380
381 void GetShortcutInfoForTab(WebContents* web_contents,
382                            ShellIntegration::ShortcutInfo* info) {
383   DCHECK(info);  // Must provide a valid info.
384
385   const FaviconTabHelper* favicon_tab_helper =
386       FaviconTabHelper::FromWebContents(web_contents);
387   const extensions::TabHelper* extensions_tab_helper =
388       extensions::TabHelper::FromWebContents(web_contents);
389   const WebApplicationInfo& app_info = extensions_tab_helper->web_app_info();
390
391   info->url = app_info.app_url.is_empty() ? web_contents->GetURL() :
392                                             app_info.app_url;
393   info->title = app_info.title.empty() ?
394       (web_contents->GetTitle().empty() ? base::UTF8ToUTF16(info->url.spec()) :
395                                           web_contents->GetTitle()) :
396       app_info.title;
397   info->description = app_info.description;
398   info->favicon.Add(favicon_tab_helper->GetFavicon());
399
400   Profile* profile =
401       Profile::FromBrowserContext(web_contents->GetBrowserContext());
402   info->profile_path = profile->GetPath();
403 }
404
405 void UpdateShortcutForTabContents(WebContents* web_contents) {
406 #if defined(OS_WIN)
407   // UpdateShortcutWorker will delete itself when it's done.
408   UpdateShortcutWorker* worker = new UpdateShortcutWorker(web_contents);
409   worker->Run();
410 #endif  // defined(OS_WIN)
411 }
412
413 void UpdateShortcutInfoForApp(const extensions::Extension& app,
414                               Profile* profile,
415                               ShellIntegration::ShortcutInfo* shortcut_info) {
416   shortcut_info->extension_id = app.id();
417   shortcut_info->is_platform_app = app.is_platform_app();
418   shortcut_info->url = extensions::AppLaunchInfo::GetLaunchWebURL(&app);
419   shortcut_info->title = base::UTF8ToUTF16(app.name());
420   shortcut_info->description = base::UTF8ToUTF16(app.description());
421   shortcut_info->extension_path = app.path();
422   shortcut_info->profile_path = profile->GetPath();
423   shortcut_info->profile_name =
424       profile->GetPrefs()->GetString(prefs::kProfileName);
425 }
426
427 void UpdateShortcutInfoAndIconForApp(
428     const extensions::Extension* extension,
429     Profile* profile,
430     const web_app::ShortcutInfoCallback& callback) {
431   ShellIntegration::ShortcutInfo shortcut_info =
432       ShortcutInfoForExtensionAndProfile(extension, profile);
433
434   std::vector<extensions::ImageLoader::ImageRepresentation> info_list;
435   for (size_t i = 0; i < kNumDesiredSizes; ++i) {
436     int size = kDesiredSizes[i];
437     extensions::ExtensionResource resource =
438         extensions::IconsInfo::GetIconResource(
439             extension, size, ExtensionIconSet::MATCH_EXACTLY);
440     if (!resource.empty()) {
441       info_list.push_back(extensions::ImageLoader::ImageRepresentation(
442           resource,
443           extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE,
444           gfx::Size(size, size),
445           ui::SCALE_FACTOR_100P));
446     }
447   }
448
449   if (info_list.empty()) {
450     size_t i = kNumDesiredSizes - 1;
451     int size = kDesiredSizes[i];
452
453     // If there is no icon at the desired sizes, we will resize what we can get.
454     // Making a large icon smaller is preferred to making a small icon larger,
455     // so look for a larger icon first:
456     extensions::ExtensionResource resource =
457         extensions::IconsInfo::GetIconResource(
458             extension, size, ExtensionIconSet::MATCH_BIGGER);
459     if (resource.empty()) {
460       resource = extensions::IconsInfo::GetIconResource(
461           extension, size, ExtensionIconSet::MATCH_SMALLER);
462     }
463     info_list.push_back(extensions::ImageLoader::ImageRepresentation(
464         resource,
465         extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE,
466         gfx::Size(size, size),
467         ui::SCALE_FACTOR_100P));
468   }
469
470   // |info_list| may still be empty at this point, in which case
471   // LoadImageFamilyAsync will call the OnImageLoaded callback with an empty
472   // image and exit immediately.
473   extensions::ImageLoader::Get(profile)->LoadImageFamilyAsync(
474       extension,
475       info_list,
476       base::Bind(&OnImageLoaded, shortcut_info, callback));
477 }
478
479 }  // namespace web_app