Componentize AccountReconcilor.
[chromium-blink-merge.git] / chrome / browser / web_applications / web_app_mac.mm
blob903f243f22ea4881c78bc99592d7a909b53b9200
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 "apps/app_shim/app_shim_mac.h"
11 #include "base/command_line.h"
12 #include "base/file_util.h"
13 #include "base/files/file_enumerator.h"
14 #include "base/files/scoped_temp_dir.h"
15 #include "base/mac/foundation_util.h"
16 #include "base/mac/launch_services_util.h"
17 #include "base/mac/mac_util.h"
18 #include "base/mac/scoped_cftyperef.h"
19 #include "base/mac/scoped_nsobject.h"
20 #include "base/path_service.h"
21 #include "base/process/process_handle.h"
22 #include "base/strings/string16.h"
23 #include "base/strings/string_number_conversions.h"
24 #include "base/strings/string_util.h"
25 #include "base/strings/sys_string_conversions.h"
26 #include "base/strings/utf_string_conversions.h"
27 #import "chrome/browser/mac/dock.h"
28 #include "chrome/browser/profiles/profile.h"
29 #include "chrome/common/chrome_constants.h"
30 #include "chrome/common/chrome_paths.h"
31 #include "chrome/common/chrome_switches.h"
32 #include "chrome/common/chrome_version_info.h"
33 #import "chrome/common/mac/app_mode_common.h"
34 #include "content/public/browser/browser_thread.h"
35 #include "extensions/common/extension.h"
36 #include "grit/chrome_unscaled_resources.h"
37 #include "grit/chromium_strings.h"
38 #import "skia/ext/skia_utils_mac.h"
39 #include "third_party/skia/include/core/SkBitmap.h"
40 #include "third_party/skia/include/core/SkColor.h"
41 #include "ui/base/l10n/l10n_util.h"
42 #import "ui/base/l10n/l10n_util_mac.h"
43 #include "ui/base/resource/resource_bundle.h"
44 #include "ui/gfx/image/image_family.h"
46 namespace {
48 // Launch Services Key to run as an agent app, which doesn't launch in the dock.
49 NSString* const kLSUIElement = @"LSUIElement";
51 class ScopedCarbonHandle {
52  public:
53   ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
54     DCHECK(handle_);
55     DCHECK_EQ(noErr, MemError());
56   }
57   ~ScopedCarbonHandle() { DisposeHandle(handle_); }
59   Handle Get() { return handle_; }
60   char* Data() { return *handle_; }
61   size_t HandleSize() const { return GetHandleSize(handle_); }
63   IconFamilyHandle GetAsIconFamilyHandle() {
64     return reinterpret_cast<IconFamilyHandle>(handle_);
65   }
67   bool WriteDataToFile(const base::FilePath& path) {
68     NSData* data = [NSData dataWithBytes:Data()
69                                   length:HandleSize()];
70     return [data writeToFile:base::mac::FilePathToNSString(path)
71                   atomically:NO];
72   }
74  private:
75   Handle handle_;
78 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
79   CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
81   char* argb = handle->Data();
82   SkAutoLockPixels lock(bitmap);
83   for (int y = 0; y < bitmap.height(); ++y) {
84     for (int x = 0; x < bitmap.width(); ++x) {
85       SkColor pixel = bitmap.getColor(x, y);
86       argb[0] = SkColorGetA(pixel);
87       argb[1] = SkColorGetR(pixel);
88       argb[2] = SkColorGetG(pixel);
89       argb[3] = SkColorGetB(pixel);
90       argb += 4;
91     }
92   }
95 // Adds |image| to |icon_family|. Returns true on success, false on failure.
96 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
97                              const gfx::Image& image) {
98   // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
99   // have all the representations desired here for mac, from the kDesiredSizes
100   // array in web_app.cc.
101   SkBitmap bitmap = image.AsBitmap();
102   if (bitmap.config() != SkBitmap::kARGB_8888_Config ||
103       bitmap.width() != bitmap.height()) {
104     return false;
105   }
107   OSType icon_type;
108   switch (bitmap.width()) {
109     case 512:
110       icon_type = kIconServices512PixelDataARGB;
111       break;
112     case 256:
113       icon_type = kIconServices256PixelDataARGB;
114       break;
115     case 128:
116       icon_type = kIconServices128PixelDataARGB;
117       break;
118     case 48:
119       icon_type = kIconServices48PixelDataARGB;
120       break;
121     case 32:
122       icon_type = kIconServices32PixelDataARGB;
123       break;
124     case 16:
125       icon_type = kIconServices16PixelDataARGB;
126       break;
127     default:
128       return false;
129   }
131   ScopedCarbonHandle raw_data(bitmap.getSize());
132   ConvertSkiaToARGB(bitmap, &raw_data);
133   OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
134   DCHECK_EQ(noErr, result);
135   return result == noErr;
138 base::FilePath GetWritableApplicationsDirectory() {
139   base::FilePath path;
140   if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
141     if (!base::DirectoryExists(path)) {
142       if (!base::CreateDirectory(path))
143         return base::FilePath();
145       // Create a zero-byte ".localized" file to inherit localizations from OSX
146       // for folders that have special meaning.
147       base::WriteFile(path.Append(".localized"), NULL, 0);
148     }
149     return base::PathIsWritable(path) ? path : base::FilePath();
150   }
151   return base::FilePath();
154 // Given the path to an app bundle, return the resources directory.
155 base::FilePath GetResourcesPath(const base::FilePath& app_path) {
156   return app_path.Append("Contents").Append("Resources");
159 bool HasExistingExtensionShim(const base::FilePath& destination_directory,
160                               const std::string& extension_id,
161                               const base::FilePath& own_basename) {
162   // Check if there any any other shims for the same extension.
163   base::FileEnumerator enumerator(destination_directory,
164                                   false /* recursive */,
165                                   base::FileEnumerator::DIRECTORIES);
166   for (base::FilePath shim_path = enumerator.Next();
167        !shim_path.empty(); shim_path = enumerator.Next()) {
168     if (shim_path.BaseName() != own_basename &&
169         EndsWith(shim_path.RemoveExtension().value(),
170                  extension_id,
171                  true /* case_sensitive */)) {
172       return true;
173     }
174   }
176   return false;
179 // Given the path to an app bundle, return the path to the Info.plist file.
180 NSString* GetPlistPath(const base::FilePath& bundle_path) {
181   return base::mac::FilePathToNSString(
182       bundle_path.Append("Contents").Append("Info.plist"));
185 NSMutableDictionary* ReadPlist(NSString* plist_path) {
186   return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
189 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
190 // the Info.plist starts with the current user_data_dir. This uses starts with
191 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir
192 // or the |app_data_dir_|.
193 bool HasSameUserDataDir(const base::FilePath& bundle_path) {
194   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
195   base::FilePath user_data_dir;
196   PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
197   DCHECK(!user_data_dir.empty());
198   return StartsWithASCII(
199       base::SysNSStringToUTF8(
200           [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
201       user_data_dir.value(),
202       true /* case_sensitive */);
205 void LaunchShimOnFileThread(
206     const ShellIntegration::ShortcutInfo& shortcut_info) {
207   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
208   base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info);
210   if (shim_path.empty() ||
211       !base::PathExists(shim_path) ||
212       !HasSameUserDataDir(shim_path)) {
213     // The user may have deleted the copy in the Applications folder, use the
214     // one in the web app's |app_data_dir_|.
215     base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
216         shortcut_info.profile_path, shortcut_info.extension_id, GURL());
217     shim_path = app_data_dir.Append(shim_path.BaseName());
218   }
220   if (!base::PathExists(shim_path))
221     return;
223   CommandLine command_line(CommandLine::NO_PROGRAM);
224   command_line.AppendSwitchASCII(
225       app_mode::kLaunchedByChromeProcessId,
226       base::IntToString(base::GetCurrentProcId()));
227   // Launch without activating (kLSLaunchDontSwitch).
228   base::mac::OpenApplicationWithPath(
229       shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
232 base::FilePath GetAppLoaderPath() {
233   return base::mac::PathForFrameworkBundleResource(
234       base::mac::NSToCFCast(@"app_mode_loader.app"));
237 base::FilePath GetLocalizableAppShortcutsSubdirName() {
238   static const char kChromiumAppDirName[] = "Chromium Apps.localized";
239   static const char kChromeAppDirName[] = "Chrome Apps.localized";
240   static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";
242   switch (chrome::VersionInfo::GetChannel()) {
243     case chrome::VersionInfo::CHANNEL_UNKNOWN:
244       return base::FilePath(kChromiumAppDirName);
246     case chrome::VersionInfo::CHANNEL_CANARY:
247       return base::FilePath(kChromeCanaryAppDirName);
249     default:
250       return base::FilePath(kChromeAppDirName);
251   }
254 // Creates a canvas the same size as |overlay|, copies the appropriate
255 // representation from |backgound| into it (according to Cocoa), then draws
256 // |overlay| over it using NSCompositeSourceOver.
257 NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
258   DCHECK(background);
259   NSInteger dimension = [overlay pixelsWide];
260   DCHECK_EQ(dimension, [overlay pixelsHigh]);
261   base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
262       initWithBitmapDataPlanes:NULL
263                     pixelsWide:dimension
264                     pixelsHigh:dimension
265                  bitsPerSample:8
266                samplesPerPixel:4
267                       hasAlpha:YES
268                       isPlanar:NO
269                 colorSpaceName:NSCalibratedRGBColorSpace
270                    bytesPerRow:0
271                   bitsPerPixel:0]);
273   // There isn't a colorspace name constant for sRGB, so retag.
274   NSBitmapImageRep* srgb_canvas = [canvas
275       bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
276   canvas.reset([srgb_canvas retain]);
278   // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
279   [canvas setSize:NSMakeSize(dimension, dimension)];
281   NSGraphicsContext* drawing_context =
282       [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
283   [NSGraphicsContext saveGraphicsState];
284   [NSGraphicsContext setCurrentContext:drawing_context];
285   [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
286                 fromRect:NSZeroRect
287                operation:NSCompositeCopy
288                 fraction:1.0];
289   [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
290              fromRect:NSZeroRect
291             operation:NSCompositeSourceOver
292              fraction:1.0
293        respectFlipped:NO
294                 hints:0];
295   [NSGraphicsContext restoreGraphicsState];
296   return canvas.autorelease();
299 // Adds a localized strings file for the Chrome Apps directory using the current
300 // locale. OSX will use this for the display name.
301 // + Chrome Apps.localized (|apps_directory|)
302 // | + .localized
303 // | | en.strings
304 // | | de.strings
305 void UpdateAppShortcutsSubdirLocalizedName(
306     const base::FilePath& apps_directory) {
307   base::FilePath localized = apps_directory.Append(".localized");
308   if (!base::CreateDirectory(localized))
309     return;
311   base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
312   base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
313   NSDictionary* strings_dict = @{
314       base::mac::FilePathToNSString(directory_name) :
315           base::SysUTF16ToNSString(localized_name)
316   };
318   std::string locale = l10n_util::NormalizeLocale(
319       l10n_util::GetApplicationLocale(std::string()));
321   NSString* strings_path = base::mac::FilePathToNSString(
322       localized.Append(locale + ".strings"));
323   [strings_dict writeToFile:strings_path
324                  atomically:YES];
326   // Brand the folder with an embossed app launcher logo.
327   const int kBrandResourceIds[] = {
328     IDR_APPS_FOLDER_OVERLAY_16,
329     IDR_APPS_FOLDER_OVERLAY_32,
330     IDR_APPS_FOLDER_OVERLAY_128,
331     IDR_APPS_FOLDER_OVERLAY_512,
332   };
333   ResourceBundle& rb = ResourceBundle::GetSharedInstance();
334   NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
335   base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);
336   for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
337     gfx::Image& image_rep = rb.GetNativeImageNamed(kBrandResourceIds[i]);
338     NSArray* image_reps = [image_rep.AsNSImage() representations];
339     DCHECK_EQ(1u, [image_reps count]);
340     NSImageRep* with_overlay = OverlayImageRep(base_image,
341                                                [image_reps objectAtIndex:0]);
342     DCHECK(with_overlay);
343     if (with_overlay)
344       [folder_icon_image addRepresentation:with_overlay];
345   }
346   [[NSWorkspace sharedWorkspace]
347       setIcon:folder_icon_image
348       forFile:base::mac::FilePathToNSString(apps_directory)
349       options:0];
352 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
353   DCHECK(!app_path.empty());
354   base::DeleteFile(app_path, true);
355   base::FilePath apps_folder = app_path.DirName();
356   if (base::IsDirectoryEmpty(apps_folder))
357     base::DeleteFile(apps_folder, false);
360 bool IsShimForProfile(const base::FilePath& base_name,
361                       const std::string& profile_base_name) {
362   if (!StartsWithASCII(base_name.value(), profile_base_name, true))
363     return false;
365   if (base_name.Extension() != ".app")
366     return false;
368   std::string app_id = base_name.RemoveExtension().value();
369   // Strip (profile_base_name + " ") from the start.
370   app_id = app_id.substr(profile_base_name.size() + 1);
371   return extensions::Extension::IdIsValid(app_id);
374 std::vector<base::FilePath> GetAllAppBundlesInPath(
375     const base::FilePath& internal_shortcut_path,
376     const std::string& profile_base_name) {
377   std::vector<base::FilePath> bundle_paths;
379   base::FileEnumerator enumerator(internal_shortcut_path,
380                                   true /* recursive */,
381                                   base::FileEnumerator::DIRECTORIES);
382   for (base::FilePath bundle_path = enumerator.Next();
383        !bundle_path.empty(); bundle_path = enumerator.Next()) {
384     if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
385       bundle_paths.push_back(bundle_path);
386   }
388   return bundle_paths;
391 ShellIntegration::ShortcutInfo BuildShortcutInfoFromBundle(
392     const base::FilePath& bundle_path) {
393   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
395   ShellIntegration::ShortcutInfo shortcut_info;
396   shortcut_info.extension_id = base::SysNSStringToUTF8(
397       [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
398   shortcut_info.is_platform_app = true;
399   shortcut_info.url = GURL(base::SysNSStringToUTF8(
400       [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
401   shortcut_info.title = base::SysNSStringToUTF16(
402       [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
403   shortcut_info.profile_name = base::SysNSStringToUTF8(
404       [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);
406   // Figure out the profile_path. Since the user_data_dir could contain the
407   // path to the web app data dir.
408   base::FilePath user_data_dir = base::mac::NSStringToFilePath(
409       [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
410   base::FilePath profile_base_name = base::mac::NSStringToFilePath(
411       [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
412   if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
413     shortcut_info.profile_path = user_data_dir.DirName().DirName();
414   else
415     shortcut_info.profile_path = user_data_dir.Append(profile_base_name);
417   return shortcut_info;
420 void CreateShortcutsAndRunCallback(
421     const base::Closure& close_callback,
422     const ShellIntegration::ShortcutInfo& shortcut_info) {
423   // creation_locations will be ignored by CreatePlatformShortcuts on Mac.
424   ShellIntegration::ShortcutLocations creation_locations;
425   web_app::CreateShortcuts(shortcut_info, creation_locations,
426                            web_app::SHORTCUT_CREATION_BY_USER);
427   if (!close_callback.is_null())
428     close_callback.Run();
431 }  // namespace
433 namespace chrome {
435 void ShowCreateChromeAppShortcutsDialog(gfx::NativeWindow /*parent_window*/,
436                                         Profile* profile,
437                                         const extensions::Extension* app,
438                                         const base::Closure& close_callback) {
439   // Normally we would show a dialog, but since we always create the app
440   // shortcut in /Applications there are no options for the user to choose.
441   web_app::UpdateShortcutInfoAndIconForApp(
442       app, profile,
443       base::Bind(&CreateShortcutsAndRunCallback, close_callback));
446 }  // namespace chrome
448 namespace web_app {
450 WebAppShortcutCreator::WebAppShortcutCreator(
451     const base::FilePath& app_data_dir,
452     const ShellIntegration::ShortcutInfo& shortcut_info)
453     : app_data_dir_(app_data_dir),
454       info_(shortcut_info) {}
456 WebAppShortcutCreator::~WebAppShortcutCreator() {}
458 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
459   base::FilePath applications_dir = GetApplicationsDirname();
460   return applications_dir.empty() ?
461       base::FilePath() : applications_dir.Append(GetShortcutBasename());
464 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
465   return app_data_dir_.Append(GetShortcutBasename());
468 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
469   std::string app_name;
470   // Check if there should be a separate shortcut made for different profiles.
471   // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
472   // otherwise it will be empty.
473   if (!info_.profile_name.empty()) {
474     app_name += info_.profile_path.BaseName().value();
475     app_name += ' ';
476   }
477   app_name += info_.extension_id;
478   return base::FilePath(app_name).ReplaceExtension("app");
481 bool WebAppShortcutCreator::BuildShortcut(
482     const base::FilePath& staging_path) const {
483   // Update the app's plist and icon in a temp directory. This works around
484   // a Finder bug where the app's icon doesn't properly update.
485   if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
486     LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
487                << " failed.";
488     return false;
489   }
491   return UpdatePlist(staging_path) &&
492       UpdateDisplayName(staging_path) &&
493       UpdateIcon(staging_path);
496 size_t WebAppShortcutCreator::CreateShortcutsIn(
497     const std::vector<base::FilePath>& folders) const {
498   size_t succeeded = 0;
500   base::ScopedTempDir scoped_temp_dir;
501   if (!scoped_temp_dir.CreateUniqueTempDir())
502     return 0;
504   base::FilePath app_name = GetShortcutBasename();
505   base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
506   if (!BuildShortcut(staging_path))
507     return 0;
509   for (std::vector<base::FilePath>::const_iterator it = folders.begin();
510        it != folders.end(); ++it) {
511     const base::FilePath& dst_path = *it;
512     if (!base::CreateDirectory(dst_path)) {
513       LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
514       return succeeded;
515     }
517     if (!base::CopyDirectory(staging_path, dst_path, true)) {
518       LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
519                  << " failed";
520       return succeeded;
521     }
523     base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
524     ++succeeded;
525   }
527   return succeeded;
530 bool WebAppShortcutCreator::CreateShortcuts(
531     ShortcutCreationReason creation_reason,
532     ShellIntegration::ShortcutLocations creation_locations) {
533   const base::FilePath applications_dir = GetApplicationsDirname();
534   if (applications_dir.empty() ||
535       !base::DirectoryExists(applications_dir.DirName())) {
536     LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
537     return false;
538   }
540   UpdateAppShortcutsSubdirLocalizedName(applications_dir);
542   // If non-nil, this path is added to the OSX Dock after creating shortcuts.
543   NSString* path_to_add_to_dock = nil;
545   std::vector<base::FilePath> paths;
547   // The app list shim is not tied to a particular profile, so omit the copy
548   // placed under the profile path. For shims, this copy is used when the
549   // version under Applications is removed, and not needed for app list because
550   // setting LSUIElement means there is no Dock "running" status to show.
551   const bool is_app_list = info_.extension_id == app_mode::kAppListModeId;
552   if (is_app_list) {
553     path_to_add_to_dock = base::SysUTF8ToNSString(
554         applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
555   } else {
556     paths.push_back(app_data_dir_);
557   }
558   paths.push_back(applications_dir);
560   size_t success_count = CreateShortcutsIn(paths);
561   if (success_count == 0)
562     return false;
564   if (!is_app_list)
565     UpdateInternalBundleIdentifier();
567   if (success_count != paths.size())
568     return false;
570   if (creation_locations.in_quick_launch_bar && path_to_add_to_dock) {
571     switch (dock::AddIcon(path_to_add_to_dock, nil)) {
572       case dock::IconAddFailure:
573         // If adding the icon failed, instead reveal the Finder window.
574         RevealAppShimInFinder();
575         break;
576       case dock::IconAddSuccess:
577       case dock::IconAlreadyPresent:
578         break;
579     }
580     return true;
581   }
583   if (creation_reason == SHORTCUT_CREATION_BY_USER)
584     RevealAppShimInFinder();
586   return true;
589 void WebAppShortcutCreator::DeleteShortcuts() {
590   base::FilePath app_path = GetApplicationsShortcutPath();
591   if (!app_path.empty() && HasSameUserDataDir(app_path))
592     DeletePathAndParentIfEmpty(app_path);
594   // In case the user has moved/renamed/copied the app bundle.
595   base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
596   if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
597     base::DeleteFile(bundle_path, true);
599   // Delete the internal one.
600   DeletePathAndParentIfEmpty(GetInternalShortcutPath());
603 bool WebAppShortcutCreator::UpdateShortcuts() {
604   std::vector<base::FilePath> paths;
605   base::DeleteFile(GetInternalShortcutPath(), true);
606   paths.push_back(app_data_dir_);
608   // Try to update the copy under /Applications. If that does not exist, check
609   // if a matching bundle can be found elsewhere.
610   base::FilePath app_path = GetApplicationsShortcutPath();
611   if (app_path.empty() || !base::PathExists(app_path))
612     app_path = GetAppBundleById(GetBundleIdentifier());
614   if (!app_path.empty()) {
615     base::DeleteFile(app_path, true);
616     paths.push_back(app_path.DirName());
617   }
619   size_t success_count = CreateShortcutsIn(paths);
620   if (success_count == 0)
621     return false;
623   UpdateInternalBundleIdentifier();
624   return success_count == paths.size() && !app_path.empty();
627 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
628   base::FilePath path = GetWritableApplicationsDirectory();
629   if (path.empty())
630     return path;
632   return path.Append(GetLocalizableAppShortcutsSubdirName());
635 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
636   NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
637   NSString* extension_title = base::SysUTF16ToNSString(info_.title);
638   NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
639   NSString* chrome_bundle_id =
640       base::SysUTF8ToNSString(base::mac::BaseBundleID());
641   NSDictionary* replacement_dict =
642       [NSDictionary dictionaryWithObjectsAndKeys:
643           extension_id, app_mode::kShortcutIdPlaceholder,
644           extension_title, app_mode::kShortcutNamePlaceholder,
645           extension_url, app_mode::kShortcutURLPlaceholder,
646           chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
647           nil];
649   NSString* plist_path = GetPlistPath(app_path);
650   NSMutableDictionary* plist = ReadPlist(plist_path);
651   NSArray* keys = [plist allKeys];
653   // 1. Fill in variables.
654   for (id key in keys) {
655     NSString* value = [plist valueForKey:key];
656     if (![value isKindOfClass:[NSString class]] || [value length] < 2)
657       continue;
659     // Remove leading and trailing '@'s.
660     NSString* variable =
661         [value substringWithRange:NSMakeRange(1, [value length] - 2)];
663     NSString* substitution = [replacement_dict valueForKey:variable];
664     if (substitution)
665       [plist setObject:substitution forKey:key];
666   }
668   // 2. Fill in other values.
669   [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
670             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
671   [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
672             forKey:app_mode::kCrAppModeUserDataDirKey];
673   [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName())
674             forKey:app_mode::kCrAppModeProfileDirKey];
675   [plist setObject:base::SysUTF8ToNSString(info_.profile_name)
676             forKey:app_mode::kCrAppModeProfileNameKey];
677   [plist setObject:[NSNumber numberWithBool:YES]
678             forKey:app_mode::kLSHasLocalizedDisplayNameKey];
679   if (info_.extension_id == app_mode::kAppListModeId) {
680     // Prevent the app list from bouncing in the dock, and getting a run light.
681     [plist setObject:[NSNumber numberWithBool:YES]
682               forKey:kLSUIElement];
683   }
685   base::FilePath app_name = app_path.BaseName().RemoveExtension();
686   [plist setObject:base::mac::FilePathToNSString(app_name)
687             forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
689   return [plist writeToFile:plist_path
690                  atomically:YES];
693 bool WebAppShortcutCreator::UpdateDisplayName(
694     const base::FilePath& app_path) const {
695   // OSX searches for the best language in the order of preferred languages.
696   // Since we only have one localization directory, it will choose this one.
697   base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
698   if (!base::CreateDirectory(localized_dir))
699     return false;
701   NSString* bundle_name = base::SysUTF16ToNSString(info_.title);
702   NSString* display_name = base::SysUTF16ToNSString(info_.title);
703   if (HasExistingExtensionShim(GetApplicationsDirname(),
704                                info_.extension_id,
705                                app_path.BaseName())) {
706     display_name = [bundle_name
707         stringByAppendingString:base::SysUTF8ToNSString(
708             " (" + info_.profile_name + ")")];
709   }
711   NSDictionary* strings_plist = @{
712     base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
713     app_mode::kCFBundleDisplayNameKey : display_name
714   };
716   NSString* localized_path = base::mac::FilePathToNSString(
717       localized_dir.Append("InfoPlist.strings"));
718   return [strings_plist writeToFile:localized_path
719                          atomically:YES];
722 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
723   if (info_.favicon.empty())
724     return true;
726   ScopedCarbonHandle icon_family(0);
727   bool image_added = false;
728   for (gfx::ImageFamily::const_iterator it = info_.favicon.begin();
729        it != info_.favicon.end(); ++it) {
730     if (it->IsEmpty())
731       continue;
733     // Missing an icon size is not fatal so don't fail if adding the bitmap
734     // doesn't work.
735     if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it))
736       continue;
738     image_added = true;
739   }
741   if (!image_added)
742     return false;
744   base::FilePath resources_path = GetResourcesPath(app_path);
745   if (!base::CreateDirectory(resources_path))
746     return false;
748   return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
751 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
752   NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
753   NSMutableDictionary* plist = ReadPlist(plist_path);
755   [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
756             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
757   return [plist writeToFile:plist_path
758                  atomically:YES];
761 base::FilePath WebAppShortcutCreator::GetAppBundleById(
762     const std::string& bundle_id) const {
763   base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
764       base::SysUTF8ToCFStringRef(bundle_id));
765   CFURLRef url_ref = NULL;
766   OSStatus status = LSFindApplicationForInfo(
767       kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
768   if (status != noErr)
769     return base::FilePath();
771   base::ScopedCFTypeRef<CFURLRef> url(url_ref);
772   NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
773   return base::FilePath([path_string fileSystemRepresentation]);
776 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
777   // Replace spaces in the profile path with hyphen.
778   std::string normalized_profile_path;
779   base::ReplaceChars(info_.profile_path.BaseName().value(),
780                      " ", "-", &normalized_profile_path);
782   // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
783   std::string bundle_id =
784       base::mac::BaseBundleID() + std::string(".app.") +
785       normalized_profile_path + "-" + info_.extension_id;
787   return bundle_id;
790 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
791   return GetBundleIdentifier() + "-internal";
794 void WebAppShortcutCreator::RevealAppShimInFinder() const {
795   base::FilePath app_path = GetApplicationsShortcutPath();
796   if (app_path.empty())
797     return;
799   [[NSWorkspace sharedWorkspace]
800                     selectFile:base::mac::FilePathToNSString(app_path)
801       inFileViewerRootedAtPath:nil];
804 base::FilePath GetAppInstallPath(
805     const ShellIntegration::ShortcutInfo& shortcut_info) {
806   WebAppShortcutCreator shortcut_creator(base::FilePath(), shortcut_info);
807   return shortcut_creator.GetApplicationsShortcutPath();
810 void MaybeLaunchShortcut(const ShellIntegration::ShortcutInfo& shortcut_info) {
811   if (!apps::IsAppShimsEnabled())
812     return;
814   content::BrowserThread::PostTask(
815       content::BrowserThread::FILE, FROM_HERE,
816       base::Bind(&LaunchShimOnFileThread, shortcut_info));
819 namespace internals {
821 bool CreatePlatformShortcuts(
822     const base::FilePath& app_data_path,
823     const ShellIntegration::ShortcutInfo& shortcut_info,
824     const ShellIntegration::ShortcutLocations& creation_locations,
825     ShortcutCreationReason creation_reason) {
826   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
827   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
828   return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
831 void DeletePlatformShortcuts(
832     const base::FilePath& app_data_path,
833     const ShellIntegration::ShortcutInfo& shortcut_info) {
834   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
835   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
836   shortcut_creator.DeleteShortcuts();
839 void UpdatePlatformShortcuts(
840     const base::FilePath& app_data_path,
841     const base::string16& old_app_title,
842     const ShellIntegration::ShortcutInfo& shortcut_info) {
843   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
844   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
845   shortcut_creator.UpdateShortcuts();
848 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
849   const std::string profile_base_name = profile_path.BaseName().value();
850   std::vector<base::FilePath> bundles = GetAllAppBundlesInPath(
851       profile_path.Append(chrome::kWebAppDirname), profile_base_name);
853   for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
854        it != bundles.end(); ++it) {
855     ShellIntegration::ShortcutInfo shortcut_info =
856         BuildShortcutInfoFromBundle(*it);
857     WebAppShortcutCreator shortcut_creator(it->DirName(), shortcut_info);
858     shortcut_creator.DeleteShortcuts();
859   }
862 }  // namespace internals
864 }  // namespace web_app