| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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> |
| #import <Foundation/Foundation.h> |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <signal.h> |
| |
| #include "base/logging.h" |
| #include "base/mac/launchd.h" |
| #include "base/mac/mac_logging.h" |
| #include "base/mac/mac_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/strings/sys_string_conversions.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"; |
| |
| // A wrapper around _CFURLCopyPropertyListRepresentation that operates on |
| // Foundation data types and returns an autoreleased NSDictionary. |
| NSDictionary* NSURLCopyDictionary(NSURL* url) { |
| CFURLRef url_cf = base::mac::NSToCFCast(url); |
| base::ScopedCFTypeRef<CFPropertyListRef> property_list( |
| _CFURLCopyPropertyListRepresentation(url_cf)); |
| CFDictionaryRef dictionary_cf = |
| base::mac::CFCast<CFDictionaryRef>(property_list); |
| NSDictionary* dictionary = base::mac::CFToNSCast(dictionary_cf); |
| |
| if (!dictionary) { |
| return nil; |
| } |
| |
| NSMakeCollectable(property_list.release()); |
| return [dictionary autorelease]; |
| } |
| |
| // A wrapper around _CFURLCreateFromPropertyListRepresentation that operates |
| // on Foundation data types and returns an autoreleased NSURL. |
| NSURL* NSURLCreateFromDictionary(NSDictionary* dictionary) { |
| CFDictionaryRef dictionary_cf = base::mac::NSToCFCast(dictionary); |
| base::ScopedCFTypeRef<CFURLRef> url_cf( |
| _CFURLCreateFromPropertyListRepresentation(NULL, dictionary_cf)); |
| NSURL* url = base::mac::CFToNSCast(url_cf); |
| |
| if (!url) { |
| return nil; |
| } |
| |
| NSMakeCollectable(url_cf.release()); |
| return [url autorelease]; |
| } |
| |
| // 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) { |
| 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 objectForKey:kDockTileDataKey]; |
| if (![tile_data isKindOfClass:[NSDictionary class]]) { |
| LOG(ERROR) << "tile_data not NSDictionary"; |
| return nil; |
| } |
| |
| NSDictionary* file_data = [tile_data objectForKey: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 = NSURLCreateFromDictionary(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; |
| } |
| |
| // Restart the Dock process by sending it a SIGHUP. |
| 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 SIGHUP 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, SIGHUP); |
| } |
| |
| } // namespace |
| |
| 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. |
| |
| // There's enough potential allocation in this function to justify a |
| // distinct pool. |
| base::mac::ScopedNSAutoreleasePool autorelease_pool; |
| |
| NSString* const kDockDomain = @"com.apple.dock"; |
| NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults]; |
| |
| NSDictionary* dock_plist_const = |
| [user_defaults persistentDomainForName:kDockDomain]; |
| if (![dock_plist_const isKindOfClass:[NSDictionary class]]) { |
| LOG(ERROR) << "dock_plist_const not NSDictionary"; |
| return IconAddFailure; |
| } |
| NSMutableDictionary* dock_plist = |
| [NSMutableDictionary dictionaryWithDictionary:dock_plist_const]; |
| |
| NSString* const kDockPersistentAppsKey = @"persistent-apps"; |
| NSArray* persistent_apps_const = |
| [dock_plist objectForKey:kDockPersistentAppsKey]; |
| if (![persistent_apps_const isKindOfClass:[NSArray class]]) { |
| LOG(ERROR) << "persistent_apps_const not NSArray"; |
| return IconAddFailure; |
| } |
| NSMutableArray* persistent_apps = |
| [NSMutableArray arrayWithArray:persistent_apps_const]; |
| |
| 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 objectAtIndex: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 objectAtIndex:index] lastPathComponent]; |
| if ([dock_app_name isEqualToString:app_name]) { |
| app_index = index; |
| break; |
| } |
| } |
| } |
| |
| #if defined(GOOGLE_CHROME_BUILD) |
| if (app_index == NSNotFound) { |
| // If this is an officially-branded Chrome (including Canary) and an |
| // application matching the "other" flavor is already in the Dock, put |
| // them next to each other. Google Chrome will precede Google Chrome |
| // Canary in the Dock. |
| NSString* chrome_name = @"Google Chrome.app"; |
| NSString* canary_name = @"Google Chrome Canary.app"; |
| for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { |
| NSString* dock_app_name = |
| [[persistent_app_paths objectAtIndex:index] lastPathComponent]; |
| if ([dock_app_name isEqualToString:canary_name] && |
| [app_name isEqualToString:chrome_name]) { |
| app_index = index; |
| |
| // Break: put Google Chrome.app before the first Google Chrome |
| // Canary.app. |
| break; |
| } else if ([dock_app_name isEqualToString:chrome_name] && |
| [app_name isEqualToString:canary_name]) { |
| app_index = index + 1; |
| |
| // No break: put Google Chrome Canary.app after the last Google |
| // Chrome.app. |
| } |
| } |
| } |
| #endif // GOOGLE_CHROME_BUILD |
| |
| if (app_index == NSNotFound) { |
| // Put the new application after the last browser application already |
| // present in the Dock. |
| NSArray* other_browser_app_names = |
| [NSArray arrayWithObjects: |
| #if defined(GOOGLE_CHROME_BUILD) |
| @"Chromium.app", // Unbranded Google Chrome |
| #else |
| @"Google Chrome.app", |
| @"Google Chrome Canary.app", |
| #endif |
| @"Safari.app", |
| @"Firefox.app", |
| @"Camino.app", |
| @"Opera.app", |
| @"OmniWeb.app", |
| @"WebKit.app", // Safari nightly |
| @"Aurora.app", // Firefox dev |
| @"Nightly.app", // Firefox nightly |
| nil]; |
| for (NSUInteger index = 0; index < [persistent_apps count]; ++index) { |
| NSString* dock_app_name = |
| [[persistent_app_paths objectAtIndex:index] lastPathComponent]; |
| if ([other_browser_app_names containsObject:dock_app_name]) { |
| app_index = index + 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 = NSURLCopyDictionary(url); |
| if (!url_dict) { |
| LOG(ERROR) << "couldn't create url_dict"; |
| return IconAddFailure; |
| } |
| |
| NSDictionary* new_tile_data = |
| [NSDictionary dictionaryWithObject:url_dict |
| forKey:kDockFileDataKey]; |
| NSDictionary* new_tile = |
| [NSDictionary dictionaryWithObject:new_tile_data |
| forKey:kDockTileDataKey]; |
| |
| // 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 setObject:persistent_apps forKey:kDockPersistentAppsKey]; |
| [user_defaults setPersistentDomain:dock_plist forName:kDockDomain]; |
| |
| Restart(); |
| return IconAddSuccess; |
| } |
| |
| } // namespace dock |