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