Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / mac / dock.mm
blobd3a4e16b575db8c7c5669de56c45619d20ad43ae
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.
5 #import "chrome/browser/mac/dock.h"
7 #include <ApplicationServices/ApplicationServices.h>
8 #include <CoreFoundation/CoreFoundation.h>
9 #import <Foundation/Foundation.h>
10 #include <signal.h>
12 #include "base/logging.h"
13 #include "base/mac/foundation_util.h"
14 #include "base/mac/launchd.h"
15 #include "base/mac/mac_logging.h"
16 #include "base/mac/scoped_cftyperef.h"
17 #include "base/mac/scoped_nsautorelease_pool.h"
18 #include "base/strings/sys_string_conversions.h"
20 extern "C" {
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.
34 CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
35 CFURLRef _CFURLCreateFromPropertyListRepresentation(
36     CFAllocatorRef allocator, CFPropertyListRef property_list_representation);
38 }  // extern "C"
40 namespace dock {
41 namespace {
43 NSString* const kDockTileDataKey = @"tile-data";
44 NSString* const kDockFileDataKey = @"file-data";
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);
56   if (!dictionary) {
57     return nil;
58   }
60   NSMakeCollectable(property_list.release());
61   return [dictionary autorelease];
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);
72   if (!url) {
73     return nil;
74   }
76   NSMakeCollectable(url_cf.release());
77   return [url autorelease];
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]];
87   for (NSDictionary* app in persistent_apps) {
88     if (![app isKindOfClass:[NSDictionary class]]) {
89       LOG(ERROR) << "app not NSDictionary";
90       return nil;
91     }
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     }
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     }
108     NSURL* url = NSURLCreateFromDictionary(file_data);
109     if (!url) {
110       LOG(ERROR) << "no URL";
111       return nil;
112     }
114     if (![url isFileURL]) {
115       LOG(ERROR) << "non-file URL";
116       return nil;
117     }
119     NSString* path = [url path];
120     [app_paths addObject:path];
121   }
123   return app_paths;
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   }
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);
144 }  // namespace
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.
159   // There's enough potential allocation in this function to justify a
160   // distinct pool.
161   base::mac::ScopedNSAutoreleasePool autorelease_pool;
163   NSString* const kDockDomain = @"com.apple.dock";
164   NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults];
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];
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];
185   NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps);
186   if (!persistent_app_paths) {
187     return IconAddFailure;
188   }
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   }
211   bool made_change = false;
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   }
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];
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     }
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;
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;
260           // No break: put Google Chrome Canary.app after the last Google
261           // Chrome.app.
262         }
263       }
264     }
265 #endif  // GOOGLE_CHROME_BUILD
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     }
296     if (app_index == NSNotFound) {
297       // Put the new application last in the Dock.
298       app_index = [persistent_apps count];
299     }
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     }
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];
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   }
322   // Verify that the arrays are still parallel.
323   DCHECK_EQ([persistent_apps count], [persistent_app_paths count]);
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   }
331   // Rewrite the plist.
332   [dock_plist setObject:persistent_apps forKey:kDockPersistentAppsKey];
333   [user_defaults setPersistentDomain:dock_plist forName:kDockDomain];
335   Restart();
336   return IconAddSuccess;
339 }  // namespace dock