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