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#include <CoreFoundation/CoreFoundation.h> 9#import <Foundation/Foundation.h> 10#include <signal.h> 11 12#include "base/logging.h" 13#include "base/mac/bundle_locations.h" 14#include "base/mac/foundation_util.h" 15#include "base/mac/launchd.h" 16#include "base/mac/mac_logging.h" 17#include "base/mac/scoped_cftyperef.h" 18#include "base/mac/scoped_nsautorelease_pool.h" 19#include "base/strings/sys_string_conversions.h" 20#include "build/branding_buildflags.h" 21 22extern "C" { 23 24// Undocumented private internal CFURL functions. The Dock uses these to 25// serialize and deserialize CFURLs for use in its plist's file-data keys. See 26// 10.5.8 CF-476.19 and 10.7.2 CF-635.15's CFPriv.h and CFURL.c. The property 27// list representation will contain, at the very least, the _CFURLStringType 28// and _CFURLString keys. _CFURLStringType is a number that defines the 29// interpretation of the _CFURLString. It may be a CFURLPathStyle value, or 30// the CFURL-internal FULL_URL_REPRESENTATION value (15). Prior to Mac OS X 31// 10.7.2, the Dock plist always used kCFURLPOSIXPathStyle (0), formatting 32// _CFURLString as a POSIX path. In Mac OS X 10.7.2 (CF-635.15), it uses 33// FULL_URL_REPRESENTATION along with a file:/// URL. This is due to a change 34// in _CFURLInit. 35 36CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url); 37CFURLRef _CFURLCreateFromPropertyListRepresentation( 38 CFAllocatorRef allocator, CFPropertyListRef property_list_representation); 39 40} // extern "C" 41 42namespace dock { 43namespace { 44 45NSString* const kDockTileDataKey = @"tile-data"; 46NSString* const kDockFileDataKey = @"file-data"; 47NSString* const kDockDomain = @"com.apple.dock"; 48NSString* const kDockPersistentAppsKey = @"persistent-apps"; 49 50// A wrapper around _CFURLCopyPropertyListRepresentation that operates on 51// Foundation data types and returns an autoreleased NSDictionary. 52NSDictionary* NSURLCopyDictionary(NSURL* url) { 53 CFURLRef url_cf = base::mac::NSToCFCast(url); 54 base::ScopedCFTypeRef<CFPropertyListRef> property_list( 55 _CFURLCopyPropertyListRepresentation(url_cf)); 56 CFDictionaryRef dictionary_cf = 57 base::mac::CFCast<CFDictionaryRef>(property_list); 58 NSDictionary* dictionary = base::mac::CFToNSCast(dictionary_cf); 59 60 if (!dictionary) { 61 return nil; 62 } 63 64 NSMakeCollectable(property_list.release()); 65 return [dictionary autorelease]; 66} 67 68// A wrapper around _CFURLCreateFromPropertyListRepresentation that operates 69// on Foundation data types and returns an autoreleased NSURL. 70NSURL* NSURLCreateFromDictionary(NSDictionary* dictionary) { 71 CFDictionaryRef dictionary_cf = base::mac::NSToCFCast(dictionary); 72 base::ScopedCFTypeRef<CFURLRef> url_cf( 73 _CFURLCreateFromPropertyListRepresentation(NULL, dictionary_cf)); 74 NSURL* url = base::mac::CFToNSCast(url_cf); 75 76 if (!url) { 77 return nil; 78 } 79 80 NSMakeCollectable(url_cf.release()); 81 return [url autorelease]; 82} 83 84// Returns an array parallel to |persistent_apps| containing only the 85// pathnames of the Dock tiles contained therein. Returns nil on failure, such 86// as when the structure of |persistent_apps| is not understood. 87NSMutableArray* PersistentAppPaths(NSArray* persistent_apps) { 88 if (!persistent_apps) { 89 return nil; 90 } 91 92 NSMutableArray* app_paths = 93 [NSMutableArray arrayWithCapacity:[persistent_apps count]]; 94 95 for (NSDictionary* app in persistent_apps) { 96 if (![app isKindOfClass:[NSDictionary class]]) { 97 LOG(ERROR) << "app not NSDictionary"; 98 return nil; 99 } 100 101 NSDictionary* tile_data = app[kDockTileDataKey]; 102 if (![tile_data isKindOfClass:[NSDictionary class]]) { 103 LOG(ERROR) << "tile_data not NSDictionary"; 104 return nil; 105 } 106 107 NSDictionary* file_data = tile_data[kDockFileDataKey]; 108 if (![file_data isKindOfClass:[NSDictionary class]]) { 109 // Some apps (e.g. Dashboard) have no file data, but instead have a 110 // special value for the tile-type key. For these, add an empty string to 111 // align indexes with the source array. 112 [app_paths addObject:@""]; 113 continue; 114 } 115 116 NSURL* url = NSURLCreateFromDictionary(file_data); 117 if (!url) { 118 LOG(ERROR) << "no URL"; 119 return nil; 120 } 121 122 if (![url isFileURL]) { 123 LOG(ERROR) << "non-file URL"; 124 return nil; 125 } 126 127 NSString* path = [url path]; 128 [app_paths addObject:path]; 129 } 130 131 return app_paths; 132} 133 134// Restart the Dock process by sending it a SIGTERM. 135void Restart() { 136 // Doing this via launchd using the proper job label is the safest way to 137 // handle the restart. Unlike "killall Dock", looking this up via launchd 138 // guarantees that only the right process will be targeted. 139 pid_t pid = base::mac::PIDForJob("com.apple.Dock.agent"); 140 if (pid <= 0) { 141 return; 142 } 143 144 // Sending a SIGTERM to the Dock seems to be a more reliable way to get the 145 // replacement Dock process to read the newly written plist than using the 146 // equivalent of "launchctl stop" (even if followed by "launchctl start.") 147 // Note that this is a potential race in that pid may no longer be valid or 148 // may even have been reused. 149 kill(pid, SIGTERM); 150} 151 152NSDictionary* DockPlistFromUserDefaults() { 153 NSDictionary* dock_plist = [[NSUserDefaults standardUserDefaults] 154 persistentDomainForName:kDockDomain]; 155 if (![dock_plist isKindOfClass:[NSDictionary class]]) { 156 LOG(ERROR) << "dock_plist is not an NSDictionary"; 157 return nil; 158 } 159 return dock_plist; 160} 161 162NSArray* PersistentAppsFromDockPlist(NSDictionary* dock_plist) { 163 if (!dock_plist) { 164 return nil; 165 } 166 NSArray* persistent_apps = dock_plist[kDockPersistentAppsKey]; 167 if (![persistent_apps isKindOfClass:[NSArray class]]) { 168 LOG(ERROR) << "persistent_apps is not an NSArray"; 169 return nil; 170 } 171 return persistent_apps; 172} 173 174} // namespace 175 176ChromeInDockStatus ChromeIsInTheDock() { 177 NSDictionary* dock_plist = DockPlistFromUserDefaults(); 178 NSArray* persistent_apps = PersistentAppsFromDockPlist(dock_plist); 179 180 if (!persistent_apps) { 181 return ChromeInDockFailure; 182 } 183 184 NSString* launch_path = [base::mac::OuterBundle() bundlePath]; 185 186 return [PersistentAppPaths(persistent_apps) containsObject:launch_path] 187 ? ChromeInDockTrue 188 : ChromeInDockFalse; 189} 190 191AddIconStatus AddIcon(NSString* installed_path, NSString* dmg_app_path) { 192 // ApplicationServices.framework/Frameworks/HIServices.framework contains an 193 // undocumented function, CoreDockAddFileToDock, that is able to add items 194 // to the Dock "live" without requiring a Dock restart. Under the hood, it 195 // communicates with the Dock via Mach IPC. It is available as of Mac OS X 196 // 10.6. AddIcon could call CoreDockAddFileToDock if available, but 197 // CoreDockAddFileToDock seems to always to add the new Dock icon last, 198 // where AddIcon takes care to position the icon appropriately. Based on 199 // disassembly, the signature of the undocumented function appears to be 200 // extern "C" OSStatus CoreDockAddFileToDock(CFURLRef url, int); 201 // The int argument doesn't appear to have any effect. It's not used as the 202 // position to place the icon as hoped. 203 204 // There's enough potential allocation in this function to justify a 205 // distinct pool. 206 base::mac::ScopedNSAutoreleasePool autorelease_pool; 207 208 NSMutableDictionary* dock_plist = [NSMutableDictionary 209 dictionaryWithDictionary:DockPlistFromUserDefaults()]; 210 NSMutableArray* persistent_apps = 211 [NSMutableArray arrayWithArray:PersistentAppsFromDockPlist(dock_plist)]; 212 213 NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps); 214 if (!persistent_app_paths) { 215 return IconAddFailure; 216 } 217 218 NSUInteger already_installed_app_index = NSNotFound; 219 NSUInteger app_index = NSNotFound; 220 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 221 NSString* app_path = persistent_app_paths[index]; 222 if ([app_path isEqualToString:installed_path]) { 223 // If the Dock already contains a reference to the newly installed 224 // application, don't add another one. 225 already_installed_app_index = index; 226 } else if ([app_path isEqualToString:dmg_app_path]) { 227 // If the Dock contains a reference to the application on the disk 228 // image, replace it with a reference to the newly installed 229 // application. However, if the Dock contains a reference to both the 230 // application on the disk image and the newly installed application, 231 // just remove the one referencing the disk image. 232 // 233 // This case is only encountered when the user drags the icon from the 234 // disk image volume window in the Finder directly into the Dock. 235 app_index = index; 236 } 237 } 238 239 bool made_change = false; 240 241 if (app_index != NSNotFound) { 242 // Remove the Dock's reference to the application on the disk image. 243 [persistent_apps removeObjectAtIndex:app_index]; 244 [persistent_app_paths removeObjectAtIndex:app_index]; 245 made_change = true; 246 } 247 248 if (already_installed_app_index == NSNotFound) { 249 // The Dock doesn't yet have a reference to the icon at the 250 // newly installed path. Figure out where to put the new icon. 251 NSString* app_name = [installed_path lastPathComponent]; 252 253 if (app_index == NSNotFound) { 254 // If an application with this name is already in the Dock, put the new 255 // one right before it. 256 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 257 NSString* dock_app_name = 258 [persistent_app_paths[index] lastPathComponent]; 259 if ([dock_app_name isEqualToString:app_name]) { 260 app_index = index; 261 break; 262 } 263 } 264 } 265 266#if BUILDFLAG(GOOGLE_CHROME_BRANDING) 267 if (app_index == NSNotFound) { 268 // If this is an officially-branded Chrome (including Canary) and an 269 // application matching the "other" flavor is already in the Dock, put 270 // them next to each other. Google Chrome will precede Google Chrome 271 // Canary in the Dock. 272 NSString* chrome_name = @"Google Chrome.app"; 273 NSString* canary_name = @"Google Chrome Canary.app"; 274 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 275 NSString* dock_app_name = 276 [[persistent_app_paths objectAtIndex:index] lastPathComponent]; 277 if ([dock_app_name isEqualToString:canary_name] && 278 [app_name isEqualToString:chrome_name]) { 279 app_index = index; 280 281 // Break: put Google Chrome.app before the first Google Chrome 282 // Canary.app. 283 break; 284 } else if ([dock_app_name isEqualToString:chrome_name] && 285 [app_name isEqualToString:canary_name]) { 286 app_index = index + 1; 287 288 // No break: put Google Chrome Canary.app after the last Google 289 // Chrome.app. 290 } 291 } 292 } 293#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING) 294 295 if (app_index == NSNotFound) { 296 // Put the new application after the last browser application already 297 // present in the Dock. 298 NSArray* other_browser_app_names = 299 [NSArray arrayWithObjects: 300#if BUILDFLAG(GOOGLE_CHROME_BRANDING) 301 @"Chromium.app", // Unbranded Google Chrome 302#else 303 @"Google Chrome.app", @"Google Chrome Canary.app", 304#endif 305 @"Safari.app", @"Firefox.app", @"Camino.app", 306 @"Opera.app", @"OmniWeb.app", 307 @"WebKit.app", // Safari nightly 308 @"Aurora.app", // Firefox dev 309 @"Nightly.app", // Firefox nightly 310 nil]; 311 for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { 312 NSString* dock_app_name = 313 [persistent_app_paths[index] lastPathComponent]; 314 if ([other_browser_app_names containsObject:dock_app_name]) { 315 app_index = index + 1; 316 } 317 } 318 } 319 320 if (app_index == NSNotFound) { 321 // Put the new application last in the Dock. 322 app_index = [persistent_apps count]; 323 } 324 325 // Set up the new Dock tile. 326 NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES]; 327 NSDictionary* url_dict = NSURLCopyDictionary(url); 328 if (!url_dict) { 329 LOG(ERROR) << "couldn't create url_dict"; 330 return IconAddFailure; 331 } 332 333 NSDictionary* new_tile_data = @{kDockFileDataKey : url_dict}; 334 NSDictionary* new_tile = @{kDockTileDataKey : new_tile_data}; 335 336 // Add the new tile to the Dock. 337 [persistent_apps insertObject:new_tile atIndex:app_index]; 338 [persistent_app_paths insertObject:installed_path atIndex:app_index]; 339 made_change = true; 340 } 341 342 // Verify that the arrays are still parallel. 343 DCHECK_EQ([persistent_apps count], [persistent_app_paths count]); 344 345 if (!made_change) { 346 // If no changes were made, there's no point in rewriting the Dock's 347 // plist or restarting the Dock. 348 return IconAlreadyPresent; 349 } 350 351 // Rewrite the plist. 352 dock_plist[kDockPersistentAppsKey] = persistent_apps; 353 [[NSUserDefaults standardUserDefaults] setPersistentDomain:dock_plist 354 forName:kDockDomain]; 355 356 Restart(); 357 return IconAddSuccess; 358} 359 360} // namespace dock 361