Home | History | Annotate | Download | only in mac
      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       // 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.
    127 void 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 
    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.
    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