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