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