1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/mac/dock.h"
7 #include <ApplicationServices/ApplicationServices.h>
8 #import <Foundation/Foundation.h>
9 #include <CoreFoundation/CoreFoundation.h>
12 #include "base/logging.h"
13 #include "base/mac/launchd.h"
14 #include "base/mac/mac_logging.h"
15 #include "base/mac/mac_util.h"
16 #include "base/mac/scoped_cftyperef.h"
17 #include "base/mac/scoped_nsautorelease_pool.h"
18 #include "base/strings/sys_string_conversions.h"
22 // Undocumented private internal CFURL functions. The Dock uses these to
23 // serialize and deserialize CFURLs for use in its plist's file-data keys. See
24 // 10.5.8 CF-476.19 and 10.7.2 CF-635.15's CFPriv.h and CFURL.c. The property
25 // list representation will contain, at the very least, the _CFURLStringType
26 // and _CFURLString keys. _CFURLStringType is a number that defines the
27 // interpretation of the _CFURLString. It may be a CFURLPathStyle value, or
28 // the CFURL-internal FULL_URL_REPRESENTATION value (15). Prior to Mac OS X
29 // 10.7.2, the Dock plist always used kCFURLPOSIXPathStyle (0), formatting
30 // _CFURLString as a POSIX path. In Mac OS X 10.7.2 (CF-635.15), it uses
31 // FULL_URL_REPRESENTATION along with a file:/// URL. This is due to a change
34 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
35 CFURLRef _CFURLCreateFromPropertyListRepresentation(
36 CFAllocatorRef allocator, CFPropertyListRef property_list_representation);
43 NSString* const kDockTileDataKey = @"tile-data";
44 NSString* const kDockFileDataKey = @"file-data";
46 // A wrapper around _CFURLCopyPropertyListRepresentation that operates on
47 // Foundation data types and returns an autoreleased NSDictionary.
48 NSDictionary* NSURLCopyDictionary(NSURL* url) {
49 CFURLRef url_cf = base::mac::NSToCFCast(url);
50 base::ScopedCFTypeRef<CFPropertyListRef> property_list(
51 _CFURLCopyPropertyListRepresentation(url_cf));
52 CFDictionaryRef dictionary_cf =
53 base::mac::CFCast<CFDictionaryRef>(property_list);
54 NSDictionary* dictionary = base::mac::CFToNSCast(dictionary_cf);
60 NSMakeCollectable(property_list.release());
61 return [dictionary autorelease];
64 // A wrapper around _CFURLCreateFromPropertyListRepresentation that operates
65 // on Foundation data types and returns an autoreleased NSURL.
66 NSURL* NSURLCreateFromDictionary(NSDictionary* dictionary) {
67 CFDictionaryRef dictionary_cf = base::mac::NSToCFCast(dictionary);
68 base::ScopedCFTypeRef<CFURLRef> url_cf(
69 _CFURLCreateFromPropertyListRepresentation(NULL, dictionary_cf));
70 NSURL* url = base::mac::CFToNSCast(url_cf);
76 NSMakeCollectable(url_cf.release());
77 return [url autorelease];
80 // Returns an array parallel to |persistent_apps| containing only the
81 // pathnames of the Dock tiles contained therein. Returns nil on failure, such
82 // as when the structure of |persistent_apps| is not understood.
83 NSMutableArray* PersistentAppPaths(NSArray* persistent_apps) {
84 NSMutableArray* app_paths =
85 [NSMutableArray arrayWithCapacity:[persistent_apps count]];
87 for (NSDictionary* app in persistent_apps) {
88 if (![app isKindOfClass:[NSDictionary class]]) {
89 LOG(ERROR) << "app not NSDictionary";
93 NSDictionary* tile_data = [app objectForKey:kDockTileDataKey];
94 if (![tile_data isKindOfClass:[NSDictionary class]]) {
95 LOG(ERROR) << "tile_data not NSDictionary";
99 NSDictionary* file_data = [tile_data objectForKey:kDockFileDataKey];
100 if (![file_data isKindOfClass:[NSDictionary class]]) {
101 // Some apps (e.g. Dashboard) have no file data, but instead have a
102 // special value for the tile-type key. For these, add an empty string to
103 // align indexes with the source array.
104 [app_paths addObject:@""];
108 NSURL* url = NSURLCreateFromDictionary(file_data);
110 LOG(ERROR) << "no URL";
114 if (![url isFileURL]) {
115 LOG(ERROR) << "non-file URL";
119 NSString* path = [url path];
120 [app_paths addObject:path];
126 // Restart the Dock process by sending it a SIGHUP.
128 // Doing this via launchd using the proper job label is the safest way to
129 // handle the restart. Unlike "killall Dock", looking this up via launchd
130 // guarantees that only the right process will be targeted.
131 pid_t pid = base::mac::PIDForJob("com.apple.Dock.agent");
136 // Sending a SIGHUP to the Dock seems to be a more reliable way to get the
137 // replacement Dock process to read the newly written plist than using the
138 // equivalent of "launchctl stop" (even if followed by "launchctl start.")
139 // Note that this is a potential race in that pid may no longer be valid or
140 // may even have been reused.
146 AddIconStatus AddIcon(NSString* installed_path, NSString* dmg_app_path) {
147 // ApplicationServices.framework/Frameworks/HIServices.framework contains an
148 // undocumented function, CoreDockAddFileToDock, that is able to add items
149 // to the Dock "live" without requiring a Dock restart. Under the hood, it
150 // communicates with the Dock via Mach IPC. It is available as of Mac OS X
151 // 10.6. AddIcon could call CoreDockAddFileToDock if available, but
152 // CoreDockAddFileToDock seems to always to add the new Dock icon last,
153 // where AddIcon takes care to position the icon appropriately. Based on
154 // disassembly, the signature of the undocumented function appears to be
155 // extern "C" OSStatus CoreDockAddFileToDock(CFURLRef url, int);
156 // The int argument doesn't appear to have any effect. It's not used as the
157 // position to place the icon as hoped.
159 // There's enough potential allocation in this function to justify a
161 base::mac::ScopedNSAutoreleasePool autorelease_pool;
163 NSString* const kDockDomain = @"com.apple.dock";
164 NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults];
166 NSDictionary* dock_plist_const =
167 [user_defaults persistentDomainForName:kDockDomain];
168 if (![dock_plist_const isKindOfClass:[NSDictionary class]]) {
169 LOG(ERROR) << "dock_plist_const not NSDictionary";
170 return IconAddFailure;
172 NSMutableDictionary* dock_plist =
173 [NSMutableDictionary dictionaryWithDictionary:dock_plist_const];
175 NSString* const kDockPersistentAppsKey = @"persistent-apps";
176 NSArray* persistent_apps_const =
177 [dock_plist objectForKey:kDockPersistentAppsKey];
178 if (![persistent_apps_const isKindOfClass:[NSArray class]]) {
179 LOG(ERROR) << "persistent_apps_const not NSArray";
180 return IconAddFailure;
182 NSMutableArray* persistent_apps =
183 [NSMutableArray arrayWithArray:persistent_apps_const];
185 NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps);
186 if (!persistent_app_paths) {
187 return IconAddFailure;
190 NSUInteger already_installed_app_index = NSNotFound;
191 NSUInteger app_index = NSNotFound;
192 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
193 NSString* app_path = [persistent_app_paths objectAtIndex:index];
194 if ([app_path isEqualToString:installed_path]) {
195 // If the Dock already contains a reference to the newly installed
196 // application, don't add another one.
197 already_installed_app_index = index;
198 } else if ([app_path isEqualToString:dmg_app_path]) {
199 // If the Dock contains a reference to the application on the disk
200 // image, replace it with a reference to the newly installed
201 // application. However, if the Dock contains a reference to both the
202 // application on the disk image and the newly installed application,
203 // just remove the one referencing the disk image.
205 // This case is only encountered when the user drags the icon from the
206 // disk image volume window in the Finder directly into the Dock.
211 bool made_change = false;
213 if (app_index != NSNotFound) {
214 // Remove the Dock's reference to the application on the disk image.
215 [persistent_apps removeObjectAtIndex:app_index];
216 [persistent_app_paths removeObjectAtIndex:app_index];
220 if (already_installed_app_index == NSNotFound) {
221 // The Dock doesn't yet have a reference to the icon at the
222 // newly installed path. Figure out where to put the new icon.
223 NSString* app_name = [installed_path lastPathComponent];
225 if (app_index == NSNotFound) {
226 // If an application with this name is already in the Dock, put the new
227 // one right before it.
228 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
229 NSString* dock_app_name =
230 [[persistent_app_paths objectAtIndex:index] lastPathComponent];
231 if ([dock_app_name isEqualToString:app_name]) {
238 #if defined(GOOGLE_CHROME_BUILD)
239 if (app_index == NSNotFound) {
240 // If this is an officially-branded Chrome (including Canary) and an
241 // application matching the "other" flavor is already in the Dock, put
242 // them next to each other. Google Chrome will precede Google Chrome
243 // Canary in the Dock.
244 NSString* chrome_name = @"Google Chrome.app";
245 NSString* canary_name = @"Google Chrome Canary.app";
246 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
247 NSString* dock_app_name =
248 [[persistent_app_paths objectAtIndex:index] lastPathComponent];
249 if ([dock_app_name isEqualToString:canary_name] &&
250 [app_name isEqualToString:chrome_name]) {
253 // Break: put Google Chrome.app before the first Google Chrome
256 } else if ([dock_app_name isEqualToString:chrome_name] &&
257 [app_name isEqualToString:canary_name]) {
258 app_index = index + 1;
260 // No break: put Google Chrome Canary.app after the last Google
265 #endif // GOOGLE_CHROME_BUILD
267 if (app_index == NSNotFound) {
268 // Put the new application after the last browser application already
269 // present in the Dock.
270 NSArray* other_browser_app_names =
271 [NSArray arrayWithObjects:
272 #if defined(GOOGLE_CHROME_BUILD)
273 @"Chromium.app", // Unbranded Google Chrome
275 @"Google Chrome.app",
276 @"Google Chrome Canary.app",
283 @"WebKit.app", // Safari nightly
284 @"Aurora.app", // Firefox dev
285 @"Nightly.app", // Firefox nightly
287 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
288 NSString* dock_app_name =
289 [[persistent_app_paths objectAtIndex:index] lastPathComponent];
290 if ([other_browser_app_names containsObject:dock_app_name]) {
291 app_index = index + 1;
296 if (app_index == NSNotFound) {
297 // Put the new application last in the Dock.
298 app_index = [persistent_apps count];
301 // Set up the new Dock tile.
302 NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES];
303 NSDictionary* url_dict = NSURLCopyDictionary(url);
305 LOG(ERROR) << "couldn't create url_dict";
306 return IconAddFailure;
309 NSDictionary* new_tile_data =
310 [NSDictionary dictionaryWithObject:url_dict
311 forKey:kDockFileDataKey];
312 NSDictionary* new_tile =
313 [NSDictionary dictionaryWithObject:new_tile_data
314 forKey:kDockTileDataKey];
316 // Add the new tile to the Dock.
317 [persistent_apps insertObject:new_tile atIndex:app_index];
318 [persistent_app_paths insertObject:installed_path atIndex:app_index];
322 // Verify that the arrays are still parallel.
323 DCHECK_EQ([persistent_apps count], [persistent_app_paths count]);
326 // If no changes were made, there's no point in rewriting the Dock's
327 // plist or restarting the Dock.
328 return IconAlreadyPresent;
331 // Rewrite the plist.
332 [dock_plist setObject:persistent_apps forKey:kDockPersistentAppsKey];
333 [user_defaults setPersistentDomain:dock_plist forName:kDockDomain];
336 return IconAddSuccess;