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. 4 5#import "chrome/browser/mac/dock.h" 6 7#include <ApplicationServices/ApplicationServices.h> 8#import <Foundation/Foundation.h> 9#include <CoreFoundation/CoreFoundation.h> 10#include <signal.h> 11 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" 19 20extern "C" { 21 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 32// in _CFURLInit. 33 34CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url); 35CFURLRef _CFURLCreateFromPropertyListRepresentation( 36 CFAllocatorRef allocator, CFPropertyListRef property_list_representation); 37 38} // extern "C" 39 40namespace dock { 41namespace { 42 43NSString* const kDockTileDataKey = @"tile-data"; 44NSString* const kDockFileDataKey = @"file-data"; 45 46// A wrapper around _CFURLCopyPropertyListRepresentation that operates on 47// Foundation data types and returns an autoreleased NSDictionary. 48NSDictionary* 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); 55 56 if (!dictionary) { 57 return nil; 58 } 59 60 NSMakeCollectable(property_list.release()); 61 return [dictionary autorelease]; 62} 63 64// A wrapper around _CFURLCreateFromPropertyListRepresentation that operates 65// on Foundation data types and returns an autoreleased NSURL. 66NSURL* 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); 71 72 if (!url) { 73 return nil; 74 } 75 76 NSMakeCollectable(url_cf.release()); 77 return [url autorelease]; 78} 79 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. 83NSMutableArray* PersistentAppPaths(NSArray* persistent_apps) { 84 NSMutableArray* app_paths = 85 [NSMutableArray arrayWithCapacity:[persistent_apps count]]; 86 87 for (NSDictionary* app in persistent_apps) { 88 if (![app isKindOfClass:[NSDictionary class]]) { 89 LOG(ERROR) << "app not NSDictionary"; 90 return nil; 91 } 92 93 NSDictionary* tile_data = [app objectForKey:kDockTileDataKey]; 94 if (![tile_data isKindOfClass:[NSDictionary class]]) { 95 LOG(ERROR) << "tile_data not NSDictionary"; 96 return nil; 97 } 98 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:@""]; 105 continue; 106 } 107 108 NSURL* url = NSURLCreateFromDictionary(file_data); 109 if (!url) { 110 LOG(ERROR) << "no URL"; 111 return nil; 112 } 113 114 if (![url isFileURL]) { 115 LOG(ERROR) << "non-file URL"; 116 return nil; 117 } 118 119 NSString* path = [url path]; 120 [app_paths addObject:path]; 121 } 122 123 return app_paths; 124} 125 126// Restart the Dock process by sending it a SIGHUP. 127void Restart() { 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"); 132 if (pid <= 0) { 133 return; 134 } 135 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. 141 kill(pid, SIGHUP); 142} 143 144} // namespace 145 146AddIconStatus 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. 158 159 // There's enough potential allocation in this function to justify a 160 // distinct pool. 161 base::mac::ScopedNSAutoreleasePool autorelease_pool; 162 163 NSString* const kDockDomain = @"com.apple.dock"; 164 NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults]; 165 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; 171 } 172 NSMutableDictionary* dock_plist = 173 [NSMutableDictionary dictionaryWithDictionary:dock_plist_const]; 174 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; 181 } 182 NSMutableArray* persistent_apps = 183 [NSMutableArray arrayWithArray:persistent_apps_const]; 184 185 NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps); 186 if (!persistent_app_paths) { 187 return IconAddFailure; 188 } 189 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. 204 // 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. 207 app_index = index; 208 } 209 } 210 211 bool made_change = false; 212 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]; 217 made_change = true; 218 } 219 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]; 224 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]) { 232 app_index = index; 233 break; 234 } 235 } 236 } 237 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]) { 251 app_index = index; 252 253 // Break: put Google Chrome.app before the first Google Chrome 254 // Canary.app. 255 break; 256 } else if ([dock_app_name isEqualToString:chrome_name] && 257 [app_name isEqualToString:canary_name]) { 258 app_index = index + 1; 259 260 // No break: put Google Chrome Canary.app after the last Google 261 // Chrome.app. 262 } 263 } 264 } 265#endif // GOOGLE_CHROME_BUILD 266 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 274#else 275 @"Google Chrome.app", 276 @"Google Chrome Canary.app", 277#endif 278 @"Safari.app", 279 @"Firefox.app", 280 @"Camino.app", 281 @"Opera.app", 282 @"OmniWeb.app", 283 @"WebKit.app", // Safari nightly 284 @"Aurora.app", // Firefox dev 285 @"Nightly.app", // Firefox nightly 286 nil]; 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; 292 } 293 } 294 } 295 296 if (app_index == NSNotFound) { 297 // Put the new application last in the Dock. 298 app_index = [persistent_apps count]; 299 } 300 301 // Set up the new Dock tile. 302 NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES]; 303 NSDictionary* url_dict = NSURLCopyDictionary(url); 304 if (!url_dict) { 305 LOG(ERROR) << "couldn't create url_dict"; 306 return IconAddFailure; 307 } 308 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]; 315 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]; 319 made_change = true; 320 } 321 322 // Verify that the arrays are still parallel. 323 DCHECK_EQ([persistent_apps count], [persistent_app_paths count]); 324 325 if (!made_change) { 326 // If no changes were made, there's no point in rewriting the Dock's 327 // plist or restarting the Dock. 328 return IconAlreadyPresent; 329 } 330 331 // Rewrite the plist. 332 [dock_plist setObject:persistent_apps forKey:kDockPersistentAppsKey]; 333 [user_defaults setPersistentDomain:dock_plist forName:kDockDomain]; 334 335 Restart(); 336 return IconAddSuccess; 337} 338 339} // namespace dock 340