blob: 2fc0d6b3f182096aef2800fa493cc9cd71c354f7 [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/components/web_app_shortcut_mac.h"
#import <Cocoa/Cocoa.h>
#include <stdint.h>
#include <algorithm>
#include <list>
#include <map>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.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/bundle_locations.h"
#include "base/mac/foundation_util.h"
#import "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/macros.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.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/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/version.h"
#import "chrome/browser/mac/dock.h"
#include "chrome/browser/shell_integration.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/chrome_unscaled_resources.h"
#include "components/crx_file/id_util.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_switches.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 "third_party/skia/include/utils/mac/SkCGUtils.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_family.h"
// A TerminationObserver observes a NSRunningApplication for when it
// terminates. On termination, it will run the specified callback on the UI
// thread and release itself.
@interface TerminationObserver : NSObject {
base::scoped_nsobject<NSRunningApplication> _app;
base::OnceClosure _callback;
}
- (id)initWithRunningApplication:(NSRunningApplication*)app
callback:(base::OnceClosure)callback;
@end
@implementation TerminationObserver
- (id)initWithRunningApplication:(NSRunningApplication*)app
callback:(base::OnceClosure)callback {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (self = [super init]) {
_callback = std::move(callback);
_app.reset(app, base::scoped_policy::RETAIN);
// Note that |observeValueForKeyPath| will be called with the initial value
// within the |addObserver| call.
[_app addObserver:self
forKeyPath:@"isTerminated"
options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionInitial
context:nullptr];
}
return self;
}
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
NSNumber* newNumberValue = [change objectForKey:NSKeyValueChangeNewKey];
BOOL newValue = [newNumberValue boolValue];
if (newValue) {
base::scoped_nsobject<TerminationObserver> scoped_self(
self, base::scoped_policy::RETAIN);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(
[](base::scoped_nsobject<TerminationObserver> observer) {
[observer onTerminated];
},
scoped_self));
}
}
- (void)onTerminated {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// If |onTerminated| is called repeatedly (which in theory it should not),
// then ensure that we only call removeObserver and release once by doing an
// early-out if |callback_| has already been made.
if (!_callback)
return;
std::move(_callback).Run();
DCHECK(!_callback);
[_app removeObserver:self forKeyPath:@"isTerminated" context:nullptr];
[self release];
}
@end
// TODO(https://crbug.com/941909): Change all launch functions to take a single
// callback that returns a NSRunningApplication, rather than separate launch and
// termination callbacks.
void RunAppLaunchCallbacks(
base::scoped_nsobject<NSRunningApplication> app,
base::OnceCallback<void(base::Process)> launch_callback,
base::OnceClosure termination_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(app);
// If the app doesn't have a valid pid, or if the application has been
// terminated, then indicate failure in |launch_callback|.
base::Process process([app processIdentifier]);
if (!process.IsValid() || [app isTerminated]) {
std::move(launch_callback).Run(base::Process());
return;
}
// Otherwise, indicate successful launch, and watch for termination.
// TODO(https://crbug.com/941909): This watches for termination indefinitely,
// but we only need to watch for termination until the app establishes a
// (whereupon termination will be noticed by the mojo connection closing).
std::move(launch_callback).Run(std::move(process));
[[TerminationObserver alloc]
initWithRunningApplication:app
callback:std::move(termination_callback)];
}
bool g_app_shims_allow_update_and_launch_in_tests = false;
namespace web_app {
namespace {
// UMA metric name for creating shortcut result.
constexpr const char* kCreateShortcutResult = "Apps.CreateShortcuts.Mac.Result";
// Result of creating app shortcut.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class CreateShortcutResult {
kSuccess = 0,
kApplicationDirNotFound = 1,
kFailToLocalizeApplication = 2,
kFailToGetApplicationPaths = 3,
kFailToCreateTempDir = 4,
kStagingDirectoryNotExist = 5,
kFailToCreateExecutablePath = 6,
kFailToCopyExecutablePath = 7,
kFailToCopyPlist = 8,
kFailToWritePkgInfoFile = 9,
kFailToUpdatePlist = 10,
kFailToUpdateDisplayName = 11,
kFailToUpdateIcon = 12,
kFailToCreateParentDir = 13,
kFailToCopyApp = 14,
kMaxValue = kFailToCopyApp
};
// Records the result of creating shortcut to UMA.
void RecordCreateShortcut(CreateShortcutResult result) {
UMA_HISTOGRAM_ENUMERATION(kCreateShortcutResult, result);
}
// The maximum number to append to to an app name before giving up and using the
// extension id.
constexpr int kMaxConflictNumber = 999;
// Writes |icons| to |path| in .icns format.
bool WriteIconsToFile(const std::vector<gfx::Image>& icons,
const base::FilePath& path) {
base::scoped_nsobject<NSMutableData> data(
[[NSMutableData alloc] initWithCapacity:0]);
base::ScopedCFTypeRef<CGImageDestinationRef> image_destination(
CGImageDestinationCreateWithData(base::mac::NSToCFCast(data),
kUTTypeAppleICNS, icons.size(),
nullptr));
DCHECK(image_destination);
for (const gfx::Image& image : icons) {
base::ScopedCFTypeRef<CGImageRef> cg_image(SkCreateCGImageRefWithColorspace(
image.AsBitmap(), base::mac::GetSRGBColorSpace()));
CGImageDestinationAddImage(image_destination, cg_image, nullptr);
}
if (!CGImageDestinationFinalize(image_destination)) {
NOTREACHED() << "CGImageDestinationFinalize failed.";
return false;
}
return [data writeToFile:base::mac::FilePathToNSString(path) atomically:NO];
}
// Returns true if |image| can be used for an icon resource.
bool IsImageValidForIcon(const gfx::Image& image) {
if (image.IsEmpty())
return false;
// 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;
}
switch (bitmap.width()) {
case 512:
case 256:
case 128:
case 48:
case 32:
case 16:
return true;
}
return false;
}
// Remove the leading . from the entries of |extensions|. Any items that do not
// have a leading . are removed.
std::set<std::string> GetFileHandlerExtensionsWithoutDot(
const std::set<std::string>& file_extensions) {
std::set<std::string> result;
for (const auto& file_extension : file_extensions) {
if (file_extension.length() <= 1 || file_extension[0] != '.')
continue;
result.insert(file_extension.substr(1));
}
return result;
}
bool AppShimCreationDisabledForTest() {
// 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");
}
// 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"));
}
// Data and helpers for an Info.plist under a given bundle path.
class BundleInfoPlist {
public:
// Retrieve info from all app shims found in |apps_path|.
static std::list<BundleInfoPlist> GetAllInPath(
const base::FilePath& apps_path,
bool recursive) {
std::list<BundleInfoPlist> bundles;
base::FileEnumerator enumerator(apps_path, recursive,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath shim_path = enumerator.Next(); !shim_path.empty();
shim_path = enumerator.Next()) {
bundles.emplace_back(shim_path);
}
return bundles;
}
// Retrieve info from the specified app shim in |bundle_path|.
explicit BundleInfoPlist(const base::FilePath& bundle_path)
: bundle_path_(bundle_path) {
NSString* plist_path = GetPlistPath(bundle_path_);
plist_.reset([NSDictionary dictionaryWithContentsOfFile:plist_path],
base::scoped_policy::RETAIN);
}
BundleInfoPlist(const BundleInfoPlist& other) = default;
BundleInfoPlist& operator=(const BundleInfoPlist& other) = default;
~BundleInfoPlist() = default;
const base::FilePath& bundle_path() const { return bundle_path_; }
// Checks that the CrAppModeUserDataDir in the Info.plist is, or is a subpath
// of 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 IsForCurrentUserDataDir() const {
base::FilePath user_data_dir;
base::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);
}
// Checks if kCrAppModeProfileDirKey corresponds to the specified profile
// path.
bool IsForProfile(const base::FilePath& test_profile_path) const {
std::string profile_path = base::SysNSStringToUTF8(
[plist_ valueForKey:app_mode::kCrAppModeProfileDirKey]);
return profile_path == test_profile_path.BaseName().value();
}
// Return the full profile path (as a subpath of the user_data_dir).
base::FilePath GetFullProfilePath() const {
// 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)
return user_data_dir.DirName().DirName();
return user_data_dir.Append(profile_base_name);
}
std::string GetExtensionId() const {
return base::SysNSStringToUTF8(
[plist_ valueForKey:app_mode::kCrAppModeShortcutIDKey]);
}
std::string GetProfileName() const {
return base::SysNSStringToUTF8(
[plist_ valueForKey:app_mode::kCrAppModeProfileNameKey]);
}
GURL GetURL() const {
return GURL(base::SysNSStringToUTF8(
[plist_ valueForKey:app_mode::kCrAppModeShortcutURLKey]));
}
base::string16 GetTitle() const {
return base::SysNSStringToUTF16(
[plist_ valueForKey:app_mode::kCrAppModeShortcutNameKey]);
}
base::Version GetVersion() const {
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];
}
return base::Version(base::SysNSStringToUTF8(version_string));
}
std::string GetBundleId() const {
return base::SysNSStringToUTF8(
[plist_ valueForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]);
}
private:
// The path of the app bundle from this this info was read.
base::FilePath bundle_path_;
// Data read from the Info.plist.
base::scoped_nsobject<NSDictionary> plist_;
};
bool HasExistingExtensionShimForDifferentProfile(
const base::FilePath& destination_directory,
const std::string& extension_id,
const base::FilePath& profile_dir) {
std::list<BundleInfoPlist> bundles_info = BundleInfoPlist::GetAllInPath(
destination_directory, false /* recursive */);
for (const auto& info : bundles_info) {
if (info.GetExtensionId() == extension_id &&
!info.IsForProfile(profile_dir)) {
return true;
}
}
return false;
}
void LaunchShimOnFileThread(LaunchShimUpdateBehavior update_behavior,
ShimLaunchedCallback launched_callback,
ShimTerminatedCallback terminated_callback,
const ShortcutInfo& shortcut_info) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
WebAppShortcutCreator shortcut_creator(
internals::GetShortcutDataDir(shortcut_info), &shortcut_info);
// Recreate shims if requested, and populate |shim_paths| with the paths to
// attempt to launch.
bool launched_after_rebuild = false;
std::vector<base::FilePath> shim_paths;
switch (update_behavior) {
case LaunchShimUpdateBehavior::DO_NOT_RECREATE:
// Attempt to locate the shim's path using LaunchServices.
shim_paths = shortcut_creator.GetAppBundlesById();
break;
case LaunchShimUpdateBehavior::RECREATE_IF_INSTALLED:
// Only attempt to launch shims that were updated.
launched_after_rebuild = true;
shortcut_creator.UpdateShortcuts(false /* create_if_needed */,
&shim_paths);
break;
case LaunchShimUpdateBehavior::RECREATE_UNCONDITIONALLY:
// Likewise, only attempt to launch shims that were updated.
launched_after_rebuild = true;
shortcut_creator.UpdateShortcuts(true /* create_if_needed */,
&shim_paths);
break;
}
// Attempt to launch the shim.
for (const auto& shim_path : shim_paths) {
if (!base::PathExists(shim_path))
continue;
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
command_line.AppendSwitchASCII(
app_mode::kLaunchedByChromeProcessId,
base::NumberToString(base::GetCurrentProcId()));
if (launched_after_rebuild)
command_line.AppendSwitch(app_mode::kLaunchedAfterRebuild);
// Launch without activating (NSWorkspaceLaunchWithoutActivation).
base::scoped_nsobject<NSRunningApplication> app(
base::mac::OpenApplicationWithPath(
shim_path, command_line,
NSWorkspaceLaunchDefault | NSWorkspaceLaunchWithoutActivation),
base::scoped_policy::RETAIN);
if (app) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&RunAppLaunchCallbacks, app,
std::move(launched_callback),
std::move(terminated_callback)));
return;
}
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(launched_callback), base::Process()));
}
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);
}
}
base::FilePath* GetOverriddenApplicationsFolder() {
static base::NoDestructor<base::FilePath> overridden_path;
return overridden_path.get();
}
// 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.
base::scoped_nsobject<NSImageRep> ImageRepForGFXImage(const gfx::Image& image) {
NSArray* image_reps = [image.AsNSImage() representations];
DCHECK_EQ(1u, [image_reps count]);
return base::scoped_nsobject<NSImageRep>([image_reps objectAtIndex:0],
base::scoped_policy::RETAIN);
}
using ResourceIDToImage = std::map<int, base::scoped_nsobject<NSImageRep>>;
// Generates a map of NSImageReps used by SetWorkspaceIconOnFILEThread and
// passes it to |io_task|. Since ui::ResourceBundle can only be used on UI
// thread, this function also needs to run on UI thread, and the gfx::Images
// need to be converted to NSImageReps on the UI thread due to non-thread-safety
// of gfx::Image.
void GetImageResourcesOnUIThread(
base::OnceCallback<void(std::unique_ptr<ResourceIDToImage>)> io_task) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
std::unique_ptr<ResourceIDToImage> result =
std::make_unique<ResourceIDToImage>();
// These resource ID should match to the ones used by
// SetWorkspaceIconOnFILEThread below.
for (int id : {IDR_APPS_FOLDER_16, IDR_APPS_FOLDER_32,
IDR_APPS_FOLDER_OVERLAY_128, IDR_APPS_FOLDER_OVERLAY_512}) {
gfx::Image image = resource_bundle.GetNativeImageNamed(id);
(*result)[id] = ImageRepForGFXImage(image);
}
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(std::move(io_task), std::move(result)));
}
void SetWorkspaceIconOnWorkerThread(const base::FilePath& apps_directory,
std::unique_ptr<ResourceIDToImage> images) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
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.
for (int id : {IDR_APPS_FOLDER_16, IDR_APPS_FOLDER_32}) {
const auto& found = images->find(id);
DCHECK(found != images->end());
[folder_icon_image addRepresentation:found->second];
}
// 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.
NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
for (int id : {IDR_APPS_FOLDER_OVERLAY_128, IDR_APPS_FOLDER_OVERLAY_512}) {
const auto& found = images->find(id);
DCHECK(found != images->end());
NSImageRep* with_overlay = OverlayImageRep(base_image, found->second);
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];
}
// 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
bool UpdateAppShortcutsSubdirLocalizedName(
const base::FilePath& apps_directory) {
base::FilePath localized = apps_directory.Append(".localized");
if (!base::CreateDirectory(localized))
return false;
base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
base::string16 localized_name =
shell_integration::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];
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&GetImageResourcesOnUIThread,
base::BindOnce(&SetWorkspaceIconOnWorkerThread,
apps_directory)));
return true;
}
std::unique_ptr<ShortcutInfo> BuildShortcutInfoFromBundle(
const base::FilePath& bundle_path) {
BundleInfoPlist bundle_info(bundle_path);
std::unique_ptr<ShortcutInfo> shortcut_info(new ShortcutInfo);
shortcut_info->extension_id = bundle_info.GetExtensionId();
shortcut_info->url = bundle_info.GetURL();
shortcut_info->title = bundle_info.GetTitle();
shortcut_info->profile_name = bundle_info.GetProfileName();
shortcut_info->profile_path = bundle_info.GetFullProfilePath();
return shortcut_info;
}
base::FilePath GetMultiProfileAppDataDir(base::FilePath app_data_dir) {
// The kCrAppModeUserDataDirKey is expected to be a path in kWebAppDirname,
// and the true user data dir is extracted by going three directories up.
// For profile-agnostic apps, remove this reference to the profile name.
// TODO(https://crbug.com/1021237): Do not specify kCrAppModeUserDataDirKey
// if Chrome is using the default user data dir.
// Strip the app name directory.
base::FilePath app_name_dir = app_data_dir.BaseName();
app_data_dir = app_data_dir.DirName();
// Strip kWebAppDirname.
base::FilePath web_app_dir = app_data_dir.BaseName();
app_data_dir = app_data_dir.DirName();
// Strip the profile and replace it with kNewProfilePath.
app_data_dir = app_data_dir.DirName();
const std::string kNewProfilePath("-");
return app_data_dir.Append(kNewProfilePath)
.Append(web_app_dir)
.Append(app_name_dir);
}
// Returns the bundle identifier for an app. If |profile_path| is unset, then
// the returned bundle id will be profile-agnostic.
std::string GetBundleIdentifier(
const std::string& app_id,
const base::FilePath& profile_path = base::FilePath()) {
// Note that this matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
if (!profile_path.empty()) {
// Replace spaces in the profile path with hyphen.
std::string normalized_profile_path;
base::ReplaceChars(profile_path.BaseName().value(), " ", "-",
&normalized_profile_path);
return base::mac::BaseBundleID() + std::string(".app.") +
normalized_profile_path + "-" + app_id;
}
return base::mac::BaseBundleID() + std::string(".app.") + app_id;
}
// Return all bundles with the specified |bundle_id| which are for the current
// user data dir.
std::list<BundleInfoPlist> SearchForBundlesById(const std::string& bundle_id) {
std::list<BundleInfoPlist> infos;
// First search using LaunchServices
base::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
base::SysUTF8ToCFStringRef(bundle_id));
base::scoped_nsobject<NSArray> bundle_urls(base::mac::CFToNSCast(
LSCopyApplicationURLsForBundleIdentifier(bundle_id_cf.get(), nullptr)));
for (NSURL* url : bundle_urls.get()) {
NSString* path_string = [url path];
base::FilePath bundle_path([path_string fileSystemRepresentation]);
BundleInfoPlist info(bundle_path);
if (!info.IsForCurrentUserDataDir())
continue;
infos.push_back(info);
}
if (!infos.empty())
return infos;
// LaunchServices can fail to locate a recently-created bundle. Search
// for an app in the applications folder to handle this case.
// https://crbug.com/937703
infos = BundleInfoPlist::GetAllInPath(GetChromeAppsFolder(),
true /* recursive */);
for (auto it = infos.begin(); it != infos.end();) {
const BundleInfoPlist& info = *it;
if (info.GetBundleId() == bundle_id && info.IsForCurrentUserDataDir()) {
++it;
} else {
infos.erase(it++);
}
}
return infos;
}
} // namespace
std::unique_ptr<ShortcutInfo> RecordAppShimErrorAndBuildShortcutInfo(
const base::FilePath& bundle_path) {
base::Version full_version = BundleInfoPlist(bundle_path).GetVersion();
uint32_t major_version = 0;
if (full_version.IsValid())
major_version = full_version.components()[0];
base::UmaHistogramSparse("Apps.AppShimErrorVersion", major_version);
return BuildShortcutInfoFromBundle(bundle_path);
}
bool AppShimLaunchDisabled() {
return AppShimCreationDisabledForTest() &&
!g_app_shims_allow_update_and_launch_in_tests;
}
base::FilePath GetChromeAppsFolder() {
if (!GetOverriddenApplicationsFolder()->empty())
return *GetOverriddenApplicationsFolder();
base::FilePath path = GetWritableApplicationsDirectory();
if (path.empty())
return path;
return path.Append(GetLocalizableAppShortcutsSubdirName());
}
void SetChromeAppsFolderForTesting(const base::FilePath& path) {
*GetOverriddenApplicationsFolder() = path;
}
WebAppShortcutCreator::WebAppShortcutCreator(const base::FilePath& app_data_dir,
const ShortcutInfo* shortcut_info)
: app_data_dir_(app_data_dir), info_(shortcut_info) {
DCHECK(shortcut_info);
}
WebAppShortcutCreator::~WebAppShortcutCreator() {}
base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath(
bool avoid_conflicts) const {
if (g_app_shims_allow_update_and_launch_in_tests)
return app_data_dir_.Append(GetShortcutBasename());
base::FilePath applications_dir = GetChromeAppsFolder();
if (applications_dir.empty())
return base::FilePath();
if (!avoid_conflicts)
return applications_dir.Append(GetShortcutBasename());
// Attempt to use the application's title for the file name. Resolve conflicts
// by appending 1 through kMaxConflictNumber, before giving up and using the
// concatenated profile and extension for a name name.
for (int i = 1; i <= kMaxConflictNumber; ++i) {
base::FilePath path = applications_dir.Append(GetShortcutBasename(i));
if (base::DirectoryExists(path))
continue;
return path;
}
// If all of those are taken, then use the combination of profile and
// extension id.
return applications_dir.Append(GetFallbackBasename());
}
base::FilePath WebAppShortcutCreator::GetShortcutBasename(
int copy_number) const {
// For profile-less shortcuts, use the fallback naming scheme to avoid change.
if (info_->profile_name.empty())
return GetFallbackBasename();
// Strip all preceding '.'s from the path.
base::string16 title = info_->title;
size_t first_non_dot = 0;
while (first_non_dot < title.size() && title[first_non_dot] == '.')
first_non_dot += 1;
title = title.substr(first_non_dot);
if (title.empty())
return GetFallbackBasename();
// Finder will display ':' as '/', so replace all '/' instances with ':'.
std::replace(title.begin(), title.end(), '/', ':');
// Append the copy number.
std::string title_utf8 = base::UTF16ToUTF8(title);
if (copy_number != 1)
title_utf8 += base::StringPrintf(" %d", copy_number);
return base::FilePath(title_utf8 + ".app");
}
base::FilePath WebAppShortcutCreator::GetFallbackBasename() 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 {
if (!base::DirectoryExists(staging_path.DirName())) {
RecordCreateShortcut(CreateShortcutResult::kStagingDirectoryNotExist);
LOG(ERROR) << "Staging path directory does not exist: "
<< staging_path.DirName();
return false;
}
const base::FilePath framework_bundle_path = base::mac::FrameworkBundlePath();
const base::FilePath executable_path =
framework_bundle_path.Append("Helpers").Append("app_mode_loader");
const base::FilePath plist_path =
framework_bundle_path.Append("Resources").Append("app_mode-Info.plist");
const base::FilePath destination_contents_path =
staging_path.Append("Contents");
const base::FilePath destination_executable_path =
destination_contents_path.Append("MacOS");
// First create the .app bundle directory structure.
// Use NSFileManager so that the permissions can be set appropriately. The
// base::CreateDirectory() routine forces mode 0700.
NSError* error = nil;
if (![[NSFileManager defaultManager]
createDirectoryAtURL:base::mac::FilePathToNSURL(
destination_executable_path)
withIntermediateDirectories:YES
attributes:@{
NSFilePosixPermissions : @(0755)
}
error:&error]) {
RecordCreateShortcut(CreateShortcutResult::kFailToCreateExecutablePath);
LOG(ERROR) << "Failed to create destination executable path: "
<< destination_executable_path
<< ", error=" << base::SysNSStringToUTF8([error description]);
return false;
}
// Copy the executable file.
if (!base::CopyFile(executable_path, destination_executable_path.Append(
executable_path.BaseName()))) {
RecordCreateShortcut(CreateShortcutResult::kFailToCopyExecutablePath);
LOG(ERROR) << "Failed to copy executable: " << executable_path;
return false;
}
// Copy the Info.plist.
if (!base::CopyFile(plist_path,
destination_contents_path.Append("Info.plist"))) {
RecordCreateShortcut(CreateShortcutResult::kFailToCopyPlist);
LOG(ERROR) << "Failed to copy plist: " << plist_path;
return false;
}
// Write the PkgInfo file.
constexpr char kPkgInfoData[] = "APPL????";
constexpr size_t kPkgInfoDataSize = base::size(kPkgInfoData) - 1;
if (base::WriteFile(destination_contents_path.Append("PkgInfo"), kPkgInfoData,
kPkgInfoDataSize) != kPkgInfoDataSize) {
RecordCreateShortcut(CreateShortcutResult::kFailToWritePkgInfoFile);
LOG(ERROR) << "Failed to write PkgInfo file: " << destination_contents_path;
return false;
}
bool result = UpdatePlist(staging_path);
if (!result) {
RecordCreateShortcut(CreateShortcutResult::kFailToUpdatePlist);
return result;
}
result = UpdateDisplayName(staging_path);
if (!result) {
RecordCreateShortcut(CreateShortcutResult::kFailToUpdateDisplayName);
return result;
}
result = UpdateIcon(staging_path);
if (!result) {
RecordCreateShortcut(CreateShortcutResult::kFailToUpdateIcon);
}
return result;
}
// Returns a reference to the static UpdateShortcuts lock.
// See https://crbug.com/1090548 for more info.
base::Lock& GetUpdateShortcutsLock() {
static base::NoDestructor<base::Lock> lock;
return *lock;
}
void WebAppShortcutCreator::CreateShortcutsAt(
const std::vector<base::FilePath>& dst_app_paths,
std::vector<base::FilePath>* updated_paths) const {
DCHECK(updated_paths && updated_paths->empty());
DCHECK(!dst_app_paths.empty());
// CreateShortcutsAt() modifies the app shim on disk, first by deleting
// the destination app shim (if it exists), then by copying a new app shim
// from the source app to the destination. To ensure that process works,
// we must guarantee that no more than one CreateShortcutsAt() call will
// ever run at a time. We have an UpdateShortcuts lock for this purpose,
// so check that lock has been acquired on this thread before proceeding.
// See https://crbug.com/1090548 for more info.
GetUpdateShortcutsLock().AssertAcquired();
base::ScopedTempDir scoped_temp_dir;
if (!scoped_temp_dir.CreateUniqueTempDir()) {
RecordCreateShortcut(CreateShortcutResult::kFailToCreateTempDir);
return;
}
// Create the bundle in |staging_path|. Note that the staging path will be
// encoded in CFBundleName, and only .apps with that exact name will have
// their display name overridden by localization. To that end, use the base
// name from dst_app_paths.front(), to ensure that the Applications copy has
// its display name set appropriately.
base::FilePath staging_path =
scoped_temp_dir.GetPath().Append(dst_app_paths.front().BaseName());
if (!BuildShortcut(staging_path))
return;
// Copy to each destination in |dst_app_paths|.
for (const auto& dst_app_path : dst_app_paths) {
// Create the parent directory for the app.
base::FilePath dst_parent_dir = dst_app_path.DirName();
if (!base::CreateDirectory(dst_parent_dir)) {
RecordCreateShortcut(CreateShortcutResult::kFailToCreateParentDir);
LOG(ERROR) << "Creating directory " << dst_parent_dir.value()
<< " failed.";
continue;
}
// Delete any old copies that may exist.
base::DeletePathRecursively(dst_app_path);
// Copy the bundle to |dst_app_path|.
if (!base::CopyDirectory(staging_path, dst_app_path, true)) {
RecordCreateShortcut(CreateShortcutResult::kFailToCopyApp);
LOG(ERROR) << "Copying app to dst dir: " << dst_parent_dir.value()
<< " failed";
continue;
}
// Remove the quarantine attribute from both the bundle and the executable.
base::mac::RemoveQuarantineAttribute(dst_app_path);
base::mac::RemoveQuarantineAttribute(dst_app_path.Append("Contents")
.Append("MacOS")
.Append("app_mode_loader"));
updated_paths->push_back(dst_app_path);
}
}
bool WebAppShortcutCreator::CreateShortcuts(
ShortcutCreationReason creation_reason,
ShortcutLocations creation_locations) {
DCHECK_NE(creation_locations.applications_menu_location,
APP_MENU_LOCATION_HIDDEN);
std::vector<base::FilePath> updated_app_paths;
if (!UpdateShortcuts(true /* create_if_needed */, &updated_app_paths))
return false;
if (creation_reason == SHORTCUT_CREATION_BY_USER)
RevealAppShimInFinder(updated_app_paths[0]);
RecordCreateShortcut(CreateShortcutResult::kSuccess);
return true;
}
bool WebAppShortcutCreator::UpdateShortcuts(
bool create_if_needed,
std::vector<base::FilePath>* updated_paths) {
DCHECK(updated_paths && updated_paths->empty());
if (create_if_needed) {
const base::FilePath applications_dir = GetChromeAppsFolder();
if (applications_dir.empty() ||
!base::DirectoryExists(applications_dir.DirName())) {
RecordCreateShortcut(CreateShortcutResult::kApplicationDirNotFound);
LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
return false;
}
// Only set folder icons and a localized name once. This avoids concurrent
// calls to -[NSWorkspace setIcon:..], which is not reentrant.
static bool once = UpdateAppShortcutsSubdirLocalizedName(applications_dir);
if (!once) {
RecordCreateShortcut(CreateShortcutResult::kFailToLocalizeApplication);
LOG(ERROR) << "Failed to localize " << applications_dir.value();
}
}
// Acquire the UpdateShortcuts lock. This ensures only a single
// UpdateShortcuts call at a time will run at once past here. Not
// protecting against that can result in multiple CreateShortcutsAt()
// calls deleting and creating the app shim folder at once.
// See https://crbug.com/1090548 for more info.
base::AutoLock auto_lock(GetUpdateShortcutsLock());
// Get the list of paths to (re)create by bundle id (wherever it was moved
// or copied by the user).
std::vector<base::FilePath> app_paths = GetAppBundlesById();
// If that path does not exist, create a new entry in ~/Applications if
// requested.
if (app_paths.empty() && create_if_needed) {
app_paths.push_back(
GetApplicationsShortcutPath(true /* avoid_conflicts */));
}
if (app_paths.empty()) {
RecordCreateShortcut(CreateShortcutResult::kFailToGetApplicationPaths);
return false;
}
CreateShortcutsAt(app_paths, updated_paths);
return updated_paths->size() == app_paths.size();
}
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 =
[NSMutableDictionary dictionaryWithContentsOfFile: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];
if (IsMultiProfile()) {
[plist setObject:base::SysUTF8ToNSString(
GetBundleIdentifier(info_->extension_id))
forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
base::FilePath data_dir = GetMultiProfileAppDataDir(app_data_dir_);
[plist setObject:base::mac::FilePathToNSString(data_dir)
forKey:app_mode::kCrAppModeUserDataDirKey];
} else {
[plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier(
info_->extension_id, info_->profile_path))
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];
[plist setObject:[NSNumber numberWithBool:YES]
forKey:app_mode::kNSHighResolutionCapableKey];
// 3. Fill in file handlers.
const auto file_handler_extensions =
GetFileHandlerExtensionsWithoutDot(info_->file_handler_extensions);
if (!file_handler_extensions.empty() ||
!info_->file_handler_mime_types.empty()) {
base::scoped_nsobject<NSMutableArray> doc_types_value(
[[NSMutableArray alloc] init]);
base::scoped_nsobject<NSMutableDictionary> doc_types_dict(
[[NSMutableDictionary alloc] init]);
if (!file_handler_extensions.empty()) {
base::scoped_nsobject<NSMutableArray> extensions(
[[NSMutableArray alloc] init]);
for (const auto& file_extension : file_handler_extensions)
[extensions addObject:base::SysUTF8ToNSString(file_extension)];
[doc_types_dict setObject:extensions
forKey:app_mode::kCFBundleTypeExtensionsKey];
}
if (!info_->file_handler_mime_types.empty()) {
base::scoped_nsobject<NSMutableArray> mime_types(
[[NSMutableArray alloc] init]);
for (const auto& mime_type : info_->file_handler_mime_types)
[mime_types addObject:base::SysUTF8ToNSString(mime_type)];
[doc_types_dict setObject:mime_types
forKey:app_mode::kCFBundleTypeMIMETypesKey];
}
[doc_types_value addObject:doc_types_dict];
[plist setObject:doc_types_value
forKey:app_mode::kCFBundleDocumentTypesKey];
}
if (IsMultiProfile()) {
[plist setObject:base::SysUTF16ToNSString(info_->title)
forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
} else {
// The appropriate bundle name is |info_->title|. Avoiding changing the
// behavior of non-multi-profile apps when fixing
// https://crbug.com/1021804.
base::FilePath app_name = app_path.BaseName().RemoveFinalExtension();
[plist setObject:base::mac::FilePathToNSString(app_name)
forKey:base::mac::CFToNSCast(kCFBundleNameKey)];
}
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 (!IsMultiProfile() &&
HasExistingExtensionShimForDifferentProfile(
GetChromeAppsFolder(), info_->extension_id, info_->profile_path)) {
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;
std::vector<gfx::Image> valid_icons;
for (gfx::ImageFamily::const_iterator it = info_->favicon.begin();
it != info_->favicon.end(); ++it) {
if (IsImageValidForIcon(*it))
valid_icons.push_back(*it);
}
if (valid_icons.empty())
return false;
base::FilePath resources_path = GetResourcesPath(app_path);
if (!base::CreateDirectory(resources_path))
return false;
return WriteIconsToFile(valid_icons, resources_path.Append("app.icns"));
}
std::vector<base::FilePath> WebAppShortcutCreator::GetAppBundlesByIdUnsorted()
const {
base::scoped_nsobject<NSMutableArray> urls([[NSMutableArray alloc] init]);
// Search using LaunchServices using the default bundle id.
const std::string bundle_id = GetBundleIdentifier(
info_->extension_id,
IsMultiProfile() ? base::FilePath() : info_->profile_path);
auto bundle_infos = SearchForBundlesById(bundle_id);
// If in multi-profile mode, search using the profile-scoped bundle id, in
// case the user has an old shim hanging around.
if (bundle_infos.empty() && IsMultiProfile()) {
const std::string profile_scoped_bundle_id =
GetBundleIdentifier(info_->extension_id, info_->profile_path);
bundle_infos = SearchForBundlesById(profile_scoped_bundle_id);
}
std::vector<base::FilePath> bundle_paths;
for (const auto& bundle_info : bundle_infos)
bundle_paths.push_back(bundle_info.bundle_path());
return bundle_paths;
}
std::vector<base::FilePath> WebAppShortcutCreator::GetAppBundlesById() const {
std::vector<base::FilePath> paths = GetAppBundlesByIdUnsorted();
// Sort the matches by preference.
base::FilePath default_path =
GetApplicationsShortcutPath(false /* avoid_conflicts */);
// When testing, use only the default path.
if (g_app_shims_allow_update_and_launch_in_tests) {
paths.clear();
if (base::PathExists(default_path))
paths.push_back(default_path);
return paths;
}
base::FilePath apps_dir = GetChromeAppsFolder();
auto compare = [default_path, apps_dir](const base::FilePath& a,
const base::FilePath& b) {
if (a == b)
return false;
// The default install path is preferred above all others.
if (a == default_path)
return true;
if (b == default_path)
return false;
// Paths in ~/Applications are preferred to paths not in ~/Applications.
bool a_in_apps_dir = apps_dir.IsParent(a);
bool b_in_apps_dir = apps_dir.IsParent(b);
if (a_in_apps_dir != b_in_apps_dir)
return a_in_apps_dir > b_in_apps_dir;
return a < b;
};
std::sort(paths.begin(), paths.end(), compare);
return paths;
}
bool WebAppShortcutCreator::IsMultiProfile() const {
return info_->is_multi_profile;
}
void WebAppShortcutCreator::RevealAppShimInFinder(
const base::FilePath& app_path) const {
auto closure = base::BindOnce(
[](const base::FilePath& 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:@""];
},
app_path);
// Perform the call to NSWorkSpace on the UI thread. Calling it on the IO
// thread appears to cause crashes.
// https://crbug.com/1067367
content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(closure));
}
void LaunchShim(LaunchShimUpdateBehavior update_behavior,
ShimLaunchedCallback launched_callback,
ShimTerminatedCallback terminated_callback,
std::unique_ptr<ShortcutInfo> shortcut_info) {
if (AppShimLaunchDisabled()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(std::move(launched_callback), base::Process()));
return;
}
internals::PostShortcutIOTask(
base::BindOnce(&LaunchShimOnFileThread, update_behavior,
std::move(launched_callback),
std::move(terminated_callback)),
std::move(shortcut_info));
}
namespace internals {
bool CreatePlatformShortcuts(const base::FilePath& app_data_path,
const ShortcutLocations& creation_locations,
ShortcutCreationReason creation_reason,
const ShortcutInfo& shortcut_info) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
if (AppShimCreationDisabledForTest())
return true;
WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info);
return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
}
bool DeletePlatformShortcuts(const base::FilePath& app_data_path,
const ShortcutInfo& shortcut_info) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
const std::string bundle_id = GetBundleIdentifier(shortcut_info.extension_id,
shortcut_info.profile_path);
auto bundle_infos = SearchForBundlesById(bundle_id);
bool result = true;
for (const auto& bundle_info : bundle_infos) {
if (!base::DeletePathRecursively(bundle_info.bundle_path()))
result = false;
}
return result;
}
void DeleteMultiProfileShortcutsForApp(const std::string& app_id) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
const std::string bundle_id = GetBundleIdentifier(app_id);
auto bundle_infos = SearchForBundlesById(bundle_id);
for (const auto& bundle_info : bundle_infos) {
base::DeletePathRecursively(bundle_info.bundle_path());
}
}
void UpdatePlatformShortcuts(const base::FilePath& app_data_path,
const base::string16& old_app_title,
const ShortcutInfo& shortcut_info) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
if (AppShimLaunchDisabled())
return;
WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info);
std::vector<base::FilePath> updated_shim_paths;
bool create_if_needed = false;
// Tests use UpdateAllShortcuts to force shim creation (rather than
// relying on asynchronous creation at installation.
if (g_app_shims_allow_update_and_launch_in_tests)
create_if_needed = true;
shortcut_creator.UpdateShortcuts(create_if_needed, &updated_shim_paths);
}
void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
std::list<BundleInfoPlist> bundles_info = BundleInfoPlist::GetAllInPath(
GetChromeAppsFolder(), true /* recursive */);
for (const auto& info : bundles_info) {
if (!info.IsForCurrentUserDataDir())
continue;
if (!info.IsForProfile(profile_path))
continue;
base::DeletePathRecursively(info.bundle_path());
}
}
} // namespace internals
} // namespace web_app