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/browser/ui/cocoa/key_equivalent_constants.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 "chrome/grit/generated_resources.h"
42 #include "components/crx_file/id_util.h"
43 #include "content/public/browser/browser_thread.h"
44 #include "extensions/browser/extension_registry.h"
45 #include "extensions/common/extension.h"
46 #include "grit/chrome_unscaled_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"
55 bool g_app_shims_allow_update_and_launch_in_tests = false;
59 // Launch Services Key to run as an agent app, which doesn't launch in the dock.
60 NSString* const kLSUIElement = @"LSUIElement";
62 class ScopedCarbonHandle {
64 ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
66 DCHECK_EQ(noErr, MemError());
68 ~ScopedCarbonHandle() { DisposeHandle(handle_); }
70 Handle Get() { return handle_; }
71 char* Data() { return *handle_; }
72 size_t HandleSize() const { return GetHandleSize(handle_); }
74 IconFamilyHandle GetAsIconFamilyHandle() {
75 return reinterpret_cast<IconFamilyHandle>(handle_);
78 bool WriteDataToFile(const base::FilePath& path) {
79 NSData* data = [NSData dataWithBytes:Data()
81 return [data writeToFile:base::mac::FilePathToNSString(path)
89 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
90 CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
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);
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()) {
119 switch (bitmap.width()) {
121 icon_type = kIconServices512PixelDataARGB;
124 icon_type = kIconServices256PixelDataARGB;
127 icon_type = kIconServices128PixelDataARGB;
130 icon_type = kIconServices48PixelDataARGB;
133 icon_type = kIconServices32PixelDataARGB;
136 icon_type = kIconServices16PixelDataARGB;
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;
149 bool AppShimsDisabledForTest() {
150 // Disable app shims in tests because shims created in ~/Applications will not
152 return base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType);
155 base::FilePath GetWritableApplicationsDirectory() {
157 if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
158 if (!base::DirectoryExists(path)) {
159 if (!base::CreateDirectory(path))
160 return base::FilePath();
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);
166 return base::PathIsWritable(path) ? path : base::FilePath();
168 return base::FilePath();
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");
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(),
188 true /* case_sensitive */)) {
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"));
202 NSMutableDictionary* ReadPlist(NSString* plist_path) {
203 return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
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 */);
222 void LaunchShimOnFileThread(scoped_ptr<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);
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());
237 if (!base::PathExists(shim_path))
240 base::CommandLine command_line(base::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);
251 base::FilePath GetAppLoaderPath() {
252 return base::mac::PathForFrameworkBundleResource(
253 base::mac::NSToCFCast(@"app_mode_loader.app"));
256 void UpdatePlatformShortcutsInternal(
257 const base::FilePath& app_data_path,
258 const base::string16& old_app_title,
259 const web_app::ShortcutInfo& shortcut_info,
260 const extensions::FileHandlersInfo& file_handlers_info) {
261 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
262 if (AppShimsDisabledForTest() &&
263 !g_app_shims_allow_update_and_launch_in_tests) {
267 web_app::WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info,
269 shortcut_creator.UpdateShortcuts();
272 void UpdateAndLaunchShimOnFileThread(
273 scoped_ptr<web_app::ShortcutInfo> shortcut_info,
274 const extensions::FileHandlersInfo& file_handlers_info) {
275 base::FilePath shortcut_data_dir = web_app::GetWebAppDataDirectory(
276 shortcut_info->profile_path, shortcut_info->extension_id, GURL());
277 UpdatePlatformShortcutsInternal(shortcut_data_dir, base::string16(),
278 *shortcut_info, file_handlers_info);
279 LaunchShimOnFileThread(shortcut_info.Pass(), true);
282 void UpdateAndLaunchShim(
283 scoped_ptr<web_app::ShortcutInfo> shortcut_info,
284 const extensions::FileHandlersInfo& file_handlers_info) {
285 content::BrowserThread::PostTask(
286 content::BrowserThread::FILE, FROM_HERE,
287 base::Bind(&UpdateAndLaunchShimOnFileThread, base::Passed(&shortcut_info),
288 file_handlers_info));
291 void RebuildAppAndLaunch(scoped_ptr<web_app::ShortcutInfo> shortcut_info) {
292 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
293 if (shortcut_info->extension_id == app_mode::kAppListModeId) {
294 AppListService* app_list_service =
295 AppListService::Get(chrome::HOST_DESKTOP_TYPE_NATIVE);
296 app_list_service->CreateShortcut();
297 app_list_service->Show();
301 ProfileManager* profile_manager = g_browser_process->profile_manager();
303 profile_manager->GetProfileByPath(shortcut_info->profile_path);
304 if (!profile || !profile_manager->IsValidProfile(profile))
307 extensions::ExtensionRegistry* registry =
308 extensions::ExtensionRegistry::Get(profile);
309 const extensions::Extension* extension = registry->GetExtensionById(
310 shortcut_info->extension_id, extensions::ExtensionRegistry::ENABLED);
311 if (!extension || !extension->is_platform_app())
314 web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim));
317 base::FilePath GetLocalizableAppShortcutsSubdirName() {
318 static const char kChromiumAppDirName[] = "Chromium Apps.localized";
319 static const char kChromeAppDirName[] = "Chrome Apps.localized";
320 static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";
322 switch (chrome::VersionInfo::GetChannel()) {
323 case chrome::VersionInfo::CHANNEL_UNKNOWN:
324 return base::FilePath(kChromiumAppDirName);
326 case chrome::VersionInfo::CHANNEL_CANARY:
327 return base::FilePath(kChromeCanaryAppDirName);
330 return base::FilePath(kChromeAppDirName);
334 // Creates a canvas the same size as |overlay|, copies the appropriate
335 // representation from |backgound| into it (according to Cocoa), then draws
336 // |overlay| over it using NSCompositeSourceOver.
337 NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
339 NSInteger dimension = [overlay pixelsWide];
340 DCHECK_EQ(dimension, [overlay pixelsHigh]);
341 base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
342 initWithBitmapDataPlanes:NULL
349 colorSpaceName:NSCalibratedRGBColorSpace
353 // There isn't a colorspace name constant for sRGB, so retag.
354 NSBitmapImageRep* srgb_canvas = [canvas
355 bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
356 canvas.reset([srgb_canvas retain]);
358 // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
359 [canvas setSize:NSMakeSize(dimension, dimension)];
361 NSGraphicsContext* drawing_context =
362 [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
363 [NSGraphicsContext saveGraphicsState];
364 [NSGraphicsContext setCurrentContext:drawing_context];
365 [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
367 operation:NSCompositeCopy
369 [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
371 operation:NSCompositeSourceOver
375 [NSGraphicsContext restoreGraphicsState];
376 return canvas.autorelease();
379 // Helper function to extract the single NSImageRep held in a resource bundle
381 NSImageRep* ImageRepForResource(int resource_id) {
383 ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
384 NSArray* image_reps = [image.AsNSImage() representations];
385 DCHECK_EQ(1u, [image_reps count]);
386 return [image_reps objectAtIndex:0];
389 // Adds a localized strings file for the Chrome Apps directory using the current
390 // locale. OSX will use this for the display name.
391 // + Chrome Apps.localized (|apps_directory|)
395 void UpdateAppShortcutsSubdirLocalizedName(
396 const base::FilePath& apps_directory) {
397 base::FilePath localized = apps_directory.Append(".localized");
398 if (!base::CreateDirectory(localized))
401 base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
402 base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
403 NSDictionary* strings_dict = @{
404 base::mac::FilePathToNSString(directory_name) :
405 base::SysUTF16ToNSString(localized_name)
408 std::string locale = l10n_util::NormalizeLocale(
409 l10n_util::GetApplicationLocale(std::string()));
411 NSString* strings_path = base::mac::FilePathToNSString(
412 localized.Append(locale + ".strings"));
413 [strings_dict writeToFile:strings_path
416 base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);
418 // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a
419 // bug when dealing with named NSImages where it incorrectly handles alpha
420 // premultiplication. This is most noticable with small assets since the 1px
421 // border is a much larger component of the small icons.
422 // See http://crbug.com/305373 for details.
423 [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)];
424 [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)];
426 // Brand larger folder assets with an embossed app launcher logo to conserve
427 // distro size and for better consistency with changing hue across OSX
428 // versions. The folder is textured, so compresses poorly without this.
429 const int kBrandResourceIds[] = {
430 IDR_APPS_FOLDER_OVERLAY_128,
431 IDR_APPS_FOLDER_OVERLAY_512,
433 NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
434 for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
435 NSImageRep* with_overlay =
436 OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i]));
437 DCHECK(with_overlay);
439 [folder_icon_image addRepresentation:with_overlay];
441 [[NSWorkspace sharedWorkspace]
442 setIcon:folder_icon_image
443 forFile:base::mac::FilePathToNSString(apps_directory)
447 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
448 DCHECK(!app_path.empty());
449 base::DeleteFile(app_path, true);
450 base::FilePath apps_folder = app_path.DirName();
451 if (base::IsDirectoryEmpty(apps_folder))
452 base::DeleteFile(apps_folder, false);
455 bool IsShimForProfile(const base::FilePath& base_name,
456 const std::string& profile_base_name) {
457 if (!StartsWithASCII(base_name.value(), profile_base_name, true))
460 if (base_name.Extension() != ".app")
463 std::string app_id = base_name.RemoveExtension().value();
464 // Strip (profile_base_name + " ") from the start.
465 app_id = app_id.substr(profile_base_name.size() + 1);
466 return crx_file::id_util::IdIsValid(app_id);
469 std::vector<base::FilePath> GetAllAppBundlesInPath(
470 const base::FilePath& internal_shortcut_path,
471 const std::string& profile_base_name) {
472 std::vector<base::FilePath> bundle_paths;
474 base::FileEnumerator enumerator(internal_shortcut_path,
475 true /* recursive */,
476 base::FileEnumerator::DIRECTORIES);
477 for (base::FilePath bundle_path = enumerator.Next();
478 !bundle_path.empty(); bundle_path = enumerator.Next()) {
479 if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
480 bundle_paths.push_back(bundle_path);
486 scoped_ptr<web_app::ShortcutInfo> BuildShortcutInfoFromBundle(
487 const base::FilePath& bundle_path) {
488 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
490 scoped_ptr<web_app::ShortcutInfo> shortcut_info(new web_app::ShortcutInfo);
491 shortcut_info->extension_id = base::SysNSStringToUTF8(
492 [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
493 shortcut_info->is_platform_app = true;
494 shortcut_info->url = GURL(base::SysNSStringToUTF8(
495 [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
496 shortcut_info->title = base::SysNSStringToUTF16(
497 [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
498 shortcut_info->profile_name = base::SysNSStringToUTF8(
499 [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);
501 // Figure out the profile_path. Since the user_data_dir could contain the
502 // path to the web app data dir.
503 base::FilePath user_data_dir = base::mac::NSStringToFilePath(
504 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
505 base::FilePath profile_base_name = base::mac::NSStringToFilePath(
506 [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
507 if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
508 shortcut_info->profile_path = user_data_dir.DirName().DirName();
510 shortcut_info->profile_path = user_data_dir.Append(profile_base_name);
512 return shortcut_info;
515 scoped_ptr<web_app::ShortcutInfo> RecordAppShimErrorAndBuildShortcutInfo(
516 const base::FilePath& bundle_path) {
517 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
518 NSString* version_string = [plist valueForKey:app_mode::kCrBundleVersionKey];
519 if (!version_string) {
520 // Older bundles have the Chrome version in the following key.
522 [plist valueForKey:app_mode::kCFBundleShortVersionStringKey];
524 base::Version full_version(base::SysNSStringToUTF8(version_string));
525 uint32_t major_version = 0;
526 if (full_version.IsValid())
527 major_version = full_version.components()[0];
528 UMA_HISTOGRAM_SPARSE_SLOWLY("Apps.AppShimErrorVersion", major_version);
530 return BuildShortcutInfoFromBundle(bundle_path);
533 void UpdateFileTypes(NSMutableDictionary* plist,
534 const extensions::FileHandlersInfo& file_handlers_info) {
535 NSMutableArray* document_types =
536 [NSMutableArray arrayWithCapacity:file_handlers_info.size()];
538 for (extensions::FileHandlersInfo::const_iterator info_it =
539 file_handlers_info.begin();
540 info_it != file_handlers_info.end();
542 const extensions::FileHandlerInfo& info = *info_it;
544 NSMutableArray* file_extensions =
545 [NSMutableArray arrayWithCapacity:info.extensions.size()];
546 for (std::set<std::string>::iterator it = info.extensions.begin();
547 it != info.extensions.end();
549 [file_extensions addObject:base::SysUTF8ToNSString(*it)];
552 NSMutableArray* mime_types =
553 [NSMutableArray arrayWithCapacity:info.types.size()];
554 for (std::set<std::string>::iterator it = info.types.begin();
555 it != info.types.end();
557 [mime_types addObject:base::SysUTF8ToNSString(*it)];
560 NSDictionary* type_dictionary = @{
561 // TODO(jackhou): Add the type name and and icon file once the manifest
563 // app_mode::kCFBundleTypeNameKey : ,
564 // app_mode::kCFBundleTypeIconFileKey : ,
565 app_mode::kCFBundleTypeExtensionsKey : file_extensions,
566 app_mode::kCFBundleTypeMIMETypesKey : mime_types,
567 app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer
569 [document_types addObject:type_dictionary];
572 [plist setObject:document_types
573 forKey:app_mode::kCFBundleDocumentTypesKey];
576 void RevealAppShimInFinderForAppOnFileThread(
577 scoped_ptr<web_app::ShortcutInfo> shortcut_info,
578 const base::FilePath& app_path) {
579 web_app::WebAppShortcutCreator shortcut_creator(
580 app_path, shortcut_info.get(), extensions::FileHandlersInfo());
581 shortcut_creator.RevealAppShimInFinder();
586 @interface CrCreateAppShortcutCheckboxObserver : NSObject {
589 NSButton* continueButton_;
592 - (id)initWithCheckbox:(NSButton*)checkbox
593 continueButton:(NSButton*)continueButton;
594 - (void)startObserving;
595 - (void)stopObserving;
598 @implementation CrCreateAppShortcutCheckboxObserver
600 - (id)initWithCheckbox:(NSButton*)checkbox
601 continueButton:(NSButton*)continueButton {
602 if ((self = [super init])) {
603 checkbox_ = checkbox;
604 continueButton_ = continueButton;
609 - (void)startObserving {
610 [checkbox_ addObserver:self
611 forKeyPath:@"cell.state"
616 - (void)stopObserving {
617 [checkbox_ removeObserver:self
618 forKeyPath:@"cell.state"];
621 - (void)observeValueForKeyPath:(NSString*)keyPath
623 change:(NSDictionary*)change
624 context:(void*)context {
625 [continueButton_ setEnabled:([checkbox_ state] == NSOnState)];
632 WebAppShortcutCreator::WebAppShortcutCreator(
633 const base::FilePath& app_data_dir,
634 const ShortcutInfo* shortcut_info,
635 const extensions::FileHandlersInfo& file_handlers_info)
636 : app_data_dir_(app_data_dir),
637 info_(shortcut_info),
638 file_handlers_info_(file_handlers_info) {
639 DCHECK(shortcut_info);
642 WebAppShortcutCreator::~WebAppShortcutCreator() {}
644 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
645 base::FilePath applications_dir = GetApplicationsDirname();
646 return applications_dir.empty() ?
647 base::FilePath() : applications_dir.Append(GetShortcutBasename());
650 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
651 return app_data_dir_.Append(GetShortcutBasename());
654 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
655 std::string app_name;
656 // Check if there should be a separate shortcut made for different profiles.
657 // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
658 // otherwise it will be empty.
659 if (!info_->profile_name.empty()) {
660 app_name += info_->profile_path.BaseName().value();
663 app_name += info_->extension_id;
664 return base::FilePath(app_name).ReplaceExtension("app");
667 bool WebAppShortcutCreator::BuildShortcut(
668 const base::FilePath& staging_path) const {
669 // Update the app's plist and icon in a temp directory. This works around
670 // a Finder bug where the app's icon doesn't properly update.
671 if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
672 LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
677 return UpdatePlist(staging_path) &&
678 UpdateDisplayName(staging_path) &&
679 UpdateIcon(staging_path);
682 size_t WebAppShortcutCreator::CreateShortcutsIn(
683 const std::vector<base::FilePath>& folders) const {
684 size_t succeeded = 0;
686 base::ScopedTempDir scoped_temp_dir;
687 if (!scoped_temp_dir.CreateUniqueTempDir())
690 base::FilePath app_name = GetShortcutBasename();
691 base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
692 if (!BuildShortcut(staging_path))
695 for (std::vector<base::FilePath>::const_iterator it = folders.begin();
696 it != folders.end(); ++it) {
697 const base::FilePath& dst_path = *it;
698 if (!base::CreateDirectory(dst_path)) {
699 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
703 if (!base::CopyDirectory(staging_path, dst_path, true)) {
704 LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
709 // Remove the quarantine attribute from both the bundle and the executable.
710 base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
711 base::mac::RemoveQuarantineAttribute(
712 dst_path.Append(app_name)
713 .Append("Contents").Append("MacOS").Append("app_mode_loader"));
720 bool WebAppShortcutCreator::CreateShortcuts(
721 ShortcutCreationReason creation_reason,
722 ShortcutLocations creation_locations) {
723 const base::FilePath applications_dir = GetApplicationsDirname();
724 if (applications_dir.empty() ||
725 !base::DirectoryExists(applications_dir.DirName())) {
726 LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
730 UpdateAppShortcutsSubdirLocalizedName(applications_dir);
732 // If non-nil, this path is added to the OSX Dock after creating shortcuts.
733 NSString* path_to_add_to_dock = nil;
735 std::vector<base::FilePath> paths;
737 // The app list shim is not tied to a particular profile, so omit the copy
738 // placed under the profile path. For shims, this copy is used when the
739 // version under Applications is removed, and not needed for app list because
740 // setting LSUIElement means there is no Dock "running" status to show.
741 const bool is_app_list = info_->extension_id == app_mode::kAppListModeId;
743 path_to_add_to_dock = base::SysUTF8ToNSString(
744 applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
746 paths.push_back(app_data_dir_);
749 bool shortcut_visible =
750 creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN;
751 if (shortcut_visible)
752 paths.push_back(applications_dir);
754 DCHECK(!paths.empty());
755 size_t success_count = CreateShortcutsIn(paths);
756 if (success_count == 0)
760 UpdateInternalBundleIdentifier();
762 if (success_count != paths.size())
765 if (creation_locations.in_quick_launch_bar && path_to_add_to_dock &&
767 switch (dock::AddIcon(path_to_add_to_dock, nil)) {
768 case dock::IconAddFailure:
769 // If adding the icon failed, instead reveal the Finder window.
770 RevealAppShimInFinder();
772 case dock::IconAddSuccess:
773 case dock::IconAlreadyPresent:
779 if (creation_reason == SHORTCUT_CREATION_BY_USER)
780 RevealAppShimInFinder();
785 void WebAppShortcutCreator::DeleteShortcuts() {
786 base::FilePath app_path = GetApplicationsShortcutPath();
787 if (!app_path.empty() && HasSameUserDataDir(app_path))
788 DeletePathAndParentIfEmpty(app_path);
790 // In case the user has moved/renamed/copied the app bundle.
791 base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
792 if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
793 base::DeleteFile(bundle_path, true);
795 // Delete the internal one.
796 DeletePathAndParentIfEmpty(GetInternalShortcutPath());
799 bool WebAppShortcutCreator::UpdateShortcuts() {
800 std::vector<base::FilePath> paths;
801 base::DeleteFile(GetInternalShortcutPath(), true);
802 paths.push_back(app_data_dir_);
804 // Try to update the copy under /Applications. If that does not exist, check
805 // if a matching bundle can be found elsewhere.
806 base::FilePath app_path = GetApplicationsShortcutPath();
807 if (app_path.empty() || !base::PathExists(app_path))
808 app_path = GetAppBundleById(GetBundleIdentifier());
810 if (!app_path.empty()) {
811 base::DeleteFile(app_path, true);
812 paths.push_back(app_path.DirName());
815 size_t success_count = CreateShortcutsIn(paths);
816 if (success_count == 0)
819 UpdateInternalBundleIdentifier();
820 return success_count == paths.size() && !app_path.empty();
823 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
824 base::FilePath path = GetWritableApplicationsDirectory();
828 return path.Append(GetLocalizableAppShortcutsSubdirName());
831 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
832 NSString* extension_id = base::SysUTF8ToNSString(info_->extension_id);
833 NSString* extension_title = base::SysUTF16ToNSString(info_->title);
834 NSString* extension_url = base::SysUTF8ToNSString(info_->url.spec());
835 NSString* chrome_bundle_id =
836 base::SysUTF8ToNSString(base::mac::BaseBundleID());
837 NSDictionary* replacement_dict =
838 [NSDictionary dictionaryWithObjectsAndKeys:
839 extension_id, app_mode::kShortcutIdPlaceholder,
840 extension_title, app_mode::kShortcutNamePlaceholder,
841 extension_url, app_mode::kShortcutURLPlaceholder,
842 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
845 NSString* plist_path = GetPlistPath(app_path);
846 NSMutableDictionary* plist = ReadPlist(plist_path);
847 NSArray* keys = [plist allKeys];
849 // 1. Fill in variables.
850 for (id key in keys) {
851 NSString* value = [plist valueForKey:key];
852 if (![value isKindOfClass:[NSString class]] || [value length] < 2)
855 // Remove leading and trailing '@'s.
857 [value substringWithRange:NSMakeRange(1, [value length] - 2)];
859 NSString* substitution = [replacement_dict valueForKey:variable];
861 [plist setObject:substitution forKey:key];
864 // 2. Fill in other values.
865 [plist setObject:base::SysUTF8ToNSString(chrome::VersionInfo().Version())
866 forKey:app_mode::kCrBundleVersionKey];
867 [plist setObject:base::SysUTF8ToNSString(info_->version_for_display)
868 forKey:app_mode::kCFBundleShortVersionStringKey];
869 [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
870 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
871 [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
872 forKey:app_mode::kCrAppModeUserDataDirKey];
873 [plist setObject:base::mac::FilePathToNSString(info_->profile_path.BaseName())
874 forKey:app_mode::kCrAppModeProfileDirKey];
875 [plist setObject:base::SysUTF8ToNSString(info_->profile_name)
876 forKey:app_mode::kCrAppModeProfileNameKey];
877 [plist setObject:[NSNumber numberWithBool:YES]
878 forKey:app_mode::kLSHasLocalizedDisplayNameKey];
879 if (info_->extension_id == app_mode::kAppListModeId) {
880 // Prevent the app list from bouncing in the dock, and getting a run light.
881 [plist setObject:[NSNumber numberWithBool:YES]
882 forKey:kLSUIElement];
885 base::FilePath app_name = app_path.BaseName().RemoveExtension();
886 [plist setObject:base::mac::FilePathToNSString(app_name)
887 forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
889 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
890 switches::kEnableAppsFileAssociations)) {
891 UpdateFileTypes(plist, file_handlers_info_);
894 return [plist writeToFile:plist_path
898 bool WebAppShortcutCreator::UpdateDisplayName(
899 const base::FilePath& app_path) const {
900 // OSX searches for the best language in the order of preferred languages.
901 // Since we only have one localization directory, it will choose this one.
902 base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
903 if (!base::CreateDirectory(localized_dir))
906 NSString* bundle_name = base::SysUTF16ToNSString(info_->title);
907 NSString* display_name = base::SysUTF16ToNSString(info_->title);
908 if (HasExistingExtensionShim(GetApplicationsDirname(), info_->extension_id,
909 app_path.BaseName())) {
910 display_name = [bundle_name
911 stringByAppendingString:base::SysUTF8ToNSString(
912 " (" + info_->profile_name + ")")];
915 NSDictionary* strings_plist = @{
916 base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
917 app_mode::kCFBundleDisplayNameKey : display_name
920 NSString* localized_path = base::mac::FilePathToNSString(
921 localized_dir.Append("InfoPlist.strings"));
922 return [strings_plist writeToFile:localized_path
926 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
927 if (info_->favicon.empty())
930 ScopedCarbonHandle icon_family(0);
931 bool image_added = false;
932 for (gfx::ImageFamily::const_iterator it = info_->favicon.begin();
933 it != info_->favicon.end(); ++it) {
937 // Missing an icon size is not fatal so don't fail if adding the bitmap
939 if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it))
948 base::FilePath resources_path = GetResourcesPath(app_path);
949 if (!base::CreateDirectory(resources_path))
952 return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
955 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
956 NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
957 NSMutableDictionary* plist = ReadPlist(plist_path);
959 [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
960 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
961 return [plist writeToFile:plist_path
965 base::FilePath WebAppShortcutCreator::GetAppBundleById(
966 const std::string& bundle_id) const {
967 base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
968 base::SysUTF8ToCFStringRef(bundle_id));
969 CFURLRef url_ref = NULL;
970 OSStatus status = LSFindApplicationForInfo(
971 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
973 return base::FilePath();
975 base::ScopedCFTypeRef<CFURLRef> url(url_ref);
976 NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
977 return base::FilePath([path_string fileSystemRepresentation]);
980 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
981 // Replace spaces in the profile path with hyphen.
982 std::string normalized_profile_path;
983 base::ReplaceChars(info_->profile_path.BaseName().value(), " ", "-",
984 &normalized_profile_path);
986 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
987 std::string bundle_id = base::mac::BaseBundleID() + std::string(".app.") +
988 normalized_profile_path + "-" + info_->extension_id;
993 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
994 return GetBundleIdentifier() + "-internal";
997 void WebAppShortcutCreator::RevealAppShimInFinder() const {
998 base::FilePath app_path = GetApplicationsShortcutPath();
999 if (app_path.empty())
1002 // Check if the app shim exists.
1003 if (base::PathExists(app_path)) {
1004 // Use selectFile to show the contents of parent directory with the app
1006 [[NSWorkspace sharedWorkspace]
1007 selectFile:base::mac::FilePathToNSString(app_path)
1008 inFileViewerRootedAtPath:nil];
1012 // Otherwise, go up a directory.
1013 app_path = app_path.DirName();
1014 // Check if the Chrome apps folder exists, otherwise go up to ~/Applications.
1015 if (!base::PathExists(app_path))
1016 app_path = app_path.DirName();
1017 // Since |app_path| is a directory, use openFile to show the contents of
1018 // that directory in Finder.
1019 [[NSWorkspace sharedWorkspace]
1020 openFile:base::mac::FilePathToNSString(app_path)];
1023 base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) {
1024 WebAppShortcutCreator shortcut_creator(base::FilePath(), &shortcut_info,
1025 extensions::FileHandlersInfo());
1026 return shortcut_creator.GetApplicationsShortcutPath();
1029 void MaybeLaunchShortcut(scoped_ptr<ShortcutInfo> shortcut_info) {
1030 if (AppShimsDisabledForTest() &&
1031 !g_app_shims_allow_update_and_launch_in_tests) {
1035 content::BrowserThread::PostTask(
1036 content::BrowserThread::FILE, FROM_HERE,
1037 base::Bind(&LaunchShimOnFileThread, base::Passed(&shortcut_info), false));
1040 bool MaybeRebuildShortcut(const base::CommandLine& command_line) {
1041 if (!command_line.HasSwitch(app_mode::kAppShimError))
1044 base::PostTaskAndReplyWithResult(
1045 content::BrowserThread::GetBlockingPool(),
1047 base::Bind(&RecordAppShimErrorAndBuildShortcutInfo,
1048 command_line.GetSwitchValuePath(app_mode::kAppShimError)),
1049 base::Bind(&RebuildAppAndLaunch));
1053 // Called when the app's ShortcutInfo (with icon) is loaded when creating app
1055 void CreateAppShortcutInfoLoaded(
1057 const extensions::Extension* app,
1058 const base::Callback<void(bool)>& close_callback,
1059 scoped_ptr<ShortcutInfo> shortcut_info) {
1060 base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
1062 NSButton* continue_button = [alert
1063 addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)];
1064 [continue_button setKeyEquivalent:kKeyEquivalentReturn];
1066 NSButton* cancel_button =
1067 [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)];
1068 [cancel_button setKeyEquivalent:kKeyEquivalentEscape];
1070 [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)];
1071 [alert setAlertStyle:NSInformationalAlertStyle];
1073 base::scoped_nsobject<NSButton> application_folder_checkbox(
1074 [[NSButton alloc] initWithFrame:NSZeroRect]);
1075 [application_folder_checkbox setButtonType:NSSwitchButton];
1076 [application_folder_checkbox
1077 setTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_APP_FOLDER_CHKBOX)];
1078 [application_folder_checkbox setState:NSOnState];
1079 [application_folder_checkbox sizeToFit];
1081 base::scoped_nsobject<CrCreateAppShortcutCheckboxObserver> checkbox_observer(
1082 [[CrCreateAppShortcutCheckboxObserver alloc]
1083 initWithCheckbox:application_folder_checkbox
1084 continueButton:continue_button]);
1085 [checkbox_observer startObserving];
1087 [alert setAccessoryView:application_folder_checkbox];
1089 const int kIconPreviewSizePixels = 128;
1090 const int kIconPreviewTargetSize = 64;
1091 const gfx::Image* icon = shortcut_info->favicon.GetBest(
1092 kIconPreviewSizePixels, kIconPreviewSizePixels);
1094 if (icon && !icon->IsEmpty()) {
1095 NSImage* icon_image = icon->ToNSImage();
1097 setSize:NSMakeSize(kIconPreviewTargetSize, kIconPreviewTargetSize)];
1098 [alert setIcon:icon_image];
1101 bool dialog_accepted = false;
1102 if ([alert runModal] == NSAlertFirstButtonReturn &&
1103 [application_folder_checkbox state] == NSOnState) {
1104 dialog_accepted = true;
1106 SHORTCUT_CREATION_BY_USER, ShortcutLocations(), profile, app);
1109 [checkbox_observer stopObserving];
1111 if (!close_callback.is_null())
1112 close_callback.Run(dialog_accepted);
1115 void UpdateShortcutsForAllApps(Profile* profile,
1116 const base::Closure& callback) {
1117 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
1119 extensions::ExtensionRegistry* registry =
1120 extensions::ExtensionRegistry::Get(profile);
1125 scoped_ptr<extensions::ExtensionSet> everything =
1126 registry->GenerateInstalledExtensionsSet();
1127 for (extensions::ExtensionSet::const_iterator it = everything->begin();
1128 it != everything->end(); ++it) {
1129 if (web_app::ShouldCreateShortcutFor(SHORTCUT_CREATION_AUTOMATED, profile,
1131 web_app::UpdateAllShortcuts(base::string16(), profile, it->get());
1138 void RevealAppShimInFinderForApp(Profile* profile,
1139 const extensions::Extension* app) {
1140 scoped_ptr<web_app::ShortcutInfo> shortcut_info =
1141 ShortcutInfoForExtensionAndProfile(app, profile);
1142 content::BrowserThread::PostTask(
1143 content::BrowserThread::FILE, FROM_HERE,
1144 base::Bind(&RevealAppShimInFinderForAppOnFileThread,
1145 base::Passed(&shortcut_info), app->path()));
1148 namespace internals {
1150 bool CreatePlatformShortcuts(
1151 const base::FilePath& app_data_path,
1152 scoped_ptr<ShortcutInfo> shortcut_info,
1153 const extensions::FileHandlersInfo& file_handlers_info,
1154 const ShortcutLocations& creation_locations,
1155 ShortcutCreationReason creation_reason) {
1156 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
1157 if (AppShimsDisabledForTest())
1160 WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info.get(),
1161 file_handlers_info);
1162 return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
1165 void DeletePlatformShortcuts(const base::FilePath& app_data_path,
1166 scoped_ptr<ShortcutInfo> shortcut_info) {
1167 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
1168 WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info.get(),
1169 extensions::FileHandlersInfo());
1170 shortcut_creator.DeleteShortcuts();
1173 void UpdatePlatformShortcuts(
1174 const base::FilePath& app_data_path,
1175 const base::string16& old_app_title,
1176 scoped_ptr<ShortcutInfo> shortcut_info,
1177 const extensions::FileHandlersInfo& file_handlers_info) {
1178 UpdatePlatformShortcutsInternal(app_data_path, old_app_title, *shortcut_info,
1179 file_handlers_info);
1182 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
1183 const std::string profile_base_name = profile_path.BaseName().value();
1184 std::vector<base::FilePath> bundles = GetAllAppBundlesInPath(
1185 profile_path.Append(chrome::kWebAppDirname), profile_base_name);
1187 for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
1188 it != bundles.end(); ++it) {
1189 scoped_ptr<web_app::ShortcutInfo> shortcut_info =
1190 BuildShortcutInfoFromBundle(*it);
1191 WebAppShortcutCreator shortcut_creator(it->DirName(), shortcut_info.get(),
1192 extensions::FileHandlersInfo());
1193 shortcut_creator.DeleteShortcuts();
1197 } // namespace internals
1199 } // namespace web_app
1203 void ShowCreateChromeAppShortcutsDialog(
1204 gfx::NativeWindow /*parent_window*/,
1206 const extensions::Extension* app,
1207 const base::Callback<void(bool)>& close_callback) {
1208 web_app::GetShortcutInfoForApp(
1211 base::Bind(&web_app::CreateAppShortcutInfoLoaded,
1217 } // namespace chrome