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