// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/mac/dock.h"
#include <ApplicationServices/ApplicationServices.h>
#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#include <signal.h>
#include <tuple>
#include "base/apple/bridging.h"
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/logging.h"
#include "base/mac/launchd.h"
#include "build/branding_buildflags.h"
extern "C" {
// Undocumented private internal CFURL functions. The Dock uses these to
// serialize and deserialize CFURLs for use in its plist's file-data keys. See
// 10.5.8 CF-476.19 and 10.7.2 CF-635.15's CFPriv.h and CFURL.c. The property
// list representation will contain, at the very least, the _CFURLStringType
// and _CFURLString keys. _CFURLStringType is a number that defines the
// interpretation of the _CFURLString. It may be a CFURLPathStyle value, or
// the CFURL-internal FULL_URL_REPRESENTATION value (15). Prior to Mac OS X
// 10.7.2, the Dock plist always used kCFURLPOSIXPathStyle (0), formatting
// _CFURLString as a POSIX path. In Mac OS X 10.7.2 (CF-635.15), it uses
// FULL_URL_REPRESENTATION along with a file:/// URL. This is due to a change
// in _CFURLInit.
CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
CFURLRef _CFURLCreateFromPropertyListRepresentation(
CFAllocatorRef allocator, CFPropertyListRef property_list_representation);
} // extern "C"
namespace dock {
namespace {
NSString* const kDockTileDataKey = @"tile-data";
NSString* const kDockFileDataKey = @"file-data";
NSString* const kDockDomain = @"com.apple.dock";
NSString* const kDockPersistentAppsKey = @"persistent-apps";
// A wrapper around _CFURLCopyPropertyListRepresentation that operates on
// Foundation data types and returns an autoreleased NSDictionary.
NSDictionary* DockFileDataDictionaryForURL(NSURL* url) {
base::apple::ScopedCFTypeRef<CFPropertyListRef> property_list(
_CFURLCopyPropertyListRepresentation(base::apple::NSToCFPtrCast(url)));
CFDictionaryRef dictionary =
base::apple::CFCast<CFDictionaryRef>(property_list.get());
if (!dictionary)
return nil;
return base::apple::CFToNSOwnershipCast(
(CFDictionaryRef)property_list.release());
}
// A wrapper around _CFURLCreateFromPropertyListRepresentation that operates
// on Foundation data types and returns an autoreleased NSURL.
NSURL* URLFromDockFileDataDictionary(NSDictionary* dictionary) {
base::apple::ScopedCFTypeRef<CFURLRef> url(
_CFURLCreateFromPropertyListRepresentation(
kCFAllocatorDefault, base::apple::NSToCFPtrCast(dictionary)));
if (!url)
return nil;
return base::apple::CFToNSOwnershipCast(url.release());
}
// Returns an array parallel to |persistent_apps| containing only the
// pathnames of the Dock tiles contained therein. Returns nil on failure, such
// as when the structure of |persistent_apps| is not understood.
NSMutableArray* PersistentAppPaths(NSArray* persistent_apps) {
if (!persistent_apps) {
return nil;
}
NSMutableArray* app_paths =
[NSMutableArray arrayWithCapacity:[persistent_apps count]];
for (NSDictionary* app in persistent_apps) {
if (![app isKindOfClass:[NSDictionary class]]) {
LOG(ERROR) << "app not NSDictionary";
return nil;
}
NSDictionary* tile_data = app[kDockTileDataKey];
if (![tile_data isKindOfClass:[NSDictionary class]]) {
LOG(ERROR) << "tile_data not NSDictionary";
return nil;
}
NSDictionary* file_data = tile_data[kDockFileDataKey];
if (![file_data isKindOfClass:[NSDictionary class]]) {
// Some apps (e.g. Dashboard) have no file data, but instead have a
// special value for the tile-type key. For these, add an empty string to
// align indexes with the source array.
[app_paths addObject:@""];
continue;
}
NSURL* url = URLFromDockFileDataDictionary(file_data);
if (!url) {
LOG(ERROR) << "no URL";
return nil;
}
if (![url isFileURL]) {
LOG(ERROR) << "non-file URL";
return nil;
}
NSString* path = [url path];
[app_paths addObject:path];
}
return app_paths;
}
BOOL IsAppAtPathAWebBrowser(NSString* app_path) {
NSBundle* app_bundle = [NSBundle bundleWithPath:app_path];
if (!app_bundle)
return NO;
NSArray* activities = base::apple::ObjCCast<NSArray>(
[app_bundle objectForInfoDictionaryKey:@"NSUserActivityTypes"]);
if (!activities)
return NO;
return [activities containsObject:NSUserActivityTypeBrowsingWeb];
}
// Restart the Dock process by sending it a SIGTERM.
void Restart() {
// Doing this via launchd using the proper job label is the safest way to
// handle the restart. Unlike "killall Dock", looking this up via launchd
// guarantees that only the right process will be targeted.
pid_t pid = base::mac::PIDForJob("com.apple.Dock.agent");
if (pid <= 0) {
return;
}
// Sending a SIGTERM to the Dock seems to be a more reliable way to get the
// replacement Dock process to read the newly written plist than using the
// equivalent of "launchctl stop" (even if followed by "launchctl start.")
// Note that this is a potential race in that pid may no longer be valid or
// may even have been reused.
kill(pid, SIGTERM);
}
NSDictionary* DockPlistFromUserDefaults() {
NSDictionary* dock_plist = [[NSUserDefaults standardUserDefaults]
persistentDomainForName:kDockDomain];
if (![dock_plist isKindOfClass:[NSDictionary class]]) {
LOG(ERROR) << "dock_plist is not an NSDictionary";
return nil;
}
return dock_plist;
}
NSArray* PersistentAppsFromDockPlist(NSDictionary* dock_plist) {
if (!dock_plist) {
return nil;
}
NSArray* persistent_apps = dock_plist[kDockPersistentAppsKey];
if (![persistent_apps isKindOfClass:[NSArray class]]) {
LOG(ERROR) << "persistent_apps is not an NSArray";
return nil;
}
return persistent_apps;
}
} // namespace
ChromeInDockStatus ChromeIsInTheDock() {
NSDictionary* dock_plist = DockPlistFromUserDefaults();
NSArray* persistent_apps = PersistentAppsFromDockPlist(dock_plist);
if (!persistent_apps) {
return ChromeInDockFailure;
}
NSString* launch_path = [base::apple::OuterBundle() bundlePath];
return [PersistentAppPaths(persistent_apps) containsObject:launch_path]
? ChromeInDockTrue
: ChromeInDockFalse;
}
AddIconStatus AddIcon(NSString* installed_path, NSString* dmg_app_path) {
// ApplicationServices.framework/Frameworks/HIServices.framework contains an
// undocumented function, CoreDockAddFileToDock, that is able to add items
// to the Dock "live" without requiring a Dock restart. Under the hood, it
// communicates with the Dock via Mach IPC. It is available as of Mac OS X
// 10.6. AddIcon could call CoreDockAddFileToDock if available, but
// CoreDockAddFileToDock seems to always to add the new Dock icon last,
// where AddIcon takes care to position the icon appropriately. Based on
// disassembly, the signature of the undocumented function appears to be
// extern "C" OSStatus CoreDockAddFileToDock(CFURLRef url, int);
// The int argument doesn't appear to have any effect. It's not used as the
// position to place the icon as hoped.
@autoreleasepool {
NSMutableDictionary* dock_plist = [NSMutableDictionary
dictionaryWithDictionary:DockPlistFromUserDefaults()];
NSMutableArray* persistent_apps =
[NSMutableArray arrayWithArray:PersistentAppsFromDockPlist(dock_plist)];
NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps);
if (!persistent_app_paths) {
return IconAddFailure;
}
NSUInteger already_installed_app_index = NSNotFound;
NSUInteger app_index = NSNotFound;
for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
NSString* app_path = persistent_app_paths[index];
if ([app_path isEqualToString:installed_path]) {
// If the Dock already contains a reference to the newly installed
// application, don't add another one.
already_installed_app_index = index;
} else if ([app_path isEqualToString:dmg_app_path]) {
// If the Dock contains a reference to the application on the disk
// image, replace it with a reference to the newly installed
// application. However, if the Dock contains a reference to both the
// application on the disk image and the newly installed application,
// just remove the one referencing the disk image.
//
// This case is only encountered when the user drags the icon from the
// disk image volume window in the Finder directly into the Dock.
app_index = index;
}
}
bool made_change = false;
if (app_index != NSNotFound) {
// Remove the Dock's reference to the application on the disk image.
[persistent_apps removeObjectAtIndex:app_index];
[persistent_app_paths removeObjectAtIndex:app_index];
made_change = true;
}
if (already_installed_app_index == NSNotFound) {
// The Dock doesn't yet have a reference to the icon at the
// newly installed path. Figure out where to put the new icon.
NSString* app_name = [installed_path lastPathComponent];
if (app_index == NSNotFound) {
// If an application with this name is already in the Dock, put the new
// one right before it.
for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
NSString* dock_app_name =
[persistent_app_paths[index] lastPathComponent];
if ([dock_app_name isEqualToString:app_name]) {
app_index = index;
break;
}
}
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
if (app_index == NSNotFound) {
// If this is an officially-branded Chrome and another flavor is already
// in the Dock, put them next to each other. With side-by-side, there
// can be multiple Chromes already in the dock; pick the first one found
// in order, and put this Chrome next to it.
NSArray* app_name_order = @[
@"Google Chrome.app", @"Google Chrome Beta.app",
@"Google Chrome Dev.app", @"Google Chrome Canary.app"
];
NSUInteger app_name_index = [app_name_order indexOfObject:app_name];
if (app_name_index != NSNotFound) {
for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
NSString* dock_app_name =
[persistent_app_paths[index] lastPathComponent];
NSUInteger dock_app_name_index =
[app_name_order indexOfObject:dock_app_name];
if (dock_app_name_index == NSNotFound)
continue;
if (app_name_index < dock_app_name_index)
app_index = index;
else
app_index = index + 1;
break;
}
}
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
if (app_index == NSNotFound) {
// Put the new application after the last browser application already
// present in the Dock.
NSUInteger last_browser = [persistent_app_paths
indexOfObjectWithOptions:NSEnumerationReverse
passingTest:^(NSString* app_path, NSUInteger idx,
BOOL* stop) {
return IsAppAtPathAWebBrowser(app_path);
}];
if (last_browser != NSNotFound)
app_index = last_browser + 1;
}
if (app_index == NSNotFound) {
// Put the new application last in the Dock.
app_index = [persistent_apps count];
}
// Set up the new Dock tile.
NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES];
NSDictionary* url_dict = DockFileDataDictionaryForURL(url);
if (!url_dict) {
LOG(ERROR) << "couldn't create url_dict";
return IconAddFailure;
}
NSDictionary* new_tile_data = @{kDockFileDataKey : url_dict};
NSDictionary* new_tile = @{kDockTileDataKey : new_tile_data};
// Add the new tile to the Dock.
[persistent_apps insertObject:new_tile atIndex:app_index];
[persistent_app_paths insertObject:installed_path atIndex:app_index];
made_change = true;
}
// Verify that the arrays are still parallel.
DCHECK_EQ([persistent_apps count], [persistent_app_paths count]);
if (!made_change) {
// If no changes were made, there's no point in rewriting the Dock's
// plist or restarting the Dock.
return IconAlreadyPresent;
}
// Rewrite the plist.
dock_plist[kDockPersistentAppsKey] = persistent_apps;
[[NSUserDefaults standardUserDefaults] setPersistentDomain:dock_plist
forName:kDockDomain];
Restart();
return IconAddSuccess;
}
}
} // namespace dock