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/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"
53
54bool g_app_shims_allow_update_and_launch_in_tests = false;
55
56namespace {
57
58// Launch Services Key to run as an agent app, which doesn't launch in the dock.
59NSString* const kLSUIElement = @"LSUIElement";
60
61class 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_); }
68
69  Handle Get() { return handle_; }
70  char* Data() { return *handle_; }
71  size_t HandleSize() const { return GetHandleSize(handle_); }
72
73  IconFamilyHandle GetAsIconFamilyHandle() {
74    return reinterpret_cast<IconFamilyHandle>(handle_);
75  }
76
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  }
83
84 private:
85  Handle handle_;
86};
87
88void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
89  CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
90
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  }
103}
104
105// Adds |image| to |icon_family|. Returns true on success, false on failure.
106bool 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  }
116
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  }
140
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;
146}
147
148bool 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);
152}
153
154base::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();
160
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();
168}
169
170// Given the path to an app bundle, return the resources directory.
171base::FilePath GetResourcesPath(const base::FilePath& app_path) {
172  return app_path.Append("Contents").Append("Resources");
173}
174
175bool 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  }
191
192  return false;
193}
194
195// Given the path to an app bundle, return the path to the Info.plist file.
196NSString* GetPlistPath(const base::FilePath& bundle_path) {
197  return base::mac::FilePathToNSString(
198      bundle_path.Append("Contents").Append("Info.plist"));
199}
200
201NSMutableDictionary* ReadPlist(NSString* plist_path) {
202  return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
203}
204
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_|.
209bool 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 */);
219}
220
221void 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);
225
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  }
235
236  if (!base::PathExists(shim_path))
237    return;
238
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);
248}
249
250base::FilePath GetAppLoaderPath() {
251  return base::mac::PathForFrameworkBundleResource(
252      base::mac::NSToCFCast(@"app_mode_loader.app"));
253}
254
255void 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);
263}
264
265void 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));
273}
274
275void 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  }
284
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;
290
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;
297
298  web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim));
299}
300
301base::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";
305
306  switch (chrome::VersionInfo::GetChannel()) {
307    case chrome::VersionInfo::CHANNEL_UNKNOWN:
308      return base::FilePath(kChromiumAppDirName);
309
310    case chrome::VersionInfo::CHANNEL_CANARY:
311      return base::FilePath(kChromeCanaryAppDirName);
312
313    default:
314      return base::FilePath(kChromeAppDirName);
315  }
316}
317
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.
321NSImageRep* 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]);
336
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]);
341
342  // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
343  [canvas setSize:NSMakeSize(dimension, dimension)];
344
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();
361}
362
363// Helper function to extract the single NSImageRep held in a resource bundle
364// image.
365NSImageRep* 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];
371}
372
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
379void UpdateAppShortcutsSubdirLocalizedName(
380    const base::FilePath& apps_directory) {
381  base::FilePath localized = apps_directory.Append(".localized");
382  if (!base::CreateDirectory(localized))
383    return;
384
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  };
391
392  std::string locale = l10n_util::NormalizeLocale(
393      l10n_util::GetApplicationLocale(std::string()));
394
395  NSString* strings_path = base::mac::FilePathToNSString(
396      localized.Append(locale + ".strings"));
397  [strings_dict writeToFile:strings_path
398                 atomically:YES];
399
400  base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);
401
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)];
409
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];
429}
430
431void 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);
437}
438
439bool 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;
443
444  if (base_name.Extension() != ".app")
445    return false;
446
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);
451}
452
453std::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;
457
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  }
466
467  return bundle_paths;
468}
469
470web_app::ShortcutInfo BuildShortcutInfoFromBundle(
471    const base::FilePath& bundle_path) {
472  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
473
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]);
484
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);
495
496  return shortcut_info;
497}
498
499web_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);
508
509  return BuildShortcutInfoFromBundle(bundle_path);
510}
511
512void UpdateFileTypes(NSMutableDictionary* plist,
513                     const extensions::FileHandlersInfo& file_handlers_info) {
514  NSMutableArray* document_types =
515      [NSMutableArray arrayWithCapacity:file_handlers_info.size()];
516
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;
522
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    }
530
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    }
538
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  }
550
551  [plist setObject:document_types
552            forKey:app_mode::kCFBundleDocumentTypesKey];
553}
554
555}  // namespace
556
557@interface CrCreateAppShortcutCheckboxObserver : NSObject {
558 @private
559  NSButton* checkbox_;
560  NSButton* continueButton_;
561}
562
563- (id)initWithCheckbox:(NSButton*)checkbox
564        continueButton:(NSButton*)continueButton;
565- (void)startObserving;
566- (void)stopObserving;
567@end
568
569@implementation CrCreateAppShortcutCheckboxObserver
570
571- (id)initWithCheckbox:(NSButton*)checkbox
572        continueButton:(NSButton*)continueButton {
573  if ((self = [super init])) {
574    checkbox_ = checkbox;
575    continueButton_ = continueButton;
576  }
577  return self;
578}
579
580- (void)startObserving {
581  [checkbox_ addObserver:self
582              forKeyPath:@"cell.state"
583                 options:0
584                 context:nil];
585}
586
587- (void)stopObserving {
588  [checkbox_ removeObserver:self
589                 forKeyPath:@"cell.state"];
590}
591
592- (void)observeValueForKeyPath:(NSString*)keyPath
593                      ofObject:(id)object
594                        change:(NSDictionary*)change
595                       context:(void*)context {
596  [continueButton_ setEnabled:([checkbox_ state] == NSOnState)];
597}
598
599@end
600
601namespace web_app {
602
603WebAppShortcutCreator::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) {}
610
611WebAppShortcutCreator::~WebAppShortcutCreator() {}
612
613base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
614  base::FilePath applications_dir = GetApplicationsDirname();
615  return applications_dir.empty() ?
616      base::FilePath() : applications_dir.Append(GetShortcutBasename());
617}
618
619base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
620  return app_data_dir_.Append(GetShortcutBasename());
621}
622
623base::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");
634}
635
636bool 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  }
645
646  return UpdatePlist(staging_path) &&
647      UpdateDisplayName(staging_path) &&
648      UpdateIcon(staging_path);
649}
650
651size_t WebAppShortcutCreator::CreateShortcutsIn(
652    const std::vector<base::FilePath>& folders) const {
653  size_t succeeded = 0;
654
655  base::ScopedTempDir scoped_temp_dir;
656  if (!scoped_temp_dir.CreateUniqueTempDir())
657    return 0;
658
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;
663
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    }
671
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    }
677
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  }
685
686  return succeeded;
687}
688
689bool 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  }
698
699  UpdateAppShortcutsSubdirLocalizedName(applications_dir);
700
701  // If non-nil, this path is added to the OSX Dock after creating shortcuts.
702  NSString* path_to_add_to_dock = nil;
703
704  std::vector<base::FilePath> paths;
705
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  }
717
718  bool shortcut_visible =
719      creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN;
720  if (shortcut_visible)
721    paths.push_back(applications_dir);
722
723  DCHECK(!paths.empty());
724  size_t success_count = CreateShortcutsIn(paths);
725  if (success_count == 0)
726    return false;
727
728  if (!is_app_list)
729    UpdateInternalBundleIdentifier();
730
731  if (success_count != paths.size())
732    return false;
733
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  }
747
748  if (creation_reason == SHORTCUT_CREATION_BY_USER)
749    RevealAppShimInFinder();
750
751  return true;
752}
753
754void WebAppShortcutCreator::DeleteShortcuts() {
755  base::FilePath app_path = GetApplicationsShortcutPath();
756  if (!app_path.empty() && HasSameUserDataDir(app_path))
757    DeletePathAndParentIfEmpty(app_path);
758
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);
763
764  // Delete the internal one.
765  DeletePathAndParentIfEmpty(GetInternalShortcutPath());
766}
767
768bool WebAppShortcutCreator::UpdateShortcuts() {
769  std::vector<base::FilePath> paths;
770  base::DeleteFile(GetInternalShortcutPath(), true);
771  paths.push_back(app_data_dir_);
772
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());
778
779  if (!app_path.empty()) {
780    base::DeleteFile(app_path, true);
781    paths.push_back(app_path.DirName());
782  }
783
784  size_t success_count = CreateShortcutsIn(paths);
785  if (success_count == 0)
786    return false;
787
788  UpdateInternalBundleIdentifier();
789  return success_count == paths.size() && !app_path.empty();
790}
791
792base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
793  base::FilePath path = GetWritableApplicationsDirectory();
794  if (path.empty())
795    return path;
796
797  return path.Append(GetLocalizableAppShortcutsSubdirName());
798}
799
800bool 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];
813
814  NSString* plist_path = GetPlistPath(app_path);
815  NSMutableDictionary* plist = ReadPlist(plist_path);
816  NSArray* keys = [plist allKeys];
817
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;
823
824    // Remove leading and trailing '@'s.
825    NSString* variable =
826        [value substringWithRange:NSMakeRange(1, [value length] - 2)];
827
828    NSString* substitution = [replacement_dict valueForKey:variable];
829    if (substitution)
830      [plist setObject:substitution forKey:key];
831  }
832
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  }
849
850  base::FilePath app_name = app_path.BaseName().RemoveExtension();
851  [plist setObject:base::mac::FilePathToNSString(app_name)
852            forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
853
854  if (CommandLine::ForCurrentProcess()->HasSwitch(
855          switches::kEnableAppsFileAssociations)) {
856    UpdateFileTypes(plist, file_handlers_info_);
857  }
858
859  return [plist writeToFile:plist_path
860                 atomically:YES];
861}
862
863bool 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;
870
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  }
880
881  NSDictionary* strings_plist = @{
882    base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
883    app_mode::kCFBundleDisplayNameKey : display_name
884  };
885
886  NSString* localized_path = base::mac::FilePathToNSString(
887      localized_dir.Append("InfoPlist.strings"));
888  return [strings_plist writeToFile:localized_path
889                         atomically:YES];
890}
891
892bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
893  if (info_.favicon.empty())
894    return true;
895
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;
902
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;
907
908    image_added = true;
909  }
910
911  if (!image_added)
912    return false;
913
914  base::FilePath resources_path = GetResourcesPath(app_path);
915  if (!base::CreateDirectory(resources_path))
916    return false;
917
918  return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
919}
920
921bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
922  NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
923  NSMutableDictionary* plist = ReadPlist(plist_path);
924
925  [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
926            forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
927  return [plist writeToFile:plist_path
928                 atomically:YES];
929}
930
931base::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();
940
941  base::ScopedCFTypeRef<CFURLRef> url(url_ref);
942  NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
943  return base::FilePath([path_string fileSystemRepresentation]);
944}
945
946std::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);
951
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;
956
957  return bundle_id;
958}
959
960std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
961  return GetBundleIdentifier() + "-internal";
962}
963
964void WebAppShortcutCreator::RevealAppShimInFinder() const {
965  base::FilePath app_path = GetApplicationsShortcutPath();
966  if (app_path.empty())
967    return;
968
969  [[NSWorkspace sharedWorkspace]
970                    selectFile:base::mac::FilePathToNSString(app_path)
971      inFileViewerRootedAtPath:nil];
972}
973
974base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) {
975  WebAppShortcutCreator shortcut_creator(
976      base::FilePath(), shortcut_info, extensions::FileHandlersInfo());
977  return shortcut_creator.GetApplicationsShortcutPath();
978}
979
980void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) {
981  if (AppShimsDisabledForTest() &&
982      !g_app_shims_allow_update_and_launch_in_tests) {
983    return;
984  }
985
986  content::BrowserThread::PostTask(
987      content::BrowserThread::FILE,
988      FROM_HERE,
989      base::Bind(&LaunchShimOnFileThread, shortcut_info, false));
990}
991
992bool MaybeRebuildShortcut(const CommandLine& command_line) {
993  if (!command_line.HasSwitch(app_mode::kAppShimError))
994    return false;
995
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;
1003}
1004
1005// Called when the app's ShortcutInfo (with icon) is loaded when creating app
1006// shortcuts.
1007void 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]);
1013
1014  NSButton* continue_button = [alert
1015      addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)];
1016  [continue_button setKeyEquivalent:@""];
1017
1018  NSButton* cancel_button =
1019      [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)];
1020  [cancel_button setKeyEquivalent:@"\r"];
1021
1022  [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)];
1023  [alert setAlertStyle:NSInformationalAlertStyle];
1024
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];
1032
1033  base::scoped_nsobject<CrCreateAppShortcutCheckboxObserver> checkbox_observer(
1034      [[CrCreateAppShortcutCheckboxObserver alloc]
1035          initWithCheckbox:application_folder_checkbox
1036            continueButton:continue_button]);
1037  [checkbox_observer startObserving];
1038
1039  [alert setAccessoryView:application_folder_checkbox];
1040
1041  const int kIconPreviewSizePixels = 128;
1042  const int kIconPreviewTargetSize = 64;
1043  const gfx::Image* icon = shortcut_info.favicon.GetBest(
1044      kIconPreviewSizePixels, kIconPreviewSizePixels);
1045
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  }
1052
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  }
1060
1061  [checkbox_observer stopObserving];
1062
1063  if (!close_callback.is_null())
1064    close_callback.Run(dialog_accepted);
1065}
1066
1067void UpdateShortcutsForAllApps(Profile* profile,
1068                               const base::Closure& callback) {
1069  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1070
1071  extensions::ExtensionRegistry* registry =
1072      extensions::ExtensionRegistry::Get(profile);
1073  if (!registry)
1074    return;
1075
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  }
1084
1085  callback.Run();
1086}
1087
1088namespace internals {
1089
1090bool 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;
1099
1100  WebAppShortcutCreator shortcut_creator(
1101      app_data_path, shortcut_info, file_handlers_info);
1102  return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
1103}
1104
1105void 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();
1111}
1112
1113void 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  }
1123
1124  WebAppShortcutCreator shortcut_creator(
1125      app_data_path, shortcut_info, file_handlers_info);
1126  shortcut_creator.UpdateShortcuts();
1127}
1128
1129void 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);
1133
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  }
1142}
1143
1144}  // namespace internals
1145
1146}  // namespace web_app
1147
1148namespace chrome {
1149
1150void 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));
1162}
1163
1164}  // namespace chrome
1165