Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / web_applications / web_app_mac.mm
blobf1a73d3b5705fb327f808e120c9f7f2737564f39
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/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/profiles/profile.h"
28 #include "chrome/browser/ui/web_applications/web_app_ui.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/chromium_strings.h"
37 #import "skia/ext/skia_utils_mac.h"
38 #include "third_party/skia/include/core/SkBitmap.h"
39 #include "third_party/skia/include/core/SkColor.h"
40 #include "ui/base/l10n/l10n_util.h"
41 #import "ui/base/l10n/l10n_util_mac.h"
42 #include "ui/gfx/image/image_family.h"
44 namespace {
46 // Launch Services Key to run as an agent app, which doesn't launch in the dock.
47 NSString* const kLSUIElement = @"LSUIElement";
49 class ScopedCarbonHandle {
50  public:
51   ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
52     DCHECK(handle_);
53     DCHECK_EQ(noErr, MemError());
54   }
55   ~ScopedCarbonHandle() { DisposeHandle(handle_); }
57   Handle Get() { return handle_; }
58   char* Data() { return *handle_; }
59   size_t HandleSize() const { return GetHandleSize(handle_); }
61   IconFamilyHandle GetAsIconFamilyHandle() {
62     return reinterpret_cast<IconFamilyHandle>(handle_);
63   }
65   bool WriteDataToFile(const base::FilePath& path) {
66     NSData* data = [NSData dataWithBytes:Data()
67                                   length:HandleSize()];
68     return [data writeToFile:base::mac::FilePathToNSString(path)
69                   atomically:NO];
70   }
72  private:
73   Handle handle_;
76 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
77   CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
79   char* argb = handle->Data();
80   SkAutoLockPixels lock(bitmap);
81   for (int y = 0; y < bitmap.height(); ++y) {
82     for (int x = 0; x < bitmap.width(); ++x) {
83       SkColor pixel = bitmap.getColor(x, y);
84       argb[0] = SkColorGetA(pixel);
85       argb[1] = SkColorGetR(pixel);
86       argb[2] = SkColorGetG(pixel);
87       argb[3] = SkColorGetB(pixel);
88       argb += 4;
89     }
90   }
93 // Adds |image| to |icon_family|. Returns true on success, false on failure.
94 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
95                              const gfx::Image& image) {
96   // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
97   // have all the representations desired here for mac, from the kDesiredSizes
98   // array in web_app_ui.cc.
99   SkBitmap bitmap = image.AsBitmap();
100   if (bitmap.config() != SkBitmap::kARGB_8888_Config ||
101       bitmap.width() != bitmap.height()) {
102     return false;
103   }
105   OSType icon_type;
106   switch (bitmap.width()) {
107     case 512:
108       icon_type = kIconServices512PixelDataARGB;
109       break;
110     case 256:
111       icon_type = kIconServices256PixelDataARGB;
112       break;
113     case 128:
114       icon_type = kIconServices128PixelDataARGB;
115       break;
116     case 48:
117       icon_type = kIconServices48PixelDataARGB;
118       break;
119     case 32:
120       icon_type = kIconServices32PixelDataARGB;
121       break;
122     case 16:
123       icon_type = kIconServices16PixelDataARGB;
124       break;
125     default:
126       return false;
127   }
129   ScopedCarbonHandle raw_data(bitmap.getSize());
130   ConvertSkiaToARGB(bitmap, &raw_data);
131   OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
132   DCHECK_EQ(noErr, result);
133   return result == noErr;
136 base::FilePath GetWritableApplicationsDirectory() {
137   base::FilePath path;
138   if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
139     if (!base::DirectoryExists(path)) {
140       if (!base::CreateDirectory(path))
141         return base::FilePath();
143       // Create a zero-byte ".localized" file to inherit localizations from OSX
144       // for folders that have special meaning.
145       file_util::WriteFile(path.Append(".localized"), NULL, 0);
146     }
147     return base::PathIsWritable(path) ? path : base::FilePath();
148   }
149   return base::FilePath();
152 // Given the path to an app bundle, return the resources directory.
153 base::FilePath GetResourcesPath(const base::FilePath& app_path) {
154   return app_path.Append("Contents").Append("Resources");
157 bool HasExistingExtensionShim(const base::FilePath& destination_directory,
158                               const std::string& extension_id,
159                               const base::FilePath& own_basename) {
160   // Check if there any any other shims for the same extension.
161   base::FileEnumerator enumerator(destination_directory,
162                                   false /* recursive */,
163                                   base::FileEnumerator::DIRECTORIES);
164   for (base::FilePath shim_path = enumerator.Next();
165        !shim_path.empty(); shim_path = enumerator.Next()) {
166     if (shim_path.BaseName() != own_basename &&
167         EndsWith(shim_path.RemoveExtension().value(),
168                  extension_id,
169                  true /* case_sensitive */)) {
170       return true;
171     }
172   }
174   return false;
177 // Given the path to an app bundle, return the path to the Info.plist file.
178 NSString* GetPlistPath(const base::FilePath& bundle_path) {
179   return base::mac::FilePathToNSString(
180       bundle_path.Append("Contents").Append("Info.plist"));
183 NSMutableDictionary* ReadPlist(NSString* plist_path) {
184   return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
187 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
188 // the Info.plist starts with the current user_data_dir. This uses starts with
189 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir
190 // or the |app_data_dir_|.
191 bool HasSameUserDataDir(const base::FilePath& bundle_path) {
192   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
193   base::FilePath user_data_dir;
194   PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
195   DCHECK(!user_data_dir.empty());
196   return StartsWithASCII(
197       base::SysNSStringToUTF8(
198           [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
199       user_data_dir.value(),
200       true /* case_sensitive */);
203 void LaunchShimOnFileThread(
204     const ShellIntegration::ShortcutInfo& shortcut_info) {
205   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
206   base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info);
208   if (shim_path.empty() ||
209       !base::PathExists(shim_path) ||
210       !HasSameUserDataDir(shim_path)) {
211     // The user may have deleted the copy in the Applications folder, use the
212     // one in the web app's |app_data_dir_|.
213     base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
214         shortcut_info.profile_path, shortcut_info.extension_id, GURL());
215     shim_path = app_data_dir.Append(shim_path.BaseName());
216   }
218   if (!base::PathExists(shim_path))
219     return;
221   CommandLine command_line(CommandLine::NO_PROGRAM);
222   command_line.AppendSwitchASCII(
223       app_mode::kLaunchedByChromeProcessId,
224       base::IntToString(base::GetCurrentProcId()));
225   // Launch without activating (kLSLaunchDontSwitch).
226   base::mac::OpenApplicationWithPath(
227       shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
230 base::FilePath GetAppLoaderPath() {
231   return base::mac::PathForFrameworkBundleResource(
232       base::mac::NSToCFCast(@"app_mode_loader.app"));
235 base::FilePath GetLocalizableAppShortcutsSubdirName() {
236   static const char kChromiumAppDirName[] = "Chromium Apps.localized";
237   static const char kChromeAppDirName[] = "Chrome Apps.localized";
238   static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";
240   switch (chrome::VersionInfo::GetChannel()) {
241     case chrome::VersionInfo::CHANNEL_UNKNOWN:
242       return base::FilePath(kChromiumAppDirName);
244     case chrome::VersionInfo::CHANNEL_CANARY:
245       return base::FilePath(kChromeCanaryAppDirName);
247     default:
248       return base::FilePath(kChromeAppDirName);
249   }
252 // Adds a localized strings file for the Chrome Apps directory using the current
253 // locale. OSX will use this for the display name.
254 // + Chrome Apps.localized (|apps_directory|)
255 // | + .localized
256 // | | en.strings
257 // | | de.strings
258 void UpdateAppShortcutsSubdirLocalizedName(
259     const base::FilePath& apps_directory) {
260   base::FilePath localized = apps_directory.Append(".localized");
261   if (!base::CreateDirectory(localized))
262     return;
264   base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
265   base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
266   NSDictionary* strings_dict = @{
267       base::mac::FilePathToNSString(directory_name) :
268           base::SysUTF16ToNSString(localized_name)
269   };
271   std::string locale = l10n_util::NormalizeLocale(
272       l10n_util::GetApplicationLocale(std::string()));
274   NSString* strings_path = base::mac::FilePathToNSString(
275       localized.Append(locale + ".strings"));
276   [strings_dict writeToFile:strings_path
277                  atomically:YES];
280 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
281   DCHECK(!app_path.empty());
282   base::DeleteFile(app_path, true);
283   base::FilePath apps_folder = app_path.DirName();
284   if (base::IsDirectoryEmpty(apps_folder))
285     base::DeleteFile(apps_folder, false);
288 bool IsShimForProfile(const base::FilePath& base_name,
289                       const std::string& profile_base_name) {
290   if (!StartsWithASCII(base_name.value(), profile_base_name, true))
291     return false;
293   if (base_name.Extension() != ".app")
294     return false;
296   std::string app_id = base_name.RemoveExtension().value();
297   // Strip (profile_base_name + " ") from the start.
298   app_id = app_id.substr(profile_base_name.size() + 1);
299   return extensions::Extension::IdIsValid(app_id);
302 std::vector<base::FilePath> GetAllAppBundlesInPath(
303     const base::FilePath& internal_shortcut_path,
304     const std::string& profile_base_name) {
305   std::vector<base::FilePath> bundle_paths;
307   base::FileEnumerator enumerator(internal_shortcut_path,
308                                   true /* recursive */,
309                                   base::FileEnumerator::DIRECTORIES);
310   for (base::FilePath bundle_path = enumerator.Next();
311        !bundle_path.empty(); bundle_path = enumerator.Next()) {
312     if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
313       bundle_paths.push_back(bundle_path);
314   }
316   return bundle_paths;
319 ShellIntegration::ShortcutInfo BuildShortcutInfoFromBundle(
320     const base::FilePath& bundle_path) {
321   NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
323   ShellIntegration::ShortcutInfo shortcut_info;
324   shortcut_info.extension_id = base::SysNSStringToUTF8(
325       [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
326   shortcut_info.is_platform_app = true;
327   shortcut_info.url = GURL(base::SysNSStringToUTF8(
328       [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
329   shortcut_info.title = base::SysNSStringToUTF16(
330       [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
331   shortcut_info.profile_name = base::SysNSStringToUTF8(
332       [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);
334   // Figure out the profile_path. Since the user_data_dir could contain the
335   // path to the web app data dir.
336   base::FilePath user_data_dir = base::mac::NSStringToFilePath(
337       [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
338   base::FilePath profile_base_name = base::mac::NSStringToFilePath(
339       [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
340   if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
341     shortcut_info.profile_path = user_data_dir.DirName().DirName();
342   else
343     shortcut_info.profile_path = user_data_dir.Append(profile_base_name);
345   return shortcut_info;
348 void CreateShortcutsAndRunCallback(
349     const base::Closure& close_callback,
350     const ShellIntegration::ShortcutInfo& shortcut_info) {
351   // creation_locations will be ignored by CreatePlatformShortcuts on Mac.
352   ShellIntegration::ShortcutLocations creation_locations;
353   web_app::CreateShortcuts(shortcut_info, creation_locations,
354                            web_app::SHORTCUT_CREATION_BY_USER);
355   if (!close_callback.is_null())
356     close_callback.Run();
359 }  // namespace
361 namespace chrome {
363 void ShowCreateChromeAppShortcutsDialog(gfx::NativeWindow /*parent_window*/,
364                                         Profile* profile,
365                                         const extensions::Extension* app,
366                                         const base::Closure& close_callback) {
367   // Normally we would show a dialog, but since we always create the app
368   // shortcut in /Applications there are no options for the user to choose.
369   web_app::UpdateShortcutInfoAndIconForApp(
370       *app, profile,
371       base::Bind(&CreateShortcutsAndRunCallback, close_callback));
374 }  // namespace chrome
376 namespace web_app {
378 WebAppShortcutCreator::WebAppShortcutCreator(
379     const base::FilePath& app_data_dir,
380     const ShellIntegration::ShortcutInfo& shortcut_info)
381     : app_data_dir_(app_data_dir),
382       info_(shortcut_info) {}
384 WebAppShortcutCreator::~WebAppShortcutCreator() {}
386 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
387   base::FilePath applications_dir = GetApplicationsDirname();
388   return applications_dir.empty() ?
389       base::FilePath() : applications_dir.Append(GetShortcutBasename());
392 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
393   return app_data_dir_.Append(GetShortcutBasename());
396 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
397   std::string app_name;
398   // Check if there should be a separate shortcut made for different profiles.
399   // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
400   // otherwise it will be empty.
401   if (!info_.profile_name.empty()) {
402     app_name += info_.profile_path.BaseName().value();
403     app_name += ' ';
404   }
405   app_name += info_.extension_id;
406   return base::FilePath(app_name).ReplaceExtension("app");
409 bool WebAppShortcutCreator::BuildShortcut(
410     const base::FilePath& staging_path) const {
411   // Update the app's plist and icon in a temp directory. This works around
412   // a Finder bug where the app's icon doesn't properly update.
413   if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
414     LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
415                << " failed.";
416     return false;
417   }
419   return UpdatePlist(staging_path) &&
420       UpdateDisplayName(staging_path) &&
421       UpdateIcon(staging_path);
424 size_t WebAppShortcutCreator::CreateShortcutsIn(
425     const std::vector<base::FilePath>& folders) const {
426   size_t succeeded = 0;
428   base::ScopedTempDir scoped_temp_dir;
429   if (!scoped_temp_dir.CreateUniqueTempDir())
430     return 0;
432   base::FilePath app_name = GetShortcutBasename();
433   base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
434   if (!BuildShortcut(staging_path))
435     return 0;
437   for (std::vector<base::FilePath>::const_iterator it = folders.begin();
438        it != folders.end(); ++it) {
439     const base::FilePath& dst_path = *it;
440     if (!base::CreateDirectory(dst_path)) {
441       LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
442       return succeeded;
443     }
445     if (!base::CopyDirectory(staging_path, dst_path, true)) {
446       LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
447                  << " failed";
448       return succeeded;
449     }
451     base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
452     ++succeeded;
453   }
455   return succeeded;
458 bool WebAppShortcutCreator::CreateShortcuts(
459     ShortcutCreationReason creation_reason,
460     ShellIntegration::ShortcutLocations creation_locations) {
461   const base::FilePath applications_dir = GetApplicationsDirname();
462   if (applications_dir.empty() ||
463       !base::DirectoryExists(applications_dir.DirName())) {
464     LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
465     return false;
466   }
468   UpdateAppShortcutsSubdirLocalizedName(applications_dir);
470   // If non-nil, this path is added to the OSX Dock after creating shortcuts.
471   NSString* path_to_add_to_dock = nil;
473   std::vector<base::FilePath> paths;
475   // The app list shim is not tied to a particular profile, so omit the copy
476   // placed under the profile path. For shims, this copy is used when the
477   // version under Applications is removed, and not needed for app list because
478   // setting LSUIElement means there is no Dock "running" status to show.
479   const bool is_app_list = info_.extension_id == app_mode::kAppListModeId;
480   if (is_app_list) {
481     path_to_add_to_dock = base::SysUTF8ToNSString(
482         applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
483   } else {
484     paths.push_back(app_data_dir_);
485   }
486   paths.push_back(applications_dir);
488   size_t success_count = CreateShortcutsIn(paths);
489   if (success_count == 0)
490     return false;
492   if (!is_app_list)
493     UpdateInternalBundleIdentifier();
495   if (success_count != paths.size())
496     return false;
498   if (creation_locations.in_quick_launch_bar && path_to_add_to_dock) {
499     switch (dock::AddIcon(path_to_add_to_dock, nil)) {
500       case dock::IconAddFailure:
501         // If adding the icon failed, instead reveal the Finder window.
502         RevealAppShimInFinder();
503         break;
504       case dock::IconAddSuccess:
505       case dock::IconAlreadyPresent:
506         break;
507     }
508     return true;
509   }
511   if (creation_reason == SHORTCUT_CREATION_BY_USER)
512     RevealAppShimInFinder();
514   return true;
517 void WebAppShortcutCreator::DeleteShortcuts() {
518   base::FilePath app_path = GetApplicationsShortcutPath();
519   if (!app_path.empty() && HasSameUserDataDir(app_path))
520     DeletePathAndParentIfEmpty(app_path);
522   // In case the user has moved/renamed/copied the app bundle.
523   base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
524   if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
525     base::DeleteFile(bundle_path, true);
527   // Delete the internal one.
528   DeletePathAndParentIfEmpty(GetInternalShortcutPath());
531 bool WebAppShortcutCreator::UpdateShortcuts() {
532   std::vector<base::FilePath> paths;
533   base::DeleteFile(GetInternalShortcutPath(), true);
534   paths.push_back(app_data_dir_);
536   // Try to update the copy under /Applications. If that does not exist, check
537   // if a matching bundle can be found elsewhere.
538   base::FilePath app_path = GetApplicationsShortcutPath();
539   if (app_path.empty() || !base::PathExists(app_path))
540     app_path = GetAppBundleById(GetBundleIdentifier());
542   if (!app_path.empty()) {
543     base::DeleteFile(app_path, true);
544     paths.push_back(app_path.DirName());
545   }
547   size_t success_count = CreateShortcutsIn(paths);
548   if (success_count == 0)
549     return false;
551   UpdateInternalBundleIdentifier();
552   return success_count == paths.size() && !app_path.empty();
555 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
556   base::FilePath path = GetWritableApplicationsDirectory();
557   if (path.empty())
558     return path;
560   return path.Append(GetLocalizableAppShortcutsSubdirName());
563 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
564   NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
565   NSString* extension_title = base::SysUTF16ToNSString(info_.title);
566   NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
567   NSString* chrome_bundle_id =
568       base::SysUTF8ToNSString(base::mac::BaseBundleID());
569   NSDictionary* replacement_dict =
570       [NSDictionary dictionaryWithObjectsAndKeys:
571           extension_id, app_mode::kShortcutIdPlaceholder,
572           extension_title, app_mode::kShortcutNamePlaceholder,
573           extension_url, app_mode::kShortcutURLPlaceholder,
574           chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
575           nil];
577   NSString* plist_path = GetPlistPath(app_path);
578   NSMutableDictionary* plist = ReadPlist(plist_path);
579   NSArray* keys = [plist allKeys];
581   // 1. Fill in variables.
582   for (id key in keys) {
583     NSString* value = [plist valueForKey:key];
584     if (![value isKindOfClass:[NSString class]] || [value length] < 2)
585       continue;
587     // Remove leading and trailing '@'s.
588     NSString* variable =
589         [value substringWithRange:NSMakeRange(1, [value length] - 2)];
591     NSString* substitution = [replacement_dict valueForKey:variable];
592     if (substitution)
593       [plist setObject:substitution forKey:key];
594   }
596   // 2. Fill in other values.
597   [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
598             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
599   [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
600             forKey:app_mode::kCrAppModeUserDataDirKey];
601   [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName())
602             forKey:app_mode::kCrAppModeProfileDirKey];
603   [plist setObject:base::SysUTF8ToNSString(info_.profile_name)
604             forKey:app_mode::kCrAppModeProfileNameKey];
605   [plist setObject:[NSNumber numberWithBool:YES]
606             forKey:app_mode::kLSHasLocalizedDisplayNameKey];
607   if (info_.extension_id == app_mode::kAppListModeId) {
608     // Prevent the app list from bouncing in the dock, and getting a run light.
609     [plist setObject:[NSNumber numberWithBool:YES]
610               forKey:kLSUIElement];
611   }
613   base::FilePath app_name = app_path.BaseName().RemoveExtension();
614   [plist setObject:base::mac::FilePathToNSString(app_name)
615             forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
617   return [plist writeToFile:plist_path
618                  atomically:YES];
621 bool WebAppShortcutCreator::UpdateDisplayName(
622     const base::FilePath& app_path) const {
623   // OSX searches for the best language in the order of preferred languages.
624   // Since we only have one localization directory, it will choose this one.
625   base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
626   if (!base::CreateDirectory(localized_dir))
627     return false;
629   NSString* bundle_name = base::SysUTF16ToNSString(info_.title);
630   NSString* display_name = base::SysUTF16ToNSString(info_.title);
631   if (HasExistingExtensionShim(GetApplicationsDirname(),
632                                info_.extension_id,
633                                app_path.BaseName())) {
634     display_name = [bundle_name
635         stringByAppendingString:base::SysUTF8ToNSString(
636             " (" + info_.profile_name + ")")];
637   }
639   NSDictionary* strings_plist = @{
640     base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
641     app_mode::kCFBundleDisplayNameKey : display_name
642   };
644   NSString* localized_path = base::mac::FilePathToNSString(
645       localized_dir.Append("InfoPlist.strings"));
646   return [strings_plist writeToFile:localized_path
647                          atomically:YES];
650 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
651   if (info_.favicon.empty())
652     return true;
654   ScopedCarbonHandle icon_family(0);
655   bool image_added = false;
656   for (gfx::ImageFamily::const_iterator it = info_.favicon.begin();
657        it != info_.favicon.end(); ++it) {
658     if (it->IsEmpty())
659       continue;
661     // Missing an icon size is not fatal so don't fail if adding the bitmap
662     // doesn't work.
663     if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it))
664       continue;
666     image_added = true;
667   }
669   if (!image_added)
670     return false;
672   base::FilePath resources_path = GetResourcesPath(app_path);
673   if (!base::CreateDirectory(resources_path))
674     return false;
676   return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
679 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
680   NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
681   NSMutableDictionary* plist = ReadPlist(plist_path);
683   [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
684             forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
685   return [plist writeToFile:plist_path
686                  atomically:YES];
689 base::FilePath WebAppShortcutCreator::GetAppBundleById(
690     const std::string& bundle_id) const {
691   base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
692       base::SysUTF8ToCFStringRef(bundle_id));
693   CFURLRef url_ref = NULL;
694   OSStatus status = LSFindApplicationForInfo(
695       kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
696   if (status != noErr)
697     return base::FilePath();
699   base::ScopedCFTypeRef<CFURLRef> url(url_ref);
700   NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
701   return base::FilePath([path_string fileSystemRepresentation]);
704 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
705   // Replace spaces in the profile path with hyphen.
706   std::string normalized_profile_path;
707   base::ReplaceChars(info_.profile_path.BaseName().value(),
708                      " ", "-", &normalized_profile_path);
710   // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
711   std::string bundle_id =
712       base::mac::BaseBundleID() + std::string(".app.") +
713       normalized_profile_path + "-" + info_.extension_id;
715   return bundle_id;
718 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
719   return GetBundleIdentifier() + "-internal";
722 void WebAppShortcutCreator::RevealAppShimInFinder() const {
723   base::FilePath app_path = GetApplicationsShortcutPath();
724   if (app_path.empty())
725     return;
727   [[NSWorkspace sharedWorkspace]
728                     selectFile:base::mac::FilePathToNSString(app_path)
729       inFileViewerRootedAtPath:nil];
732 base::FilePath GetAppInstallPath(
733     const ShellIntegration::ShortcutInfo& shortcut_info) {
734   WebAppShortcutCreator shortcut_creator(base::FilePath(), shortcut_info);
735   return shortcut_creator.GetApplicationsShortcutPath();
738 void MaybeLaunchShortcut(const ShellIntegration::ShortcutInfo& shortcut_info) {
739   if (!apps::IsAppShimsEnabled())
740     return;
742   content::BrowserThread::PostTask(
743       content::BrowserThread::FILE, FROM_HERE,
744       base::Bind(&LaunchShimOnFileThread, shortcut_info));
747 namespace internals {
749 bool CreatePlatformShortcuts(
750     const base::FilePath& app_data_path,
751     const ShellIntegration::ShortcutInfo& shortcut_info,
752     const ShellIntegration::ShortcutLocations& creation_locations,
753     ShortcutCreationReason creation_reason) {
754   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
755   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
756   return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
759 void DeletePlatformShortcuts(
760     const base::FilePath& app_data_path,
761     const ShellIntegration::ShortcutInfo& shortcut_info) {
762   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
763   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
764   shortcut_creator.DeleteShortcuts();
767 void UpdatePlatformShortcuts(
768     const base::FilePath& app_data_path,
769     const base::string16& old_app_title,
770     const ShellIntegration::ShortcutInfo& shortcut_info) {
771   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
772   WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info);
773   shortcut_creator.UpdateShortcuts();
776 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
777   const std::string profile_base_name = profile_path.BaseName().value();
778   std::vector<base::FilePath> bundles = GetAllAppBundlesInPath(
779       profile_path.Append(chrome::kWebAppDirname), profile_base_name);
781   for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
782        it != bundles.end(); ++it) {
783     ShellIntegration::ShortcutInfo shortcut_info =
784         BuildShortcutInfoFromBundle(*it);
785     WebAppShortcutCreator shortcut_creator(it->DirName(), shortcut_info);
786     shortcut_creator.DeleteShortcuts();
787   }
790 }  // namespace internals
792 }  // namespace web_app