mac_util.mm revision dc0f95d653279beabeb9817299e2902918ba123e
1// Copyright (c) 2011 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#include "base/mac/mac_util.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include "base/file_path.h"
10#include "base/logging.h"
11#include "base/mac/scoped_cftyperef.h"
12#include "base/scoped_nsobject.h"
13#include "base/sys_string_conversions.h"
14
15namespace base {
16namespace mac {
17
18namespace {
19
20// a count of currently outstanding requests for full screen mode from browser
21// windows, plugins, etc.
22int g_full_screen_requests[kNumFullScreenModes] = { 0, 0, 0};
23
24// Sets the appropriate SystemUIMode based on the current full screen requests.
25// Since only one SystemUIMode can be active at a given time, full screen
26// requests are ordered by priority.  If there are no outstanding full screen
27// requests, reverts to normal mode.  If the correct SystemUIMode is already
28// set, does nothing.
29void SetUIMode() {
30  // Get the current UI mode.
31  SystemUIMode current_mode;
32  GetSystemUIMode(&current_mode, NULL);
33
34  // Determine which mode should be active, based on which requests are
35  // currently outstanding.  More permissive requests take precedence.  For
36  // example, plugins request |kFullScreenModeAutoHideAll|, while browser
37  // windows request |kFullScreenModeHideDock| when the fullscreen overlay is
38  // down.  Precedence goes to plugins in this case, so AutoHideAll wins over
39  // HideDock.
40  SystemUIMode desired_mode = kUIModeNormal;
41  SystemUIOptions desired_options = 0;
42  if (g_full_screen_requests[kFullScreenModeAutoHideAll] > 0) {
43    desired_mode = kUIModeAllHidden;
44    desired_options = kUIOptionAutoShowMenuBar;
45  } else if (g_full_screen_requests[kFullScreenModeHideDock] > 0) {
46    desired_mode = kUIModeContentHidden;
47  } else if (g_full_screen_requests[kFullScreenModeHideAll] > 0) {
48    desired_mode = kUIModeAllHidden;
49  }
50
51  if (current_mode != desired_mode)
52    SetSystemUIMode(desired_mode, desired_options);
53}
54
55bool WasLaunchedAsLoginItem() {
56  ProcessSerialNumber psn = { 0, kCurrentProcess };
57
58  scoped_nsobject<NSDictionary> process_info(
59      CFToNSCast(ProcessInformationCopyDictionary(&psn,
60                     kProcessDictionaryIncludeAllInformationMask)));
61
62  long long temp = [[process_info objectForKey:@"ParentPSN"] longLongValue];
63  ProcessSerialNumber parent_psn =
64      { (temp >> 32) & 0x00000000FFFFFFFFLL, temp & 0x00000000FFFFFFFFLL };
65
66  scoped_nsobject<NSDictionary> parent_info(
67      CFToNSCast(ProcessInformationCopyDictionary(&parent_psn,
68                     kProcessDictionaryIncludeAllInformationMask)));
69
70  // Check that creator process code is that of loginwindow.
71  BOOL result =
72      [[parent_info objectForKey:@"FileCreator"] isEqualToString:@"lgnw"];
73
74  return result == YES;
75}
76
77// Looks into Shared File Lists corresponding to Login Items for the item
78// representing the current application. If such an item is found, returns
79// retained reference to it. Caller is responsible for releasing the reference.
80LSSharedFileListItemRef GetLoginItemForApp() {
81  ScopedCFTypeRef<LSSharedFileListRef> login_items(LSSharedFileListCreate(
82      NULL, kLSSharedFileListSessionLoginItems, NULL));
83
84  if (!login_items.get()) {
85    LOG(ERROR) << "Couldn't get a Login Items list.";
86    return NULL;
87  }
88
89  scoped_nsobject<NSArray> login_items_array(
90      CFToNSCast(LSSharedFileListCopySnapshot(login_items, NULL)));
91
92  NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
93
94  for(NSUInteger i = 0; i < [login_items_array count]; ++i) {
95    LSSharedFileListItemRef item = reinterpret_cast<LSSharedFileListItemRef>(
96        [login_items_array objectAtIndex:i]);
97    CFURLRef item_url_ref = NULL;
98
99    if (LSSharedFileListItemResolve(item, 0, &item_url_ref, NULL) == noErr) {
100      ScopedCFTypeRef<CFURLRef> item_url(item_url_ref);
101      if (CFEqual(item_url, url)) {
102        CFRetain(item);
103        return item;
104      }
105    }
106  }
107
108  return NULL;
109}
110
111#if !defined(MAC_OS_X_VERSION_10_6) || \
112    MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6
113// kLSSharedFileListLoginItemHidden is supported on
114// 10.5, but missing from the 10.5 headers.
115// http://openradar.appspot.com/6482251
116static NSString* kLSSharedFileListLoginItemHidden =
117    @"com.apple.loginitem.HideOnLaunch";
118#endif
119
120bool IsHiddenLoginItem(LSSharedFileListItemRef item) {
121  ScopedCFTypeRef<CFBooleanRef> hidden(reinterpret_cast<CFBooleanRef>(
122      LSSharedFileListItemCopyProperty(item,
123          reinterpret_cast<CFStringRef>(kLSSharedFileListLoginItemHidden))));
124
125  return hidden && hidden == kCFBooleanTrue;
126}
127
128}  // namespace
129
130std::string PathFromFSRef(const FSRef& ref) {
131  ScopedCFTypeRef<CFURLRef> url(
132      CFURLCreateFromFSRef(kCFAllocatorDefault, &ref));
133  NSString *path_string = [(NSURL *)url.get() path];
134  return [path_string fileSystemRepresentation];
135}
136
137bool FSRefFromPath(const std::string& path, FSRef* ref) {
138  OSStatus status = FSPathMakeRef((const UInt8*)path.c_str(),
139                                  ref, nil);
140  return status == noErr;
141}
142
143CGColorSpaceRef GetSRGBColorSpace() {
144  // Leaked.  That's OK, it's scoped to the lifetime of the application.
145  static CGColorSpaceRef g_color_space_sRGB =
146      CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
147  LOG_IF(ERROR, !g_color_space_sRGB) << "Couldn't get the sRGB color space";
148  return g_color_space_sRGB;
149}
150
151CGColorSpaceRef GetSystemColorSpace() {
152  // Leaked.  That's OK, it's scoped to the lifetime of the application.
153  // Try to get the main display's color space.
154  static CGColorSpaceRef g_system_color_space =
155      CGDisplayCopyColorSpace(CGMainDisplayID());
156
157  if (!g_system_color_space) {
158    // Use a generic RGB color space.  This is better than nothing.
159    g_system_color_space = CGColorSpaceCreateDeviceRGB();
160
161    if (g_system_color_space) {
162      LOG(WARNING) <<
163          "Couldn't get the main display's color space, using generic";
164    } else {
165      LOG(ERROR) << "Couldn't get any color space";
166    }
167  }
168
169  return g_system_color_space;
170}
171
172// Add a request for full screen mode.  Must be called on the main thread.
173void RequestFullScreen(FullScreenMode mode) {
174  DCHECK_LT(mode, kNumFullScreenModes);
175  if (mode >= kNumFullScreenModes)
176    return;
177
178  DCHECK_GE(g_full_screen_requests[mode], 0);
179  g_full_screen_requests[mode] = std::max(g_full_screen_requests[mode] + 1, 1);
180  SetUIMode();
181}
182
183// Release a request for full screen mode.  Must be called on the main thread.
184void ReleaseFullScreen(FullScreenMode mode) {
185  DCHECK_LT(mode, kNumFullScreenModes);
186  if (mode >= kNumFullScreenModes)
187    return;
188
189  DCHECK_GT(g_full_screen_requests[mode], 0);
190  g_full_screen_requests[mode] = std::max(g_full_screen_requests[mode] - 1, 0);
191  SetUIMode();
192}
193
194// Switches full screen modes.  Releases a request for |from_mode| and adds a
195// new request for |to_mode|.  Must be called on the main thread.
196void SwitchFullScreenModes(FullScreenMode from_mode, FullScreenMode to_mode) {
197  DCHECK_LT(from_mode, kNumFullScreenModes);
198  DCHECK_LT(to_mode, kNumFullScreenModes);
199  if (from_mode >= kNumFullScreenModes || to_mode >= kNumFullScreenModes)
200    return;
201
202  DCHECK_GT(g_full_screen_requests[from_mode], 0);
203  DCHECK_GE(g_full_screen_requests[to_mode], 0);
204  g_full_screen_requests[from_mode] =
205      std::max(g_full_screen_requests[from_mode] - 1, 0);
206  g_full_screen_requests[to_mode] =
207      std::max(g_full_screen_requests[to_mode] + 1, 1);
208  SetUIMode();
209}
210
211void SetCursorVisibility(bool visible) {
212  if (visible)
213    [NSCursor unhide];
214  else
215    [NSCursor hide];
216}
217
218bool ShouldWindowsMiniaturizeOnDoubleClick() {
219  // We use an undocumented method in Cocoa; if it doesn't exist, default to
220  // |true|. If it ever goes away, we can do (using an undocumented pref key):
221  //   NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
222  //   return ![defaults objectForKey:@"AppleMiniaturizeOnDoubleClick"] ||
223  //          [defaults boolForKey:@"AppleMiniaturizeOnDoubleClick"];
224  BOOL methodImplemented =
225      [NSWindow respondsToSelector:@selector(_shouldMiniaturizeOnDoubleClick)];
226  DCHECK(methodImplemented);
227  return !methodImplemented ||
228      [NSWindow performSelector:@selector(_shouldMiniaturizeOnDoubleClick)];
229}
230
231void ActivateProcess(pid_t pid) {
232  ProcessSerialNumber process;
233  OSStatus status = GetProcessForPID(pid, &process);
234  if (status == noErr) {
235    SetFrontProcess(&process);
236  } else {
237    LOG(WARNING) << "Unable to get process for pid " << pid;
238  }
239}
240
241bool SetFileBackupExclusion(const FilePath& file_path, bool exclude) {
242  NSString* filePath =
243      [NSString stringWithUTF8String:file_path.value().c_str()];
244
245  // If being asked to exclude something in a tmp directory, just lie and say it
246  // was done.  TimeMachine will already ignore tmp directories.  This keeps the
247  // temporary profiles used by unittests from being added to the exclude list.
248  // Otherwise, as /Library/Preferences/com.apple.TimeMachine.plist grows the
249  // bots slow down due to reading/writing all the temporary profiles used over
250  // time.
251
252  NSString* tmpDir = NSTemporaryDirectory();
253  // Make sure the temp dir is terminated with a slash
254  if (tmpDir && ![tmpDir hasSuffix:@"/"])
255    tmpDir = [tmpDir stringByAppendingString:@"/"];
256  // '/var' is a link to '/private/var', make sure to check both forms.
257  NSString* privateTmpDir = nil;
258  if ([tmpDir hasPrefix:@"/var/"])
259    privateTmpDir = [@"/private" stringByAppendingString:tmpDir];
260
261  if ((tmpDir && [filePath hasPrefix:tmpDir]) ||
262      (privateTmpDir && [filePath hasPrefix:privateTmpDir]) ||
263      [filePath hasPrefix:@"/tmp/"] ||
264      [filePath hasPrefix:@"/var/tmp/"] ||
265      [filePath hasPrefix:@"/private/tmp/"] ||
266      [filePath hasPrefix:@"/private/var/tmp/"]) {
267    return true;
268  }
269
270  NSURL* url = [NSURL fileURLWithPath:filePath];
271  // Note that we always set CSBackupSetItemExcluded's excludeByPath param
272  // to true.  This prevents a problem with toggling the setting: if the file
273  // is excluded with excludeByPath set to true then excludeByPath must
274  // also be true when un-excluding the file, otherwise the un-excluding
275  // will be ignored.
276  bool success =
277      CSBackupSetItemExcluded((CFURLRef)url, exclude, true) == noErr;
278  if (!success)
279    LOG(WARNING) << "Failed to set backup excluson for file '"
280                 << file_path.value().c_str() << "'.  Continuing.";
281  return success;
282}
283
284void SetProcessName(CFStringRef process_name) {
285  if (!process_name || CFStringGetLength(process_name) == 0) {
286    NOTREACHED() << "SetProcessName given bad name.";
287    return;
288  }
289
290  if (![NSThread isMainThread]) {
291    NOTREACHED() << "Should only set process name from main thread.";
292    return;
293  }
294
295  // Warning: here be dragons! This is SPI reverse-engineered from WebKit's
296  // plugin host, and could break at any time (although realistically it's only
297  // likely to break in a new major release).
298  // When 10.7 is available, check that this still works, and update this
299  // comment for 10.8.
300
301  // Private CFType used in these LaunchServices calls.
302  typedef CFTypeRef PrivateLSASN;
303  typedef PrivateLSASN (*LSGetCurrentApplicationASNType)();
304  typedef OSStatus (*LSSetApplicationInformationItemType)(int, PrivateLSASN,
305                                                          CFStringRef,
306                                                          CFStringRef,
307                                                          CFDictionaryRef*);
308
309  static LSGetCurrentApplicationASNType ls_get_current_application_asn_func =
310      NULL;
311  static LSSetApplicationInformationItemType
312      ls_set_application_information_item_func = NULL;
313  static CFStringRef ls_display_name_key = NULL;
314
315  static bool did_symbol_lookup = false;
316  if (!did_symbol_lookup) {
317    did_symbol_lookup = true;
318    CFBundleRef launch_services_bundle =
319        CFBundleGetBundleWithIdentifier(CFSTR("com.apple.LaunchServices"));
320    if (!launch_services_bundle) {
321      LOG(ERROR) << "Failed to look up LaunchServices bundle";
322      return;
323    }
324
325    ls_get_current_application_asn_func =
326        reinterpret_cast<LSGetCurrentApplicationASNType>(
327            CFBundleGetFunctionPointerForName(
328                launch_services_bundle, CFSTR("_LSGetCurrentApplicationASN")));
329    if (!ls_get_current_application_asn_func)
330      LOG(ERROR) << "Could not find _LSGetCurrentApplicationASN";
331
332    ls_set_application_information_item_func =
333        reinterpret_cast<LSSetApplicationInformationItemType>(
334            CFBundleGetFunctionPointerForName(
335                launch_services_bundle,
336                CFSTR("_LSSetApplicationInformationItem")));
337    if (!ls_set_application_information_item_func)
338      LOG(ERROR) << "Could not find _LSSetApplicationInformationItem";
339
340    CFStringRef* key_pointer = reinterpret_cast<CFStringRef*>(
341        CFBundleGetDataPointerForName(launch_services_bundle,
342                                      CFSTR("_kLSDisplayNameKey")));
343    ls_display_name_key = key_pointer ? *key_pointer : NULL;
344    if (!ls_display_name_key)
345      LOG(ERROR) << "Could not find _kLSDisplayNameKey";
346
347    // Internally, this call relies on the Mach ports that are started up by the
348    // Carbon Process Manager.  In debug builds this usually happens due to how
349    // the logging layers are started up; but in release, it isn't started in as
350    // much of a defined order.  So if the symbols had to be loaded, go ahead
351    // and force a call to make sure the manager has been initialized and hence
352    // the ports are opened.
353    ProcessSerialNumber psn;
354    GetCurrentProcess(&psn);
355  }
356  if (!ls_get_current_application_asn_func ||
357      !ls_set_application_information_item_func ||
358      !ls_display_name_key) {
359    return;
360  }
361
362  PrivateLSASN asn = ls_get_current_application_asn_func();
363  // Constant used by WebKit; what exactly it means is unknown.
364  const int magic_session_constant = -2;
365  OSErr err =
366      ls_set_application_information_item_func(magic_session_constant, asn,
367                                               ls_display_name_key,
368                                               process_name,
369                                               NULL /* optional out param */);
370  LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
371}
372
373// Converts a NSImage to a CGImageRef.  Normally, the system frameworks can do
374// this fine, especially on 10.6.  On 10.5, however, CGImage cannot handle
375// converting a PDF-backed NSImage into a CGImageRef.  This function will
376// rasterize the PDF into a bitmap CGImage.  The caller is responsible for
377// releasing the return value.
378CGImageRef CopyNSImageToCGImage(NSImage* image) {
379  // This is based loosely on http://www.cocoadev.com/index.pl?CGImageRef .
380  NSSize size = [image size];
381  ScopedCFTypeRef<CGContextRef> context(
382      CGBitmapContextCreate(NULL,  // Allow CG to allocate memory.
383                            size.width,
384                            size.height,
385                            8,  // bitsPerComponent
386                            0,  // bytesPerRow - CG will calculate by default.
387                            [[NSColorSpace genericRGBColorSpace] CGColorSpace],
388                            kCGBitmapByteOrder32Host |
389                                kCGImageAlphaPremultipliedFirst));
390  if (!context.get())
391    return NULL;
392
393  [NSGraphicsContext saveGraphicsState];
394  [NSGraphicsContext setCurrentContext:
395      [NSGraphicsContext graphicsContextWithGraphicsPort:context.get()
396                                                 flipped:NO]];
397  [image drawInRect:NSMakeRect(0,0, size.width, size.height)
398           fromRect:NSZeroRect
399          operation:NSCompositeCopy
400           fraction:1.0];
401  [NSGraphicsContext restoreGraphicsState];
402
403  return CGBitmapContextCreateImage(context);
404}
405
406bool CheckLoginItemStatus(bool* is_hidden) {
407  ScopedCFTypeRef<LSSharedFileListItemRef> item(GetLoginItemForApp());
408  if (!item.get())
409    return false;
410
411  if (is_hidden)
412    *is_hidden = IsHiddenLoginItem(item);
413
414  return true;
415}
416
417void AddToLoginItems(bool hide_on_startup) {
418  ScopedCFTypeRef<LSSharedFileListItemRef> item(GetLoginItemForApp());
419  if (item.get() && (IsHiddenLoginItem(item) == hide_on_startup)) {
420    return;  // Already is a login item with required hide flag.
421  }
422
423  ScopedCFTypeRef<LSSharedFileListRef> login_items(LSSharedFileListCreate(
424      NULL, kLSSharedFileListSessionLoginItems, NULL));
425
426  if (!login_items.get()) {
427    LOG(ERROR) << "Couldn't get a Login Items list.";
428    return;
429  }
430
431  // Remove the old item, it has wrong hide flag, we'll create a new one.
432  if (item.get()) {
433    LSSharedFileListItemRemove(login_items, item);
434  }
435
436  NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
437
438  BOOL hide = hide_on_startup ? YES : NO;
439  NSDictionary* properties =
440      [NSDictionary
441        dictionaryWithObject:[NSNumber numberWithBool:hide]
442                      forKey:(NSString*)kLSSharedFileListLoginItemHidden];
443
444  ScopedCFTypeRef<LSSharedFileListItemRef> new_item;
445  new_item.reset(LSSharedFileListInsertItemURL(
446      login_items, kLSSharedFileListItemLast, NULL, NULL,
447      reinterpret_cast<CFURLRef>(url),
448      reinterpret_cast<CFDictionaryRef>(properties), NULL));
449
450  if (!new_item.get()) {
451    LOG(ERROR) << "Couldn't insert current app into Login Items list.";
452  }
453}
454
455void RemoveFromLoginItems() {
456  ScopedCFTypeRef<LSSharedFileListItemRef> item(GetLoginItemForApp());
457  if (!item.get())
458    return;
459
460  ScopedCFTypeRef<LSSharedFileListRef> login_items(LSSharedFileListCreate(
461      NULL, kLSSharedFileListSessionLoginItems, NULL));
462
463  if (!login_items.get()) {
464    LOG(ERROR) << "Couldn't get a Login Items list.";
465    return;
466  }
467
468  LSSharedFileListItemRemove(login_items, item);
469}
470
471bool WasLaunchedAsHiddenLoginItem() {
472  if (!WasLaunchedAsLoginItem())
473    return false;
474
475  ScopedCFTypeRef<LSSharedFileListItemRef> item(GetLoginItemForApp());
476  if (!item.get()) {
477    LOG(ERROR) << "Process launched at Login but can't access Login Item List.";
478    return false;
479  }
480  return IsHiddenLoginItem(item);
481}
482
483// Definitions for the corresponding CF_TO_NS_CAST_DECL macros in mac_util.h.
484#define CF_TO_NS_CAST_DEFN(TypeCF, TypeNS) \
485\
486TypeNS* CFToNSCast(TypeCF##Ref cf_val) { \
487  DCHECK(!cf_val || TypeCF##GetTypeID() == CFGetTypeID(cf_val)); \
488  TypeNS* ns_val = \
489      const_cast<TypeNS*>(reinterpret_cast<const TypeNS*>(cf_val)); \
490  return ns_val; \
491} \
492\
493TypeCF##Ref NSToCFCast(TypeNS* ns_val) { \
494  TypeCF##Ref cf_val = reinterpret_cast<TypeCF##Ref>(ns_val); \
495  DCHECK(!cf_val || TypeCF##GetTypeID() == CFGetTypeID(cf_val)); \
496  return cf_val; \
497} \
498
499#define CF_TO_NS_MUTABLE_CAST_DEFN(name) \
500CF_TO_NS_CAST_DEFN(CF##name, NS##name) \
501\
502NSMutable##name* CFToNSCast(CFMutable##name##Ref cf_val) { \
503  DCHECK(!cf_val || CF##name##GetTypeID() == CFGetTypeID(cf_val)); \
504  NSMutable##name* ns_val = reinterpret_cast<NSMutable##name*>(cf_val); \
505  return ns_val; \
506} \
507\
508CFMutable##name##Ref NSToCFCast(NSMutable##name* ns_val) { \
509  CFMutable##name##Ref cf_val = \
510      reinterpret_cast<CFMutable##name##Ref>(ns_val); \
511  DCHECK(!cf_val || CF##name##GetTypeID() == CFGetTypeID(cf_val)); \
512  return cf_val; \
513} \
514
515CF_TO_NS_MUTABLE_CAST_DEFN(Array);
516CF_TO_NS_MUTABLE_CAST_DEFN(AttributedString);
517CF_TO_NS_CAST_DEFN(CFCalendar, NSCalendar);
518CF_TO_NS_MUTABLE_CAST_DEFN(CharacterSet);
519CF_TO_NS_MUTABLE_CAST_DEFN(Data);
520CF_TO_NS_CAST_DEFN(CFDate, NSDate);
521CF_TO_NS_MUTABLE_CAST_DEFN(Dictionary);
522CF_TO_NS_CAST_DEFN(CFError, NSError);
523CF_TO_NS_CAST_DEFN(CFLocale, NSLocale);
524CF_TO_NS_CAST_DEFN(CFNumber, NSNumber);
525CF_TO_NS_CAST_DEFN(CFRunLoopTimer, NSTimer);
526CF_TO_NS_CAST_DEFN(CFTimeZone, NSTimeZone);
527CF_TO_NS_MUTABLE_CAST_DEFN(Set);
528CF_TO_NS_CAST_DEFN(CFReadStream, NSInputStream);
529CF_TO_NS_CAST_DEFN(CFWriteStream, NSOutputStream);
530CF_TO_NS_MUTABLE_CAST_DEFN(String);
531CF_TO_NS_CAST_DEFN(CFURL, NSURL);
532
533}  // namespace mac
534}  // namespace base
535
536std::ostream& operator<<(std::ostream& o, const CFStringRef string) {
537  return o << base::SysCFStringRefToUTF8(string);
538}
539
540std::ostream& operator<<(std::ostream& o, const CFErrorRef err) {
541  base::mac::ScopedCFTypeRef<CFStringRef> desc(CFErrorCopyDescription(err));
542  base::mac::ScopedCFTypeRef<CFDictionaryRef> user_info(
543      CFErrorCopyUserInfo(err));
544  CFStringRef errorDesc = NULL;
545  if (user_info.get()) {
546    errorDesc = reinterpret_cast<CFStringRef>(
547        CFDictionaryGetValue(user_info.get(), kCFErrorDescriptionKey));
548  }
549  o << "Code: " << CFErrorGetCode(err)
550    << " Domain: " << CFErrorGetDomain(err)
551    << " Desc: " << desc.get();
552  if(errorDesc) {
553    o << "(" << errorDesc << ")";
554  }
555  return o;
556}
557