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