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/memory/scoped_nsobject.h" 13#include "base/sys_string_conversions.h" 14 15namespace base { 16namespace mac { 17 18namespace { 19 20// The current count of 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(¤t_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 a 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 exclusion 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