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 "apps/app_shim/app_shim_mac.h"
11 #include "base/command_line.h"
12 #include "base/file_util.h"
13 #include "base/files/file_enumerator.h"
14 #include "base/files/scoped_temp_dir.h"
15 #include "base/mac/foundation_util.h"
16 #include "base/mac/launch_services_util.h"
17 #include "base/mac/mac_util.h"
18 #include "base/mac/scoped_cftyperef.h"
19 #include "base/mac/scoped_nsobject.h"
20 #include "base/path_service.h"
21 #include "base/process/process_handle.h"
22 #include "base/strings/string16.h"
23 #include "base/strings/string_number_conversions.h"
24 #include "base/strings/string_util.h"
25 #include "base/strings/sys_string_conversions.h"
26 #include "base/strings/utf_string_conversions.h"
27 #import "chrome/browser/mac/dock.h"
28 #include "chrome/browser/profiles/profile.h"
29 #include "chrome/browser/shell_integration.h"
30 #include "chrome/common/chrome_constants.h"
31 #include "chrome/common/chrome_paths.h"
32 #include "chrome/common/chrome_switches.h"
33 #include "chrome/common/chrome_version_info.h"
34 #import "chrome/common/mac/app_mode_common.h"
35 #include "content/public/browser/browser_thread.h"
36 #include "extensions/common/extension.h"
37 #include "grit/chrome_unscaled_resources.h"
38 #include "grit/chromium_strings.h"
39 #include "grit/generated_resources.h"
40 #import "skia/ext/skia_utils_mac.h"
41 #include "third_party/skia/include/core/SkBitmap.h"
42 #include "third_party/skia/include/core/SkColor.h"
43 #include "ui/base/l10n/l10n_util.h"
44 #import "ui/base/l10n/l10n_util_mac.h"
45 #include "ui/base/resource/resource_bundle.h"
46 #include "ui/gfx/image/image_family.h"
50 // Launch Services Key to run as an agent app, which doesn't launch in the dock.
51 NSString* const kLSUIElement = @"LSUIElement";
53 class ScopedCarbonHandle {
55 ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
57 DCHECK_EQ(noErr, MemError());
59 ~ScopedCarbonHandle() { DisposeHandle(handle_); }
61 Handle Get() { return handle_; }
62 char* Data() { return *handle_; }
63 size_t HandleSize() const { return GetHandleSize(handle_); }
65 IconFamilyHandle GetAsIconFamilyHandle() {
66 return reinterpret_cast<IconFamilyHandle>(handle_);
69 bool WriteDataToFile(const base::FilePath& path) {
70 NSData* data = [NSData dataWithBytes:Data()
72 return [data writeToFile:base::mac::FilePathToNSString(path)
80 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
81 CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());
83 char* argb = handle->Data();
84 SkAutoLockPixels lock(bitmap);
85 for (int y = 0; y < bitmap.height(); ++y) {
86 for (int x = 0; x < bitmap.width(); ++x) {
87 SkColor pixel = bitmap.getColor(x, y);
88 argb[0] = SkColorGetA(pixel);
89 argb[1] = SkColorGetR(pixel);
90 argb[2] = SkColorGetG(pixel);
91 argb[3] = SkColorGetB(pixel);
97 // Adds |image| to |icon_family|. Returns true on success, false on failure.
98 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
99 const gfx::Image& image) {
100 // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
101 // have all the representations desired here for mac, from the kDesiredSizes
102 // array in web_app.cc.
103 SkBitmap bitmap = image.AsBitmap();
104 if (bitmap.config() != SkBitmap::kARGB_8888_Config ||
105 bitmap.width() != bitmap.height()) {
110 switch (bitmap.width()) {
112 icon_type = kIconServices512PixelDataARGB;
115 icon_type = kIconServices256PixelDataARGB;
118 icon_type = kIconServices128PixelDataARGB;
121 icon_type = kIconServices48PixelDataARGB;
124 icon_type = kIconServices32PixelDataARGB;
127 icon_type = kIconServices16PixelDataARGB;
133 ScopedCarbonHandle raw_data(bitmap.getSize());
134 ConvertSkiaToARGB(bitmap, &raw_data);
135 OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
136 DCHECK_EQ(noErr, result);
137 return result == noErr;
140 base::FilePath GetWritableApplicationsDirectory() {
142 if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
143 if (!base::DirectoryExists(path)) {
144 if (!base::CreateDirectory(path))
145 return base::FilePath();
147 // Create a zero-byte ".localized" file to inherit localizations from OSX
148 // for folders that have special meaning.
149 base::WriteFile(path.Append(".localized"), NULL, 0);
151 return base::PathIsWritable(path) ? path : base::FilePath();
153 return base::FilePath();
156 // Given the path to an app bundle, return the resources directory.
157 base::FilePath GetResourcesPath(const base::FilePath& app_path) {
158 return app_path.Append("Contents").Append("Resources");
161 bool HasExistingExtensionShim(const base::FilePath& destination_directory,
162 const std::string& extension_id,
163 const base::FilePath& own_basename) {
164 // Check if there any any other shims for the same extension.
165 base::FileEnumerator enumerator(destination_directory,
166 false /* recursive */,
167 base::FileEnumerator::DIRECTORIES);
168 for (base::FilePath shim_path = enumerator.Next();
169 !shim_path.empty(); shim_path = enumerator.Next()) {
170 if (shim_path.BaseName() != own_basename &&
171 EndsWith(shim_path.RemoveExtension().value(),
173 true /* case_sensitive */)) {
181 // Given the path to an app bundle, return the path to the Info.plist file.
182 NSString* GetPlistPath(const base::FilePath& bundle_path) {
183 return base::mac::FilePathToNSString(
184 bundle_path.Append("Contents").Append("Info.plist"));
187 NSMutableDictionary* ReadPlist(NSString* plist_path) {
188 return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
191 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
192 // the Info.plist starts with the current user_data_dir. This uses starts with
193 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir
194 // or the |app_data_dir_|.
195 bool HasSameUserDataDir(const base::FilePath& bundle_path) {
196 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
197 base::FilePath user_data_dir;
198 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
199 DCHECK(!user_data_dir.empty());
200 return StartsWithASCII(
201 base::SysNSStringToUTF8(
202 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
203 user_data_dir.value(),
204 true /* case_sensitive */);
207 void LaunchShimOnFileThread(
208 const web_app::ShortcutInfo& shortcut_info) {
209 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
210 base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info);
212 if (shim_path.empty() ||
213 !base::PathExists(shim_path) ||
214 !HasSameUserDataDir(shim_path)) {
215 // The user may have deleted the copy in the Applications folder, use the
216 // one in the web app's |app_data_dir_|.
217 base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
218 shortcut_info.profile_path, shortcut_info.extension_id, GURL());
219 shim_path = app_data_dir.Append(shim_path.BaseName());
222 if (!base::PathExists(shim_path))
225 CommandLine command_line(CommandLine::NO_PROGRAM);
226 command_line.AppendSwitchASCII(
227 app_mode::kLaunchedByChromeProcessId,
228 base::IntToString(base::GetCurrentProcId()));
229 // Launch without activating (kLSLaunchDontSwitch).
230 base::mac::OpenApplicationWithPath(
231 shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
234 base::FilePath GetAppLoaderPath() {
235 return base::mac::PathForFrameworkBundleResource(
236 base::mac::NSToCFCast(@"app_mode_loader.app"));
239 base::FilePath GetLocalizableAppShortcutsSubdirName() {
240 static const char kChromiumAppDirName[] = "Chromium Apps.localized";
241 static const char kChromeAppDirName[] = "Chrome Apps.localized";
242 static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";
244 switch (chrome::VersionInfo::GetChannel()) {
245 case chrome::VersionInfo::CHANNEL_UNKNOWN:
246 return base::FilePath(kChromiumAppDirName);
248 case chrome::VersionInfo::CHANNEL_CANARY:
249 return base::FilePath(kChromeCanaryAppDirName);
252 return base::FilePath(kChromeAppDirName);
256 // Creates a canvas the same size as |overlay|, copies the appropriate
257 // representation from |backgound| into it (according to Cocoa), then draws
258 // |overlay| over it using NSCompositeSourceOver.
259 NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
261 NSInteger dimension = [overlay pixelsWide];
262 DCHECK_EQ(dimension, [overlay pixelsHigh]);
263 base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
264 initWithBitmapDataPlanes:NULL
271 colorSpaceName:NSCalibratedRGBColorSpace
275 // There isn't a colorspace name constant for sRGB, so retag.
276 NSBitmapImageRep* srgb_canvas = [canvas
277 bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
278 canvas.reset([srgb_canvas retain]);
280 // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
281 [canvas setSize:NSMakeSize(dimension, dimension)];
283 NSGraphicsContext* drawing_context =
284 [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
285 [NSGraphicsContext saveGraphicsState];
286 [NSGraphicsContext setCurrentContext:drawing_context];
287 [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
289 operation:NSCompositeCopy
291 [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
293 operation:NSCompositeSourceOver
297 [NSGraphicsContext restoreGraphicsState];
298 return canvas.autorelease();
301 // Adds a localized strings file for the Chrome Apps directory using the current
302 // locale. OSX will use this for the display name.
303 // + Chrome Apps.localized (|apps_directory|)
307 void UpdateAppShortcutsSubdirLocalizedName(
308 const base::FilePath& apps_directory) {
309 base::FilePath localized = apps_directory.Append(".localized");
310 if (!base::CreateDirectory(localized))
313 base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
314 base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
315 NSDictionary* strings_dict = @{
316 base::mac::FilePathToNSString(directory_name) :
317 base::SysUTF16ToNSString(localized_name)
320 std::string locale = l10n_util::NormalizeLocale(
321 l10n_util::GetApplicationLocale(std::string()));
323 NSString* strings_path = base::mac::FilePathToNSString(
324 localized.Append(locale + ".strings"));
325 [strings_dict writeToFile:strings_path
328 // Brand the folder with an embossed app launcher logo.
329 const int kBrandResourceIds[] = {
330 IDR_APPS_FOLDER_OVERLAY_16,
331 IDR_APPS_FOLDER_OVERLAY_32,
332 IDR_APPS_FOLDER_OVERLAY_128,
333 IDR_APPS_FOLDER_OVERLAY_512,
335 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
336 NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
337 base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);
338 for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
339 gfx::Image& image_rep = rb.GetNativeImageNamed(kBrandResourceIds[i]);
340 NSArray* image_reps = [image_rep.AsNSImage() representations];
341 DCHECK_EQ(1u, [image_reps count]);
342 NSImageRep* with_overlay = OverlayImageRep(base_image,
343 [image_reps objectAtIndex:0]);
344 DCHECK(with_overlay);
346 [folder_icon_image addRepresentation:with_overlay];
348 [[NSWorkspace sharedWorkspace]
349 setIcon:folder_icon_image
350 forFile:base::mac::FilePathToNSString(apps_directory)
354 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
355 DCHECK(!app_path.empty());
356 base::DeleteFile(app_path, true);
357 base::FilePath apps_folder = app_path.DirName();
358 if (base::IsDirectoryEmpty(apps_folder))
359 base::DeleteFile(apps_folder, false);
362 bool IsShimForProfile(const base::FilePath& base_name,
363 const std::string& profile_base_name) {
364 if (!StartsWithASCII(base_name.value(), profile_base_name, true))
367 if (base_name.Extension() != ".app")
370 std::string app_id = base_name.RemoveExtension().value();
371 // Strip (profile_base_name + " ") from the start.
372 app_id = app_id.substr(profile_base_name.size() + 1);
373 return extensions::Extension::IdIsValid(app_id);
376 std::vector<base::FilePath> GetAllAppBundlesInPath(
377 const base::FilePath& internal_shortcut_path,
378 const std::string& profile_base_name) {
379 std::vector<base::FilePath> bundle_paths;
381 base::FileEnumerator enumerator(internal_shortcut_path,
382 true /* recursive */,
383 base::FileEnumerator::DIRECTORIES);
384 for (base::FilePath bundle_path = enumerator.Next();
385 !bundle_path.empty(); bundle_path = enumerator.Next()) {
386 if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
387 bundle_paths.push_back(bundle_path);
393 web_app::ShortcutInfo BuildShortcutInfoFromBundle(
394 const base::FilePath& bundle_path) {
395 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
397 web_app::ShortcutInfo shortcut_info;
398 shortcut_info.extension_id = base::SysNSStringToUTF8(
399 [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
400 shortcut_info.is_platform_app = true;
401 shortcut_info.url = GURL(base::SysNSStringToUTF8(
402 [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
403 shortcut_info.title = base::SysNSStringToUTF16(
404 [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
405 shortcut_info.profile_name = base::SysNSStringToUTF8(
406 [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);
408 // Figure out the profile_path. Since the user_data_dir could contain the
409 // path to the web app data dir.
410 base::FilePath user_data_dir = base::mac::NSStringToFilePath(
411 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
412 base::FilePath profile_base_name = base::mac::NSStringToFilePath(
413 [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
414 if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
415 shortcut_info.profile_path = user_data_dir.DirName().DirName();
417 shortcut_info.profile_path = user_data_dir.Append(profile_base_name);
419 return shortcut_info;
422 void UpdateFileTypes(NSMutableDictionary* plist,
423 const extensions::FileHandlersInfo& file_handlers_info) {
424 const std::vector<extensions::FileHandlerInfo>& handlers =
425 file_handlers_info.handlers;
426 NSMutableArray* document_types =
427 [NSMutableArray arrayWithCapacity:handlers.size()];
429 for (std::vector<extensions::FileHandlerInfo>::const_iterator info_it =
431 info_it != handlers.end();
433 const extensions::FileHandlerInfo& info = *info_it;
435 NSMutableArray* file_extensions =
436 [NSMutableArray arrayWithCapacity:info.extensions.size()];
437 for (std::set<std::string>::iterator it = info.extensions.begin();
438 it != info.extensions.end();
440 [file_extensions addObject:base::SysUTF8ToNSString(*it)];
443 NSMutableArray* mime_types =
444 [NSMutableArray arrayWithCapacity:info.types.size()];
445 for (std::set<std::string>::iterator it = info.types.begin();
446 it != info.types.end();
448 [mime_types addObject:base::SysUTF8ToNSString(*it)];
451 NSDictionary* type_dictionary = @{
452 // TODO(jackhou): Add the type name and and icon file once the manifest
454 // app_mode::kCFBundleTypeNameKey : ,
455 // app_mode::kCFBundleTypeIconFileKey : ,
456 app_mode::kCFBundleTypeExtensionsKey : file_extensions,
457 app_mode::kCFBundleTypeMIMETypesKey : mime_types,
458 app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer
460 [document_types addObject:type_dictionary];
463 [plist setObject:document_types
464 forKey:app_mode::kCFBundleDocumentTypesKey];
471 WebAppShortcutCreator::WebAppShortcutCreator(
472 const base::FilePath& app_data_dir,
473 const web_app::ShortcutInfo& shortcut_info,
474 const extensions::FileHandlersInfo& file_handlers_info)
475 : app_data_dir_(app_data_dir),
476 info_(shortcut_info),
477 file_handlers_info_(file_handlers_info) {}
479 WebAppShortcutCreator::~WebAppShortcutCreator() {}
481 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
482 base::FilePath applications_dir = GetApplicationsDirname();
483 return applications_dir.empty() ?
484 base::FilePath() : applications_dir.Append(GetShortcutBasename());
487 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
488 return app_data_dir_.Append(GetShortcutBasename());
491 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
492 std::string app_name;
493 // Check if there should be a separate shortcut made for different profiles.
494 // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
495 // otherwise it will be empty.
496 if (!info_.profile_name.empty()) {
497 app_name += info_.profile_path.BaseName().value();
500 app_name += info_.extension_id;
501 return base::FilePath(app_name).ReplaceExtension("app");
504 bool WebAppShortcutCreator::BuildShortcut(
505 const base::FilePath& staging_path) const {
506 // Update the app's plist and icon in a temp directory. This works around
507 // a Finder bug where the app's icon doesn't properly update.
508 if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
509 LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
514 return UpdatePlist(staging_path) &&
515 UpdateDisplayName(staging_path) &&
516 UpdateIcon(staging_path);
519 size_t WebAppShortcutCreator::CreateShortcutsIn(
520 const std::vector<base::FilePath>& folders) const {
521 size_t succeeded = 0;
523 base::ScopedTempDir scoped_temp_dir;
524 if (!scoped_temp_dir.CreateUniqueTempDir())
527 base::FilePath app_name = GetShortcutBasename();
528 base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
529 if (!BuildShortcut(staging_path))
532 for (std::vector<base::FilePath>::const_iterator it = folders.begin();
533 it != folders.end(); ++it) {
534 const base::FilePath& dst_path = *it;
535 if (!base::CreateDirectory(dst_path)) {
536 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
540 if (!base::CopyDirectory(staging_path, dst_path, true)) {
541 LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
546 base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
553 bool WebAppShortcutCreator::CreateShortcuts(
554 ShortcutCreationReason creation_reason,
555 web_app::ShortcutLocations creation_locations) {
556 const base::FilePath applications_dir = GetApplicationsDirname();
557 if (applications_dir.empty() ||
558 !base::DirectoryExists(applications_dir.DirName())) {
559 LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
563 UpdateAppShortcutsSubdirLocalizedName(applications_dir);
565 // If non-nil, this path is added to the OSX Dock after creating shortcuts.
566 NSString* path_to_add_to_dock = nil;
568 std::vector<base::FilePath> paths;
570 // The app list shim is not tied to a particular profile, so omit the copy
571 // placed under the profile path. For shims, this copy is used when the
572 // version under Applications is removed, and not needed for app list because
573 // setting LSUIElement means there is no Dock "running" status to show.
574 const bool is_app_list = info_.extension_id == app_mode::kAppListModeId;
576 path_to_add_to_dock = base::SysUTF8ToNSString(
577 applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
579 paths.push_back(app_data_dir_);
581 paths.push_back(applications_dir);
583 size_t success_count = CreateShortcutsIn(paths);
584 if (success_count == 0)
588 UpdateInternalBundleIdentifier();
590 if (success_count != paths.size())
593 if (creation_locations.in_quick_launch_bar && path_to_add_to_dock) {
594 switch (dock::AddIcon(path_to_add_to_dock, nil)) {
595 case dock::IconAddFailure:
596 // If adding the icon failed, instead reveal the Finder window.
597 RevealAppShimInFinder();
599 case dock::IconAddSuccess:
600 case dock::IconAlreadyPresent:
606 if (creation_reason == SHORTCUT_CREATION_BY_USER)
607 RevealAppShimInFinder();
612 void WebAppShortcutCreator::DeleteShortcuts() {
613 base::FilePath app_path = GetApplicationsShortcutPath();
614 if (!app_path.empty() && HasSameUserDataDir(app_path))
615 DeletePathAndParentIfEmpty(app_path);
617 // In case the user has moved/renamed/copied the app bundle.
618 base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
619 if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
620 base::DeleteFile(bundle_path, true);
622 // Delete the internal one.
623 DeletePathAndParentIfEmpty(GetInternalShortcutPath());
626 bool WebAppShortcutCreator::UpdateShortcuts() {
627 std::vector<base::FilePath> paths;
628 base::DeleteFile(GetInternalShortcutPath(), true);
629 paths.push_back(app_data_dir_);
631 // Try to update the copy under /Applications. If that does not exist, check
632 // if a matching bundle can be found elsewhere.
633 base::FilePath app_path = GetApplicationsShortcutPath();
634 if (app_path.empty() || !base::PathExists(app_path))
635 app_path = GetAppBundleById(GetBundleIdentifier());
637 if (!app_path.empty()) {
638 base::DeleteFile(app_path, true);
639 paths.push_back(app_path.DirName());
642 size_t success_count = CreateShortcutsIn(paths);
643 if (success_count == 0)
646 UpdateInternalBundleIdentifier();
647 return success_count == paths.size() && !app_path.empty();
650 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
651 base::FilePath path = GetWritableApplicationsDirectory();
655 return path.Append(GetLocalizableAppShortcutsSubdirName());
658 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
659 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
660 NSString* extension_title = base::SysUTF16ToNSString(info_.title);
661 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
662 NSString* chrome_bundle_id =
663 base::SysUTF8ToNSString(base::mac::BaseBundleID());
664 NSDictionary* replacement_dict =
665 [NSDictionary dictionaryWithObjectsAndKeys:
666 extension_id, app_mode::kShortcutIdPlaceholder,
667 extension_title, app_mode::kShortcutNamePlaceholder,
668 extension_url, app_mode::kShortcutURLPlaceholder,
669 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
672 NSString* plist_path = GetPlistPath(app_path);
673 NSMutableDictionary* plist = ReadPlist(plist_path);
674 NSArray* keys = [plist allKeys];
676 // 1. Fill in variables.
677 for (id key in keys) {
678 NSString* value = [plist valueForKey:key];
679 if (![value isKindOfClass:[NSString class]] || [value length] < 2)
682 // Remove leading and trailing '@'s.
684 [value substringWithRange:NSMakeRange(1, [value length] - 2)];
686 NSString* substitution = [replacement_dict valueForKey:variable];
688 [plist setObject:substitution forKey:key];
691 // 2. Fill in other values.
692 [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
693 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
694 [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
695 forKey:app_mode::kCrAppModeUserDataDirKey];
696 [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName())
697 forKey:app_mode::kCrAppModeProfileDirKey];
698 [plist setObject:base::SysUTF8ToNSString(info_.profile_name)
699 forKey:app_mode::kCrAppModeProfileNameKey];
700 [plist setObject:[NSNumber numberWithBool:YES]
701 forKey:app_mode::kLSHasLocalizedDisplayNameKey];
702 if (info_.extension_id == app_mode::kAppListModeId) {
703 // Prevent the app list from bouncing in the dock, and getting a run light.
704 [plist setObject:[NSNumber numberWithBool:YES]
705 forKey:kLSUIElement];
708 base::FilePath app_name = app_path.BaseName().RemoveExtension();
709 [plist setObject:base::mac::FilePathToNSString(app_name)
710 forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
712 if (CommandLine::ForCurrentProcess()->HasSwitch(
713 switches::kEnableAppsFileAssociations)) {
714 UpdateFileTypes(plist, file_handlers_info_);
717 return [plist writeToFile:plist_path
721 bool WebAppShortcutCreator::UpdateDisplayName(
722 const base::FilePath& app_path) const {
723 // OSX searches for the best language in the order of preferred languages.
724 // Since we only have one localization directory, it will choose this one.
725 base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
726 if (!base::CreateDirectory(localized_dir))
729 NSString* bundle_name = base::SysUTF16ToNSString(info_.title);
730 NSString* display_name = base::SysUTF16ToNSString(info_.title);
731 if (HasExistingExtensionShim(GetApplicationsDirname(),
733 app_path.BaseName())) {
734 display_name = [bundle_name
735 stringByAppendingString:base::SysUTF8ToNSString(
736 " (" + info_.profile_name + ")")];
739 NSDictionary* strings_plist = @{
740 base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
741 app_mode::kCFBundleDisplayNameKey : display_name
744 NSString* localized_path = base::mac::FilePathToNSString(
745 localized_dir.Append("InfoPlist.strings"));
746 return [strings_plist writeToFile:localized_path
750 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const {
751 if (info_.favicon.empty())
754 ScopedCarbonHandle icon_family(0);
755 bool image_added = false;
756 for (gfx::ImageFamily::const_iterator it = info_.favicon.begin();
757 it != info_.favicon.end(); ++it) {
761 // Missing an icon size is not fatal so don't fail if adding the bitmap
763 if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it))
772 base::FilePath resources_path = GetResourcesPath(app_path);
773 if (!base::CreateDirectory(resources_path))
776 return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
779 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const {
780 NSString* plist_path = GetPlistPath(GetInternalShortcutPath());
781 NSMutableDictionary* plist = ReadPlist(plist_path);
783 [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier())
784 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
785 return [plist writeToFile:plist_path
789 base::FilePath WebAppShortcutCreator::GetAppBundleById(
790 const std::string& bundle_id) const {
791 base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
792 base::SysUTF8ToCFStringRef(bundle_id));
793 CFURLRef url_ref = NULL;
794 OSStatus status = LSFindApplicationForInfo(
795 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
797 return base::FilePath();
799 base::ScopedCFTypeRef<CFURLRef> url(url_ref);
800 NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
801 return base::FilePath([path_string fileSystemRepresentation]);
804 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
805 // Replace spaces in the profile path with hyphen.
806 std::string normalized_profile_path;
807 base::ReplaceChars(info_.profile_path.BaseName().value(),
808 " ", "-", &normalized_profile_path);
810 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
811 std::string bundle_id =
812 base::mac::BaseBundleID() + std::string(".app.") +
813 normalized_profile_path + "-" + info_.extension_id;
818 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const {
819 return GetBundleIdentifier() + "-internal";
822 void WebAppShortcutCreator::RevealAppShimInFinder() const {
823 base::FilePath app_path = GetApplicationsShortcutPath();
824 if (app_path.empty())
827 [[NSWorkspace sharedWorkspace]
828 selectFile:base::mac::FilePathToNSString(app_path)
829 inFileViewerRootedAtPath:nil];
832 base::FilePath GetAppInstallPath(
833 const web_app::ShortcutInfo& shortcut_info) {
834 WebAppShortcutCreator shortcut_creator(
835 base::FilePath(), shortcut_info, extensions::FileHandlersInfo());
836 return shortcut_creator.GetApplicationsShortcutPath();
839 void MaybeLaunchShortcut(const web_app::ShortcutInfo& shortcut_info) {
840 if (!apps::IsAppShimsEnabled())
843 content::BrowserThread::PostTask(
844 content::BrowserThread::FILE, FROM_HERE,
845 base::Bind(&LaunchShimOnFileThread, shortcut_info));
848 // Called when the app's ShortcutInfo (with icon) is loaded when creating app
850 void CreateAppShortcutInfoLoaded(
852 const extensions::Extension* app,
853 const base::Callback<void(bool)>& close_callback,
854 const web_app::ShortcutInfo& shortcut_info) {
855 base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]);
857 NSButton* continue_button = [alert
858 addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)];
859 [continue_button setKeyEquivalent:@""];
861 NSButton* cancel_button =
862 [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)];
863 [cancel_button setKeyEquivalent:@"\r"];
865 [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)];
866 [alert setAlertStyle:NSInformationalAlertStyle];
868 base::scoped_nsobject<NSButton> application_folder_checkbox(
869 [[NSButton alloc] initWithFrame:NSZeroRect]);
870 [application_folder_checkbox setButtonType:NSSwitchButton];
871 [application_folder_checkbox
872 setTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_APP_FOLDER_CHKBOX)];
873 [application_folder_checkbox setState:NSOnState];
874 [application_folder_checkbox sizeToFit];
875 [alert setAccessoryView:application_folder_checkbox];
877 const int kIconPreviewSizePixels = 128;
878 const int kIconPreviewTargetSize = 64;
879 const gfx::Image* icon = shortcut_info.favicon.GetBest(
880 kIconPreviewSizePixels, kIconPreviewSizePixels);
882 if (icon && !icon->IsEmpty()) {
883 NSImage* icon_image = icon->ToNSImage();
885 setSize:NSMakeSize(kIconPreviewTargetSize, kIconPreviewTargetSize)];
886 [alert setIcon:icon_image];
889 bool dialog_accepted = false;
890 if ([alert runModal] == NSAlertFirstButtonReturn &&
891 [application_folder_checkbox state] == NSOnState) {
892 dialog_accepted = true;
893 web_app::CreateShortcuts(web_app::SHORTCUT_CREATION_BY_USER,
894 web_app::ShortcutLocations(),
899 if (!close_callback.is_null())
900 close_callback.Run(dialog_accepted);
903 namespace internals {
905 bool CreatePlatformShortcuts(
906 const base::FilePath& app_data_path,
907 const web_app::ShortcutInfo& shortcut_info,
908 const extensions::FileHandlersInfo& file_handlers_info,
909 const web_app::ShortcutLocations& creation_locations,
910 ShortcutCreationReason creation_reason) {
911 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
912 WebAppShortcutCreator shortcut_creator(
913 app_data_path, shortcut_info, file_handlers_info);
914 return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
917 void DeletePlatformShortcuts(
918 const base::FilePath& app_data_path,
919 const web_app::ShortcutInfo& shortcut_info) {
920 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
921 WebAppShortcutCreator shortcut_creator(
922 app_data_path, shortcut_info, extensions::FileHandlersInfo());
923 shortcut_creator.DeleteShortcuts();
926 void UpdatePlatformShortcuts(
927 const base::FilePath& app_data_path,
928 const base::string16& old_app_title,
929 const web_app::ShortcutInfo& shortcut_info,
930 const extensions::FileHandlersInfo& file_handlers_info) {
931 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
932 WebAppShortcutCreator shortcut_creator(
933 app_data_path, shortcut_info, file_handlers_info);
934 shortcut_creator.UpdateShortcuts();
937 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
938 const std::string profile_base_name = profile_path.BaseName().value();
939 std::vector<base::FilePath> bundles = GetAllAppBundlesInPath(
940 profile_path.Append(chrome::kWebAppDirname), profile_base_name);
942 for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
943 it != bundles.end(); ++it) {
944 web_app::ShortcutInfo shortcut_info =
945 BuildShortcutInfoFromBundle(*it);
946 WebAppShortcutCreator shortcut_creator(
947 it->DirName(), shortcut_info, extensions::FileHandlersInfo());
948 shortcut_creator.DeleteShortcuts();
952 } // namespace internals
954 } // namespace web_app
958 void ShowCreateChromeAppShortcutsDialog(
959 gfx::NativeWindow /*parent_window*/,
961 const extensions::Extension* app,
962 const base::Callback<void(bool)>& close_callback) {
963 web_app::UpdateShortcutInfoAndIconForApp(
966 base::Bind(&web_app::CreateAppShortcutInfoLoaded,
972 } // namespace chrome