blob: 51022c41cf1fbe1d9e2c71c14ae80e7471953f18 [file] [log] [blame]
// 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 <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>
#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/channel_info.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#import "chrome/common/mac/app_mode_common.h"
#include "chrome/grit/generated_resources.h"
#include "components/crx_file/id_util.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_switches.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<IconFamilyHandle>(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,
base::CompareCase::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<web_app::ShortcutInfo> 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<web_app::ShortcutInfo> 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<web_app::ShortcutInfo> 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<web_app::ShortcutInfo> 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::GetChannel()) {
case version_info::Channel::UNKNOWN:
return base::FilePath(kChromiumAppDirName);
case version_info::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<NSBitmapImageRep> 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<NSImage> 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<base::FilePath> GetAllAppBundlesInPath(
const base::FilePath& internal_shortcut_path,
const std::string& profile_base_name) {
std::vector<base::FilePath> 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<web_app::ShortcutInfo> BuildShortcutInfoFromBundle(
const base::FilePath& bundle_path) {
NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
scoped_ptr<web_app::ShortcutInfo> 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<web_app::ShortcutInfo> 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<std::string>::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<std::string>::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<web_app::ShortcutInfo> 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<base::FilePath>& 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<base::FilePath>::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<base::FilePath> 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<base::FilePath> 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()) {
if (info_->from_bookmark) {
// The bookmark app shortcut has been deleted by the user. Restore it, as
// the Mac UI for bookmark apps creates the expectation that the app will
// be added to Applications.
app_path = GetApplicationsDirname();
paths.push_back(app_path);
}
} else {
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(version_info::GetVersionNumber())
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<CFStringRef> 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<CFURLRef> 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:@""];
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<ShortcutInfo> 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<void(bool)>& close_callback,
scoped_ptr<ShortcutInfo> shortcut_info) {
base::scoped_nsobject<NSAlert> 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<NSButton> 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<CrCreateAppShortcutCheckboxObserver> 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<extensions::ExtensionSet> 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<web_app::ShortcutInfo> 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<ShortcutInfo> 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<ShortcutInfo> 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<ShortcutInfo> 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<base::FilePath> bundles = GetAllAppBundlesInPath(
profile_path.Append(chrome::kWebAppDirname), profile_base_name);
for (std::vector<base::FilePath>::const_iterator it = bundles.begin();
it != bundles.end(); ++it) {
scoped_ptr<web_app::ShortcutInfo> 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<void(bool)>& close_callback) {
web_app::GetShortcutInfoForApp(
app,
profile,
base::Bind(&web_app::CreateAppShortcutInfoLoaded,
profile,
app,
close_callback));
}
} // namespace chrome