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 20 extern "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 34 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url); 35 CFURLRef _CFURLCreateFromPropertyListRepresentation( 36 CFAllocatorRef allocator, CFPropertyListRef property_list_representation); 37 38 } // extern "C" 39 40 namespace dock { 41 namespace { 42 43 NSString* const kDockTileDataKey = @"tile-data"; 44 NSString* const kDockFileDataKey = @"file-data"; 45 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); 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. 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); 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. 83 NSMutableArray* 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 LOG(ERROR) << "file_data not NSDictionary"; 102 return nil; 103 } 104 105 NSURL* url = NSURLCreateFromDictionary(file_data); 106 if (!url) { 107 LOG(ERROR) << "no URL"; 108 return nil; 109 } 110 111 if (![url isFileURL]) { 112 LOG(ERROR) << "non-file URL"; 113 return nil; 114 } 115 116 NSString* path = [url path]; 117 [app_paths addObject:path]; 118 } 119 120 return app_paths; 121 } 122 123 // Restart the Dock process by sending it a SIGHUP. 124 void Restart() { 125 // Doing this via launchd using the proper job label is the safest way to 126 // handle the restart. Unlike "killall Dock", looking this up via launchd 127 // guarantees that only the right process will be targeted. 128 pid_t pid = base::mac::PIDForJob("com.apple.Dock.agent"); 129 if (pid <= 0) { 130 return; 131 } 132 133 // Sending a SIGHUP to the Dock seems to be a more reliable way to get the 134 // replacement Dock process to read the newly written plist than using the 135 // equivalent of "launchctl stop" (even if followed by "launchctl start.") 136 // Note that this is a potential race in that pid may no longer be valid or 137 // may even have been reused. 138 kill(pid, SIGHUP); 139 } 140 141 } // namespace 142 143 void AddIcon(NSString* installed_path, NSString* dmg_app_path) { 144 // ApplicationServices.framework/Frameworks/HIServices.framework contains an 145 // undocumented function, CoreDockAddFileToDock, that is able to add items 146 // to the Dock "live" without requiring a Dock restart. Under the hood, it 147 // communicates with the Dock via Mach IPC. It is available as of Mac OS X 148 // 10.6. AddIcon could call CoreDockAddFileToDock if available, but 149 // CoreDockAddFileToDock seems to always to add the new Dock icon last, 150 // where AddIcon takes care to position the icon appropriately. Based on 151 // disassembly, the signature of the undocumented function appears to be 152 // extern "C" OSStatus CoreDockAddFileToDock(CFURLRef url, int); 153 // The int argument doesn't appear to have any effect. It's not used as the 154 // position to place the icon as hoped. 155 156 // There's enough potential allocation in this function to justify a 157 // distinct pool. 158 base::mac::ScopedNSAutoreleasePool autorelease_pool; 159 160 NSString* const kDockDomain = @"com.apple.dock"; 161 NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults]; 162 163 NSDictionary* dock_plist_const = 164 [user_defaults persistentDomainForName:kDockDomain]; 165 if (![dock_plist_const isKindOfClass:[NSDictionary class]]) { 166 LOG(ERROR) << "dock_plist_const not NSDictionary"; 167 return; 168 } 169 NSMutableDictionary* dock_plist = 170 [NSMutableDictionary dictionaryWithDictionary:dock_plist_const]; 171 172 NSString* const kDockPersistentAppsKey = @"persistent-apps"; 173 NSArray* persistent_apps_const = 174 [dock_plist objectForKey:kDockPersistentAppsKey]; 175 if (![persistent_apps_const isKindOfClass:[NSArray class]]) { 176 LOG(ERROR) << "persistent_apps_const not NSArray"; 177 return; 178 } 179 NSMutableArray* persistent_apps = 180 [NSMutableArray arrayWithArray:persistent_apps_const]; 181 182 NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps); 183 if (!persistent_app_paths) { 184 return; 185 } 186 187 NSUInteger already_installed_app_index = NSNotFound; 188 NSUInteger app_index = NSNotFound; 189 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 190 NSString* app_path = [persistent_app_paths objectAtIndex:index]; 191 if ([app_path isEqualToString:installed_path]) { 192 // If the Dock already contains a reference to the newly installed 193 // application, don't add another one. 194 already_installed_app_index = index; 195 } else if ([app_path isEqualToString:dmg_app_path]) { 196 // If the Dock contains a reference to the application on the disk 197 // image, replace it with a reference to the newly installed 198 // application. However, if the Dock contains a reference to both the 199 // application on the disk image and the newly installed application, 200 // just remove the one referencing the disk image. 201 // 202 // This case is only encountered when the user drags the icon from the 203 // disk image volume window in the Finder directly into the Dock. 204 app_index = index; 205 } 206 } 207 208 bool made_change = false; 209 210 if (app_index != NSNotFound) { 211 // Remove the Dock's reference to the application on the disk image. 212 [persistent_apps removeObjectAtIndex:app_index]; 213 [persistent_app_paths removeObjectAtIndex:app_index]; 214 made_change = true; 215 } 216 217 if (already_installed_app_index == NSNotFound) { 218 // The Dock doesn't yet have a reference to the icon at the 219 // newly installed path. Figure out where to put the new icon. 220 NSString* app_name = [installed_path lastPathComponent]; 221 222 if (app_index == NSNotFound) { 223 // If an application with this name is already in the Dock, put the new 224 // one right before it. 225 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 226 NSString* dock_app_name = 227 [[persistent_app_paths objectAtIndex:index] lastPathComponent]; 228 if ([dock_app_name isEqualToString:app_name]) { 229 app_index = index; 230 break; 231 } 232 } 233 } 234 235 #if defined(GOOGLE_CHROME_BUILD) 236 if (app_index == NSNotFound) { 237 // If this is an officially-branded Chrome (including Canary) and an 238 // application matching the "other" flavor is already in the Dock, put 239 // them next to each other. Google Chrome will precede Google Chrome 240 // Canary in the Dock. 241 NSString* chrome_name = @"Google Chrome.app"; 242 NSString* canary_name = @"Google Chrome Canary.app"; 243 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 244 NSString* dock_app_name = 245 [[persistent_app_paths objectAtIndex:index] lastPathComponent]; 246 if ([dock_app_name isEqualToString:canary_name] && 247 [app_name isEqualToString:chrome_name]) { 248 app_index = index; 249 250 // Break: put Google Chrome.app before the first Google Chrome 251 // Canary.app. 252 break; 253 } else if ([dock_app_name isEqualToString:chrome_name] && 254 [app_name isEqualToString:canary_name]) { 255 app_index = index + 1; 256 257 // No break: put Google Chrome Canary.app after the last Google 258 // Chrome.app. 259 } 260 } 261 } 262 #endif // GOOGLE_CHROME_BUILD 263 264 if (app_index == NSNotFound) { 265 // Put the new application after the last browser application already 266 // present in the Dock. 267 NSArray* other_browser_app_names = 268 [NSArray arrayWithObjects: 269 #if defined(GOOGLE_CHROME_BUILD) 270 @"Chromium.app", // Unbranded Google Chrome 271 #else 272 @"Google Chrome.app", 273 @"Google Chrome Canary.app", 274 #endif 275 @"Safari.app", 276 @"Firefox.app", 277 @"Camino.app", 278 @"Opera.app", 279 @"OmniWeb.app", 280 @"WebKit.app", // Safari nightly 281 @"Aurora.app", // Firefox dev 282 @"Nightly.app", // Firefox nightly 283 nil]; 284 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 285 NSString* dock_app_name = 286 [[persistent_app_paths objectAtIndex:index] lastPathComponent]; 287 if ([other_browser_app_names containsObject:dock_app_name]) { 288 app_index = index + 1; 289 } 290 } 291 } 292 293 if (app_index == NSNotFound) { 294 // Put the new application last in the Dock. 295 app_index = [persistent_apps count]; 296 } 297 298 // Set up the new Dock tile. 299 NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES]; 300 NSDictionary* url_dict = NSURLCopyDictionary(url); 301 if (!url_dict) { 302 LOG(ERROR) << "couldn't create url_dict"; 303 return; 304 } 305 306 NSDictionary* new_tile_data = 307 [NSDictionary dictionaryWithObject:url_dict 308 forKey:kDockFileDataKey]; 309 NSDictionary* new_tile = 310 [NSDictionary dictionaryWithObject:new_tile_data 311 forKey:kDockTileDataKey]; 312 313 // Add the new tile to the Dock. 314 [persistent_apps insertObject:new_tile atIndex:app_index]; 315 [persistent_app_paths insertObject:installed_path atIndex:app_index]; 316 made_change = true; 317 } 318 319 // Verify that the arrays are still parallel. 320 DCHECK_EQ([persistent_apps count], [persistent_app_paths count]); 321 322 if (!made_change) { 323 // If no changes were made, there's no point in rewriting the Dock's 324 // plist or restarting the Dock. 325 return; 326 } 327 328 // Rewrite the plist. 329 [dock_plist setObject:persistent_apps forKey:kDockPersistentAppsKey]; 330 [user_defaults setPersistentDomain:dock_plist forName:kDockDomain]; 331 332 Restart(); 333 } 334 335 } // namespace dock 336