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/profiles/profile_shortcut_manager_win.h"
6
7 #include <shlobj.h> // For SHChangeNotify().
8
9 #include <string>
10 #include <vector>
11
12 #include "base/bind.h"
13 #include "base/command_line.h"
14 #include "base/file_util.h"
15 #include "base/files/file_enumerator.h"
16 #include "base/path_service.h"
17 #include "base/prefs/pref_service.h"
18 #include "base/strings/string16.h"
19 #include "base/strings/string_util.h"
20 #include "base/strings/stringprintf.h"
21 #include "base/strings/utf_string_conversions.h"
22 #include "base/win/shortcut.h"
23 #include "chrome/browser/app_icon_win.h"
24 #include "chrome/browser/browser_process.h"
25 #include "chrome/browser/chrome_notification_types.h"
26 #include "chrome/browser/profiles/profile_info_cache_observer.h"
27 #include "chrome/browser/profiles/profile_info_util.h"
28 #include "chrome/browser/profiles/profile_manager.h"
29 #include "chrome/browser/shell_integration.h"
30 #include "chrome/common/chrome_switches.h"
31 #include "chrome/common/pref_names.h"
32 #include "chrome/installer/util/browser_distribution.h"
33 #include "chrome/installer/util/product.h"
34 #include "chrome/installer/util/shell_util.h"
35 #include "content/public/browser/browser_thread.h"
36 #include "content/public/browser/notification_service.h"
37 #include "grit/chrome_unscaled_resources.h"
38 #include "grit/chromium_strings.h"
39 #include "skia/ext/image_operations.h"
40 #include "skia/ext/platform_canvas.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #include "ui/base/resource/resource_bundle.h"
43 #include "ui/gfx/icon_util.h"
44 #include "ui/gfx/image/image.h"
45 #include "ui/gfx/image/image_family.h"
46 #include "ui/gfx/rect.h"
47 #include "ui/gfx/skia_util.h"
48
49 using content::BrowserThread;
50
51 namespace {
52
53 // Name of the badged icon file generated for a given profile.
54 const char kProfileIconFileName[] = "Google Profile.ico";
55
56 // Characters that are not allowed in Windows filenames. Taken from
57 // http://msdn.microsoft.com/en-us/library/aa365247.aspx
58 const char16 kReservedCharacters[] = L"<>:\"/\\|?*\x01\x02\x03\x04\x05\x06\x07"
59 L"\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"
60 L"\x1A\x1B\x1C\x1D\x1E\x1F";
61
62 // The maximum number of characters allowed in profile shortcuts' file names.
63 // Warning: migration code will be needed if this is changed later, since
64 // existing shortcuts might no longer be found if the name is generated
65 // differently than it was when a shortcut was originally created.
66 const int kMaxProfileShortcutFileNameLength = 64;
67
68 // The avatar badge size needs to be half of the shortcut icon size because
69 // the Windows taskbar icon is 32x32 and the avatar icon overlay is 16x16. So to
70 // get the shortcut avatar badge and the avatar icon overlay to match up, we
71 // need to preserve those ratios when creating the shortcut icon.
72 const int kShortcutIconSize = 48;
73 const int kProfileAvatarBadgeSize = kShortcutIconSize / 2;
74
75 const int kCurrentProfileIconVersion = 2;
76
77 // 2x sized profile avatar icons. Mirrors |kDefaultAvatarIconResources| in
78 // profile_info_cache.cc.
79 const int kProfileAvatarIconResources2x[] = {
80 IDR_PROFILE_AVATAR_2X_0,
81 IDR_PROFILE_AVATAR_2X_1,
82 IDR_PROFILE_AVATAR_2X_2,
83 IDR_PROFILE_AVATAR_2X_3,
84 IDR_PROFILE_AVATAR_2X_4,
85 IDR_PROFILE_AVATAR_2X_5,
86 IDR_PROFILE_AVATAR_2X_6,
87 IDR_PROFILE_AVATAR_2X_7,
88 IDR_PROFILE_AVATAR_2X_8,
89 IDR_PROFILE_AVATAR_2X_9,
90 IDR_PROFILE_AVATAR_2X_10,
91 IDR_PROFILE_AVATAR_2X_11,
92 IDR_PROFILE_AVATAR_2X_12,
93 IDR_PROFILE_AVATAR_2X_13,
94 IDR_PROFILE_AVATAR_2X_14,
95 IDR_PROFILE_AVATAR_2X_15,
96 IDR_PROFILE_AVATAR_2X_16,
97 IDR_PROFILE_AVATAR_2X_17,
98 IDR_PROFILE_AVATAR_2X_18,
99 IDR_PROFILE_AVATAR_2X_19,
100 IDR_PROFILE_AVATAR_2X_20,
101 IDR_PROFILE_AVATAR_2X_21,
102 IDR_PROFILE_AVATAR_2X_22,
103 IDR_PROFILE_AVATAR_2X_23,
104 IDR_PROFILE_AVATAR_2X_24,
105 IDR_PROFILE_AVATAR_2X_25,
106 };
107
108 // Badges |app_icon_bitmap| with |avatar_bitmap| at the bottom right corner and
109 // returns the resulting SkBitmap.
BadgeIcon(const SkBitmap & app_icon_bitmap,const SkBitmap & avatar_bitmap,int scale_factor)110 SkBitmap BadgeIcon(const SkBitmap& app_icon_bitmap,
111 const SkBitmap& avatar_bitmap,
112 int scale_factor) {
113 // TODO(rlp): Share this chunk of code with
114 // avatar_menu_button::DrawTaskBarDecoration.
115 SkBitmap source_bitmap = avatar_bitmap;
116 if ((avatar_bitmap.width() == scale_factor * profiles::kAvatarIconWidth) &&
117 (avatar_bitmap.height() == scale_factor * profiles::kAvatarIconHeight)) {
118 // Shave a couple of columns so the bitmap is more square. So when
119 // resized to a square aspect ratio it looks pretty.
120 gfx::Rect frame(scale_factor * profiles::kAvatarIconWidth,
121 scale_factor * profiles::kAvatarIconHeight);
122 frame.Inset(scale_factor * 2, 0, scale_factor * 2, 0);
123 avatar_bitmap.extractSubset(&source_bitmap, gfx::RectToSkIRect(frame));
124 } else {
125 NOTREACHED();
126 }
127 int avatar_badge_size = kProfileAvatarBadgeSize;
128 if (app_icon_bitmap.width() != kShortcutIconSize) {
129 avatar_badge_size =
130 app_icon_bitmap.width() * kProfileAvatarBadgeSize / kShortcutIconSize;
131 }
132 SkBitmap sk_icon = skia::ImageOperations::Resize(
133 source_bitmap, skia::ImageOperations::RESIZE_LANCZOS3, avatar_badge_size,
134 source_bitmap.height() * avatar_badge_size / source_bitmap.width());
135
136 // Overlay the avatar on the icon, anchoring it to the bottom-right of the
137 // icon.
138 scoped_ptr<SkCanvas> offscreen_canvas(
139 skia::CreateBitmapCanvas(app_icon_bitmap.width(),
140 app_icon_bitmap.height(),
141 false));
142 DCHECK(offscreen_canvas);
143 offscreen_canvas->drawBitmap(app_icon_bitmap, 0, 0);
144 offscreen_canvas->drawBitmap(sk_icon,
145 app_icon_bitmap.width() - sk_icon.width(),
146 app_icon_bitmap.height() - sk_icon.height());
147 const SkBitmap& badged_bitmap =
148 offscreen_canvas->getDevice()->accessBitmap(false);
149 SkBitmap badged_bitmap_copy;
150 badged_bitmap.deepCopyTo(&badged_bitmap_copy, badged_bitmap.getConfig());
151 return badged_bitmap_copy;
152 }
153
154 // Updates the preferences with the current icon version on icon creation
155 // success.
OnProfileIconCreateSuccess(base::FilePath profile_path)156 void OnProfileIconCreateSuccess(base::FilePath profile_path) {
157 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
158 if (!g_browser_process->profile_manager())
159 return;
160 Profile* profile =
161 g_browser_process->profile_manager()->GetProfileByPath(profile_path);
162 if (profile) {
163 profile->GetPrefs()->SetInteger(prefs::kProfileIconVersion,
164 kCurrentProfileIconVersion);
165 }
166 }
167
168 // Creates a desktop shortcut icon file (.ico) on the disk for a given profile,
169 // badging the browser distribution icon with the profile avatar.
170 // Returns a path to the shortcut icon file on disk, which is empty if this
171 // fails. Use index 0 when assigning the resulting file as the icon. If both
172 // given bitmaps are empty, an unbadged icon is created.
173 // Returns the path to the created icon on success and an empty base::FilePath
174 // on failure.
175 // TODO(calamity): Ideally we'd just copy the app icon verbatim from the exe's
176 // resources in the case of an unbadged icon.
CreateOrUpdateShortcutIconForProfile(const base::FilePath & profile_path,const SkBitmap & avatar_bitmap_1x,const SkBitmap & avatar_bitmap_2x)177 base::FilePath CreateOrUpdateShortcutIconForProfile(
178 const base::FilePath& profile_path,
179 const SkBitmap& avatar_bitmap_1x,
180 const SkBitmap& avatar_bitmap_2x) {
181 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
182
183 if (!base::PathExists(profile_path)) {
184 LOG(ERROR) << "Profile directory " << profile_path.value()
185 << " did not exist when trying to create profile icon";
186 return base::FilePath();
187 }
188
189 scoped_ptr<SkBitmap> app_icon_bitmap(GetAppIconForSize(kShortcutIconSize));
190 if (!app_icon_bitmap)
191 return base::FilePath();
192
193 gfx::ImageFamily badged_bitmaps;
194 if (!avatar_bitmap_1x.empty()) {
195 badged_bitmaps.Add(gfx::Image::CreateFrom1xBitmap(
196 BadgeIcon(*app_icon_bitmap, avatar_bitmap_1x, 1)));
197 }
198
199 scoped_ptr<SkBitmap> large_app_icon_bitmap(
200 GetAppIconForSize(IconUtil::kLargeIconSize));
201 if (large_app_icon_bitmap && !avatar_bitmap_2x.empty()) {
202 badged_bitmaps.Add(gfx::Image::CreateFrom1xBitmap(
203 BadgeIcon(*large_app_icon_bitmap, avatar_bitmap_2x, 2)));
204 }
205
206 // If we have no badged bitmaps, we should just use the default chrome icon.
207 if (badged_bitmaps.empty()) {
208 badged_bitmaps.Add(gfx::Image::CreateFrom1xBitmap(*app_icon_bitmap));
209 if (large_app_icon_bitmap) {
210 badged_bitmaps.Add(
211 gfx::Image::CreateFrom1xBitmap(*large_app_icon_bitmap));
212 }
213 }
214 // Finally, write the .ico file containing this new bitmap.
215 const base::FilePath icon_path =
216 profiles::internal::GetProfileIconPath(profile_path);
217 const bool had_icon = base::PathExists(icon_path);
218
219 if (!IconUtil::CreateIconFileFromImageFamily(badged_bitmaps, icon_path)) {
220 // This can happen in theory if the profile directory is deleted between the
221 // beginning of this function and here; however this is extremely unlikely
222 // and this check will help catch any regression where this call would start
223 // failing constantly.
224 NOTREACHED();
225 return base::FilePath();
226 }
227
228 if (had_icon) {
229 // This invalidates the Windows icon cache and causes the icon changes to
230 // register with the taskbar and desktop. SHCNE_ASSOCCHANGED will cause a
231 // desktop flash and we would like to avoid that if possible.
232 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
233 } else {
234 SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, icon_path.value().c_str(), NULL);
235 }
236 BrowserThread::PostTask(
237 BrowserThread::UI, FROM_HERE,
238 base::Bind(&OnProfileIconCreateSuccess, profile_path));
239 return icon_path;
240 }
241
242 // Gets the user and system directories for desktop shortcuts. Parameters may
243 // be NULL if a directory type is not needed. Returns true on success.
GetDesktopShortcutsDirectories(base::FilePath * user_shortcuts_directory,base::FilePath * system_shortcuts_directory)244 bool GetDesktopShortcutsDirectories(
245 base::FilePath* user_shortcuts_directory,
246 base::FilePath* system_shortcuts_directory) {
247 BrowserDistribution* distribution = BrowserDistribution::GetDistribution();
248 if (user_shortcuts_directory &&
249 !ShellUtil::GetShortcutPath(ShellUtil::SHORTCUT_LOCATION_DESKTOP,
250 distribution, ShellUtil::CURRENT_USER,
251 user_shortcuts_directory)) {
252 NOTREACHED();
253 return false;
254 }
255 if (system_shortcuts_directory &&
256 !ShellUtil::GetShortcutPath(ShellUtil::SHORTCUT_LOCATION_DESKTOP,
257 distribution, ShellUtil::SYSTEM_LEVEL,
258 system_shortcuts_directory)) {
259 NOTREACHED();
260 return false;
261 }
262 return true;
263 }
264
265 // Returns the long form of |path|, which will expand any shortened components
266 // like "foo~2" to their full names.
ConvertToLongPath(const base::FilePath & path)267 base::FilePath ConvertToLongPath(const base::FilePath& path) {
268 const size_t length = GetLongPathName(path.value().c_str(), NULL, 0);
269 if (length != 0 && length != path.value().length()) {
270 std::vector<wchar_t> long_path(length);
271 if (GetLongPathName(path.value().c_str(), &long_path[0], length) != 0)
272 return base::FilePath(&long_path[0]);
273 }
274 return path;
275 }
276
277 // Returns true if the file at |path| is a Chrome shortcut and returns its
278 // command line in output parameter |command_line|.
IsChromeShortcut(const base::FilePath & path,const base::FilePath & chrome_exe,base::string16 * command_line)279 bool IsChromeShortcut(const base::FilePath& path,
280 const base::FilePath& chrome_exe,
281 base::string16* command_line) {
282 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
283
284 if (path.Extension() != installer::kLnkExt)
285 return false;
286
287 base::FilePath target_path;
288 if (!base::win::ResolveShortcut(path, &target_path, command_line))
289 return false;
290 // One of the paths may be in short (elided) form. Compare long paths to
291 // ensure these are still properly matched.
292 return ConvertToLongPath(target_path) == ConvertToLongPath(chrome_exe);
293 }
294
295 // Populates |paths| with the file paths of Chrome desktop shortcuts that have
296 // the specified |command_line|. If |include_empty_command_lines| is true,
297 // Chrome desktop shortcuts with empty command lines will also be included.
ListDesktopShortcutsWithCommandLine(const base::FilePath & chrome_exe,const base::string16 & command_line,bool include_empty_command_lines,std::vector<base::FilePath> * paths)298 void ListDesktopShortcutsWithCommandLine(const base::FilePath& chrome_exe,
299 const base::string16& command_line,
300 bool include_empty_command_lines,
301 std::vector<base::FilePath>* paths) {
302 base::FilePath user_shortcuts_directory;
303 if (!GetDesktopShortcutsDirectories(&user_shortcuts_directory, NULL))
304 return;
305
306 base::FileEnumerator enumerator(user_shortcuts_directory, false,
307 base::FileEnumerator::FILES);
308 for (base::FilePath path = enumerator.Next(); !path.empty();
309 path = enumerator.Next()) {
310 base::string16 shortcut_command_line;
311 if (!IsChromeShortcut(path, chrome_exe, &shortcut_command_line))
312 continue;
313
314 // TODO(asvitkine): Change this to build a CommandLine object and ensure all
315 // args from |command_line| are present in the shortcut's CommandLine. This
316 // will be more robust when |command_line| contains multiple args.
317 if ((shortcut_command_line.empty() && include_empty_command_lines) ||
318 (shortcut_command_line.find(command_line) != base::string16::npos)) {
319 paths->push_back(path);
320 }
321 }
322 }
323
324 // Renames the given desktop shortcut and informs the shell of this change.
RenameDesktopShortcut(const base::FilePath & old_shortcut_path,const base::FilePath & new_shortcut_path)325 bool RenameDesktopShortcut(const base::FilePath& old_shortcut_path,
326 const base::FilePath& new_shortcut_path) {
327 if (!base::Move(old_shortcut_path, new_shortcut_path))
328 return false;
329
330 // Notify the shell of the rename, which allows the icon to keep its position
331 // on the desktop when renamed. Note: This only works if either SHCNF_FLUSH or
332 // SHCNF_FLUSHNOWAIT is specified as a flag.
333 SHChangeNotify(SHCNE_RENAMEITEM, SHCNF_PATH | SHCNF_FLUSHNOWAIT,
334 old_shortcut_path.value().c_str(),
335 new_shortcut_path.value().c_str());
336 return true;
337 }
338
339 // Renames an existing Chrome desktop profile shortcut. Must be called on the
340 // FILE thread.
RenameChromeDesktopShortcutForProfile(const base::string16 & old_shortcut_filename,const base::string16 & new_shortcut_filename)341 void RenameChromeDesktopShortcutForProfile(
342 const base::string16& old_shortcut_filename,
343 const base::string16& new_shortcut_filename) {
344 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
345
346 base::FilePath user_shortcuts_directory;
347 base::FilePath system_shortcuts_directory;
348 if (!GetDesktopShortcutsDirectories(&user_shortcuts_directory,
349 &system_shortcuts_directory)) {
350 return;
351 }
352
353 const base::FilePath old_shortcut_path =
354 user_shortcuts_directory.Append(old_shortcut_filename);
355 const base::FilePath new_shortcut_path =
356 user_shortcuts_directory.Append(new_shortcut_filename);
357
358 if (base::PathExists(old_shortcut_path)) {
359 // Rename the old shortcut unless a system-level shortcut exists at the
360 // destination, in which case the old shortcut is simply deleted.
361 const base::FilePath possible_new_system_shortcut =
362 system_shortcuts_directory.Append(new_shortcut_filename);
363 if (base::PathExists(possible_new_system_shortcut))
364 base::DeleteFile(old_shortcut_path, false);
365 else if (!RenameDesktopShortcut(old_shortcut_path, new_shortcut_path))
366 DLOG(ERROR) << "Could not rename Windows profile desktop shortcut.";
367 } else {
368 // If the shortcut does not exist, it may have been renamed by the user. In
369 // that case, its name should not be changed.
370 // It's also possible that a system-level shortcut exists instead - this
371 // should only be the case for the original Chrome shortcut from an
372 // installation. If that's the case, copy that one over - it will get its
373 // properties updated by
374 // |CreateOrUpdateDesktopShortcutsAndIconForProfile()|.
375 const base::FilePath possible_old_system_shortcut =
376 system_shortcuts_directory.Append(old_shortcut_filename);
377 if (base::PathExists(possible_old_system_shortcut))
378 base::CopyFile(possible_old_system_shortcut, new_shortcut_path);
379 }
380 }
381
382 struct CreateOrUpdateShortcutsParams {
CreateOrUpdateShortcutsParams__anon265c980f0111::CreateOrUpdateShortcutsParams383 CreateOrUpdateShortcutsParams(
384 base::FilePath profile_path,
385 ProfileShortcutManagerWin::CreateOrUpdateMode create_mode,
386 ProfileShortcutManagerWin::NonProfileShortcutAction action)
387 : profile_path(profile_path), create_mode(create_mode), action(action) {}
~CreateOrUpdateShortcutsParams__anon265c980f0111::CreateOrUpdateShortcutsParams388 ~CreateOrUpdateShortcutsParams() {}
389
390 ProfileShortcutManagerWin::CreateOrUpdateMode create_mode;
391 ProfileShortcutManagerWin::NonProfileShortcutAction action;
392
393 // The path for this profile.
394 base::FilePath profile_path;
395 // The profile name before this update. Empty on create.
396 base::string16 old_profile_name;
397 // The new profile name.
398 base::string16 profile_name;
399 // Avatar images for this profile.
400 SkBitmap avatar_image_1x;
401 SkBitmap avatar_image_2x;
402 };
403
404 // Updates all desktop shortcuts for the given profile to have the specified
405 // parameters. If |params.create_mode| is CREATE_WHEN_NONE_FOUND, a new shortcut
406 // is created if no existing ones were found. Whether non-profile shortcuts
407 // should be updated is specified by |params.action|. Must be called on the FILE
408 // thread.
CreateOrUpdateDesktopShortcutsAndIconForProfile(const CreateOrUpdateShortcutsParams & params)409 void CreateOrUpdateDesktopShortcutsAndIconForProfile(
410 const CreateOrUpdateShortcutsParams& params) {
411 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
412
413 const base::FilePath shortcut_icon =
414 CreateOrUpdateShortcutIconForProfile(params.profile_path,
415 params.avatar_image_1x,
416 params.avatar_image_2x);
417 if (shortcut_icon.empty() ||
418 params.create_mode ==
419 ProfileShortcutManagerWin::CREATE_OR_UPDATE_ICON_ONLY) {
420 return;
421 }
422
423 base::FilePath chrome_exe;
424 if (!PathService::Get(base::FILE_EXE, &chrome_exe)) {
425 NOTREACHED();
426 return;
427 }
428
429 BrowserDistribution* distribution = BrowserDistribution::GetDistribution();
430 // Ensure that the distribution supports creating shortcuts. If it doesn't,
431 // the following code may result in NOTREACHED() being hit.
432 DCHECK(distribution->CanCreateDesktopShortcuts());
433
434 if (params.old_profile_name != params.profile_name) {
435 const base::string16 old_shortcut_filename =
436 profiles::internal::GetShortcutFilenameForProfile(
437 params.old_profile_name,
438 distribution);
439 const base::string16 new_shortcut_filename =
440 profiles::internal::GetShortcutFilenameForProfile(params.profile_name,
441 distribution);
442 RenameChromeDesktopShortcutForProfile(old_shortcut_filename,
443 new_shortcut_filename);
444 }
445
446 ShellUtil::ShortcutProperties properties(ShellUtil::CURRENT_USER);
447 installer::Product product(distribution);
448 product.AddDefaultShortcutProperties(chrome_exe, &properties);
449
450 const base::string16 command_line =
451 profiles::internal::CreateProfileShortcutFlags(params.profile_path);
452
453 // Only set the profile-specific properties when |profile_name| is non empty.
454 // If it is empty, it means the shortcut being created should be a regular,
455 // non-profile Chrome shortcut.
456 if (!params.profile_name.empty()) {
457 properties.set_arguments(command_line);
458 properties.set_icon(shortcut_icon, 0);
459 } else {
460 // Set the arguments explicitly to the empty string to ensure that
461 // |ShellUtil::CreateOrUpdateShortcut| updates that part of the shortcut.
462 properties.set_arguments(base::string16());
463 }
464
465 properties.set_app_id(
466 ShellIntegration::GetChromiumModelIdForProfile(params.profile_path));
467
468 ShellUtil::ShortcutOperation operation =
469 ShellUtil::SHELL_SHORTCUT_REPLACE_EXISTING;
470
471 std::vector<base::FilePath> shortcuts;
472 ListDesktopShortcutsWithCommandLine(chrome_exe, command_line,
473 params.action == ProfileShortcutManagerWin::UPDATE_NON_PROFILE_SHORTCUTS,
474 &shortcuts);
475 if (params.create_mode == ProfileShortcutManagerWin::CREATE_WHEN_NONE_FOUND &&
476 shortcuts.empty()) {
477 const base::string16 shortcut_name =
478 profiles::internal::GetShortcutFilenameForProfile(params.profile_name,
479 distribution);
480 shortcuts.push_back(base::FilePath(shortcut_name));
481 operation = ShellUtil::SHELL_SHORTCUT_CREATE_IF_NO_SYSTEM_LEVEL;
482 }
483
484 for (size_t i = 0; i < shortcuts.size(); ++i) {
485 const base::FilePath shortcut_name =
486 shortcuts[i].BaseName().RemoveExtension();
487 properties.set_shortcut_name(shortcut_name.value());
488 ShellUtil::CreateOrUpdateShortcut(ShellUtil::SHORTCUT_LOCATION_DESKTOP,
489 distribution, properties, operation);
490 }
491 }
492
493 // Returns true if any desktop shortcuts exist with target |chrome_exe|,
494 // regardless of their command line arguments.
ChromeDesktopShortcutsExist(const base::FilePath & chrome_exe)495 bool ChromeDesktopShortcutsExist(const base::FilePath& chrome_exe) {
496 base::FilePath user_shortcuts_directory;
497 if (!GetDesktopShortcutsDirectories(&user_shortcuts_directory, NULL))
498 return false;
499
500 base::FileEnumerator enumerator(user_shortcuts_directory, false,
501 base::FileEnumerator::FILES);
502 for (base::FilePath path = enumerator.Next(); !path.empty();
503 path = enumerator.Next()) {
504 if (IsChromeShortcut(path, chrome_exe, NULL))
505 return true;
506 }
507
508 return false;
509 }
510
511 // Deletes all desktop shortcuts for the specified profile. If
512 // |ensure_shortcuts_remain| is true, then a regular non-profile shortcut will
513 // be created if this function would otherwise delete the last Chrome desktop
514 // shortcut(s). Must be called on the FILE thread.
DeleteDesktopShortcuts(const base::FilePath & profile_path,bool ensure_shortcuts_remain)515 void DeleteDesktopShortcuts(const base::FilePath& profile_path,
516 bool ensure_shortcuts_remain) {
517 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
518
519 base::FilePath chrome_exe;
520 if (!PathService::Get(base::FILE_EXE, &chrome_exe)) {
521 NOTREACHED();
522 return;
523 }
524
525 const base::string16 command_line =
526 profiles::internal::CreateProfileShortcutFlags(profile_path);
527 std::vector<base::FilePath> shortcuts;
528 ListDesktopShortcutsWithCommandLine(chrome_exe, command_line, false,
529 &shortcuts);
530
531 for (size_t i = 0; i < shortcuts.size(); ++i) {
532 // Use base::DeleteFile() instead of ShellUtil::RemoveShortcuts(), as the
533 // latter causes non-profile taskbar shortcuts to be removed since it
534 // doesn't consider the command-line of the shortcuts it deletes.
535 // TODO(huangs): Refactor with ShellUtil::RemoveShortcuts().
536 base::win::TaskbarUnpinShortcutLink(shortcuts[i].value().c_str());
537 base::DeleteFile(shortcuts[i], false);
538 // Notify the shell that the shortcut was deleted to ensure desktop refresh.
539 SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, shortcuts[i].value().c_str(),
540 NULL);
541 }
542
543 // If |ensure_shortcuts_remain| is true and deleting this profile caused the
544 // last shortcuts to be removed, re-create a regular non-profile shortcut.
545 const bool had_shortcuts = !shortcuts.empty();
546 if (ensure_shortcuts_remain && had_shortcuts &&
547 !ChromeDesktopShortcutsExist(chrome_exe)) {
548 BrowserDistribution* distribution = BrowserDistribution::GetDistribution();
549 // Ensure that the distribution supports creating shortcuts. If it doesn't,
550 // the following code may result in NOTREACHED() being hit.
551 DCHECK(distribution->CanCreateDesktopShortcuts());
552 installer::Product product(distribution);
553
554 ShellUtil::ShortcutProperties properties(ShellUtil::CURRENT_USER);
555 product.AddDefaultShortcutProperties(chrome_exe, &properties);
556 properties.set_shortcut_name(
557 profiles::internal::GetShortcutFilenameForProfile(base::string16(),
558 distribution));
559 ShellUtil::CreateOrUpdateShortcut(
560 ShellUtil::SHORTCUT_LOCATION_DESKTOP, distribution, properties,
561 ShellUtil::SHELL_SHORTCUT_CREATE_IF_NO_SYSTEM_LEVEL);
562 }
563 }
564
565 // Returns true if profile at |profile_path| has any shortcuts. Does not
566 // consider non-profile shortcuts. Must be called on the FILE thread.
HasAnyProfileShortcuts(const base::FilePath & profile_path)567 bool HasAnyProfileShortcuts(const base::FilePath& profile_path) {
568 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
569
570 base::FilePath chrome_exe;
571 if (!PathService::Get(base::FILE_EXE, &chrome_exe)) {
572 NOTREACHED();
573 return false;
574 }
575
576 const base::string16 command_line =
577 profiles::internal::CreateProfileShortcutFlags(profile_path);
578 std::vector<base::FilePath> shortcuts;
579 ListDesktopShortcutsWithCommandLine(chrome_exe, command_line, false,
580 &shortcuts);
581 return !shortcuts.empty();
582 }
583
584 // Replaces any reserved characters with spaces, and trims the resulting string
585 // to prevent any leading and trailing spaces. Also makes sure that the
586 // resulting filename doesn't exceed |kMaxProfileShortcutFileNameLength|.
587 // TODO(macourteau): find a way to limit the total path's length to MAX_PATH
588 // instead of limiting the profile's name to |kMaxProfileShortcutFileNameLength|
589 // characters.
SanitizeShortcutProfileNameString(const base::string16 & profile_name)590 base::string16 SanitizeShortcutProfileNameString(
591 const base::string16& profile_name) {
592 base::string16 sanitized = profile_name;
593 size_t pos = sanitized.find_first_of(kReservedCharacters);
594 while (pos != base::string16::npos) {
595 sanitized[pos] = L' ';
596 pos = sanitized.find_first_of(kReservedCharacters, pos + 1);
597 }
598
599 TrimWhitespace(sanitized, TRIM_LEADING, &sanitized);
600 if (sanitized.size() > kMaxProfileShortcutFileNameLength)
601 sanitized.erase(kMaxProfileShortcutFileNameLength);
602 TrimWhitespace(sanitized, TRIM_TRAILING, &sanitized);
603
604 return sanitized;
605 }
606
607 // Returns a copied SkBitmap for the given resource id that can be safely passed
608 // to another thread.
GetImageResourceSkBitmapCopy(int resource_id)609 SkBitmap GetImageResourceSkBitmapCopy(int resource_id) {
610 const gfx::Image image =
611 ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
612 DCHECK(!image.IsEmpty());
613
614 const SkBitmap* image_bitmap = image.ToSkBitmap();
615 SkBitmap bitmap_copy;
616 image_bitmap->deepCopyTo(&bitmap_copy, image_bitmap->getConfig());
617 return bitmap_copy;
618 }
619
620 } // namespace
621
622 namespace profiles {
623 namespace internal {
624
GetProfileIconPath(const base::FilePath & profile_path)625 base::FilePath GetProfileIconPath(const base::FilePath& profile_path) {
626 return profile_path.AppendASCII(kProfileIconFileName);
627 }
628
GetShortcutFilenameForProfile(const base::string16 & profile_name,BrowserDistribution * distribution)629 base::string16 GetShortcutFilenameForProfile(
630 const base::string16& profile_name,
631 BrowserDistribution* distribution) {
632 base::string16 shortcut_name;
633 if (!profile_name.empty()) {
634 shortcut_name.append(SanitizeShortcutProfileNameString(profile_name));
635 shortcut_name.append(L" - ");
636 shortcut_name.append(l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME));
637 } else {
638 shortcut_name.append(
639 distribution->GetShortcutName(BrowserDistribution::SHORTCUT_CHROME));
640 }
641 return shortcut_name + installer::kLnkExt;
642 }
643
CreateProfileShortcutFlags(const base::FilePath & profile_path)644 base::string16 CreateProfileShortcutFlags(const base::FilePath& profile_path) {
645 return base::StringPrintf(L"--%ls=\"%ls\"",
646 ASCIIToUTF16(switches::kProfileDirectory).c_str(),
647 profile_path.BaseName().value().c_str());
648 }
649
650 } // namespace internal
651 } // namespace profiles
652
653 // static
IsFeatureEnabled()654 bool ProfileShortcutManager::IsFeatureEnabled() {
655 CommandLine* command_line = CommandLine::ForCurrentProcess();
656 return command_line->HasSwitch(switches::kEnableProfileShortcutManager) ||
657 (BrowserDistribution::GetDistribution()->CanCreateDesktopShortcuts() &&
658 !command_line->HasSwitch(switches::kUserDataDir));
659 }
660
661 // static
Create(ProfileManager * manager)662 ProfileShortcutManager* ProfileShortcutManager::Create(
663 ProfileManager* manager) {
664 return new ProfileShortcutManagerWin(manager);
665 }
666
ProfileShortcutManagerWin(ProfileManager * manager)667 ProfileShortcutManagerWin::ProfileShortcutManagerWin(ProfileManager* manager)
668 : profile_manager_(manager) {
669 DCHECK_EQ(
670 arraysize(kProfileAvatarIconResources2x),
671 profile_manager_->GetProfileInfoCache().GetDefaultAvatarIconCount());
672
673 registrar_.Add(this, chrome::NOTIFICATION_PROFILE_CREATED,
674 content::NotificationService::AllSources());
675
676 profile_manager_->GetProfileInfoCache().AddObserver(this);
677 }
678
~ProfileShortcutManagerWin()679 ProfileShortcutManagerWin::~ProfileShortcutManagerWin() {
680 profile_manager_->GetProfileInfoCache().RemoveObserver(this);
681 }
682
CreateOrUpdateProfileIcon(const base::FilePath & profile_path)683 void ProfileShortcutManagerWin::CreateOrUpdateProfileIcon(
684 const base::FilePath& profile_path) {
685 CreateOrUpdateShortcutsForProfileAtPath(profile_path,
686 CREATE_OR_UPDATE_ICON_ONLY,
687 IGNORE_NON_PROFILE_SHORTCUTS);
688 }
689
CreateProfileShortcut(const base::FilePath & profile_path)690 void ProfileShortcutManagerWin::CreateProfileShortcut(
691 const base::FilePath& profile_path) {
692 CreateOrUpdateShortcutsForProfileAtPath(profile_path, CREATE_WHEN_NONE_FOUND,
693 IGNORE_NON_PROFILE_SHORTCUTS);
694 }
695
RemoveProfileShortcuts(const base::FilePath & profile_path)696 void ProfileShortcutManagerWin::RemoveProfileShortcuts(
697 const base::FilePath& profile_path) {
698 BrowserThread::PostTask(
699 BrowserThread::FILE, FROM_HERE,
700 base::Bind(&DeleteDesktopShortcuts, profile_path, false));
701 }
702
HasProfileShortcuts(const base::FilePath & profile_path,const base::Callback<void (bool)> & callback)703 void ProfileShortcutManagerWin::HasProfileShortcuts(
704 const base::FilePath& profile_path,
705 const base::Callback<void(bool)>& callback) {
706 BrowserThread::PostTaskAndReplyWithResult(
707 BrowserThread::FILE, FROM_HERE,
708 base::Bind(&HasAnyProfileShortcuts, profile_path), callback);
709 }
710
GetShortcutProperties(const base::FilePath & profile_path,CommandLine * command_line,base::string16 * name,base::FilePath * icon_path)711 void ProfileShortcutManagerWin::GetShortcutProperties(
712 const base::FilePath& profile_path,
713 CommandLine* command_line,
714 base::string16* name,
715 base::FilePath* icon_path) {
716 base::FilePath chrome_exe;
717 if (!PathService::Get(base::FILE_EXE, &chrome_exe)) {
718 NOTREACHED();
719 return;
720 }
721
722 const ProfileInfoCache& cache = profile_manager_->GetProfileInfoCache();
723 size_t profile_index = cache.GetIndexOfProfileWithPath(profile_path);
724 DCHECK_LT(profile_index, cache.GetNumberOfProfiles());
725
726 // The used profile name should be empty if there is only 1 profile.
727 base::string16 shortcut_profile_name;
728 if (cache.GetNumberOfProfiles() > 1)
729 shortcut_profile_name = cache.GetNameOfProfileAtIndex(profile_index);
730
731 *name = base::FilePath(profiles::internal::GetShortcutFilenameForProfile(
732 shortcut_profile_name,
733 BrowserDistribution::GetDistribution())).RemoveExtension().value();
734
735 command_line->ParseFromString(L"\"" + chrome_exe.value() + L"\" " +
736 profiles::internal::CreateProfileShortcutFlags(profile_path));
737
738 *icon_path = profiles::internal::GetProfileIconPath(profile_path);
739 }
740
OnProfileAdded(const base::FilePath & profile_path)741 void ProfileShortcutManagerWin::OnProfileAdded(
742 const base::FilePath& profile_path) {
743 CreateOrUpdateProfileIcon(profile_path);
744 if (profile_manager_->GetProfileInfoCache().GetNumberOfProfiles() == 2) {
745 // When the second profile is added, make existing non-profile shortcuts
746 // point to the first profile and be badged/named appropriately.
747 CreateOrUpdateShortcutsForProfileAtPath(GetOtherProfilePath(profile_path),
748 UPDATE_EXISTING_ONLY,
749 UPDATE_NON_PROFILE_SHORTCUTS);
750 }
751 }
752
OnProfileWasRemoved(const base::FilePath & profile_path,const base::string16 & profile_name)753 void ProfileShortcutManagerWin::OnProfileWasRemoved(
754 const base::FilePath& profile_path,
755 const base::string16& profile_name) {
756 const ProfileInfoCache& cache = profile_manager_->GetProfileInfoCache();
757 // If there is only one profile remaining, remove the badging information
758 // from an existing shortcut.
759 const bool deleting_down_to_last_profile = (cache.GetNumberOfProfiles() == 1);
760 if (deleting_down_to_last_profile) {
761 // This is needed to unbadge the icon.
762 CreateOrUpdateShortcutsForProfileAtPath(cache.GetPathOfProfileAtIndex(0),
763 UPDATE_EXISTING_ONLY,
764 IGNORE_NON_PROFILE_SHORTCUTS);
765 }
766
767 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
768 base::Bind(&DeleteDesktopShortcuts,
769 profile_path,
770 deleting_down_to_last_profile));
771 }
772
OnProfileNameChanged(const base::FilePath & profile_path,const base::string16 & old_profile_name)773 void ProfileShortcutManagerWin::OnProfileNameChanged(
774 const base::FilePath& profile_path,
775 const base::string16& old_profile_name) {
776 CreateOrUpdateShortcutsForProfileAtPath(profile_path, UPDATE_EXISTING_ONLY,
777 IGNORE_NON_PROFILE_SHORTCUTS);
778 }
779
OnProfileAvatarChanged(const base::FilePath & profile_path)780 void ProfileShortcutManagerWin::OnProfileAvatarChanged(
781 const base::FilePath& profile_path) {
782 CreateOrUpdateProfileIcon(profile_path);
783 }
784
GetOtherProfilePath(const base::FilePath & profile_path)785 base::FilePath ProfileShortcutManagerWin::GetOtherProfilePath(
786 const base::FilePath& profile_path) {
787 const ProfileInfoCache& cache = profile_manager_->GetProfileInfoCache();
788 DCHECK_EQ(2U, cache.GetNumberOfProfiles());
789 // Get the index of the current profile, in order to find the index of the
790 // other profile.
791 size_t current_profile_index = cache.GetIndexOfProfileWithPath(profile_path);
792 size_t other_profile_index = (current_profile_index == 0) ? 1 : 0;
793 return cache.GetPathOfProfileAtIndex(other_profile_index);
794 }
795
CreateOrUpdateShortcutsForProfileAtPath(const base::FilePath & profile_path,CreateOrUpdateMode create_mode,NonProfileShortcutAction action)796 void ProfileShortcutManagerWin::CreateOrUpdateShortcutsForProfileAtPath(
797 const base::FilePath& profile_path,
798 CreateOrUpdateMode create_mode,
799 NonProfileShortcutAction action) {
800 DCHECK(!BrowserThread::IsThreadInitialized(BrowserThread::UI) ||
801 BrowserThread::CurrentlyOn(BrowserThread::UI));
802 CreateOrUpdateShortcutsParams params(profile_path, create_mode, action);
803
804 ProfileInfoCache* cache = &profile_manager_->GetProfileInfoCache();
805 size_t profile_index = cache->GetIndexOfProfileWithPath(profile_path);
806 if (profile_index == std::string::npos)
807 return;
808 bool remove_badging = cache->GetNumberOfProfiles() == 1;
809
810 params.old_profile_name =
811 cache->GetShortcutNameOfProfileAtIndex(profile_index);
812
813 // Exit early if the mode is to update existing profile shortcuts only and
814 // none were ever created for this profile, per the shortcut name not being
815 // set in the profile info cache.
816 if (params.old_profile_name.empty() &&
817 create_mode == UPDATE_EXISTING_ONLY &&
818 action == IGNORE_NON_PROFILE_SHORTCUTS) {
819 return;
820 }
821
822 if (!remove_badging) {
823 params.profile_name = cache->GetNameOfProfileAtIndex(profile_index);
824
825 const size_t icon_index =
826 cache->GetAvatarIconIndexOfProfileAtIndex(profile_index);
827 const int resource_id_1x =
828 cache->GetDefaultAvatarIconResourceIDAtIndex(icon_index);
829 const int resource_id_2x = kProfileAvatarIconResources2x[icon_index];
830 // Make a copy of the SkBitmaps to ensure that we can safely use the image
831 // data on the FILE thread.
832 params.avatar_image_1x = GetImageResourceSkBitmapCopy(resource_id_1x);
833 params.avatar_image_2x = GetImageResourceSkBitmapCopy(resource_id_2x);
834 }
835 BrowserThread::PostTask(
836 BrowserThread::FILE, FROM_HERE,
837 base::Bind(&CreateOrUpdateDesktopShortcutsAndIconForProfile, params));
838
839 cache->SetShortcutNameOfProfileAtIndex(profile_index,
840 params.profile_name);
841 }
842
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)843 void ProfileShortcutManagerWin::Observe(
844 int type,
845 const content::NotificationSource& source,
846 const content::NotificationDetails& details) {
847 switch (type) {
848 // This notification is triggered when a profile is loaded.
849 case chrome::NOTIFICATION_PROFILE_CREATED: {
850 Profile* profile =
851 content::Source<Profile>(source).ptr()->GetOriginalProfile();
852 if (profile->GetPrefs()->GetInteger(prefs::kProfileIconVersion) <
853 kCurrentProfileIconVersion) {
854 // Ensure the profile's icon file has been created.
855 CreateOrUpdateProfileIcon(profile->GetPath());
856 }
857 break;
858 }
859 default:
860 NOTREACHED();
861 break;
862 }
863 }
864