Framework/Source/GrowlApplicationBridge.m
author Peter Hosey <hg@boredzo.org>
Thu Sep 24 14:08:49 2009 -0700 (2009-09-24)
changeset 4430 0f739ceca8b4
parent 4429 58720a0d2d1f
child 4450 2466e516f7e2
permissions -rw-r--r--
Use AESendMessage, which comes from CoreServices, not AESend, which comes from Carbon.
     1 //
     2 //  GrowlApplicationBridge.m
     3 //  Growl
     4 //
     5 //  Created by Evan Schoenberg on Wed Jun 16 2004.
     6 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     7 //
     8 
     9 #import "GrowlApplicationBridge.h"
    10 #ifdef GROWL_WITH_INSTALLER
    11 #import "GrowlInstallationPrompt.h"
    12 #import "GrowlVersionUtilities.h"
    13 #endif
    14 #include "CFGrowlAdditions.h"
    15 #include "CFURLAdditions.h"
    16 #include "CFMutableDictionaryAdditions.h"
    17 #import "GrowlDefinesInternal.h"
    18 #import "GrowlPathUtilities.h"
    19 #import "GrowlPathway.h"
    20 
    21 #import <ApplicationServices/ApplicationServices.h>
    22 
    23 
    24 /*!
    25  * The 10.3+ exception handling can only work if -fobjc-exceptions is enabled
    26  */
    27 #if 0
    28 	#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3
    29 	# define TRY		@try {
    30 	# define ENDTRY		}
    31 	# define CATCH		@catch(NSException *localException) {
    32 	# define ENDCATCH	}
    33 	#else
    34 	# define TRY		NS_DURING
    35 	# define ENDTRY
    36 	# define CATCH		NS_HANDLER
    37 	# define ENDCATCH	NS_ENDHANDLER
    38 	#endif
    39 #else
    40 	# define TRY		NS_DURING
    41 	# define ENDTRY
    42 	# define CATCH		NS_HANDLER
    43 	# define ENDCATCH	NS_ENDHANDLER
    44 #endif
    45 
    46 @interface GrowlApplicationBridge (PRIVATE)
    47 /*!
    48  *	@method launchGrowlIfInstalled
    49  *	@abstract Launches GrowlHelperApp.
    50  *	@discussion Launches the GrowlHelperApp if it's not already running.
    51  *	 GROWL_IS_READY will be posted to the distributed notification center
    52  *	 once it is ready.
    53  *
    54  *	 Uses <code>+_launchGrowlIfInstalledWithRegistrationDictionary:</code>.
    55  *	@result Returns YES if GrowlHelperApp began launching or was already running, NO if Growl isn't installed
    56  */
    57 + (BOOL) launchGrowlIfInstalled;
    58 
    59 /*!
    60  *	@method _launchGrowlIfInstalledWithRegistrationDictionary:
    61  *	@abstract Launches GrowlHelperApp and registers.
    62  *	@discussion Launches the GrowlHelperApp if it's not already running, and passes it a registration dictionary.
    63  *	 If Growl is turned on in the Growl prefpane, GROWL_IS_READY will be posted
    64  *	 to the distributed notification center when Growl is listening for
    65  *	 notifications.
    66  *	@param regDict The dictionary with which to register.
    67  *	@result Returns YES if GrowlHelperApp began launching or was already running, NO if Growl isn't installed
    68  */
    69 + (BOOL) _launchGrowlIfInstalledWithRegistrationDictionary:(NSDictionary *)regDict;
    70 
    71 #ifdef GROWL_WITH_INSTALLER
    72 + (void) _checkForPackagedUpdateForGrowlPrefPaneBundle:(NSBundle *)growlPrefPaneBundle;
    73 #endif
    74 
    75 /*!	@method	_applicationNameForGrowlSearchingRegistrationDictionary:
    76  *	@abstract Obtain the name of the current application.
    77  *	@param regDict	The dictionary to search, or <code>nil</code> not to.
    78  *	@result	The name of the current application.
    79  *	@discussion	Does not call +bestRegistrationDictionary, and is therefore safe to call from it.
    80  */
    81 + (NSString *) _applicationNameForGrowlSearchingRegistrationDictionary:(NSDictionary *)regDict;
    82 /*!	@method	_applicationNameForGrowlSearchingRegistrationDictionary:
    83  *	@abstract Obtain the icon of the current application.
    84  *	@param regDict	The dictionary to search, or <code>nil</code> not to.
    85  *	@result	The icon of the current application, in IconFamily format (same as is used in 'icns' resources and .icns files).
    86  *	@discussion	Does not call +bestRegistrationDictionary, and is therefore safe to call from it.
    87  */
    88 + (NSData *) _applicationIconDataForGrowlSearchingRegistrationDictionary:(NSDictionary *)regDict;
    89 
    90 /*! @method growlProxy
    91  *  @abstract Obtain (creating a connection if needed) a proxy to the Growl Helper Application
    92  */
    93 + (NSProxy<GrowlNotificationProtocol> *) growlProxy;
    94 @end
    95 
    96 static NSDictionary *cachedRegistrationDictionary = nil;
    97 static NSString	*appName = nil;
    98 static NSData	*appIconData = nil;
    99 
   100 static id		delegate = nil;
   101 static BOOL		growlLaunched = NO;
   102 static NSProxy<GrowlNotificationProtocol> *growlProxy = nil;
   103 
   104 #ifdef GROWL_WITH_INSTALLER
   105 static NSMutableArray	*queuedGrowlNotifications = nil;
   106 
   107 static BOOL				userChoseNotToInstallGrowl = NO;
   108 static BOOL				promptedToInstallGrowl = NO;
   109 static BOOL				promptedToUpgradeGrowl = NO;
   110 #endif
   111 
   112 //used primarily by GIP, but could be useful elsewhere.
   113 static BOOL		registerWhenGrowlIsReady = NO;
   114 
   115 #pragma mark -
   116 
   117 @implementation GrowlApplicationBridge
   118 
   119 + (void) setGrowlDelegate:(NSObject<GrowlApplicationBridgeDelegate> *)inDelegate {
   120 	NSDistributedNotificationCenter *NSDNC = [NSDistributedNotificationCenter defaultCenter];
   121 
   122 	if (inDelegate != delegate) {
   123 		[delegate release];
   124 		delegate = [inDelegate retain];
   125 	}
   126 
   127 	[cachedRegistrationDictionary release];
   128 	cachedRegistrationDictionary = [[self bestRegistrationDictionary] retain];
   129 
   130 	//Cache the appName from the delegate or the process name
   131 	[appName autorelease];
   132 	appName = [[self _applicationNameForGrowlSearchingRegistrationDictionary:cachedRegistrationDictionary] retain];
   133 	if (!appName) {
   134 		NSLog(@"%@", @"GrowlApplicationBridge: Cannot register because the application name was not supplied and could not be determined");
   135 		return;
   136 	}
   137 
   138 	/* Cache the appIconData from the delegate if it responds to the
   139 	 * applicationIconDataForGrowl selector, or the application if not
   140 	 */
   141 	[appIconData autorelease];
   142 	appIconData = [[self _applicationIconDataForGrowlSearchingRegistrationDictionary:cachedRegistrationDictionary] retain];
   143 
   144 	//Add the observer for GROWL_IS_READY which will be triggered later if all goes well
   145 	[NSDNC addObserver:self
   146 			  selector:@selector(_growlIsReady:)
   147 				  name:GROWL_IS_READY
   148 				object:nil];
   149 
   150 	/* Watch for notification clicks if our delegate responds to the
   151 	 * growlNotificationWasClicked: selector. Notifications will come in on a
   152 	 * unique notification name based on our app name, pid and
   153 	 * GROWL_NOTIFICATION_CLICKED.
   154 	 */
   155 	int pid = [[NSProcessInfo processInfo] processIdentifier];
   156 	NSString *growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@-%d-%@",
   157 		appName, pid, GROWL_NOTIFICATION_CLICKED];
   158 	if ([delegate respondsToSelector:@selector(growlNotificationWasClicked:)])
   159 		[NSDNC addObserver:self
   160 				  selector:@selector(growlNotificationWasClicked:)
   161 					  name:growlNotificationClickedName
   162 					object:nil];
   163 	else
   164 		[NSDNC removeObserver:self
   165 						 name:growlNotificationClickedName
   166 					   object:nil];
   167 	[growlNotificationClickedName release];
   168 
   169 	NSString *growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@-%d-%@",
   170 		appName, pid, GROWL_NOTIFICATION_TIMED_OUT];
   171 	if ([delegate respondsToSelector:@selector(growlNotificationTimedOut:)])
   172 		[NSDNC addObserver:self
   173 				  selector:@selector(growlNotificationTimedOut:)
   174 					  name:growlNotificationTimedOutName
   175 					object:nil];
   176 	else
   177 		[NSDNC removeObserver:self
   178 						 name:growlNotificationTimedOutName
   179 					   object:nil];
   180 	[growlNotificationTimedOutName release];
   181 
   182 #ifdef GROWL_WITH_INSTALLER
   183 	//Determine if the user has previously told us not to ever request installation again
   184 	userChoseNotToInstallGrowl = [[NSUserDefaults standardUserDefaults] boolForKey:@"Growl Installation:Do Not Prompt Again"];
   185 #endif
   186 
   187 	growlLaunched = [self _launchGrowlIfInstalledWithRegistrationDictionary:cachedRegistrationDictionary];
   188 }
   189 
   190 + (NSObject<GrowlApplicationBridgeDelegate> *) growlDelegate {
   191 	return delegate;
   192 }
   193 
   194 #pragma mark -
   195 
   196 + (void) notifyWithTitle:(NSString *)title
   197 			 description:(NSString *)description
   198 		notificationName:(NSString *)notifName
   199 				iconData:(NSData *)iconData
   200 				priority:(int)priority
   201 				isSticky:(BOOL)isSticky
   202 			clickContext:(id)clickContext
   203 {
   204 	[GrowlApplicationBridge notifyWithTitle:title
   205 								description:description
   206 						   notificationName:notifName
   207 								   iconData:iconData
   208 								   priority:priority
   209 								   isSticky:isSticky
   210 							   clickContext:clickContext
   211 								 identifier:nil];
   212 }
   213 
   214 /* Send a notification to Growl for display.
   215  * title, description, and notifName are required.
   216  * All other id parameters may be nil to accept defaults.
   217  * priority is 0 by default; isSticky is NO by default.
   218  */
   219 + (void) notifyWithTitle:(NSString *)title
   220 			 description:(NSString *)description
   221 		notificationName:(NSString *)notifName
   222 				iconData:(NSData *)iconData
   223 				priority:(int)priority
   224 				isSticky:(BOOL)isSticky
   225 			clickContext:(id)clickContext
   226 			  identifier:(NSString *)identifier
   227 {
   228 	NSParameterAssert(notifName);	//Notification name is required.
   229 	NSParameterAssert(title || description);	//At least one of title or description is required.
   230 
   231 	// Build our noteDict from all passed parameters
   232 	NSMutableDictionary *noteDict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
   233 		notifName,	 GROWL_NOTIFICATION_NAME,
   234 		nil];
   235 
   236 	if (title)			setObjectForKey(noteDict, GROWL_NOTIFICATION_TITLE, title);
   237 	if (description)	setObjectForKey(noteDict, GROWL_NOTIFICATION_DESCRIPTION, description);
   238 	if (iconData)		setObjectForKey(noteDict, GROWL_NOTIFICATION_ICON, iconData);
   239 	if (clickContext)	setObjectForKey(noteDict, GROWL_NOTIFICATION_CLICK_CONTEXT, clickContext);
   240 	if (priority)		setIntegerForKey(noteDict, GROWL_NOTIFICATION_PRIORITY, priority);
   241 	if (isSticky)		setBooleanForKey(noteDict, GROWL_NOTIFICATION_STICKY, isSticky);
   242 	if (identifier)		setObjectForKey(noteDict, GROWL_NOTIFICATION_IDENTIFIER, identifier);
   243 
   244 	[self notifyWithDictionary:noteDict];
   245 	[noteDict release];
   246 }
   247 
   248 + (void) notifyWithDictionary:(NSDictionary *)userInfo {
   249 	//post it.
   250 	if (growlLaunched) {		
   251 		NSProxy<GrowlNotificationProtocol> *currentGrowlProxy = [self growlProxy];
   252 
   253 		//Make sure we have everything that we need (that we can retrieve from the registration dictionary).
   254 		userInfo = [self notificationDictionaryByFillingInDictionary:userInfo];
   255 
   256 		if (currentGrowlProxy) {
   257 			//Post to Growl via GrowlApplicationBridgePathway
   258 			TRY
   259 				[currentGrowlProxy postNotificationWithDictionary:userInfo];
   260 			ENDTRY
   261 			CATCH
   262 				NSLog(@"GrowlApplicationBridge: exception while sending notification: %@", localException);
   263 			ENDCATCH
   264 		} else {
   265 			//NSLog(@"GrowlApplicationBridge: could not find local GrowlApplicationBridgePathway, falling back to NSDistributedNotificationCenter");
   266 
   267 			//DNC needs a plist. this means we must pass data, not an NSImage.
   268 			Class     NSImageClass = [NSImage class];
   269 			NSImage          *icon = [userInfo objectForKey:GROWL_NOTIFICATION_ICON];
   270 			NSImage       *appIcon = [userInfo objectForKey:GROWL_NOTIFICATION_APP_ICON];
   271 			BOOL       iconIsImage =    icon &&    [icon isKindOfClass:NSImageClass];
   272 			BOOL    appIconIsImage = appIcon && [appIcon isKindOfClass:NSImageClass];
   273 			if (iconIsImage || appIconIsImage) {
   274 				NSMutableDictionary *mUserInfo = [userInfo mutableCopy];
   275 				//notification icon.
   276 				if (iconIsImage)
   277 					[mUserInfo setObject:[icon TIFFRepresentation] forKey:GROWL_NOTIFICATION_ICON];
   278 				//per-notification application icon.
   279 				if (appIconIsImage)
   280 					[mUserInfo setObject:[appIcon TIFFRepresentation] forKey:GROWL_NOTIFICATION_APP_ICON];
   281 
   282 				userInfo = [mUserInfo autorelease];
   283 			}
   284 
   285 			//Post to Growl via NSDistributedNotificationCenter
   286 			[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION
   287 																		   object:nil
   288 																		 userInfo:userInfo
   289 															   deliverImmediately:NO];
   290 		}
   291 	} else {
   292 #ifdef GROWL_WITH_INSTALLER
   293 		/*if Growl launches, and the user hasn't already said NO to installing
   294 		 *	it, store this notification for posting
   295 		 */
   296 		if (!userChoseNotToInstallGrowl) {
   297 			if (!queuedGrowlNotifications)
   298 				queuedGrowlNotifications = [[NSMutableArray alloc] init];
   299 			[queuedGrowlNotifications addObject:userInfo];
   300 
   301 			//if we have not already asked the user to install Growl, do it now
   302 			if (!promptedToInstallGrowl) {
   303 				[GrowlInstallationPrompt showInstallationPrompt];
   304 				promptedToInstallGrowl = YES;
   305 			}
   306 		}
   307 #endif
   308 	}
   309 }
   310 
   311 #pragma mark -
   312 
   313 + (BOOL) isGrowlInstalled {
   314 	return ([GrowlPathUtilities growlPrefPaneBundle] != nil);
   315 }
   316 
   317 + (BOOL) isGrowlRunning {
   318 	BOOL growlIsRunning = NO;
   319 	ProcessSerialNumber PSN = { kNoProcess, kNoProcess };
   320 
   321 	while (GetNextProcess(&PSN) == noErr) {
   322 		CFDictionaryRef infoDict = ProcessInformationCopyDictionary(&PSN, kProcessDictionaryIncludeAllInformationMask);
   323 		CFStringRef bundleId = CFDictionaryGetValue(infoDict, kCFBundleIdentifierKey);
   324 
   325 		if (bundleId && CFStringCompare(bundleId, CFSTR("com.Growl.GrowlHelperApp"), 0) == kCFCompareEqualTo) {
   326 			growlIsRunning = YES;
   327 			CFRelease(infoDict);
   328 			break;
   329 		}
   330 		CFRelease(infoDict);
   331 	}
   332 
   333 	return growlIsRunning;
   334 }
   335 
   336 + (void) displayInstallationPromptIfNeeded {
   337 #ifdef GROWL_WITH_INSTALLER
   338     //if we have not already asked the user to install Growl, do it now
   339     if (!promptedToInstallGrowl) {
   340         [GrowlInstallationPrompt showInstallationPrompt];
   341         promptedToInstallGrowl = YES;
   342     }
   343 #endif
   344 }
   345 
   346 #pragma mark -
   347 
   348 + (BOOL) registerWithDictionary:(NSDictionary *)regDict {
   349 	if (regDict)
   350 		regDict = [self registrationDictionaryByFillingInDictionary:regDict];
   351 	else
   352 		regDict = [self bestRegistrationDictionary];
   353 
   354 	[cachedRegistrationDictionary release];
   355 	cachedRegistrationDictionary = [regDict retain];
   356 
   357 	return [self _launchGrowlIfInstalledWithRegistrationDictionary:regDict];
   358 }
   359 
   360 + (void) reregisterGrowlNotifications {
   361 	[self registerWithDictionary:nil];
   362 }
   363 
   364 + (void) setWillRegisterWhenGrowlIsReady:(BOOL)flag {
   365 	registerWhenGrowlIsReady = flag;
   366 }
   367 + (BOOL) willRegisterWhenGrowlIsReady {
   368 	return registerWhenGrowlIsReady;
   369 }
   370 
   371 #pragma mark -
   372 
   373 + (NSDictionary *) registrationDictionaryFromDelegate {
   374 	NSDictionary *regDict = nil;
   375 
   376 	if (delegate && [delegate respondsToSelector:@selector(registrationDictionaryForGrowl)])
   377 		regDict = [delegate registrationDictionaryForGrowl];
   378 
   379 	return regDict;
   380 }
   381 
   382 + (NSDictionary *) registrationDictionaryFromBundle:(NSBundle *)bundle {
   383 	if (!bundle) bundle = [NSBundle mainBundle];
   384 
   385 	NSDictionary *regDict = nil;
   386 
   387 	NSString *regDictPath = [bundle pathForResource:@"Growl Registration Ticket" ofType:GROWL_REG_DICT_EXTENSION];
   388 	if (regDictPath) {
   389 		regDict = [NSDictionary dictionaryWithContentsOfFile:regDictPath];
   390 		if (!regDict)
   391 			NSLog(@"GrowlApplicationBridge: The bundle at %@ contains a registration dictionary, but it is not a valid property list. Please tell this application's developer.", [bundle bundlePath]);
   392 	}
   393 
   394 	return regDict;
   395 }
   396 
   397 + (NSDictionary *) bestRegistrationDictionary {
   398 	NSDictionary *registrationDictionary = [self registrationDictionaryFromDelegate];
   399 	if (!registrationDictionary) {
   400 		registrationDictionary = [self registrationDictionaryFromBundle:nil];
   401 		if (!registrationDictionary)
   402 			NSLog(@"GrowlApplicationBridge: The Growl delegate did not supply a registration dictionary, and the app bundle at %@ does not have one. Please tell this application's developer.", [[NSBundle mainBundle] bundlePath]);
   403 	}
   404 
   405 	return [self registrationDictionaryByFillingInDictionary:registrationDictionary];
   406 }
   407 
   408 #pragma mark -
   409 
   410 + (NSDictionary *) registrationDictionaryByFillingInDictionary:(NSDictionary *)regDict {
   411 	return [self registrationDictionaryByFillingInDictionary:regDict restrictToKeys:nil];
   412 }
   413 
   414 + (NSDictionary *) registrationDictionaryByFillingInDictionary:(NSDictionary *)regDict restrictToKeys:(NSSet *)keys {
   415 	if (!regDict) return nil;
   416 
   417 	NSMutableDictionary *mRegDict = [regDict mutableCopy];
   418 
   419 	if ((!keys) || [keys containsObject:GROWL_APP_NAME]) {
   420 		if (![mRegDict objectForKey:GROWL_APP_NAME]) {
   421 			if (!appName)
   422 				appName = [[self _applicationNameForGrowlSearchingRegistrationDictionary:regDict] retain];
   423 
   424 			[mRegDict setObject:appName
   425 			             forKey:GROWL_APP_NAME];
   426 		}
   427 	}
   428 
   429 	if ((!keys) || [keys containsObject:GROWL_APP_ICON]) {
   430 		if (![mRegDict objectForKey:GROWL_APP_ICON]) {
   431 			if (!appIconData)
   432 				appIconData = [[self _applicationIconDataForGrowlSearchingRegistrationDictionary:regDict] retain];
   433 			if (appIconData)
   434 				[mRegDict setObject:appIconData forKey:GROWL_APP_ICON];
   435 		}
   436 	}
   437 
   438 	if ((!keys) || [keys containsObject:GROWL_APP_LOCATION]) {
   439 		if (![mRegDict objectForKey:GROWL_APP_LOCATION]) {
   440 			NSURL *myURL = copyCurrentProcessURL();
   441 			if (myURL) {
   442 				NSDictionary *file_data = createDockDescriptionWithURL(myURL);
   443 				if (file_data) {
   444 					NSDictionary *location = [[NSDictionary alloc] initWithObjectsAndKeys:file_data, @"file-data", nil];
   445 					[file_data release];
   446 					[mRegDict setObject:location forKey:GROWL_APP_LOCATION];
   447 					[location release];
   448 				} else {
   449 					[mRegDict removeObjectForKey:GROWL_APP_LOCATION];
   450 				}
   451 				[myURL release];
   452 			}
   453 		}
   454 	}
   455 
   456 	if ((!keys) || [keys containsObject:GROWL_NOTIFICATIONS_DEFAULT]) {
   457 		if (![mRegDict objectForKey:GROWL_NOTIFICATIONS_DEFAULT]) {
   458 			NSArray *all = [mRegDict objectForKey:GROWL_NOTIFICATIONS_ALL];
   459 			if (all)
   460 				[mRegDict setObject:all forKey:GROWL_NOTIFICATIONS_DEFAULT];
   461 		}
   462 	}
   463 
   464 	if ((!keys) || [keys containsObject:GROWL_APP_ID])
   465 		if (![mRegDict objectForKey:GROWL_APP_ID])
   466 			[mRegDict setObject:(NSString *)CFBundleGetIdentifier(CFBundleGetMainBundle()) forKey:GROWL_APP_ID];
   467 
   468 	return [mRegDict autorelease];
   469 }
   470 
   471 + (NSDictionary *) notificationDictionaryByFillingInDictionary:(NSDictionary *)notifDict {
   472 	NSMutableDictionary *mNotifDict = [notifDict mutableCopy];
   473 
   474 	if (![mNotifDict objectForKey:GROWL_APP_NAME]) {
   475 		if (!appName)
   476 			appName = [[self _applicationNameForGrowlSearchingRegistrationDictionary:cachedRegistrationDictionary] retain];
   477 
   478 		if (appName) {
   479 			[mNotifDict setObject:appName
   480 			               forKey:GROWL_APP_NAME];
   481 		}
   482 	}
   483 
   484 	if (![mNotifDict objectForKey:GROWL_APP_ICON]) {
   485 		if (!appIconData)
   486 			appIconData = [[self _applicationIconDataForGrowlSearchingRegistrationDictionary:cachedRegistrationDictionary] retain];
   487 
   488 		if (appIconData) {
   489 			[mNotifDict setObject:appIconData
   490 			               forKey:GROWL_APP_ICON];
   491 		}
   492 	}
   493 
   494 	//Only include the PID when there's a click context. We do this because NSDNC imposes a 15-MiB limit on the serialized notification, and we wouldn't want to overrun it because of a 4-byte PID.
   495 	if ([mNotifDict objectForKey:GROWL_NOTIFICATION_CLICK_CONTEXT] && ![mNotifDict objectForKey:GROWL_APP_PID]) {
   496 		NSNumber *pidNum = [[NSNumber alloc] initWithInt:[[NSProcessInfo processInfo] processIdentifier]];
   497 
   498 		[mNotifDict setObject:pidNum
   499 		               forKey:GROWL_APP_PID];
   500 
   501 		[pidNum release];
   502 	}
   503 
   504 	return [mNotifDict autorelease];
   505 }
   506 
   507 + (NSDictionary *) frameworkInfoDictionary {
   508 #ifdef GROWL_WITH_INSTALLER
   509 	return (NSDictionary *)CFBundleGetInfoDictionary(CFBundleGetBundleWithIdentifier(CFSTR("com.growl.growlwithinstallerframework")));
   510 #else
   511 	return (NSDictionary *)CFBundleGetInfoDictionary(CFBundleGetBundleWithIdentifier(CFSTR("com.growl.growlframework")));
   512 #endif
   513 }
   514 
   515 #pragma mark -
   516 #pragma mark Private methods
   517 
   518 + (NSString *) _applicationNameForGrowlSearchingRegistrationDictionary:(NSDictionary *)regDict {
   519 	NSString *applicationNameForGrowl = nil;
   520 
   521 	if (delegate && [delegate respondsToSelector:@selector(applicationNameForGrowl)])
   522 		applicationNameForGrowl = [delegate applicationNameForGrowl];
   523 
   524 	if (!applicationNameForGrowl) {
   525 		applicationNameForGrowl = [regDict objectForKey:GROWL_APP_NAME];
   526 
   527 		if (!applicationNameForGrowl)
   528 			applicationNameForGrowl = [[NSProcessInfo processInfo] processName];
   529 	}
   530 
   531 	return applicationNameForGrowl;
   532 }
   533 + (NSData *) _applicationIconDataForGrowlSearchingRegistrationDictionary:(NSDictionary *)regDict {
   534 	NSData *iconData = nil;
   535 
   536 	if (delegate) {
   537 		if ([delegate respondsToSelector:@selector(applicationIconForGrowl)])
   538 			iconData = (NSData *)[delegate applicationIconForGrowl];
   539 		else if ([delegate respondsToSelector:@selector(applicationIconDataForGrowl)])
   540 			iconData = [delegate applicationIconDataForGrowl];
   541 	}
   542 
   543 	if (!iconData)
   544 		iconData = [regDict objectForKey:GROWL_APP_ICON];
   545 
   546 	if (iconData && [iconData isKindOfClass:[NSImage class]])
   547 		iconData = [(NSImage *)iconData TIFFRepresentation];
   548 
   549 	if (!iconData) {
   550 		NSURL *URL = copyCurrentProcessURL();
   551 		iconData = [copyIconDataForURL(URL) autorelease];
   552 		[URL release];
   553 	}
   554 
   555 	return iconData;
   556 }
   557 
   558 /*Selector called when a growl notification is clicked.  This should never be
   559  *	called manually, and the calling observer should only be registered if the
   560  *	delegate responds to growlNotificationWasClicked:.
   561  */
   562 + (void) growlNotificationWasClicked:(NSNotification *)notification {
   563 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   564 	[delegate growlNotificationWasClicked:
   565 		[[notification userInfo] objectForKey:GROWL_KEY_CLICKED_CONTEXT]];
   566 	[pool drain];
   567 }
   568 + (void) growlNotificationTimedOut:(NSNotification *)notification {
   569 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   570 	[delegate growlNotificationTimedOut:
   571 		[[notification userInfo] objectForKey:GROWL_KEY_CLICKED_CONTEXT]];
   572 	[pool drain];
   573 }
   574 
   575 #pragma mark -
   576 
   577 //When a connection dies, release our reference to its proxy
   578 + (void) connectionDidDie:(NSNotification *)notification {
   579 	[[NSNotificationCenter defaultCenter] removeObserver:self
   580 													name:NSConnectionDidDieNotification
   581 												  object:[notification object]];
   582 	[growlProxy release]; growlProxy = nil;
   583 }
   584 
   585 + (NSProxy<GrowlNotificationProtocol> *) growlProxy {
   586 	if (!growlProxy) {
   587 		NSConnection *connection = [NSConnection connectionWithRegisteredName:@"GrowlApplicationBridgePathway" host:nil];
   588 		if (connection) {
   589 			[[NSNotificationCenter defaultCenter] addObserver:self
   590 													 selector:@selector(connectionDidDie:)
   591 														 name:NSConnectionDidDieNotification
   592 													   object:connection];
   593 			
   594 			TRY
   595 			{
   596 				NSDistantObject *theProxy = [connection rootProxy];
   597 				if ([theProxy respondsToSelector:@selector(registerApplicationWithDictionary:)]) {
   598 					[theProxy setProtocolForProxy:@protocol(GrowlNotificationProtocol)];
   599 					growlProxy = [(NSProxy<GrowlNotificationProtocol> *)theProxy retain];
   600 				} else {
   601 					NSLog(@"Received a fake GrowlApplicationBridgePathway object. Some other application is interfering with Growl, or something went horribly wrong. Please file a bug report.");
   602 					growlProxy = nil;
   603 				}
   604 			}
   605 			ENDTRY
   606 				CATCH
   607 			{
   608 				NSLog(@"GrowlApplicationBridge: exception while sending notification: %@", localException);
   609 				growlProxy = nil;
   610 			}
   611 			ENDCATCH
   612 		}
   613 	}
   614 	
   615 	return growlProxy;
   616 }
   617 
   618 + (void) _growlIsReady:(NSNotification *)notification {
   619 #pragma unused(notification)
   620 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   621 
   622 	//Growl has now launched; we may get here with (growlLaunched == NO) when the user first installs
   623 	growlLaunched = YES;
   624 
   625 	//Inform our delegate if it is interested
   626 	if ([delegate respondsToSelector:@selector(growlIsReady)])
   627 		[delegate growlIsReady];
   628 
   629 	//Post a notification locally
   630 	[[NSNotificationCenter defaultCenter] postNotificationName:GROWL_IS_READY
   631 														object:nil
   632 													  userInfo:nil];
   633 
   634 	//Stop observing for GROWL_IS_READY
   635 	[[NSDistributedNotificationCenter defaultCenter] removeObserver:self
   636 															   name:GROWL_IS_READY
   637 															 object:nil];
   638 
   639 	//register (fixes #102: this is necessary if we got here by Growl having just been installed)
   640 	if (registerWhenGrowlIsReady) {
   641 		[self reregisterGrowlNotifications];
   642 		registerWhenGrowlIsReady = NO;
   643 	}
   644 
   645 #ifdef GROWL_WITH_INSTALLER
   646 	//Perform any queued notifications
   647 	NSEnumerator *enumerator = [queuedGrowlNotifications objectEnumerator];
   648 	NSDictionary *noteDict;
   649 
   650 	//Configure the growl proxy if it isn't currently configured
   651 	NSProxy<GrowlNotificationProtocol> *currentGrowlProxy = [self growlProxy];
   652 	
   653 	while ((noteDict = [enumerator nextObject])) {		
   654 		if (currentGrowlProxy) {
   655 			//Post to Growl via GrowlApplicationBridgePathway
   656 			NS_DURING
   657 				[currentGrowlProxy postNotificationWithDictionary:noteDict];
   658 			NS_HANDLER
   659 				NSLog(@"GrowlApplicationBridge: exception while sending notification: %@", localException);
   660 			NS_ENDHANDLER
   661 		} else {
   662 			//Post to Growl via NSDistributedNotificationCenter
   663 			NSLog(@"GrowlApplicationBridge: could not find local GrowlApplicationBridgePathway, falling back to NSDistributedNotificationCenter");
   664 			[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION
   665 																		   object:NULL
   666 																		 userInfo:noteDict
   667 															   deliverImmediately:FALSE];
   668 		}
   669 	}
   670 
   671 	[queuedGrowlNotifications release]; queuedGrowlNotifications = nil;
   672 #endif
   673 	
   674 	[pool drain];
   675 }
   676 
   677 #ifdef GROWL_WITH_INSTALLER
   678 /*Sent to us by GrowlInstallationPrompt if the user clicks Cancel so we can
   679  *	avoid prompting again this session (or ever if they checked Don't Ask Again)
   680  */
   681 + (void) _userChoseNotToInstallGrowl {
   682 	//Note the user's action so we stop queueing notifications, etc.
   683 	userChoseNotToInstallGrowl = YES;
   684 
   685 	//Clear our queued notifications; we won't be needing them
   686 	[queuedGrowlNotifications release]; queuedGrowlNotifications = nil;
   687 }
   688 
   689 // Check against our current version number and ensure the installed Growl pane is the same or later
   690 + (void) _checkForPackagedUpdateForGrowlPrefPaneBundle:(NSBundle *)growlPrefPaneBundle {
   691 	NSString *ourGrowlPrefPaneInfoPath;
   692 	NSDictionary *infoDictionary;
   693 	NSString *packagedVersion, *installedVersion;
   694 	BOOL upgradeIsAvailable;
   695 
   696 	ourGrowlPrefPaneInfoPath = [[NSBundle bundleWithIdentifier:@"com.growl.growlwithinstallerframework"] pathForResource:@"GrowlPrefPaneInfo"
   697 																												  ofType:@"plist"];
   698 
   699 	NSObject *infoPropertyList = createPropertyListFromURL([NSURL fileURLWithPath:ourGrowlPrefPaneInfoPath],
   700 														   kCFPropertyListImmutable,
   701 														   /* outFormat */ NULL, /* outErrorString */ NULL);
   702 	NSDictionary *infoDict = ([infoPropertyList isKindOfClass:[NSDictionary class]] ? (NSDictionary *)infoPropertyList : nil);
   703 
   704 	packagedVersion = [infoDict objectForKey:(NSString *)kCFBundleVersionKey];
   705 
   706 	infoDictionary = [growlPrefPaneBundle infoDictionary];
   707 	installedVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
   708 
   709 	//If the installed version is earlier than our packaged version, we can offer an upgrade.
   710 	upgradeIsAvailable = (compareVersionStringsTranslating1_0To0_5(packagedVersion, installedVersion) == kCFCompareGreaterThan);
   711 	if (upgradeIsAvailable && !promptedToUpgradeGrowl) {
   712 		NSString	*lastDoNotPromptVersion = [[NSUserDefaults standardUserDefaults] objectForKey:@"Growl Update:Do Not Prompt Again:Last Version"];
   713 
   714 		if (!lastDoNotPromptVersion ||
   715 			(compareVersionStringsTranslating1_0To0_5(packagedVersion, lastDoNotPromptVersion) == kCFCompareGreaterThan))
   716 		{
   717 			[GrowlInstallationPrompt showUpdatePromptForVersion:packagedVersion];
   718 			promptedToUpgradeGrowl = YES;
   719 		}
   720 	}
   721 	[infoDict release];
   722 }
   723 #endif
   724 
   725 #pragma mark -
   726 
   727 + (BOOL) _launchGrowlIfInstalledWithRegistrationDictionary:(NSDictionary *)regDict {
   728 	BOOL success = NO;
   729 	NSBundle *growlPrefPaneBundle;
   730 	NSString *growlHelperAppPath;
   731 
   732 #ifdef DEBUG
   733 	//For a debug build, first look for a running GHA. It might not actually be within a Growl prefpane bundle.
   734 	growlHelperAppPath = [[GrowlPathUtilities runningHelperAppBundle] bundlePath];
   735 	if (!growlHelperAppPath) {
   736 		growlPrefPaneBundle = [GrowlPathUtilities growlPrefPaneBundle];
   737 		growlHelperAppPath = [growlPrefPaneBundle pathForResource:@"GrowlHelperApp"
   738 														   ofType:@"app"];
   739 	}
   740 	NSLog(@"Will use GrowlHelperApp at %@", growlHelperAppPath);
   741 #else
   742 	growlPrefPaneBundle = [GrowlPathUtilities growlPrefPaneBundle];
   743 	growlHelperAppPath = [growlPrefPaneBundle pathForResource:@"GrowlHelperApp"
   744 													   ofType:@"app"];
   745 #endif
   746 
   747 #ifdef GROWL_WITH_INSTALLER
   748 	if (growlPrefPaneBundle) {
   749 		/* Check against our current version number and ensure the installed Growl pane is the same or later */
   750 		[self _checkForPackagedUpdateForGrowlPrefPaneBundle:growlPrefPaneBundle];
   751 	}
   752 #endif
   753 
   754 	//Houston, we are go for launch.
   755 	if (growlHelperAppPath) {
   756 		//Let's launch in the background (requires sending the Apple Event ourselves, as LS may activate the application anyway if it's already running)
   757 		NSURL *appURL = [NSURL fileURLWithPath:growlHelperAppPath];
   758 		if (appURL) {
   759 			OSStatus err;
   760 
   761 			//Find the PSN for GrowlHelperApp. (We'll need this later.)
   762 			struct ProcessSerialNumber appPSN = {
   763 				0, kNoProcess
   764 			};
   765 			while ((err = GetNextProcess(&appPSN)) == noErr) {
   766 				NSDictionary *dict = [(id)ProcessInformationCopyDictionary(&appPSN, kProcessDictionaryIncludeAllInformationMask) autorelease];
   767 				NSString *bundlePath = [dict objectForKey:@"BundlePath"];
   768 				if ([bundlePath isEqualToString:growlHelperAppPath]) {
   769 					//Match!
   770 					break;
   771 				}
   772 			}
   773 
   774 			if (err == noErr) {
   775 				NSURL *regItemURL = nil;
   776 				BOOL passRegDict = NO;
   777 
   778 				if (regDict) {
   779 					NSString *regDictFileName;
   780 					NSString *regDictPath;
   781 
   782 					//Obtain a truly unique file name
   783 					CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
   784 					CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
   785 					CFRelease(uuid);
   786 					regDictFileName = [[NSString stringWithFormat:@"%@-%u-%@", [self _applicationNameForGrowlSearchingRegistrationDictionary:regDict], getpid(), (NSString *)uuidString] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
   787 					CFRelease(uuidString);
   788 					if ([regDictFileName length] > NAME_MAX)
   789 						regDictFileName = [[regDictFileName substringToIndex:(NAME_MAX - [GROWL_REG_DICT_EXTENSION length])] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
   790 
   791 					//make sure it's within pathname length constraints
   792 					regDictPath = [NSTemporaryDirectory() stringByAppendingPathComponent:regDictFileName];
   793 					if ([regDictPath length] > PATH_MAX)
   794 						regDictPath = [[regDictPath substringToIndex:(PATH_MAX - [GROWL_REG_DICT_EXTENSION length])] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
   795 
   796 					//Write the registration dictionary out to the temporary directory
   797 					NSData *plistData;
   798 					NSString *error;
   799 					plistData = [NSPropertyListSerialization dataFromPropertyList:regDict
   800 																		   format:NSPropertyListBinaryFormat_v1_0
   801 																 errorDescription:&error];
   802 					if (plistData) {
   803 						if (![plistData writeToFile:regDictPath atomically:NO])
   804 							NSLog(@"GrowlApplicationBridge: Error writing registration dictionary at %@", regDictPath);
   805 					} else {
   806 						NSLog(@"GrowlApplicationBridge: Error writing registration dictionary at %@: %@", regDictPath, error);
   807 						NSLog(@"GrowlApplicationBridge: Registration dictionary follows\n%@", regDict);
   808 						[error release];
   809 					}
   810 
   811 					if ([[NSFileManager defaultManager] fileExistsAtPath:regDictPath]) {
   812 						regItemURL = [NSURL fileURLWithPath:regDictPath];
   813 						passRegDict = YES;
   814 					}
   815 				}
   816 
   817 				AEStreamRef stream = AEStreamCreateEvent(kAECoreSuite, kAEOpen,
   818 					//Target application
   819 					typeProcessSerialNumber, &appPSN, sizeof(appPSN),
   820 					kAutoGenerateReturnID, kAnyTransactionID);
   821 				if (!stream) {
   822 					NSLog(@"%@: Could not create open-document event to register this application with Growl", [self class]);
   823 				} else {
   824 					if (passRegDict) {
   825 						NSString *regItemURLString = [regItemURL absoluteString];
   826 						NSData *regItemURLUTF8Data = [regItemURLString dataUsingEncoding:NSUTF8StringEncoding];
   827 						err = AEStreamWriteKeyDesc(stream, keyDirectObject, typeFileURL, [regItemURLUTF8Data bytes], [regItemURLUTF8Data length]);
   828 						if (err != noErr) {
   829 							NSLog(@"%@: Could not set direct object of open-document event to register this application with Growl because AEStreamWriteKeyDesc returned %li/%s", [self class], (long)err, GetMacOSStatusCommentString(err));
   830 						}
   831 					}
   832 
   833 					AppleEvent event;
   834 					err = AEStreamClose(stream, &event);
   835 					if (err != noErr) {
   836 						NSLog(@"%@: Could not finish open-document event to register this application with Growl because AEStreamClose returned %li/%s", [self class], (long)err, GetMacOSStatusCommentString(err));
   837 					} else {
   838 						err = AESendMessage(&event, /*reply*/ NULL, kAENoReply | kAEDontReconnect | kAENeverInteract | kAEDontRecord, kAEDefaultTimeout);
   839 						if (err != noErr) {
   840 							NSLog(@"%@: Could not send open-document event to register this application with Growl because AESend returned %li/%s", [self class], (long)err, GetMacOSStatusCommentString(err));
   841 						}
   842 					}
   843 					
   844 					success = (err == noErr);
   845 				}
   846 			}
   847 		}
   848 	}
   849 
   850 	return success;
   851 }
   852 
   853 /*	+ (BOOL)launchGrowlIfInstalled
   854  *
   855  *Returns YES if the Growl helper app began launching or was already running.
   856  *Returns NO and performs no other action if the Growl prefPane is not properly
   857  *	installed.
   858  *If Growl is installed but disabled, the application will be registered and
   859  *	GrowlHelperApp will then quit.  This method will still return YES if Growl
   860  *	is installed but disabled.
   861  */
   862 + (BOOL) launchGrowlIfInstalled {
   863 	return [self _launchGrowlIfInstalledWithRegistrationDictionary:nil];
   864 }
   865 
   866 @end