// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "chrome/browser/web_applications/web_app_mac.h" #import #import #include "base/command_line.h" #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/mac/foundation_util.h" #include "base/mac/launch_services_util.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_cftyperef.h" #include "base/mac/scoped_nsobject.h" #include "base/metrics/sparse_histogram.h" #include "base/path_service.h" #include "base/process/process_handle.h" #include "base/strings/string16.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/version.h" #include "chrome/browser/browser_process.h" #import "chrome/browser/mac/dock.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile_manager.h" #include "chrome/browser/shell_integration.h" #include "chrome/browser/ui/app_list/app_list_service.h" #include "chrome/browser/ui/cocoa/key_equivalent_constants.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_version_info.h" #import "chrome/common/mac/app_mode_common.h" #include "chrome/grit/generated_resources.h" #include "components/crx_file/id_util.h" #include "content/public/browser/browser_thread.h" #include "extensions/browser/extension_registry.h" #include "extensions/common/extension.h" #include "grit/chrome_unscaled_resources.h" #import "skia/ext/skia_utils_mac.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/l10n/l10n_util.h" #import "ui/base/l10n/l10n_util_mac.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image_family.h" bool g_app_shims_allow_update_and_launch_in_tests = false; namespace { // Launch Services Key to run as an agent app, which doesn't launch in the dock. NSString* const kLSUIElement = @"LSUIElement"; class ScopedCarbonHandle { public: ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) { DCHECK(handle_); DCHECK_EQ(noErr, MemError()); } ~ScopedCarbonHandle() { DisposeHandle(handle_); } Handle Get() { return handle_; } char* Data() { return *handle_; } size_t HandleSize() const { return GetHandleSize(handle_); } IconFamilyHandle GetAsIconFamilyHandle() { return reinterpret_cast(handle_); } bool WriteDataToFile(const base::FilePath& path) { NSData* data = [NSData dataWithBytes:Data() length:HandleSize()]; return [data writeToFile:base::mac::FilePathToNSString(path) atomically:NO]; } private: Handle handle_; }; void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) { CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize()); char* argb = handle->Data(); SkAutoLockPixels lock(bitmap); for (int y = 0; y < bitmap.height(); ++y) { for (int x = 0; x < bitmap.width(); ++x) { SkColor pixel = bitmap.getColor(x, y); argb[0] = SkColorGetA(pixel); argb[1] = SkColorGetR(pixel); argb[2] = SkColorGetG(pixel); argb[3] = SkColorGetB(pixel); argb += 4; } } } // Adds |image| to |icon_family|. Returns true on success, false on failure. bool AddGfxImageToIconFamily(IconFamilyHandle icon_family, const gfx::Image& image) { // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will // have all the representations desired here for mac, from the kDesiredSizes // array in web_app.cc. SkBitmap bitmap = image.AsBitmap(); if (bitmap.colorType() != kN32_SkColorType || bitmap.width() != bitmap.height()) { return false; } OSType icon_type; switch (bitmap.width()) { case 512: icon_type = kIconServices512PixelDataARGB; break; case 256: icon_type = kIconServices256PixelDataARGB; break; case 128: icon_type = kIconServices128PixelDataARGB; break; case 48: icon_type = kIconServices48PixelDataARGB; break; case 32: icon_type = kIconServices32PixelDataARGB; break; case 16: icon_type = kIconServices16PixelDataARGB; break; default: return false; } ScopedCarbonHandle raw_data(bitmap.getSize()); ConvertSkiaToARGB(bitmap, &raw_data); OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get()); DCHECK_EQ(noErr, result); return result == noErr; } bool AppShimsDisabledForTest() { // Disable app shims in tests because shims created in ~/Applications will not // be cleaned up. return base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType); } base::FilePath GetWritableApplicationsDirectory() { base::FilePath path; if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) { if (!base::DirectoryExists(path)) { if (!base::CreateDirectory(path)) return base::FilePath(); // Create a zero-byte ".localized" file to inherit localizations from OSX // for folders that have special meaning. base::WriteFile(path.Append(".localized"), NULL, 0); } return base::PathIsWritable(path) ? path : base::FilePath(); } return base::FilePath(); } // Given the path to an app bundle, return the resources directory. base::FilePath GetResourcesPath(const base::FilePath& app_path) { return app_path.Append("Contents").Append("Resources"); } bool HasExistingExtensionShim(const base::FilePath& destination_directory, const std::string& extension_id, const base::FilePath& own_basename) { // Check if there any any other shims for the same extension. base::FileEnumerator enumerator(destination_directory, false /* recursive */, base::FileEnumerator::DIRECTORIES); for (base::FilePath shim_path = enumerator.Next(); !shim_path.empty(); shim_path = enumerator.Next()) { if (shim_path.BaseName() != own_basename && base::EndsWith(shim_path.RemoveExtension().value(), extension_id, true /* case_sensitive */)) { return true; } } return false; } // Given the path to an app bundle, return the path to the Info.plist file. NSString* GetPlistPath(const base::FilePath& bundle_path) { return base::mac::FilePathToNSString( bundle_path.Append("Contents").Append("Info.plist")); } NSMutableDictionary* ReadPlist(NSString* plist_path) { return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path]; } // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in // the Info.plist starts with the current user_data_dir. This uses starts with // instead of equals because the CrAppModeUserDataDir could be the user_data_dir // or the |app_data_dir_|. bool HasSameUserDataDir(const base::FilePath& bundle_path) { NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); base::FilePath user_data_dir; PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); DCHECK(!user_data_dir.empty()); return base::StartsWith( base::SysNSStringToUTF8( [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]), user_data_dir.value(), base::CompareCase::SENSITIVE); } void LaunchShimOnFileThread(scoped_ptr shortcut_info, bool launched_after_rebuild) { DCHECK_CURRENTLY_ON(content::BrowserThread::FILE); base::FilePath shim_path = web_app::GetAppInstallPath(*shortcut_info); if (shim_path.empty() || !base::PathExists(shim_path) || !HasSameUserDataDir(shim_path)) { // The user may have deleted the copy in the Applications folder, use the // one in the web app's |app_data_dir_|. base::FilePath app_data_dir = web_app::GetWebAppDataDirectory( shortcut_info->profile_path, shortcut_info->extension_id, GURL()); shim_path = app_data_dir.Append(shim_path.BaseName()); } if (!base::PathExists(shim_path)) return; base::CommandLine command_line(base::CommandLine::NO_PROGRAM); command_line.AppendSwitchASCII( app_mode::kLaunchedByChromeProcessId, base::IntToString(base::GetCurrentProcId())); if (launched_after_rebuild) command_line.AppendSwitch(app_mode::kLaunchedAfterRebuild); // Launch without activating (kLSLaunchDontSwitch). base::mac::OpenApplicationWithPath( shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL); } base::FilePath GetAppLoaderPath() { return base::mac::PathForFrameworkBundleResource( base::mac::NSToCFCast(@"app_mode_loader.app")); } void UpdatePlatformShortcutsInternal( const base::FilePath& app_data_path, const base::string16& old_app_title, const web_app::ShortcutInfo& shortcut_info, const extensions::FileHandlersInfo& file_handlers_info) { DCHECK_CURRENTLY_ON(content::BrowserThread::FILE); if (AppShimsDisabledForTest() && !g_app_shims_allow_update_and_launch_in_tests) { return; } web_app::WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info, file_handlers_info); shortcut_creator.UpdateShortcuts(); } void UpdateAndLaunchShimOnFileThread( scoped_ptr shortcut_info, const extensions::FileHandlersInfo& file_handlers_info) { base::FilePath shortcut_data_dir = web_app::GetWebAppDataDirectory( shortcut_info->profile_path, shortcut_info->extension_id, GURL()); UpdatePlatformShortcutsInternal(shortcut_data_dir, base::string16(), *shortcut_info, file_handlers_info); LaunchShimOnFileThread(shortcut_info.Pass(), true); } void UpdateAndLaunchShim( scoped_ptr shortcut_info, const extensions::FileHandlersInfo& file_handlers_info) { content::BrowserThread::PostTask( content::BrowserThread::FILE, FROM_HERE, base::Bind(&UpdateAndLaunchShimOnFileThread, base::Passed(&shortcut_info), file_handlers_info)); } void RebuildAppAndLaunch(scoped_ptr shortcut_info) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); if (shortcut_info->extension_id == app_mode::kAppListModeId) { AppListService* app_list_service = AppListService::Get(chrome::HOST_DESKTOP_TYPE_NATIVE); app_list_service->CreateShortcut(); app_list_service->Show(); return; } ProfileManager* profile_manager = g_browser_process->profile_manager(); Profile* profile = profile_manager->GetProfileByPath(shortcut_info->profile_path); if (!profile || !profile_manager->IsValidProfile(profile)) return; extensions::ExtensionRegistry* registry = extensions::ExtensionRegistry::Get(profile); const extensions::Extension* extension = registry->GetExtensionById( shortcut_info->extension_id, extensions::ExtensionRegistry::ENABLED); if (!extension || !extension->is_platform_app()) return; web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim)); } base::FilePath GetLocalizableAppShortcutsSubdirName() { static const char kChromiumAppDirName[] = "Chromium Apps.localized"; static const char kChromeAppDirName[] = "Chrome Apps.localized"; static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized"; switch (chrome::VersionInfo::GetChannel()) { case chrome::VersionInfo::CHANNEL_UNKNOWN: return base::FilePath(kChromiumAppDirName); case chrome::VersionInfo::CHANNEL_CANARY: return base::FilePath(kChromeCanaryAppDirName); default: return base::FilePath(kChromeAppDirName); } } // Creates a canvas the same size as |overlay|, copies the appropriate // representation from |backgound| into it (according to Cocoa), then draws // |overlay| over it using NSCompositeSourceOver. NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) { DCHECK(background); NSInteger dimension = [overlay pixelsWide]; DCHECK_EQ(dimension, [overlay pixelsHigh]); base::scoped_nsobject canvas([[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:dimension pixelsHigh:dimension bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bytesPerRow:0 bitsPerPixel:0]); // There isn't a colorspace name constant for sRGB, so retag. NSBitmapImageRep* srgb_canvas = [canvas bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]]; canvas.reset([srgb_canvas retain]); // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI. [canvas setSize:NSMakeSize(dimension, dimension)]; NSGraphicsContext* drawing_context = [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:drawing_context]; [background drawInRect:NSMakeRect(0, 0, dimension, dimension) fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0]; [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:NO hints:0]; [NSGraphicsContext restoreGraphicsState]; return canvas.autorelease(); } // Helper function to extract the single NSImageRep held in a resource bundle // image. NSImageRep* ImageRepForResource(int resource_id) { gfx::Image& image = ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id); NSArray* image_reps = [image.AsNSImage() representations]; DCHECK_EQ(1u, [image_reps count]); return [image_reps objectAtIndex:0]; } // Adds a localized strings file for the Chrome Apps directory using the current // locale. OSX will use this for the display name. // + Chrome Apps.localized (|apps_directory|) // | + .localized // | | en.strings // | | de.strings void UpdateAppShortcutsSubdirLocalizedName( const base::FilePath& apps_directory) { base::FilePath localized = apps_directory.Append(".localized"); if (!base::CreateDirectory(localized)) return; base::FilePath directory_name = apps_directory.BaseName().RemoveExtension(); base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName(); NSDictionary* strings_dict = @{ base::mac::FilePathToNSString(directory_name) : base::SysUTF16ToNSString(localized_name) }; std::string locale = l10n_util::NormalizeLocale( l10n_util::GetApplicationLocale(std::string())); NSString* strings_path = base::mac::FilePathToNSString( localized.Append(locale + ".strings")); [strings_dict writeToFile:strings_path atomically:YES]; base::scoped_nsobject folder_icon_image([[NSImage alloc] init]); // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a // bug when dealing with named NSImages where it incorrectly handles alpha // premultiplication. This is most noticable with small assets since the 1px // border is a much larger component of the small icons. // See http://crbug.com/305373 for details. [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)]; [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)]; // Brand larger folder assets with an embossed app launcher logo to conserve // distro size and for better consistency with changing hue across OSX // versions. The folder is textured, so compresses poorly without this. const int kBrandResourceIds[] = { IDR_APPS_FOLDER_OVERLAY_128, IDR_APPS_FOLDER_OVERLAY_512, }; NSImage* base_image = [NSImage imageNamed:NSImageNameFolder]; for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) { NSImageRep* with_overlay = OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i])); DCHECK(with_overlay); if (with_overlay) [folder_icon_image addRepresentation:with_overlay]; } [[NSWorkspace sharedWorkspace] setIcon:folder_icon_image forFile:base::mac::FilePathToNSString(apps_directory) options:0]; } void DeletePathAndParentIfEmpty(const base::FilePath& app_path) { DCHECK(!app_path.empty()); base::DeleteFile(app_path, true); base::FilePath apps_folder = app_path.DirName(); if (base::IsDirectoryEmpty(apps_folder)) base::DeleteFile(apps_folder, false); } bool IsShimForProfile(const base::FilePath& base_name, const std::string& profile_base_name) { if (!base::StartsWith(base_name.value(), profile_base_name, base::CompareCase::SENSITIVE)) return false; if (base_name.Extension() != ".app") return false; std::string app_id = base_name.RemoveExtension().value(); // Strip (profile_base_name + " ") from the start. app_id = app_id.substr(profile_base_name.size() + 1); return crx_file::id_util::IdIsValid(app_id); } std::vector GetAllAppBundlesInPath( const base::FilePath& internal_shortcut_path, const std::string& profile_base_name) { std::vector bundle_paths; base::FileEnumerator enumerator(internal_shortcut_path, true /* recursive */, base::FileEnumerator::DIRECTORIES); for (base::FilePath bundle_path = enumerator.Next(); !bundle_path.empty(); bundle_path = enumerator.Next()) { if (IsShimForProfile(bundle_path.BaseName(), profile_base_name)) bundle_paths.push_back(bundle_path); } return bundle_paths; } scoped_ptr BuildShortcutInfoFromBundle( const base::FilePath& bundle_path) { NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); scoped_ptr shortcut_info(new web_app::ShortcutInfo); shortcut_info->extension_id = base::SysNSStringToUTF8( [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]); shortcut_info->is_platform_app = true; shortcut_info->url = GURL(base::SysNSStringToUTF8( [plist valueForKey:app_mode::kCrAppModeShortcutURLKey])); shortcut_info->title = base::SysNSStringToUTF16( [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]); shortcut_info->profile_name = base::SysNSStringToUTF8( [plist valueForKey:app_mode::kCrAppModeProfileNameKey]); // Figure out the profile_path. Since the user_data_dir could contain the // path to the web app data dir. base::FilePath user_data_dir = base::mac::NSStringToFilePath( [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]); base::FilePath profile_base_name = base::mac::NSStringToFilePath( [plist valueForKey:app_mode::kCrAppModeProfileDirKey]); if (user_data_dir.DirName().DirName().BaseName() == profile_base_name) shortcut_info->profile_path = user_data_dir.DirName().DirName(); else shortcut_info->profile_path = user_data_dir.Append(profile_base_name); return shortcut_info; } scoped_ptr RecordAppShimErrorAndBuildShortcutInfo( const base::FilePath& bundle_path) { NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); NSString* version_string = [plist valueForKey:app_mode::kCrBundleVersionKey]; if (!version_string) { // Older bundles have the Chrome version in the following key. version_string = [plist valueForKey:app_mode::kCFBundleShortVersionStringKey]; } base::Version full_version(base::SysNSStringToUTF8(version_string)); uint32_t major_version = 0; if (full_version.IsValid()) major_version = full_version.components()[0]; UMA_HISTOGRAM_SPARSE_SLOWLY("Apps.AppShimErrorVersion", major_version); return BuildShortcutInfoFromBundle(bundle_path); } void UpdateFileTypes(NSMutableDictionary* plist, const extensions::FileHandlersInfo& file_handlers_info) { NSMutableArray* document_types = [NSMutableArray arrayWithCapacity:file_handlers_info.size()]; for (extensions::FileHandlersInfo::const_iterator info_it = file_handlers_info.begin(); info_it != file_handlers_info.end(); ++info_it) { const extensions::FileHandlerInfo& info = *info_it; NSMutableArray* file_extensions = [NSMutableArray arrayWithCapacity:info.extensions.size()]; for (std::set::iterator it = info.extensions.begin(); it != info.extensions.end(); ++it) { [file_extensions addObject:base::SysUTF8ToNSString(*it)]; } NSMutableArray* mime_types = [NSMutableArray arrayWithCapacity:info.types.size()]; for (std::set::iterator it = info.types.begin(); it != info.types.end(); ++it) { [mime_types addObject:base::SysUTF8ToNSString(*it)]; } NSDictionary* type_dictionary = @{ // TODO(jackhou): Add the type name and and icon file once the manifest // supports these. // app_mode::kCFBundleTypeNameKey : , // app_mode::kCFBundleTypeIconFileKey : , app_mode::kCFBundleTypeExtensionsKey : file_extensions, app_mode::kCFBundleTypeMIMETypesKey : mime_types, app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer }; [document_types addObject:type_dictionary]; } [plist setObject:document_types forKey:app_mode::kCFBundleDocumentTypesKey]; } void RevealAppShimInFinderForAppOnFileThread( scoped_ptr shortcut_info, const base::FilePath& app_path) { web_app::WebAppShortcutCreator shortcut_creator( app_path, shortcut_info.get(), extensions::FileHandlersInfo()); shortcut_creator.RevealAppShimInFinder(); } } // namespace @interface CrCreateAppShortcutCheckboxObserver : NSObject { @private NSButton* checkbox_; NSButton* continueButton_; } - (id)initWithCheckbox:(NSButton*)checkbox continueButton:(NSButton*)continueButton; - (void)startObserving; - (void)stopObserving; @end @implementation CrCreateAppShortcutCheckboxObserver - (id)initWithCheckbox:(NSButton*)checkbox continueButton:(NSButton*)continueButton { if ((self = [super init])) { checkbox_ = checkbox; continueButton_ = continueButton; } return self; } - (void)startObserving { [checkbox_ addObserver:self forKeyPath:@"cell.state" options:0 context:nil]; } - (void)stopObserving { [checkbox_ removeObserver:self forKeyPath:@"cell.state"]; } - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { [continueButton_ setEnabled:([checkbox_ state] == NSOnState)]; } @end namespace web_app { WebAppShortcutCreator::WebAppShortcutCreator( const base::FilePath& app_data_dir, const ShortcutInfo* shortcut_info, const extensions::FileHandlersInfo& file_handlers_info) : app_data_dir_(app_data_dir), info_(shortcut_info), file_handlers_info_(file_handlers_info) { DCHECK(shortcut_info); } WebAppShortcutCreator::~WebAppShortcutCreator() {} base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const { base::FilePath applications_dir = GetApplicationsDirname(); return applications_dir.empty() ? base::FilePath() : applications_dir.Append(GetShortcutBasename()); } base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const { return app_data_dir_.Append(GetShortcutBasename()); } base::FilePath WebAppShortcutCreator::GetShortcutBasename() const { std::string app_name; // Check if there should be a separate shortcut made for different profiles. // Such shortcuts will have a |profile_name| set on the ShortcutInfo, // otherwise it will be empty. if (!info_->profile_name.empty()) { app_name += info_->profile_path.BaseName().value(); app_name += ' '; } app_name += info_->extension_id; return base::FilePath(app_name).ReplaceExtension("app"); } bool WebAppShortcutCreator::BuildShortcut( const base::FilePath& staging_path) const { // Update the app's plist and icon in a temp directory. This works around // a Finder bug where the app's icon doesn't properly update. if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) { LOG(ERROR) << "Copying app to staging path: " << staging_path.value() << " failed."; return false; } return UpdatePlist(staging_path) && UpdateDisplayName(staging_path) && UpdateIcon(staging_path); } size_t WebAppShortcutCreator::CreateShortcutsIn( const std::vector& folders) const { size_t succeeded = 0; base::ScopedTempDir scoped_temp_dir; if (!scoped_temp_dir.CreateUniqueTempDir()) return 0; base::FilePath app_name = GetShortcutBasename(); base::FilePath staging_path = scoped_temp_dir.path().Append(app_name); if (!BuildShortcut(staging_path)) return 0; for (std::vector::const_iterator it = folders.begin(); it != folders.end(); ++it) { const base::FilePath& dst_path = *it; if (!base::CreateDirectory(dst_path)) { LOG(ERROR) << "Creating directory " << dst_path.value() << " failed."; return succeeded; } // Ensure the copy does not merge with stale info. base::DeleteFile(dst_path.Append(app_name), true); if (!base::CopyDirectory(staging_path, dst_path, true)) { LOG(ERROR) << "Copying app to dst path: " << dst_path.value() << " failed"; return succeeded; } // Remove the quarantine attribute from both the bundle and the executable. base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name)); base::mac::RemoveQuarantineAttribute( dst_path.Append(app_name) .Append("Contents").Append("MacOS").Append("app_mode_loader")); ++succeeded; } return succeeded; } bool WebAppShortcutCreator::CreateShortcuts( ShortcutCreationReason creation_reason, ShortcutLocations creation_locations) { const base::FilePath applications_dir = GetApplicationsDirname(); if (applications_dir.empty() || !base::DirectoryExists(applications_dir.DirName())) { LOG(ERROR) << "Couldn't find an Applications directory to copy app to."; return false; } UpdateAppShortcutsSubdirLocalizedName(applications_dir); // If non-nil, this path is added to the OSX Dock after creating shortcuts. NSString* path_to_add_to_dock = nil; std::vector paths; // The app list shim is not tied to a particular profile, so omit the copy // placed under the profile path. For shims, this copy is used when the // version under Applications is removed, and not needed for app list because // setting LSUIElement means there is no Dock "running" status to show. const bool is_app_list = info_->extension_id == app_mode::kAppListModeId; if (is_app_list) { path_to_add_to_dock = base::SysUTF8ToNSString( applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe()); } else { paths.push_back(app_data_dir_); } bool shortcut_visible = creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN; if (shortcut_visible) paths.push_back(applications_dir); DCHECK(!paths.empty()); size_t success_count = CreateShortcutsIn(paths); if (success_count == 0) return false; if (!is_app_list) UpdateInternalBundleIdentifier(); if (success_count != paths.size()) return false; if (creation_locations.in_quick_launch_bar && path_to_add_to_dock && shortcut_visible) { switch (dock::AddIcon(path_to_add_to_dock, nil)) { case dock::IconAddFailure: // If adding the icon failed, instead reveal the Finder window. RevealAppShimInFinder(); break; case dock::IconAddSuccess: case dock::IconAlreadyPresent: break; } return true; } if (creation_reason == SHORTCUT_CREATION_BY_USER) RevealAppShimInFinder(); return true; } void WebAppShortcutCreator::DeleteShortcuts() { base::FilePath app_path = GetApplicationsShortcutPath(); if (!app_path.empty() && HasSameUserDataDir(app_path)) DeletePathAndParentIfEmpty(app_path); // In case the user has moved/renamed/copied the app bundle. base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier()); if (!bundle_path.empty() && HasSameUserDataDir(bundle_path)) base::DeleteFile(bundle_path, true); // Delete the internal one. DeletePathAndParentIfEmpty(GetInternalShortcutPath()); } bool WebAppShortcutCreator::UpdateShortcuts() { std::vector paths; paths.push_back(app_data_dir_); // Try to update the copy under /Applications. If that does not exist, check // if a matching bundle can be found elsewhere. base::FilePath app_path = GetApplicationsShortcutPath(); if (app_path.empty() || !base::PathExists(app_path)) app_path = GetAppBundleById(GetBundleIdentifier()); if (!app_path.empty()) paths.push_back(app_path.DirName()); size_t success_count = CreateShortcutsIn(paths); if (success_count == 0) return false; UpdateInternalBundleIdentifier(); return success_count == paths.size() && !app_path.empty(); } base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const { base::FilePath path = GetWritableApplicationsDirectory(); if (path.empty()) return path; return path.Append(GetLocalizableAppShortcutsSubdirName()); } bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const { NSString* extension_id = base::SysUTF8ToNSString(info_->extension_id); NSString* extension_title = base::SysUTF16ToNSString(info_->title); NSString* extension_url = base::SysUTF8ToNSString(info_->url.spec()); NSString* chrome_bundle_id = base::SysUTF8ToNSString(base::mac::BaseBundleID()); NSDictionary* replacement_dict = [NSDictionary dictionaryWithObjectsAndKeys: extension_id, app_mode::kShortcutIdPlaceholder, extension_title, app_mode::kShortcutNamePlaceholder, extension_url, app_mode::kShortcutURLPlaceholder, chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder, nil]; NSString* plist_path = GetPlistPath(app_path); NSMutableDictionary* plist = ReadPlist(plist_path); NSArray* keys = [plist allKeys]; // 1. Fill in variables. for (id key in keys) { NSString* value = [plist valueForKey:key]; if (![value isKindOfClass:[NSString class]] || [value length] < 2) continue; // Remove leading and trailing '@'s. NSString* variable = [value substringWithRange:NSMakeRange(1, [value length] - 2)]; NSString* substitution = [replacement_dict valueForKey:variable]; if (substitution) [plist setObject:substitution forKey:key]; } // 2. Fill in other values. [plist setObject:base::SysUTF8ToNSString(chrome::VersionInfo().Version()) forKey:app_mode::kCrBundleVersionKey]; [plist setObject:base::SysUTF8ToNSString(info_->version_for_display) forKey:app_mode::kCFBundleShortVersionStringKey]; [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier()) forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; [plist setObject:base::mac::FilePathToNSString(app_data_dir_) forKey:app_mode::kCrAppModeUserDataDirKey]; [plist setObject:base::mac::FilePathToNSString(info_->profile_path.BaseName()) forKey:app_mode::kCrAppModeProfileDirKey]; [plist setObject:base::SysUTF8ToNSString(info_->profile_name) forKey:app_mode::kCrAppModeProfileNameKey]; [plist setObject:[NSNumber numberWithBool:YES] forKey:app_mode::kLSHasLocalizedDisplayNameKey]; if (info_->extension_id == app_mode::kAppListModeId) { // Prevent the app list from bouncing in the dock, and getting a run light. [plist setObject:[NSNumber numberWithBool:YES] forKey:kLSUIElement]; } base::FilePath app_name = app_path.BaseName().RemoveExtension(); [plist setObject:base::mac::FilePathToNSString(app_name) forKey:base::mac::CFToNSCast(kCFBundleNameKey)]; if (base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnableAppsFileAssociations)) { UpdateFileTypes(plist, file_handlers_info_); } return [plist writeToFile:plist_path atomically:YES]; } bool WebAppShortcutCreator::UpdateDisplayName( const base::FilePath& app_path) const { // Localization is used to display the app name (rather than the bundle // filename). OSX searches for the best language in the order of preferred // languages, but one of them must be found otherwise it will default to // the filename. NSString* language = [[NSLocale preferredLanguages] objectAtIndex:0]; base::FilePath localized_dir = GetResourcesPath(app_path).Append( base::SysNSStringToUTF8(language) + ".lproj"); if (!base::CreateDirectory(localized_dir)) return false; NSString* bundle_name = base::SysUTF16ToNSString(info_->title); NSString* display_name = base::SysUTF16ToNSString(info_->title); if (HasExistingExtensionShim(GetApplicationsDirname(), info_->extension_id, app_path.BaseName())) { display_name = [bundle_name stringByAppendingString:base::SysUTF8ToNSString( " (" + info_->profile_name + ")")]; } NSDictionary* strings_plist = @{ base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name, app_mode::kCFBundleDisplayNameKey : display_name }; NSString* localized_path = base::mac::FilePathToNSString( localized_dir.Append("InfoPlist.strings")); return [strings_plist writeToFile:localized_path atomically:YES]; } bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const { if (info_->favicon.empty()) return true; ScopedCarbonHandle icon_family(0); bool image_added = false; for (gfx::ImageFamily::const_iterator it = info_->favicon.begin(); it != info_->favicon.end(); ++it) { if (it->IsEmpty()) continue; // Missing an icon size is not fatal so don't fail if adding the bitmap // doesn't work. if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it)) continue; image_added = true; } if (!image_added) return false; base::FilePath resources_path = GetResourcesPath(app_path); if (!base::CreateDirectory(resources_path)) return false; return icon_family.WriteDataToFile(resources_path.Append("app.icns")); } bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const { NSString* plist_path = GetPlistPath(GetInternalShortcutPath()); NSMutableDictionary* plist = ReadPlist(plist_path); [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier()) forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; return [plist writeToFile:plist_path atomically:YES]; } base::FilePath WebAppShortcutCreator::GetAppBundleById( const std::string& bundle_id) const { base::ScopedCFTypeRef bundle_id_cf( base::SysUTF8ToCFStringRef(bundle_id)); CFURLRef url_ref = NULL; OSStatus status = LSFindApplicationForInfo( kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref); if (status != noErr) return base::FilePath(); base::ScopedCFTypeRef url(url_ref); NSString* path_string = [base::mac::CFToNSCast(url.get()) path]; return base::FilePath([path_string fileSystemRepresentation]); } std::string WebAppShortcutCreator::GetBundleIdentifier() const { // Replace spaces in the profile path with hyphen. std::string normalized_profile_path; base::ReplaceChars(info_->profile_path.BaseName().value(), " ", "-", &normalized_profile_path); // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp. std::string bundle_id = base::mac::BaseBundleID() + std::string(".app.") + normalized_profile_path + "-" + info_->extension_id; return bundle_id; } std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const { return GetBundleIdentifier() + "-internal"; } void WebAppShortcutCreator::RevealAppShimInFinder() const { base::FilePath app_path = GetApplicationsShortcutPath(); if (app_path.empty()) return; // Check if the app shim exists. if (base::PathExists(app_path)) { // Use selectFile to show the contents of parent directory with the app // shim selected. [[NSWorkspace sharedWorkspace] selectFile:base::mac::FilePathToNSString(app_path) inFileViewerRootedAtPath:nil]; return; } // Otherwise, go up a directory. app_path = app_path.DirName(); // Check if the Chrome apps folder exists, otherwise go up to ~/Applications. if (!base::PathExists(app_path)) app_path = app_path.DirName(); // Since |app_path| is a directory, use openFile to show the contents of // that directory in Finder. [[NSWorkspace sharedWorkspace] openFile:base::mac::FilePathToNSString(app_path)]; } base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) { WebAppShortcutCreator shortcut_creator(base::FilePath(), &shortcut_info, extensions::FileHandlersInfo()); return shortcut_creator.GetApplicationsShortcutPath(); } void MaybeLaunchShortcut(scoped_ptr shortcut_info) { if (AppShimsDisabledForTest() && !g_app_shims_allow_update_and_launch_in_tests) { return; } content::BrowserThread::PostTask( content::BrowserThread::FILE, FROM_HERE, base::Bind(&LaunchShimOnFileThread, base::Passed(&shortcut_info), false)); } bool MaybeRebuildShortcut(const base::CommandLine& command_line) { if (!command_line.HasSwitch(app_mode::kAppShimError)) return false; base::PostTaskAndReplyWithResult( content::BrowserThread::GetBlockingPool(), FROM_HERE, base::Bind(&RecordAppShimErrorAndBuildShortcutInfo, command_line.GetSwitchValuePath(app_mode::kAppShimError)), base::Bind(&RebuildAppAndLaunch)); return true; } // Called when the app's ShortcutInfo (with icon) is loaded when creating app // shortcuts. void CreateAppShortcutInfoLoaded( Profile* profile, const extensions::Extension* app, const base::Callback& close_callback, scoped_ptr shortcut_info) { base::scoped_nsobject alert([[NSAlert alloc] init]); NSButton* continue_button = [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)]; [continue_button setKeyEquivalent:kKeyEquivalentReturn]; NSButton* cancel_button = [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)]; [cancel_button setKeyEquivalent:kKeyEquivalentEscape]; [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)]; [alert setAlertStyle:NSInformationalAlertStyle]; base::scoped_nsobject application_folder_checkbox( [[NSButton alloc] initWithFrame:NSZeroRect]); [application_folder_checkbox setButtonType:NSSwitchButton]; [application_folder_checkbox setTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_APP_FOLDER_CHKBOX)]; [application_folder_checkbox setState:NSOnState]; [application_folder_checkbox sizeToFit]; base::scoped_nsobject checkbox_observer( [[CrCreateAppShortcutCheckboxObserver alloc] initWithCheckbox:application_folder_checkbox continueButton:continue_button]); [checkbox_observer startObserving]; [alert setAccessoryView:application_folder_checkbox]; const int kIconPreviewSizePixels = 128; const int kIconPreviewTargetSize = 64; const gfx::Image* icon = shortcut_info->favicon.GetBest( kIconPreviewSizePixels, kIconPreviewSizePixels); if (icon && !icon->IsEmpty()) { NSImage* icon_image = icon->ToNSImage(); [icon_image setSize:NSMakeSize(kIconPreviewTargetSize, kIconPreviewTargetSize)]; [alert setIcon:icon_image]; } bool dialog_accepted = false; if ([alert runModal] == NSAlertFirstButtonReturn && [application_folder_checkbox state] == NSOnState) { dialog_accepted = true; CreateShortcuts( SHORTCUT_CREATION_BY_USER, ShortcutLocations(), profile, app); } [checkbox_observer stopObserving]; if (!close_callback.is_null()) close_callback.Run(dialog_accepted); } void UpdateShortcutsForAllApps(Profile* profile, const base::Closure& callback) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); extensions::ExtensionRegistry* registry = extensions::ExtensionRegistry::Get(profile); if (!registry) return; // Update all apps. scoped_ptr everything = registry->GenerateInstalledExtensionsSet(); for (extensions::ExtensionSet::const_iterator it = everything->begin(); it != everything->end(); ++it) { if (web_app::ShouldCreateShortcutFor(SHORTCUT_CREATION_AUTOMATED, profile, it->get())) { web_app::UpdateAllShortcuts(base::string16(), profile, it->get()); } } callback.Run(); } void RevealAppShimInFinderForApp(Profile* profile, const extensions::Extension* app) { scoped_ptr shortcut_info = ShortcutInfoForExtensionAndProfile(app, profile); content::BrowserThread::PostTask( content::BrowserThread::FILE, FROM_HERE, base::Bind(&RevealAppShimInFinderForAppOnFileThread, base::Passed(&shortcut_info), app->path())); } namespace internals { bool CreatePlatformShortcuts( const base::FilePath& app_data_path, scoped_ptr shortcut_info, const extensions::FileHandlersInfo& file_handlers_info, const ShortcutLocations& creation_locations, ShortcutCreationReason creation_reason) { DCHECK_CURRENTLY_ON(content::BrowserThread::FILE); if (AppShimsDisabledForTest()) return true; WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info.get(), file_handlers_info); return shortcut_creator.CreateShortcuts(creation_reason, creation_locations); } void DeletePlatformShortcuts(const base::FilePath& app_data_path, scoped_ptr shortcut_info) { DCHECK_CURRENTLY_ON(content::BrowserThread::FILE); WebAppShortcutCreator shortcut_creator(app_data_path, shortcut_info.get(), extensions::FileHandlersInfo()); shortcut_creator.DeleteShortcuts(); } void UpdatePlatformShortcuts( const base::FilePath& app_data_path, const base::string16& old_app_title, scoped_ptr shortcut_info, const extensions::FileHandlersInfo& file_handlers_info) { UpdatePlatformShortcutsInternal(app_data_path, old_app_title, *shortcut_info, file_handlers_info); } void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) { const std::string profile_base_name = profile_path.BaseName().value(); std::vector bundles = GetAllAppBundlesInPath( profile_path.Append(chrome::kWebAppDirname), profile_base_name); for (std::vector::const_iterator it = bundles.begin(); it != bundles.end(); ++it) { scoped_ptr shortcut_info = BuildShortcutInfoFromBundle(*it); WebAppShortcutCreator shortcut_creator(it->DirName(), shortcut_info.get(), extensions::FileHandlersInfo()); shortcut_creator.DeleteShortcuts(); } } } // namespace internals } // namespace web_app namespace chrome { void ShowCreateChromeAppShortcutsDialog( gfx::NativeWindow /*parent_window*/, Profile* profile, const extensions::Extension* app, const base::Callback& close_callback) { web_app::GetShortcutInfoForApp( app, profile, base::Bind(&web_app::CreateAppShortcutInfoLoaded, profile, app, close_callback)); } } // namespace chrome