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