Ensure ephemeral apps have dock icons on mac
[chromium-blink-merge.git] / chrome / browser / web_applications / web_app_mac.mm
blob7bfa3d339bf2b0d13df6506eaca7a9e4125ea944
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/web_applications/web_app_mac.h"
7 #import <Carbon/Carbon.h>
8 #import <Cocoa/Cocoa.h>
10 #include "base/command_line.h"
11 #include "base/file_util.h"
12 #include "base/files/file_enumerator.h"
13 #include "base/files/scoped_temp_dir.h"
14 #include "base/mac/foundation_util.h"
15 #include "base/mac/launch_services_util.h"
16 #include "base/mac/mac_util.h"
17 #include "base/mac/scoped_cftyperef.h"
18 #include "base/mac/scoped_nsobject.h"
19 #include "base/path_service.h"
20 #include "base/process/process_handle.h"
21 #include "base/strings/string16.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/string_util.h"
24 #include "base/strings/sys_string_conversions.h"
25 #include "base/strings/utf_string_conversions.h"
26 #import "chrome/browser/mac/dock.h"
27 #include "chrome/browser/browser_process.h"
28 #include "chrome/browser/profiles/profile.h"
29 #include "chrome/browser/profiles/profile_manager.h"
30 #include "chrome/browser/shell_integration.h"
31 #include "chrome/common/chrome_constants.h"
32 #include "chrome/common/chrome_paths.h"
33 #include "chrome/common/chrome_switches.h"
34 #include "chrome/common/chrome_version_info.h"
35 #import "chrome/common/mac/app_mode_common.h"
36 #include "content/public/browser/browser_thread.h"
37 #include "extensions/browser/extension_registry.h"
38 #include "extensions/common/extension.h"
39 #include "grit/chrome_unscaled_resources.h"
40 #include "grit/chromium_strings.h"
41 #include "grit/generated_resources.h"
42 #import "skia/ext/skia_utils_mac.h"
43 #include "third_party/skia/include/core/SkBitmap.h"
44 #include "third_party/skia/include/core/SkColor.h"
45 #include "ui/base/l10n/l10n_util.h"
46 #import "ui/base/l10n/l10n_util_mac.h"
47 #include "ui/base/resource/resource_bundle.h"
48 #include "ui/gfx/image/image_family.h"
50 namespace {
52 // Launch Services Key to run as an agent app, which doesn't launch in the dock.
53 NSString* const kLSUIElement = @"LSUIElement";
55 class ScopedCarbonHandle {
56  public:
57   ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
58     DCHECK(handle_);
59     DCHECK_EQ(noErr, MemError());
60   }
61   ~ScopedCarbonHandle() { DisposeHandle(handle_); }
63   Handle Get() { return handle_; }
64   char* Data() { return *handle_; }
65   size_t HandleSize() const { return GetHandleSize(handle_); }
67   IconFamilyHandle GetAsIconFamilyHandle() {
68     return reinterpret_cast<IconFamilyHandle>(handle_);
69   }
71   bool WriteDataToFile(const base::FilePath& path) {
72     NSData* data = [NSData dataWithBytes:Data()
73                                   length:HandleSize()];
74     return [data writeToFile:base::mac::FilePathToNSString(path)
75                   atomically:NO];
76   }
78  private:
79   Handle handle_;
82 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
83   CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
85   char* argb = handle->Data();
86   SkAutoLockPixels lock(bitmap);
87   for (int y = 0; y < bitmap.height(); ++y) {
88     for (int x = 0; x < bitmap.width(); ++x) {
89       SkColor pixel = bitmap.getColor(x, y);
90       argb[0] = SkColorGetA(pixel);
91       argb[1] = SkColorGetR(pixel);
92       argb[2] = SkColorGetG(pixel);
93       argb[3] = SkColorGetB(pixel);
94       argb += 4;
95     }
96   }
99 // Adds |image| to |icon_family|. Returns true on success, false on failure.
100 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
101                              const gfx::Image& image) {
102   // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
103   // have all the representations desired here for mac, from the kDesiredSizes
104   // array in web_app.cc.
105   SkBitmap bitmap = image.AsBitmap();
106   if (bitmap.config() != SkBitmap::kARGB_8888_Config ||
107       bitmap.width() != bitmap.height()) {
108     return false;
109   }
111   OSType icon_type;
112   switch (bitmap.width()) {
113     case 512:
114       icon_type = kIconServices512PixelDataARGB;
115       break;
116     case 256:
117       icon_type = kIconServices256PixelDataARGB;
118       break;
119     case 128:
120       icon_type = kIconServices128PixelDataARGB;
121       break;
122     case 48:
123       icon_type = kIconServices48PixelDataARGB;
124       break;
125     case 32:
126       icon_type = kIconServices32PixelDataARGB;
127       break;
128     case 16:
129       icon_type = kIconServices16PixelDataARGB;
130       break;
131     default:
132       return false;
133   }
135   ScopedCarbonHandle raw_data(bitmap.getSize());
136   ConvertSkiaToARGB(bitmap, &raw_data);
137   OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
138   DCHECK_EQ(noErr, result);
139   return result == noErr;
142 bool AppShimsDisabledForTest() {
143   // Disable app shims in tests because shims created in ~/Applications will not
144   // be cleaned up.
145   return CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType);
148 base::FilePath GetWritableApplicationsDirectory() {
149   base::FilePath path;
150   if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
151     if (!base::DirectoryExists(path)) {
152       if (!base::CreateDirectory(path))
153         return base::FilePath();
155       // Create a zero-byte ".localized" file to inherit localizations from OSX
156       // for folders that have special meaning.
157       base::WriteFile(path.Append(".localized"), NULL, 0);
158     }
159     return base::PathIsWritable(path) ? path : base::FilePath();
160   }
161   return base::FilePath();
164 // Given the path to an app bundle, return the resources directory.
165 base::FilePath GetResourcesPath(const base::FilePath& app_path) {
166   return app_path.Append("Contents").Append("Resources");
169 bool HasExistingExtensionShim(const base::FilePath& destination_directory,
170                               const std::string& extension_id,
171                               const base::FilePath& own_basename) {
172   // Check if there any any other shims for the same extension.
173   base::FileEnumerator enumerator(destination_directory,
174                                   false /* recursive */,
175                                   base::FileEnumerator::DIRECTORIES);
176   for (base::FilePath shim_path = enumerator.Next();
177        !shim_path.empty(); shim_path = enumerator.Next()) {
178     if (shim_path.BaseName() != own_basename &&
179         EndsWith(shim_path.RemoveExtension().value(),
180                  extension_id,
181                  true /* case_sensitive */)) {
182       return true;
183     }
184   }
186   return false;
189 // Given the path to an app bundle, return the path to the Info.plist file.
190 NSString* GetPlistPath(const base::FilePath& bundle_path) {
191   return base::mac::FilePathToNSString(
192       bundle_path.Append("Contents").Append("Info.plist"));
195 NSMutableDictionary* ReadPlist(NSString* plist_path) {
196   return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
199 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
200 // the Info.plist starts with the current user_data_dir. This uses starts with
201 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir
202 // or the |app_data_dir_|.
203 bool HasSameUserDataDir(const base::FilePath& bundle_path) {
204   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
205   base::FilePath user_data_dir;
206   PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
207   DCHECK(!user_data_dir.empty());
208   return StartsWithASCII(
209       base::SysNSStringToUTF8(
210           [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
211       user_data_dir.value(),
212       true /* case_sensitive */);
215 void LaunchShimOnFileThread(
216     const web_app::ShortcutInfo& shortcut_info) {
217   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
218   base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info);
220   if (shim_path.empty() ||
221       !base::PathExists(shim_path) ||
222       !HasSameUserDataDir(shim_path)) {
223     // The user may have deleted the copy in the Applications folder, use the
224     // one in the web app's |app_data_dir_|.
225     base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
226         shortcut_info.profile_path, shortcut_info.extension_id, GURL());
227     shim_path = app_data_dir.Append(shim_path.BaseName());
228   }
230   if (!base::PathExists(shim_path))
231     return;
233   CommandLine command_line(CommandLine::NO_PROGRAM);
234   command_line.AppendSwitchASCII(
235       app_mode::kLaunchedByChromeProcessId,
236       base::IntToString(base::GetCurrentProcId()));
237   // Launch without activating (kLSLaunchDontSwitch).
238   base::mac::OpenApplicationWithPath(
239       shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
242 base::FilePath GetAppLoaderPath() {
243   return base::mac::PathForFrameworkBundleResource(
244       base::mac::NSToCFCast(@"app_mode_loader.app"));
247 base::FilePath GetLocalizableAppShortcutsSubdirName() {
248   static const char kChromiumAppDirName[] = "Chromium Apps.localized";
249   static const char kChromeAppDirName[] = "Chrome Apps.localized";
250   static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";
252   switch (chrome::VersionInfo::GetChannel()) {
253     case chrome::VersionInfo::CHANNEL_UNKNOWN:
254       return base::FilePath(kChromiumAppDirName);
256     case chrome::VersionInfo::CHANNEL_CANARY:
257       return base::FilePath(kChromeCanaryAppDirName);
259     default:
260       return base::FilePath(kChromeAppDirName);
261   }
264 // Creates a canvas the same size as |overlay|, copies the appropriate
265 // representation from |backgound| into it (according to Cocoa), then draws
266 // |overlay| over it using NSCompositeSourceOver.
267 NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
268   DCHECK(background);
269   NSInteger dimension = [overlay pixelsWide];
270   DCHECK_EQ(dimension, [overlay pixelsHigh]);
271   base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
272       initWithBitmapDataPlanes:NULL
273                     pixelsWide:dimension
274                     pixelsHigh:dimension
275                  bitsPerSample:8
276                samplesPerPixel:4
277                       hasAlpha:YES
278                       isPlanar:NO
279                 colorSpaceName:NSCalibratedRGBColorSpace
280                    bytesPerRow:0
281                   bitsPerPixel:0]);
283   // There isn't a colorspace name constant for sRGB, so retag.
284   NSBitmapImageRep* srgb_canvas = [canvas
285       bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
286   canvas.reset([srgb_canvas retain]);
288   // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
289   [canvas setSize:NSMakeSize(dimension, dimension)];
291   NSGraphicsContext* drawing_context =
292       [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
293   [NSGraphicsContext saveGraphicsState];
294   [NSGraphicsContext setCurrentContext:drawing_context];
295   [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
296                 fromRect:NSZeroRect
297                operation:NSCompositeCopy
298                 fraction:1.0];
299   [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
300              fromRect:NSZeroRect
301             operation:NSCompositeSourceOver
302              fraction:1.0
303        respectFlipped:NO
304                 hints:0];
305   [NSGraphicsContext restoreGraphicsState];
306   return canvas.autorelease();
309 // Helper function to extract the single NSImageRep held in a resource bundle
310 // image.
311 NSImageRep* ImageRepForResource(int resource_id) {
312   gfx::Image& image =
313       ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
314   NSArray* image_reps = [image.AsNSImage() representations];
315   DCHECK_EQ(1u, [image_reps count]);
316   return [image_reps objectAtIndex:0];
319 // Adds a localized strings file for the Chrome Apps directory using the current
320 // locale. OSX will use this for the display name.
321 // + Chrome Apps.localized (|apps_directory|)
322 // | + .localized
323 // | | en.strings
324 // | | de.strings
325 void UpdateAppShortcutsSubdirLocalizedName(
326     const base::FilePath& apps_directory) {
327   base::FilePath localized = apps_directory.Append(".localized");
328   if (!base::CreateDirectory(localized))
329     return;
331   base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
332   base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
333   NSDictionary* strings_dict = @{
334       base::mac::FilePathToNSString(directory_name) :
335           base::SysUTF16ToNSString(localized_name)
336   };
338   std::string locale = l10n_util::NormalizeLocale(
339       l10n_util::GetApplicationLocale(std::string()));
341   NSString* strings_path = base::mac::FilePathToNSString(
342       localized.Append(locale + ".strings"));
343   [strings_dict writeToFile:strings_path
344                  atomically:YES];
346   base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);
348   // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a
349   // bug when dealing with named NSImages where it incorrectly handles alpha
350   // premultiplication. This is most noticable with small assets since the 1px
351   // border is a much larger component of the small icons.
352   // See http://crbug.com/305373 for details.
353   [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)];
354   [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)];
356   // Brand larger folder assets with an embossed app launcher logo to conserve
357   // distro size and for better consistency with changing hue across OSX
358   // versions. The folder is textured, so compresses poorly without this.
359   const int kBrandResourceIds[] = {
360     IDR_APPS_FOLDER_OVERLAY_128,
361     IDR_APPS_FOLDER_OVERLAY_512,
362   };
363   NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
364   for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
365     NSImageRep* with_overlay =
366         OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i]));
367     DCHECK(with_overlay);
368     if (with_overlay)
369       [folder_icon_image addRepresentation:with_overlay];
370   }
371   [[NSWorkspace sharedWorkspace]
372       setIcon:folder_icon_image
373       forFile:base::mac::FilePathToNSString(apps_directory)
374       options:0];
377 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
378   DCHECK(!app_path.empty());
379   base::DeleteFile(app_path, true);
380   base::FilePath apps_folder = app_path.DirName();
381   if (base::IsDirectoryEmpty(apps_folder))
382     base::DeleteFile(apps_folder, false);
385 bool IsShimForProfile(const base::FilePath& base_name,
386                       const std::string& profile_base_name) {
387   if (!StartsWithASCII(base_name.value(), profile_base_name, true))
388     return false;
390   if (base_name.Extension() != ".app")
391     return false;
393   std::string app_id = base_name.RemoveExtension().value();
394   // Strip (profile_base_name + " ") from the start.
395   app_id = app_id.substr(profile_base_name.size() + 1);
396   return extensions::Extension::IdIsValid(app_id);
399 std::vector<base::FilePath> GetAllAppBundlesInPath(
400     const base::FilePath& internal_shortcut_path,
401     const std::string& profile_base_name) {
402   std::vector<base::FilePath> bundle_paths;
404   base::FileEnumerator enumerator(internal_shortcut_path,
405                                   true /* recursive */,
406                                   base::FileEnumerator::DIRECTORIES);
407   for (base::FilePath bundle_path = enumerator.Next();
408        !bundle_path.empty(); bundle_path = enumerator.Next()) {
409     if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
410       bundle_paths.push_back(bundle_path);
411   }
413   return bundle_paths;
416 web_app::ShortcutInfo BuildShortcutInfoFromBundle(
417     const base::FilePath& bundle_path) {
418   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
420   web_app::ShortcutInfo shortcut_info;
421   shortcut_info.extension_id = base::SysNSStringToUTF8(
422       [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
423   shortcut_info.is_platform_app = true;
424   shortcut_info.url = GURL(base::SysNSStringToUTF8(
425       [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
426   shortcut_info.title = base::SysNSStringToUTF16(
427       [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
428   shortcut_info.profile_name = base::SysNSStringToUTF8(
429       [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);
431   // Figure out the profile_path. Since the user_data_dir could contain the
432   // path to the web app data dir.
433   base::FilePath user_data_dir = base::mac::NSStringToFilePath(
434       [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
435   base::FilePath profile_base_name = base::mac::NSStringToFilePath(
436       [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
437   if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
438     shortcut_info.profile_path = user_data_dir.DirName().DirName();
439   else
440     shortcut_info.profile_path = user_data_dir.Append(profile_base_name);
442   return shortcut_info;
445 void UpdateFileTypes(NSMutableDictionary* plist,
446                      const extensions::FileHandlersInfo& file_handlers_info) {
447   NSMutableArray* document_types =
448       [NSMutableArray arrayWithCapacity:file_handlers_info.size()];
450   for (extensions::FileHandlersInfo::const_iterator info_it =
451            file_handlers_info.begin();
452        info_it != file_handlers_info.end();
453        ++info_it) {
454     const extensions::FileHandlerInfo& info = *info_it;
456     NSMutableArray* file_extensions =
457         [NSMutableArray arrayWithCapacity:info.extensions.size()];
458     for (std::set<std::string>::iterator it = info.extensions.begin();
459          it != info.extensions.end();
460          ++it) {
461       [file_extensions addObject:base::SysUTF8ToNSString(*it)];
462     }
464     NSMutableArray* mime_types =
465         [NSMutableArray arrayWithCapacity:info.types.size()];
466     for (std::set<std::string>::iterator it = info.types.begin();
467          it != info.types.end();
468          ++it) {
469       [mime_types addObject:base::SysUTF8ToNSString(*it)];
470     }
472     NSDictionary* type_dictionary = @{
473       // TODO(jackhou): Add the type name and and icon file once the manifest
474       // supports these.
475       // app_mode::kCFBundleTypeNameKey : ,
476       // app_mode::kCFBundleTypeIconFileKey : ,
477       app_mode::kCFBundleTypeExtensionsKey : file_extensions,
478       app_mode::kCFBundleTypeMIMETypesKey : mime_types,
479       app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer
480     };
481     [document_types addObject:type_dictionary];
482   }
484   [plist setObject:document_types
485             forKey:app_mode::kCFBundleDocumentTypesKey];
488 }  // namespace
490 namespace web_app {
492 WebAppShortcutCreator::WebAppShortcutCreator(
493     const base::FilePath& app_data_dir,
494     const ShortcutInfo& shortcut_info,
495     const extensions::FileHandlersInfo& file_handlers_info)
496     : app_data_dir_(app_data_dir),
497       info_(shortcut_info),
498       file_handlers_info_(file_handlers_info) {}
500 WebAppShortcutCreator::~WebAppShortcutCreator() {}
502 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
503   base::FilePath applications_dir = GetApplicationsDirname();
504   return applications_dir.empty() ?
505       base::FilePath() : applications_dir.Append(GetShortcutBasename());
508 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
509   return app_data_dir_.Append(GetShortcutBasename());
512 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
513   std::string app_name;
514   // Check if there should be a separate shortcut made for different profiles.
515   // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
516   // otherwise it will be empty.
517   if (!info_.profile_name.empty()) {
518     app_name += info_.profile_path.BaseName().value();
519     app_name += ' ';
520   }
521   app_name += info_.extension_id;
522   return base::FilePath(app_name).ReplaceExtension("app");
525 bool WebAppShortcutCreator::BuildShortcut(
526     const base::FilePath& staging_path) const {
527   // Update the app's plist and icon in a temp directory. This works around
528   // a Finder bug where the app's icon doesn't properly update.
529   if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
530     LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
531                << " failed.";
532     return false;
533   }
535   return UpdatePlist(staging_path) &&
536       UpdateDisplayName(staging_path) &&
537       UpdateIcon(staging_path);
540 size_t WebAppShortcutCreator::CreateShortcutsIn(
541     const std::vector<base::FilePath>& folders) const {
542   size_t succeeded = 0;
544   base::ScopedTempDir scoped_temp_dir;
545   if (!scoped_temp_dir.CreateUniqueTempDir())
546     return 0;
548   base::FilePath app_name = GetShortcutBasename();
549   base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
550   if (!BuildShortcut(staging_path))
551     return 0;
553   for (std::vector<base::FilePath>::const_iterator it = folders.begin();
554        it != folders.end(); ++it) {
555     const base::FilePath& dst_path = *it;
556     if (!base::CreateDirectory(dst_path)) {
557       LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
558       return succeeded;
559     }
561     if (!base::CopyDirectory(staging_path, dst_path, true)) {
562       LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
563                  << " failed";
564       return succeeded;
565     }
567     base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
568     ++succeeded;
569   }
571   return succeeded;
574 bool WebAppShortcutCreator::CreateShortcuts(
575     ShortcutCreationReason creation_reason,
576     ShortcutLocations creation_locations) {
577   const base::FilePath applications_dir = GetApplicationsDirname();
578   if (applications_dir.empty() ||
579       !base::DirectoryExists(applications_dir.DirName())) {
580     LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
581     return false;
582   }
584   UpdateAppShortcutsSubdirLocalizedName(applications_dir);
586   // If non-nil, this path is added to the OSX Dock after creating shortcuts.
587   NSString* path_to_add_to_dock = nil;
589   std::vector<base::FilePath> paths;
591   // The app list shim is not tied to a particular profile, so omit the copy
592   // placed under the profile path. For shims, this copy is used when the
593   // version under Applications is removed, and not needed for app list because
594   // setting LSUIElement means there is no Dock "running" status to show.
595   const bool is_app_list = info_.extension_id == app_mode::kAppListModeId;
596   if (is_app_list) {
597     path_to_add_to_dock = base::SysUTF8ToNSString(
598         applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
599   } else {
600     paths.push_back(app_data_dir_);
601   }
603   bool shortcut_visible =
604       creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN;
605   if (shortcut_visible)
606     paths.push_back(applications_dir);
608   DCHECK(!paths.empty());
609   size_t success_count = CreateShortcutsIn(paths);
610   if (success_count == 0)
611     return false;
613   if (!is_app_list)
614     UpdateInternalBundleIdentifier();
616   if (success_count != paths.size())
617     return false;
619   if (creation_locations.in_quick_launch_bar && path_to_add_to_dock &&
620       shortcut_visible) {
621     switch (dock::AddIcon(path_to_add_to_dock, nil)) {
622       case dock::IconAddFailure:
623         // If adding the icon failed, instead reveal the Finder window.
624         RevealAppShimInFinder();
625         break;
626       case dock::IconAddSuccess:
627       case dock::IconAlreadyPresent:
628         break;
629     }
630     return true;
631   }
633   if (creation_reason == SHORTCUT_CREATION_BY_USER)
634     RevealAppShimInFinder();
636   return true;
639 void WebAppShortcutCreator::DeleteShortcuts() {
640   base::FilePath app_path = GetApplicationsShortcutPath();
641   if (!app_path.empty() && HasSameUserDataDir(app_path))
642     DeletePathAndParentIfEmpty(app_path);
644   // In case the user has moved/renamed/copied the app bundle.
645   base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
646   if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
647     base::DeleteFile(bundle_path, true);
649   // Delete the internal one.
650   DeletePathAndParentIfEmpty(GetInternalShortcutPath());
653 bool WebAppShortcutCreator::UpdateShortcuts() {
654   std::vector<base::FilePath> paths;
655   base::DeleteFile(GetInternalShortcutPath(), true);
656   paths.push_back(app_data_dir_);
658   // Try to update the copy under /Applications. If that does not exist, check
659   // if a matching bundle can be found elsewhere.
660   base::FilePath app_path = GetApplicationsShortcutPath();
661   if (app_path.empty() || !base::PathExists(app_path))
662     app_path = GetAppBundleById(GetBundleIdentifier());
664   if (!app_path.empty()) {
665     base::DeleteFile(app_path, true);
666     paths.push_back(app_path.DirName());
667   }
669   size_t success_count = CreateShortcutsIn(paths);
670   if (success_count == 0)
671     return false;
673   UpdateInternalBundleIdentifier();
674   return success_count == paths.size() && !app_path.empty();
677 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
678   base::FilePath path = GetWritableApplicationsDirectory();
679   if (path.empty())
680     return path;
682   return path.Append(GetLocalizableAppShortcutsSubdirName());
685 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
686   NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
687   NSString* extension_title = base::SysUTF16ToNSString(info_.title);
688   NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
689   NSString* chrome_bundle_id =
690       base::SysUTF8ToNSString(base::mac::BaseBundleID());
691   NSDictionary* replacement_dict =
692       [NSDictionary dictionaryWithObjectsAndKeys:
693           extension_id, app_mode::kShortcutIdPlaceholder,
694           extension_title, app_mode::kShortcutNamePlaceholder,
695           extension_url, app_mode::kShortcutURLPlaceholder,
696           chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
697           nil];
699   NSString* plist_path = GetPlistPath(app_path);
700   NSMutableDictionary* plist = ReadPlist(plist_path);
701   NSArray* keys = [plist allKeys];
703   // 1. Fill in variables.
704   for (id key in keys) {
705     NSString* value = [plist valueForKey:key];
706     if (![value isKindOfClass:[NSString class]] || [value length] < 2)
707       continue;
709     // Remove leading and trailing '@'s.
710     NSString* variable =
711         [value substringWithRange:NSMakeRange(1, [value length] - 2)];
713     NSString* substitution = [replacement_dict valueForKey:variable];
714     if (substitution)
715       [plist setObject:substitution forKey:key];
716   }
718   // 2. Fill in other values.
719   [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
720             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
721   [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
722             forKey:app_mode::kCrAppModeUserDataDirKey];
723   [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName())
724             forKey:app_mode::kCrAppModeProfileDirKey];
725   [plist setObject:base::SysUTF8ToNSString(info_.profile_name)
726             forKey:app_mode::kCrAppModeProfileNameKey];
727   [plist setObject:[NSNumber numberWithBool:YES]
728             forKey:app_mode::kLSHasLocalizedDisplayNameKey];
729   if (info_.extension_id == app_mode::kAppListModeId) {
730     // Prevent the app list from bouncing in the dock, and getting a run light.
731     [plist setObject:[NSNumber numberWithBool:YES]
732               forKey:kLSUIElement];
733   }
735   base::FilePath app_name = app_path.BaseName().RemoveExtension();
736   [plist setObject:base::mac::FilePathToNSString(app_name)
737             forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
739   if (CommandLine::ForCurrentProcess()->HasSwitch(
740           switches::kEnableAppsFileAssociations)) {
741     UpdateFileTypes(plist, file_handlers_info_);
742   }
744   return [plist writeToFile:plist_path
745                  atomically:YES];
748 bool WebAppShortcutCreator::UpdateDisplayName(
749     const base::FilePath& app_path) const {
750   // OSX searches for the best language in the order of preferred languages.
751   // Since we only have one localization directory, it will choose this one.
752   base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
753   if (!base::CreateDirectory(localized_dir))
754     return false;
756   NSString* bundle_name = base::SysUTF16ToNSString(info_.title);
757   NSString* display_name = base::SysUTF16ToNSString(info_.title);
758   if (HasExistingExtensionShim(GetApplicationsDirname(),
759                                info_.extension_id,
760                                app_path.BaseName())) {
761     display_name = [bundle_name
762         stringByAppendingString:base::SysUTF8ToNSString(
763             " (" + info_.profile_name + ")")];
764   }
766   NSDictionary* strings_plist = @{
767     base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
768     app_mode::kCFBundleDisplayNameKey : display_name
769   };
771   NSString* localized_path = base::mac::FilePathToNSString(
772       localized_dir.Append("InfoPlist.strings"));
773   return [strings_plist writeToFile:localized_path
774                          atomically:YES];
777 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
778   if (info_.favicon.empty())
779     return true;
781   ScopedCarbonHandle icon_family(0);
782   bool image_added = false;
783   for (gfx::ImageFamily::const_iterator it = info_.favicon.begin();
784        it != info_.favicon.end(); ++it) {
785     if (it->IsEmpty())
786       continue;
788     // Missing an icon size is not fatal so don't fail if adding the bitmap
789     // doesn't work.
790     if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it))
791       continue;
793     image_added = true;
794   }
796   if (!image_added)
797     return false;
799   base::FilePath resources_path = GetResourcesPath(app_path);
800   if (!base::CreateDirectory(resources_path))
801     return false;
803   return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
806 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
807   NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
808   NSMutableDictionary* plist = ReadPlist(plist_path);
810   [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
811             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
812   return [plist writeToFile:plist_path
813                  atomically:YES];
816 base::FilePath WebAppShortcutCreator::GetAppBundleById(
817     const std::string& bundle_id) const {
818   base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
819       base::SysUTF8ToCFStringRef(bundle_id));
820   CFURLRef url_ref = NULL;
821   OSStatus status = LSFindApplicationForInfo(
822       kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
823   if (status != noErr)
824     return base::FilePath();
826   base::ScopedCFTypeRef<CFURLRef> url(url_ref);
827   NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
828   return base::FilePath([path_string fileSystemRepresentation]);
831 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
832   // Replace spaces in the profile path with hyphen.
833   std::string normalized_profile_path;
834   base::ReplaceChars(info_.profile_path.BaseName().value(),
835                      " ", "-", &normalized_profile_path);
837   // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
838   std::string bundle_id =
839       base::mac::BaseBundleID() + std::string(".app.") +
840       normalized_profile_path + "-" + info_.extension_id;
842   return bundle_id;
845 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
846   return GetBundleIdentifier() + "-internal";
849 void WebAppShortcutCreator::RevealAppShimInFinder() const {
850   base::FilePath app_path = GetApplicationsShortcutPath();
851   if (app_path.empty())
852     return;
854   [[NSWorkspace sharedWorkspace]
855                     selectFile:base::mac::FilePathToNSString(app_path)
856       inFileViewerRootedAtPath:nil];
859 base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) {
860   WebAppShortcutCreator shortcut_creator(
861       base::FilePath(), shortcut_info, extensions::FileHandlersInfo());
862   return shortcut_creator.GetApplicationsShortcutPath();
865 void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) {
866   if (AppShimsDisabledForTest())
867     return;
869   content::BrowserThread::PostTask(
870       content::BrowserThread::FILE, FROM_HERE,
871       base::Bind(&LaunchShimOnFileThread, shortcut_info));
874 // Called when the app's ShortcutInfo (with icon) is loaded when creating app
875 // shortcuts.
876 void CreateAppShortcutInfoLoaded(
877     Profile* profile,
878     const extensions::Extension* app,
879     const base::Callback<void(bool)>& close_callback,
880     const ShortcutInfo& shortcut_info) {
881   base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
883   NSButton* continue_button = [alert
884       addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)];
885   [continue_button setKeyEquivalent:@""];
887   NSButton* cancel_button =
888       [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)];
889   [cancel_button setKeyEquivalent:@"\r"];
891   [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)];
892   [alert setAlertStyle:NSInformationalAlertStyle];
894   base::scoped_nsobject<NSButton> application_folder_checkbox(
895       [[NSButton alloc] initWithFrame:NSZeroRect]);
896   [application_folder_checkbox setButtonType:NSSwitchButton];
897   [application_folder_checkbox
898       setTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_APP_FOLDER_CHKBOX)];
899   [application_folder_checkbox setState:NSOnState];
900   [application_folder_checkbox sizeToFit];
901   [alert setAccessoryView:application_folder_checkbox];
903   const int kIconPreviewSizePixels = 128;
904   const int kIconPreviewTargetSize = 64;
905   const gfx::Image* icon = shortcut_info.favicon.GetBest(
906       kIconPreviewSizePixels, kIconPreviewSizePixels);
908   if (icon && !icon->IsEmpty()) {
909     NSImage* icon_image = icon->ToNSImage();
910     [icon_image
911         setSize:NSMakeSize(kIconPreviewTargetSize, kIconPreviewTargetSize)];
912     [alert setIcon:icon_image];
913   }
915   bool dialog_accepted = false;
916   if ([alert runModal] == NSAlertFirstButtonReturn &&
917       [application_folder_checkbox state] == NSOnState) {
918     dialog_accepted = true;
919     CreateShortcuts(
920         SHORTCUT_CREATION_BY_USER, ShortcutLocations(), profile, app);
921   }
923   if (!close_callback.is_null())
924     close_callback.Run(dialog_accepted);
927 void UpdateShortcutsForAllApps(Profile* profile,
928                                const base::Closure& callback) {
929   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
931   extensions::ExtensionRegistry* registry =
932       extensions::ExtensionRegistry::Get(profile);
933   if (!registry)
934     return;
936   // Update all apps.
937   scoped_ptr<extensions::ExtensionSet> everything =
938       registry->GenerateInstalledExtensionsSet();
939   for (extensions::ExtensionSet::const_iterator it = everything->begin();
940        it != everything->end(); ++it) {
941     if (web_app::ShouldCreateShortcutFor(profile, it->get()))
942       web_app::UpdateAllShortcuts(base::string16(), profile, it->get());
943   }
945   callback.Run();
948 namespace internals {
950 bool CreatePlatformShortcuts(
951     const base::FilePath& app_data_path,
952     const ShortcutInfo& shortcut_info,
953     const extensions::FileHandlersInfo& file_handlers_info,
954     const ShortcutLocations& creation_locations,
955     ShortcutCreationReason creation_reason) {
956   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
957   if (AppShimsDisabledForTest())
958     return true;
960   WebAppShortcutCreator shortcut_creator(
961       app_data_path, shortcut_info, file_handlers_info);
962   return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
965 void DeletePlatformShortcuts(const base::FilePath& app_data_path,
966                              const ShortcutInfo& shortcut_info) {
967   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
968   WebAppShortcutCreator shortcut_creator(
969       app_data_path, shortcut_info, extensions::FileHandlersInfo());
970   shortcut_creator.DeleteShortcuts();
973 void UpdatePlatformShortcuts(
974     const base::FilePath& app_data_path,
975     const base::string16& old_app_title,
976     const ShortcutInfo& shortcut_info,
977     const extensions::FileHandlersInfo& file_handlers_info) {
978   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
979   if (AppShimsDisabledForTest())
980     return;
982   WebAppShortcutCreator shortcut_creator(
983       app_data_path, shortcut_info, file_handlers_info);
984   shortcut_creator.UpdateShortcuts();
987 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
988   const std::string profile_base_name = profile_path.BaseName().value();
989   std::vector<base::FilePath> bundles = GetAllAppBundlesInPath(
990       profile_path.Append(chrome::kWebAppDirname), profile_base_name);
992   for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
993        it != bundles.end(); ++it) {
994     web_app::ShortcutInfo shortcut_info =
995         BuildShortcutInfoFromBundle(*it);
996     WebAppShortcutCreator shortcut_creator(
997         it->DirName(), shortcut_info, extensions::FileHandlersInfo());
998     shortcut_creator.DeleteShortcuts();
999   }
1002 }  // namespace internals
1004 }  // namespace web_app
1006 namespace chrome {
1008 void ShowCreateChromeAppShortcutsDialog(
1009     gfx::NativeWindow /*parent_window*/,
1010     Profile* profile,
1011     const extensions::Extension* app,
1012     const base::Callback<void(bool)>& close_callback) {
1013   web_app::UpdateShortcutInfoAndIconForApp(
1014       app,
1015       profile,
1016       base::Bind(&web_app::CreateAppShortcutInfoLoaded,
1017                  profile,
1018                  app,
1019                  close_callback));
1022 }  // namespace chrome