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.
5 #include "chrome/browser/ui/web_applications/web_app_ui.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"
39 #if defined(OS_POSIX) && !defined(OS_MACOSX)
40 #include "base/environment.h"
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"
50 using content::BrowserThread
;
51 using content::NavigationController
;
52 using content::WebContents
;
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
);
65 const int* kDesiredSizes
= IconUtil::kIconDimensions
;
66 const size_t kNumDesiredSizes
= IconUtil::kNumIconDimensions
;
68 const int kDesiredSizes
[] = {32};
69 const size_t kNumDesiredSizes
= arraysize(kDesiredSizes
);
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
{
80 explicit UpdateShortcutWorker(WebContents
* web_contents
);
85 // Overridden from content::NotificationObserver:
86 virtual void Observe(int type
,
87 const content::NotificationSource
& source
,
88 const content::NotificationDetails
& details
);
90 // Downloads icon via the FaviconTabHelper.
93 // Favicon download callback.
94 void DidDownloadFavicon(
98 const GURL
& image_url
,
99 const std::vector
<SkBitmap
>& bitmaps
,
100 const std::vector
<gfx::Size
>& original_bitmap_sizes
);
102 // Checks if shortcuts exists on desktop, start menu and quick launch.
103 void CheckExistingShortcuts();
105 // Update shortcut files and icons.
106 void UpdateShortcuts();
107 void UpdateShortcutsOnFileThread();
109 // Callback after shortcuts are updated.
110 void OnShortcutsUpdated(bool);
112 // Deletes the worker on UI thread where it gets created.
114 void DeleteMeOnUIThread();
116 content::NotificationRegistrar registrar_
;
118 // Underlying WebContents whose shortcuts will be updated.
119 WebContents
* web_contents_
;
121 // Icons info from web_contents_'s web app data.
122 web_app::IconInfoList unprocessed_icons_
;
124 // Cached shortcut data from the web_contents_.
125 ShellIntegration::ShortcutInfo shortcut_info_
;
127 // Our copy of profile path.
128 base::FilePath profile_path_
;
130 // File name of shortcut/ico file based on app title.
131 base::FilePath file_name_
;
133 // Existing shortcuts.
134 std::vector
<base::FilePath
> shortcut_files_
;
136 DISALLOW_COPY_AND_ASSIGN(UpdateShortcutWorker
);
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
);
152 chrome::NOTIFICATION_TAB_CLOSING
,
153 content::Source
<NavigationController
>(&web_contents
->GetController()));
156 void UpdateShortcutWorker::Run() {
157 // Starting by downloading app icon.
161 void UpdateShortcutWorker::Observe(
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
;
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
));
178 if (web_contents_
== NULL
) {
179 DeleteMe(); // We are done if underlying WebContents is gone.
183 if (unprocessed_icons_
.empty()) {
184 // No app icon. Just use the favicon from WebContents.
189 int preferred_size
= std::max(unprocessed_icons_
.back().width
,
190 unprocessed_icons_
.back().height
);
191 web_contents_
->DownloadImage(
192 unprocessed_icons_
.back().url
,
194 0, // no maximum size
195 base::Bind(&UpdateShortcutWorker::DidDownloadFavicon
,
196 base::Unretained(this),
198 unprocessed_icons_
.pop_back();
201 void UpdateShortcutWorker::DidDownloadFavicon(
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
);
211 std::vector
<size_t> closest_indices
;
212 SelectFaviconFrameIndices(original_sizes
,
217 size_t closest_index
= closest_indices
[0];
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
]);
228 // Try the next icon otherwise.
233 void UpdateShortcutWorker::CheckExistingShortcuts() {
234 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
236 // Locations to check to shortcut_paths.
239 const wchar_t* sub_dir
;
242 base::DIR_USER_DESKTOP
,
245 base::DIR_START_MENU
,
248 // For Win7, create_in_quick_launch_bar means pinning to taskbar.
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"
256 for (int i
= 0; i
< arraysize(locations
); ++i
) {
258 if (!PathService::Get(locations
[i
].location_id
, &path
)) {
263 if (locations
[i
].sub_dir
!= NULL
)
264 path
= path
.Append(locations
[i
].sub_dir
);
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
);
274 void UpdateShortcutWorker::UpdateShortcuts() {
275 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE
,
276 base::Bind(&UpdateShortcutWorker::UpdateShortcutsOnFileThread
,
277 base::Unretained(this)));
280 void UpdateShortcutWorker::UpdateShortcutsOnFileThread() {
281 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
283 base::FilePath web_app_path
= web_app::GetWebAppDataDirectory(
284 profile_path_
, shortcut_info_
.extension_id
, shortcut_info_
.url
);
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
)) {
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
);
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(
304 web_app::GenerateApplicationNameFromURL(shortcut_info_
.url
)),
307 // Sanitize description
308 if (shortcut_info_
.description
.length() >= MAX_PATH
)
309 shortcut_info_
.description
.resize(MAX_PATH
- 1);
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
);
323 OnShortcutsUpdated(true);
326 void UpdateShortcutWorker::OnShortcutsUpdated(bool) {
327 DeleteMe(); // We are done.
330 void UpdateShortcutWorker::DeleteMe() {
331 if (BrowserThread::CurrentlyOn(BrowserThread::UI
)) {
332 DeleteMeOnUIThread();
334 BrowserThread::PostTask(BrowserThread::UI
, FROM_HERE
,
335 base::Bind(&UpdateShortcutWorker::DeleteMeOnUIThread
,
336 base::Unretained(this)));
340 void UpdateShortcutWorker::DeleteMeOnUIThread() {
341 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
344 #endif // defined(OS_WIN)
346 void OnImageLoaded(ShellIntegration::ShortcutInfo shortcut_info
,
347 web_app::ShortcutInfoCallback callback
,
348 const gfx::Image
& image
) {
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
.IsEmpty()) {
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
,
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
));
364 // As described in UpdateShortcutInfoAndIconForApp, image contains all of
365 // the icons, hackily put into a single ImageSkia. Separate them out into
366 // individual ImageSkias and insert them into the icon family.
367 const gfx::ImageSkia
& multires_image_skia
= image
.AsImageSkia();
368 // NOTE: We do not call ImageSkia::EnsureRepsForSupportedScales here.
369 // The image reps here are not really for different scale factors (ImageSkia
370 // is just being used as a handy container for multiple images).
371 std::vector
<gfx::ImageSkiaRep
> image_reps
=
372 multires_image_skia
.image_reps();
373 for (std::vector
<gfx::ImageSkiaRep
>::const_iterator it
= image_reps
.begin();
374 it
!= image_reps
.end(); ++it
) {
375 gfx::ImageSkia
image_skia(*it
);
376 image_skia
.MakeThreadSafe();
377 shortcut_info
.favicon
.Add(image_skia
);
381 callback
.Run(shortcut_info
);
388 ShellIntegration::ShortcutInfo
ShortcutInfoForExtensionAndProfile(
389 const extensions::Extension
* extension
, Profile
* profile
) {
390 ShellIntegration::ShortcutInfo shortcut_info
;
391 web_app::UpdateShortcutInfoForApp(*extension
, profile
, &shortcut_info
);
392 return shortcut_info
;
395 void GetShortcutInfoForTab(WebContents
* web_contents
,
396 ShellIntegration::ShortcutInfo
* info
) {
397 DCHECK(info
); // Must provide a valid info.
399 const FaviconTabHelper
* favicon_tab_helper
=
400 FaviconTabHelper::FromWebContents(web_contents
);
401 const extensions::TabHelper
* extensions_tab_helper
=
402 extensions::TabHelper::FromWebContents(web_contents
);
403 const WebApplicationInfo
& app_info
= extensions_tab_helper
->web_app_info();
405 info
->url
= app_info
.app_url
.is_empty() ? web_contents
->GetURL() :
407 info
->title
= app_info
.title
.empty() ?
408 (web_contents
->GetTitle().empty() ? base::UTF8ToUTF16(info
->url
.spec()) :
409 web_contents
->GetTitle()) :
411 info
->description
= app_info
.description
;
412 info
->favicon
.Add(favicon_tab_helper
->GetFavicon());
415 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
416 info
->profile_path
= profile
->GetPath();
419 void UpdateShortcutForTabContents(WebContents
* web_contents
) {
421 // UpdateShortcutWorker will delete itself when it's done.
422 UpdateShortcutWorker
* worker
= new UpdateShortcutWorker(web_contents
);
424 #endif // defined(OS_WIN)
427 void UpdateShortcutInfoForApp(const extensions::Extension
& app
,
429 ShellIntegration::ShortcutInfo
* shortcut_info
) {
430 shortcut_info
->extension_id
= app
.id();
431 shortcut_info
->is_platform_app
= app
.is_platform_app();
432 shortcut_info
->url
= extensions::AppLaunchInfo::GetLaunchWebURL(&app
);
433 shortcut_info
->title
= base::UTF8ToUTF16(app
.name());
434 shortcut_info
->description
= base::UTF8ToUTF16(app
.description());
435 shortcut_info
->extension_path
= app
.path();
436 shortcut_info
->profile_path
= profile
->GetPath();
437 shortcut_info
->profile_name
=
438 profile
->GetPrefs()->GetString(prefs::kProfileName
);
441 void UpdateShortcutInfoAndIconForApp(
442 const extensions::Extension
& extension
,
444 const web_app::ShortcutInfoCallback
& callback
) {
445 ShellIntegration::ShortcutInfo shortcut_info
=
446 ShortcutInfoForExtensionAndProfile(&extension
, profile
);
448 // We want to load each icon into a separate ImageSkia to insert into an
449 // ImageFamily, but LoadImagesAsync currently only builds a single ImageSkia.
450 // Hack around this by loading all images into the ImageSkia as 100%
451 // representations, and later (in OnImageLoaded), pulling them out and
452 // individually inserting them into an ImageFamily.
453 // TODO(mgiuca): Have ImageLoader build the ImageFamily directly
454 // (http://crbug.com/230184).
455 std::vector
<extensions::ImageLoader::ImageRepresentation
> info_list
;
456 for (size_t i
= 0; i
< kNumDesiredSizes
; ++i
) {
457 int size
= kDesiredSizes
[i
];
458 extensions::ExtensionResource resource
=
459 extensions::IconsInfo::GetIconResource(
460 &extension
, size
, ExtensionIconSet::MATCH_EXACTLY
);
461 if (!resource
.empty()) {
462 info_list
.push_back(extensions::ImageLoader::ImageRepresentation(
464 extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE
,
465 gfx::Size(size
, size
),
466 ui::SCALE_FACTOR_100P
));
470 if (info_list
.empty()) {
471 size_t i
= kNumDesiredSizes
- 1;
472 int size
= kDesiredSizes
[i
];
474 // If there is no icon at the desired sizes, we will resize what we can get.
475 // Making a large icon smaller is preferred to making a small icon larger,
476 // so look for a larger icon first:
477 extensions::ExtensionResource resource
=
478 extensions::IconsInfo::GetIconResource(
479 &extension
, size
, ExtensionIconSet::MATCH_BIGGER
);
480 if (resource
.empty()) {
481 resource
= extensions::IconsInfo::GetIconResource(
482 &extension
, size
, ExtensionIconSet::MATCH_SMALLER
);
484 info_list
.push_back(extensions::ImageLoader::ImageRepresentation(
486 extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE
,
487 gfx::Size(size
, size
),
488 ui::SCALE_FACTOR_100P
));
491 // |info_list| may still be empty at this point, in which case LoadImage
492 // will call the OnImageLoaded callback with an empty image and exit
494 extensions::ImageLoader::Get(profile
)->LoadImagesAsync(&extension
, info_list
,
495 base::Bind(&OnImageLoaded
, shortcut_info
, callback
));
498 } // namespace web_app