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