Core/Source/GrowlApplicationController.m
author evands
Mon Jul 28 15:23:14 2008 +0000 (2008-07-28)
changeset 4152 099dfa400709
parent 4129 3c00479d5f08
child 4185 7642860bce0c
child 4736 3945b40a3ca3
permissions -rw-r--r--
Fixed what was likely one of the major causes of Growl-to-Growl local network forwarding failing. Previously, when forwarding was configured, the destination address and port were stored as a preference and used forever-after. That's wrong when destinations aren't on static IPs, which is by far the most common situation.

We now do a proper resolve to get the IP and port of the advertised Growl TCP service.

(Note: This doesn't update the preference code to not store this information; the address information just won't be used. The network preferences pane could use a lot of work under the hood, as it's clearly not complete.)
     1 //
     2 //  GrowlApplicationController.m
     3 //  Growl
     4 //
     5 //  Created by Karl Adam on Thu Apr 22 2004.
     6 //  Renamed from GrowlController by Mac-arena the Bored Zo on 2005-06-28.
     7 //  Copyright 2004-2006 The Growl Project. All rights reserved.
     8 //
     9 // This file is under the BSD License, refer to License.txt for details
    10 
    11 #import "GrowlApplicationController.h"
    12 #import "GrowlPreferencesController.h"
    13 #import "GrowlApplicationTicket.h"
    14 #import "GrowlApplicationNotification.h"
    15 #import "GrowlTicketController.h"
    16 #import "GrowlNotificationTicket.h"
    17 #import "GrowlPathway.h"
    18 #import "GrowlPathwayController.h"
    19 #import "GrowlPropertyListFilePathway.h"
    20 #import "GrowlPathUtilities.h"
    21 #import "NSStringAdditions.h"
    22 #import "GrowlDisplayPlugin.h"
    23 #import "GrowlPluginController.h"
    24 #import "GrowlIdleStatusController.h"
    25 #import "GrowlDefines.h"
    26 #import "GrowlVersionUtilities.h"
    27 #import "SVNRevision.h"
    28 #import "GrowlLog.h"
    29 #import "GrowlNotificationCenter.h"
    30 #import "MD5Authenticator.h"
    31 #include "CFGrowlAdditions.h"
    32 #include "CFURLAdditions.h"
    33 #include "CFDictionaryAdditions.h"
    34 #include "CFMutableDictionaryAdditions.h"
    35 #include "cdsa.h"
    36 #include <SystemConfiguration/SystemConfiguration.h>
    37 #include <sys/errno.h>
    38 #include <string.h>
    39 #include <sys/socket.h>
    40 #include <sys/fcntl.h>
    41 #include <netinet/in.h>
    42 
    43 // check every 24 hours
    44 #define UPDATE_CHECK_INTERVAL	24.0*3600.0
    45 
    46 //Notifications posted by GrowlApplicationController
    47 #define UPDATE_AVAILABLE_NOTIFICATION	@"Growl update available"
    48 #define USER_WENT_IDLE_NOTIFICATION		@"User went idle"
    49 #define USER_RETURNED_NOTIFICATION		@"User returned"
    50 
    51 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon);
    52 
    53 extern CFRunLoopRef CFRunLoopGetMain(void);
    54 
    55 @interface GrowlApplicationController (PRIVATE)
    56 - (void) notificationClicked:(NSNotification *)notification;
    57 - (void) notificationTimedOut:(NSNotification *)notification;
    58 @end
    59 
    60 /*applications that go full-screen (games in particular) are expected to capture
    61  *	whatever display(s) they're using.
    62  *we [will] use this to notice, and turn on auto-sticky or something (perhaps
    63  *	to be decided by the user), when this happens.
    64  */
    65 #if 0
    66 static BOOL isAnyDisplayCaptured(void) {
    67 	BOOL result = NO;
    68 
    69 	CGDisplayCount numDisplays;
    70 	CGDisplayErr err = CGGetActiveDisplayList(/*maxDisplays*/ 0U, /*activeDisplays*/ NULL, &numDisplays);
    71 	if (err != noErr)
    72 		[[GrowlLog sharedController] writeToLog:@"Checking for captured displays: Could not count displays: %li", (long)err];
    73 	else {
    74 		CGDirectDisplayID *displays = malloc(numDisplays * sizeof(CGDirectDisplayID));
    75 		CGGetActiveDisplayList(numDisplays, displays, /*numDisplays*/ NULL);
    76 
    77 		if (!displays)
    78 			[[GrowlLog sharedController] writeToLog:@"Checking for captured displays: Could not allocate list of displays: %s", strerror(errno)];
    79 		else {
    80 			for (CGDisplayCount i = 0U; i < numDisplays; ++i) {
    81 				if (CGDisplayIsCaptured(displays[i])) {
    82 					result = YES;
    83 					break;
    84 				}
    85 			}
    86 
    87 			free(displays);
    88 		}
    89 	}
    90 
    91 	return result;
    92 }
    93 #endif
    94 
    95 //static struct Version version = { 0U, 8U, 0U, releaseType_svn, 0U, };
    96 #warning Having to update this struct manually is ugly. Use the info.plist.
    97 #warning And once code is in to automagically update this from Info.plist, the documentation in GrowlVersionUtilities.h should also be updated.
    98 static struct Version version = { 1U, 1U, 5U, releaseType_svn, 0U, };
    99 //XXX - update these constants whenever the version changes
   100 
   101 static void checkVersion(CFRunLoopTimerRef timer, void *context) {
   102 #pragma unused(timer)
   103 	GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
   104 
   105 	if (![preferences isBackgroundUpdateCheckEnabled])
   106 		return;
   107 
   108 	GrowlApplicationController *appController = (GrowlApplicationController *)context;
   109 	NSURL *versionCheckURL = [appController versionCheckURL];
   110 
   111 	NSDictionary *productVersionDict = [[NSDictionary alloc] initWithContentsOfURL:versionCheckURL];
   112 
   113 	NSString *currVersionNumber = [GrowlApplicationController growlVersion];
   114 	NSString *latestVersionNumber = [productVersionDict objectForKey:@"Growl"];
   115 
   116 	NSString *downloadURLString = [productVersionDict objectForKey:@"GrowlDownloadURL"];
   117 
   118 	/* do nothing and be quiet if there is no active connection, if the
   119 	 *	version dictionary could not be downloaded, or if the version dictionary
   120 	 *	is missing either of these keys.
   121 	 */
   122 	if (downloadURLString && latestVersionNumber) {
   123 		[preferences setObject:[NSDate date] forKey:LastUpdateCheckKey];
   124 		if (compareVersionStringsTranslating1_0To0_5(latestVersionNumber, currVersionNumber) > 0) {
   125 			CFStringRef title = CFCopyLocalizedString(CFSTR("Update Available"), /*comment*/ NULL);
   126 			CFStringRef description = CFCopyLocalizedString(CFSTR("A newer version of Growl is available online. Click here to download it now."), /*comment*/ NULL);
   127 			[GrowlApplicationBridge notifyWithTitle:(NSString *)title
   128 				                        description:(NSString *)description
   129 				                   notificationName:UPDATE_AVAILABLE_NOTIFICATION
   130 			                               iconData:[appController applicationIconDataForGrowl]
   131 			                               priority:1
   132 			                               isSticky:YES
   133 			                           clickContext:downloadURLString
   134 										 identifier:UPDATE_AVAILABLE_NOTIFICATION];
   135 			CFRelease(title);
   136 			CFRelease(description);
   137 		}
   138 	}
   139 
   140 	[productVersionDict release];
   141 }
   142 
   143 @implementation GrowlApplicationController
   144 
   145 + (GrowlApplicationController *) sharedController {
   146 	return [self sharedInstance];
   147 }
   148 
   149 - (id) initSingleton {
   150 	if ((self = [super initSingleton])) {
   151 		CSSM_RETURN crtn = cdsaInit();
   152 		if (crtn) {
   153 			NSLog(@"ERROR: Could not initialize CDSA.");
   154 			cssmPerror("cdsaInit", crtn);
   155 			[self release];
   156 			return nil;
   157 		}
   158 
   159 		// initialize GrowlPreferencesController before observing GrowlPreferencesChanged
   160 		GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
   161 
   162 		NSDistributedNotificationCenter *NSDNC = [NSDistributedNotificationCenter defaultCenter];
   163 
   164 		[NSDNC addObserver:self
   165 				  selector:@selector(preferencesChanged:)
   166 					  name:GrowlPreferencesChanged
   167 					object:nil];
   168 		[NSDNC addObserver:self
   169 				  selector:@selector(showPreview:)
   170 					  name:GrowlPreview
   171 					object:nil];
   172 		[NSDNC addObserver:self
   173 				  selector:@selector(shutdown:)
   174 					  name:GROWL_SHUTDOWN
   175 					object:nil];
   176 		[NSDNC addObserver:self
   177 				  selector:@selector(replyToPing:)
   178 					  name:GROWL_PING
   179 					object:nil];
   180 
   181 		NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
   182 		[nc addObserver:self
   183 			   selector:@selector(notificationClicked:)
   184 				   name:GROWL_NOTIFICATION_CLICKED
   185 				 object:nil];
   186 		[nc addObserver:self
   187 			   selector:@selector(notificationTimedOut:)
   188 				   name:GROWL_NOTIFICATION_TIMED_OUT
   189 				 object:nil];
   190 
   191 		ticketController = [GrowlTicketController sharedController];
   192 
   193 		[GrowlApplicationBridge setGrowlDelegate:self];
   194 		
   195 		[self versionDictionary];
   196 
   197 		NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
   198 		NSURL *fileURL = [NSURL fileURLWithPath:file];
   199 		NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
   200 		if (defaultDefaults) {
   201 			[preferences registerDefaults:defaultDefaults];
   202 			[defaultDefaults release];
   203 		}
   204 
   205 		if ([GrowlPathUtilities runningHelperAppBundle] != [NSBundle mainBundle]) {
   206 			/*We are not the real GHA.
   207 			 *We are another GHA that a pre-1.1.3 GAB has invoked to register an application by a plist file.
   208 			 *This means that we should not start up the pathway controller; we should, instead, start up the plist-file pathway directly, and wait up to one second for -application:openFile: messages, and forward them to the plist-file pathway (as appropriate), and quit one second after the last one.
   209 			 */
   210 			NSLog(@"%@", @"It appears that at least one other instance of Growl is running. This one will quit.");
   211 			quitAfterOpen = YES;
   212 		} else {
   213 			//This class doesn't exist in the prefpane.
   214 			Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   215 			if (pathwayControllerClass)
   216 				[pathwayControllerClass sharedController];
   217 		}
   218 		
   219 		[self preferencesChanged:nil];
   220 
   221 		[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
   222 															   selector:@selector(applicationLaunched:)
   223 																   name:NSWorkspaceDidLaunchApplicationNotification
   224 																 object:nil];
   225 
   226 		growlIcon = [[NSImage imageNamed:@"NSApplicationIcon"] retain];
   227 
   228 		GrowlIdleStatusController_init();
   229 		[nc addObserver:self
   230 			   selector:@selector(idleStatus:)
   231 				   name:@"GrowlIdleStatus"
   232 				 object:nil];
   233 
   234 		NSDate *lastCheck = [preferences objectForKey:LastUpdateCheckKey];
   235 		NSDate *now = [NSDate date];
   236 		if (!lastCheck || [now timeIntervalSinceDate:lastCheck] > UPDATE_CHECK_INTERVAL) {
   237 			checkVersion(NULL, self);
   238 			lastCheck = now;
   239 		}
   240 		CFRunLoopTimerContext context = {0, self, NULL, NULL, NULL};
   241 		updateTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, [[lastCheck addTimeInterval:UPDATE_CHECK_INTERVAL] timeIntervalSinceReferenceDate], UPDATE_CHECK_INTERVAL, 0, 0, checkVersion, &context);
   242 		CFRunLoopAddTimer(CFRunLoopGetMain(), updateTimer, kCFRunLoopCommonModes);
   243 
   244 		// create and register GrowlNotificationCenter
   245 		growlNotificationCenter = [[GrowlNotificationCenter alloc] init];
   246 		growlNotificationCenterConnection = [[NSConnection alloc] initWithReceivePort:[NSPort port] sendPort:nil];
   247 		//[growlNotificationCenterConnection enableMultipleThreads];
   248 		[growlNotificationCenterConnection setRootObject:growlNotificationCenter];
   249 		if (![growlNotificationCenterConnection registerName:@"GrowlNotificationCenter"])
   250 			NSLog(@"WARNING: could not register GrowlNotificationCenter for interprocess access");
   251 
   252 		soundCompletionCallback = NewSystemSoundCompletionUPP(soundCompletionCallbackProc);
   253 	}
   254 
   255 	return self;
   256 }
   257 
   258 - (void) idleStatus:(NSNotification *)notification {
   259 	if ([[notification object] isEqualToString:@"Idle"]) {
   260 		GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
   261 		int idleThreshold;
   262 		NSNumber *value = [preferences objectForKey:@"IdleThreshold"];
   263 		NSString *description;
   264 
   265 		idleThreshold = (value ? [value intValue] : MACHINE_IDLE_THRESHOLD);
   266 		description = [NSString stringWithFormat:NSLocalizedString(@"No activity for more than %d seconds.", nil), idleThreshold];
   267 		if ([preferences stickyWhenAway])
   268 			description = [description stringByAppendingString:NSLocalizedString(@" New notifications will be sticky.", nil)];
   269 
   270 		[GrowlApplicationBridge notifyWithTitle:NSLocalizedString(@"User went idle", nil)
   271 									description:description
   272 							   notificationName:USER_WENT_IDLE_NOTIFICATION
   273 									   iconData:growlIconData
   274 									   priority:-1
   275 									   isSticky:NO
   276 								   clickContext:nil
   277 									 identifier:nil];
   278 	} else {
   279 		[GrowlApplicationBridge notifyWithTitle:NSLocalizedString(@"User returned", nil)
   280 									description:NSLocalizedString(@"User activity detected. New notifications will not be sticky by default.", nil)
   281 							   notificationName:USER_RETURNED_NOTIFICATION
   282 									   iconData:growlIconData
   283 									   priority:-1
   284 									   isSticky:NO
   285 								   clickContext:nil
   286 									 identifier:nil];
   287 	}
   288 }
   289 
   290 - (void) destroy {
   291 	//free your world
   292 	[mainThread release]; mainThread = nil;
   293 	Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   294 	if (pathwayControllerClass)
   295 		[(id)[pathwayControllerClass sharedController] setServerEnabled:NO];
   296 	[destinations     release]; destinations = nil;
   297 	[growlIcon        release]; growlIcon = nil;
   298 	[defaultDisplayPlugin release]; defaultDisplayPlugin = nil;
   299 
   300 	[versionCheckURL release];
   301 
   302 	GrowlIdleStatusController_dealloc();
   303 
   304 	CFRunLoopTimerInvalidate(updateTimer);
   305 	CFRelease(updateTimer);
   306 
   307 	[[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
   308 
   309 	[growlNotificationCenterConnection invalidate];
   310 	[growlNotificationCenterConnection release]; growlNotificationCenterConnection = nil;
   311 	[growlNotificationCenter           release]; growlNotificationCenter = nil;
   312 
   313 	cdsaShutdown();
   314 	
   315 	DisposeSystemSoundCompletionUPP(soundCompletionCallback);
   316 
   317 	[super destroy];
   318 }
   319 
   320 #pragma mark Guts
   321 
   322 - (void) showPreview:(NSNotification *) note {
   323 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   324 	NSString *displayName = [note object];
   325 	GrowlDisplayPlugin *displayPlugin = (GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayName author:nil version:nil type:nil];
   326 
   327 	NSString *desc = [[NSString alloc] initWithFormat:NSLocalizedString(@"This is a preview of the %@ display", "Preview message shown when clicking Preview in the system preferences pane. %@ becomes the name of the display style being used."), displayName];
   328 	NSNumber *priority = [[NSNumber alloc] initWithInt:0];
   329 	NSNumber *sticky = [[NSNumber alloc] initWithBool:NO];
   330 	NSDictionary *info = [[NSDictionary alloc] initWithObjectsAndKeys:
   331 		@"Growl",   GROWL_APP_NAME,
   332 		@"Preview", GROWL_NOTIFICATION_NAME,
   333 		NSLocalizedString(@"Preview", "Title of the Preview notification shown to demonstrate Growl displays"), GROWL_NOTIFICATION_TITLE,
   334 		desc,       GROWL_NOTIFICATION_DESCRIPTION,
   335 		priority,   GROWL_NOTIFICATION_PRIORITY,
   336 		sticky,     GROWL_NOTIFICATION_STICKY,
   337 		growlIcon,  GROWL_NOTIFICATION_ICON,
   338 		nil];
   339 	[desc     release];
   340 	[priority release];
   341 	[sticky   release];
   342 	GrowlApplicationNotification *notification = [[GrowlApplicationNotification alloc] initWithDictionary:info];
   343 	[info release];
   344 	[displayPlugin displayNotification:notification];
   345 	[notification release];
   346 	[pool release];
   347 }
   348 
   349 /*!
   350  * @brief Get address data for a Growl server
   351  *
   352  * @param name The name of the server
   353  * @result An NSData which contains a (struct sockaddr *)'s data. This may actually be a sockaddr_in or a sockaddr_in6.
   354  */
   355 - (NSData *)addressDataForGrowlServerWithName:(NSString *)name
   356 {
   357 	NSNetService *service = [[[NSNetService alloc] initWithDomain:@"local." type:@"_growl._tcp." name:name] autorelease];
   358     if (!service) {
   359 		/* No such service exists. The computer is probably offline. */
   360         return nil;
   361     }
   362 
   363 	/* Work for 8 seconds to resolve the net service to an IP and port. We should be running
   364 	 * on a thread, so blocking is fine.
   365 	 */
   366     [service scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:@"PrivateGrowlMode"];
   367     [service resolveWithTimeout:8.0];
   368     CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + 8.0;
   369     CFTimeInterval remaining;
   370     while ((remaining = (deadline - CFAbsoluteTimeGetCurrent())) > 0 && [[service addresses] count] == 0) {
   371         CFRunLoopRunInMode((CFStringRef)@"PrivateGrowlMode", remaining, true);
   372     }
   373     [service stop];
   374 
   375     NSArray *addresses = [service addresses];
   376     if (![addresses count]) {
   377 		/* Lookup failed */
   378         return nil;
   379     }
   380 
   381 	return [addresses objectAtIndex:0];
   382 }	
   383 
   384 - (void) forwardDictionary:(NSDictionary *)dict withSelector:(SEL)forwardMethod {
   385 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   386 
   387 	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
   388 	NSNumber *requestTimeout = [defaults objectForKey:@"ForwardingRequestTimeout"];
   389 	NSNumber *replyTimeout = [defaults objectForKey:@"ForwardingReplyTimeout"];
   390 	NSEnumerator *enumerator = [destinations objectEnumerator];
   391 	NSDictionary *entry;
   392 	while ((entry = [enumerator nextObject])) {
   393 		if (getBooleanForKey(entry, @"use") && getBooleanForKey(entry, @"active")) {
   394 			/* Note: This assumes that all forwarding destinations are within the local network.
   395 			 * When domain names and IPs can be used, this needs to change.
   396 			 */
   397 			NSData *destAddress = [self addressDataForGrowlServerWithName:[entry objectForKey:@"computer"]];
   398 			if (!destAddress) {
   399 				/* No destination address. Nothing to see here; move along. */
   400 #ifdef DEBUG
   401 				NSLog(@"Could not obtain destination address for %@", [entry objectForKey:@"computer"]);
   402 #endif
   403 				continue;
   404 			}
   405 			NSString *password = [entry objectForKey:@"password"];
   406 
   407 			/* Send via DO if possible */
   408 			NSSocketPort *serverPort = [[NSSocketPort alloc]
   409 				initRemoteWithProtocolFamily:AF_INET
   410 								  socketType:SOCK_STREAM
   411 									protocol:IPPROTO_TCP
   412 									 address:destAddress];
   413 
   414 			NSConnection *connection = [[NSConnection alloc] initWithReceivePort:nil
   415 																		sendPort:serverPort];
   416 			MD5Authenticator *auth = [[MD5Authenticator alloc] initWithPassword:password];
   417 			[connection setDelegate:auth];
   418 
   419 			if (requestTimeout && [requestTimeout respondsToSelector:@selector(floatValue)])
   420 				[connection setRequestTimeout:[requestTimeout floatValue]];
   421 			if (replyTimeout && [replyTimeout respondsToSelector:@selector(floatValue)])
   422 				[connection setReplyTimeout:[replyTimeout floatValue]];
   423 
   424 			@try {
   425 				NSDistantObject *theProxy = [connection rootProxy];
   426 				[theProxy setProtocolForProxy:@protocol(GrowlNotificationProtocol)];
   427 				NSProxy <GrowlNotificationProtocol> *growlProxy = (NSProxy <GrowlNotificationProtocol> *)theProxy;
   428 				[growlProxy performSelector:forwardMethod withObject:dict];
   429 			} @catch (NSException *e) {
   430 				NSString *addressString = createStringWithAddressData(destAddress);
   431 				NSString *hostName = createHostNameForAddressData(destAddress);
   432 				if ([[e name] isEqualToString:NSFailedAuthenticationException]) {
   433 					NSLog(@"Authentication failed while forwarding to %@ (%@)",
   434 						  addressString, hostName);
   435 				} else {
   436 					NSLog(@"Warning: Exception %@ while forwarding Growl registration or notification (%@) to %@ (%@). Is that system on and connected?",
   437 						  e, NSStringFromSelector(forwardMethod), addressString, hostName);
   438 				}
   439 				[addressString release];
   440 				[hostName      release];
   441 
   442 			} @finally {
   443 				[connection invalidate];
   444 				[serverPort invalidate];
   445 				[serverPort release];
   446 				[connection release];
   447 				[auth release];
   448 			}
   449 		}
   450 	}
   451 
   452 	[pool release];
   453 }
   454 
   455 - (void) forwardNotification:(NSDictionary *)dict {
   456 	[self forwardDictionary:dict withSelector:@selector(postNotificationWithDictionary:)];
   457 }
   458 
   459 - (void) forwardRegistration:(NSDictionary *)dict {
   460 	[self forwardDictionary:dict withSelector:@selector(registerApplicationWithDictionary:)];
   461 }
   462 
   463 #pragma mark Retrieving sounds
   464 
   465 - (OSStatus) getFSRef:(out FSRef *)outRef forSoundNamed:(NSString *)soundName {
   466 	BOOL foundIt = NO;
   467 
   468 	NSArray *soundTypes = [NSSound soundUnfilteredFileTypes];
   469 
   470 	//Throw away all the HFS types, leaving only filename extensions.
   471 	NSPredicate *noHFSTypesPredicate = [NSPredicate predicateWithFormat:@"NOT (self BEGINSWITH \"'\")"];
   472 	soundTypes = [soundTypes filteredArrayUsingPredicate:noHFSTypesPredicate];
   473 
   474 	//If there are no types left, abort.
   475 	if ([soundTypes count] == 0U)
   476 		return unknownFormatErr;
   477 
   478 	//We only want the filename extensions, not the HFS types.
   479 	//Also, we want the longest one last so that we can use lastObject's length to allocate the buffer.
   480 	NSSortDescriptor *sortDesc = [[[NSSortDescriptor alloc] initWithKey:@"length" ascending:YES] autorelease];
   481 	NSArray *sortDescs = [NSArray arrayWithObject:sortDesc];
   482 	soundTypes = [soundTypes sortedArrayUsingDescriptors:sortDescs];
   483 
   484 	NSMutableArray *filenames = [NSMutableArray arrayWithCapacity:[soundTypes count]];
   485 	NSEnumerator *soundTypeEnum;
   486 	NSString *soundType;
   487 	soundTypeEnum = [soundTypes objectEnumerator];
   488 	while ((soundType = [soundTypeEnum nextObject])) {
   489 		[filenames addObject:[soundName stringByAppendingPathExtension:soundType]];
   490 	}
   491 
   492 	NSEnumerator *filenamesEnum;
   493 	NSString *filename;
   494 
   495 	//The additions are for appending '.' plus the longest filename extension.
   496 	size_t filenameLen = [soundName length] + 1U + [[soundTypes lastObject] length];
   497 	unichar *filenameBuf = malloc(filenameLen * sizeof(unichar));
   498 	if (!filenameBuf) return memFullErr;
   499 
   500 	FSRef folderRef;
   501 	OSStatus err;
   502 
   503 	err = FSFindFolder(kUserDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
   504 	if (err == noErr) {
   505 		//Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
   506 		filenamesEnum = [filenames objectEnumerator];
   507 		while ((filename = [filenamesEnum nextObject])) {
   508 			[filename getCharacters:filenameBuf];
   509 			err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
   510 			if (err == noErr) {
   511 				foundIt = YES;
   512 				break;
   513 			}
   514 		}
   515 	}
   516 
   517 	if (!foundIt) {
   518 		err = FSFindFolder(kLocalDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
   519 		if (err == noErr) {
   520 			//Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
   521 			filenamesEnum = [filenames objectEnumerator];
   522 			while ((filename = [filenamesEnum nextObject])) {
   523 				[filename getCharacters:filenameBuf];
   524 				err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
   525 				if (err == noErr) {
   526 					foundIt = YES;
   527 					break;
   528 				}
   529 			}
   530 		}
   531 	}
   532 
   533 	if (!foundIt) {
   534 		err = FSFindFolder(kSystemDomain, kSystemSoundsFolderType, kDontCreateFolder, &folderRef);
   535 		if (err == noErr) {
   536 			//Folder exists. If it didn't, FSFindFolder would have returned fnfErr.
   537 			filenamesEnum = [filenames objectEnumerator];
   538 			while ((filename = [filenamesEnum nextObject])) {
   539 				[filename getCharacters:filenameBuf];
   540 				err = FSMakeFSRefUnicode(&folderRef, [filename length], filenameBuf, kTextEncodingUnknown, outRef);
   541 				if (err == noErr) {
   542 					foundIt = YES;
   543 					break;
   544 				}
   545 			}
   546 		}
   547 	}
   548 
   549 	free(filenameBuf);
   550 
   551 	return err;
   552 }
   553 
   554 #pragma mark Dispatching notifications
   555 
   556 - (void) dispatchNotificationWithDictionary:(NSDictionary *) dict {
   557 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   558 
   559 	[[GrowlLog sharedController] writeNotificationDictionaryToLog:dict];
   560 
   561 	// Make sure this notification is actually registered
   562 	NSString *appName = [dict objectForKey:GROWL_APP_NAME];
   563 	GrowlApplicationTicket *ticket = [ticketController ticketForApplicationName:appName];
   564 	NSString *notificationName = [dict objectForKey:GROWL_NOTIFICATION_NAME];
   565 	if (!ticket || ![ticket isNotificationAllowed:notificationName]) {
   566 		// Either the app isn't registered or the notification is turned off
   567 		// We should do nothing
   568 		[pool release];
   569 		return;
   570 	}
   571 
   572 	NSMutableDictionary *aDict = [dict mutableCopy];
   573 
   574 	// Check icon
   575 	Class NSImageClass = [NSImage class];
   576 	Class NSDataClass  = [NSData  class];
   577 	NSImage *icon = nil;
   578 	id image = [aDict objectForKey:GROWL_NOTIFICATION_ICON];
   579 	if (image) {
   580 		if ([image isKindOfClass:NSImageClass])
   581 			icon = [image copy];
   582 		else if ([image isKindOfClass:NSDataClass])
   583 			icon = [[NSImage alloc] initWithData:image];
   584 	}
   585 	if (!icon)
   586 		icon = [[ticket icon] copy];
   587 
   588 	if (icon) {
   589 		[aDict setObject:icon forKey:GROWL_NOTIFICATION_ICON];
   590 		[icon release];
   591 	} else {
   592 		//We get here when no image existed, and if an NSData existed, an image could not be created from it.
   593 		//In the latter case, we don't need to keep that non-image NSData around.
   594 		[aDict removeObjectForKey:GROWL_NOTIFICATION_ICON];
   595 	}
   596 
   597 	// If app icon present, convert to NSImage
   598 	icon = nil;
   599 	image = [aDict objectForKey:GROWL_NOTIFICATION_APP_ICON];
   600 	if (image) {
   601 		if ([image isKindOfClass:NSImageClass])
   602 			icon = [image copy];
   603 		else if ([image isKindOfClass:NSDataClass])
   604 			icon = [[NSImage alloc] initWithData:image];
   605 	}
   606 	if (icon) {
   607 		[aDict setObject:icon forKey:GROWL_NOTIFICATION_APP_ICON];
   608 		[icon release];
   609 	} else
   610 		[aDict removeObjectForKey:GROWL_NOTIFICATION_APP_ICON];
   611 
   612 	// To avoid potential exceptions, make sure we have both text and title
   613 	if (![aDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION])
   614 		[aDict setObject:@"" forKey:GROWL_NOTIFICATION_DESCRIPTION];
   615 	if (![aDict objectForKey:GROWL_NOTIFICATION_TITLE])
   616 		[aDict setObject:@"" forKey:GROWL_NOTIFICATION_TITLE];
   617 
   618 	//Retrieve and set the the priority of the notification
   619 	GrowlNotificationTicket *notification = [ticket notificationTicketForName:notificationName];
   620 	int priority = [notification priority];
   621 	NSNumber *value;
   622 	if (priority == GrowlPriorityUnset) {
   623 		value = [dict objectForKey:GROWL_NOTIFICATION_PRIORITY];
   624 		if (!value)
   625 			value = [NSNumber numberWithInt:0];
   626 	} else
   627 		value = [NSNumber numberWithInt:priority];
   628 	[aDict setObject:value forKey:GROWL_NOTIFICATION_PRIORITY];
   629 
   630 	GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
   631 
   632 	// Retrieve and set the sticky bit of the notification
   633 	int sticky = [notification sticky];
   634 	if (sticky >= 0)
   635 		setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, sticky);
   636 	else if ([preferences stickyWhenAway] && !getBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY))
   637 		setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, GrowlIdleStatusController_isIdle());
   638 
   639 	BOOL saveScreenshot = [[NSUserDefaults standardUserDefaults] boolForKey:GROWL_SCREENSHOT_MODE];
   640 	setBooleanForKey(aDict, GROWL_SCREENSHOT_MODE, saveScreenshot);
   641 	setBooleanForKey(aDict, @"ClickHandlerEnabled", [ticket clickHandlersEnabled]);
   642 
   643 	if (![preferences squelchMode]) {
   644 		GrowlDisplayPlugin *display = [notification displayPlugin];
   645 
   646 		if (!display)
   647 			display = [ticket displayPlugin];
   648 
   649 		if (!display) {
   650 			if (!defaultDisplayPlugin) {
   651 				NSString *displayPluginName = [[GrowlPreferencesController sharedController] defaultDisplayPluginName];
   652 				defaultDisplayPlugin = [(GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayPluginName author:nil version:nil type:nil] retain];
   653 				if (!defaultDisplayPlugin) {
   654 					//User's selected default display has gone AWOL. Change to the default default.
   655 					NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
   656 					NSURL *fileURL = [NSURL fileURLWithPath:file];
   657 					NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
   658 					if (defaultDefaults) {
   659 						displayPluginName = [defaultDefaults objectForKey:GrowlDisplayPluginKey];
   660 						if (!displayPluginName)
   661 							GrowlLog_log(@"No default display specified in default preferences! Perhaps your Growl installation is corrupted?");
   662 						else {
   663 							defaultDisplayPlugin = (GrowlDisplayPlugin *)[[[GrowlPluginController sharedController] displayPluginDictionaryWithName:displayPluginName author:nil version:nil type:nil] pluginInstance];
   664 
   665 							//Now fix the user's preferences to forget about the missing display plug-in.
   666 							[preferences setObject:displayPluginName forKey:GrowlDisplayPluginKey];
   667 						}
   668 
   669 						[defaultDefaults release];
   670 					}
   671 				}
   672 			}
   673 			display = defaultDisplayPlugin;
   674 		}
   675 
   676 		GrowlApplicationNotification *appNotification = [[GrowlApplicationNotification alloc] initWithDictionary:aDict];
   677 		[display displayNotification:appNotification];
   678 		[appNotification release];
   679 
   680 		NSString *soundName = [notification sound];
   681 		if (soundName) {
   682 			NSError *error = nil;
   683 			NSDictionary *userInfo;
   684 
   685 			FSRef soundRef;
   686 			OSStatus err = [self getFSRef:&soundRef forSoundNamed:soundName];
   687 			if (err == noErr) {
   688 				SystemSoundActionID actionID;
   689 				err = SystemSoundGetActionID(&soundRef, &actionID);
   690 				if (err == noErr) {
   691 					err = SystemSoundSetCompletionRoutine(actionID, CFRunLoopGetCurrent(), /*runLoopMode*/ NULL, soundCompletionCallback, /*refcon*/ NULL);
   692 					SystemSoundPlay(actionID);
   693 					userInfo = nil;
   694 				} else {
   695 					userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
   696 						[NSString stringWithFormat:NSLocalizedString(@"Could not load and play sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
   697 						nil];
   698 				}					
   699 			} else {
   700 				userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
   701 					[NSString stringWithFormat:NSLocalizedString(@"Could not find sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
   702 					nil];
   703 			}
   704 
   705 			if (err != noErr) {
   706 				error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:userInfo];
   707 				[NSApp presentError:error];
   708 			}
   709 		}
   710 	}
   711 
   712 	// send to DO observers
   713 	[growlNotificationCenter notifyObservers:aDict];
   714 
   715 	[aDict release];
   716 
   717 	// forward to remote destinations
   718 	if (enableForward && ![dict objectForKey:GROWL_REMOTE_ADDRESS]) {
   719 		if ([NSThread currentThread] == mainThread)
   720 			[NSThread detachNewThreadSelector:@selector(forwardNotification:)
   721 									 toTarget:self
   722 								   withObject:dict];
   723 		else
   724 			[self forwardNotification:dict];
   725 	}
   726 
   727 	[pool release];
   728 }
   729 
   730 - (BOOL) registerApplicationWithDictionary:(NSDictionary *)userInfo {
   731 	[[GrowlLog sharedController] writeRegistrationDictionaryToLog:userInfo];
   732 
   733 	NSString *appName = [userInfo objectForKey:GROWL_APP_NAME];
   734 
   735 	GrowlApplicationTicket *newApp = [ticketController ticketForApplicationName:appName];
   736 
   737 	if (newApp) {
   738 		[newApp reregisterWithDictionary:userInfo];
   739 	} else {
   740 		newApp = [[[GrowlApplicationTicket alloc] initWithDictionary:userInfo] autorelease];
   741 	}
   742 
   743 	BOOL success = YES;
   744 
   745 	if (appName && newApp) {
   746 		if ([newApp hasChanged])
   747 			[newApp saveTicket];
   748 		[ticketController addTicket:newApp];
   749 
   750 		if (enableForward && ![userInfo objectForKey:GROWL_REMOTE_ADDRESS]) {
   751 			if ([NSThread currentThread] == mainThread)
   752 				[NSThread detachNewThreadSelector:@selector(forwardRegistration:)
   753 										 toTarget:self
   754 									   withObject:userInfo];
   755 			else
   756 				[self forwardRegistration:userInfo];
   757 		}
   758 	} else { //!(appName && newApp)
   759 		NSString *filename = [(appName ? appName : @"unknown-application") stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
   760 
   761 		//We'll be writing the file to ~/Library/Logs/Failed Growl registrations.
   762 		NSFileManager *mgr = [NSFileManager defaultManager];
   763 		NSString *userLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES) lastObject];
   764 		NSString *logsFolder = [userLibraryFolder stringByAppendingPathComponent:@"Logs"];
   765 		[mgr createDirectoryAtPath:logsFolder attributes:nil];
   766 		NSString *failedTicketsFolder = [logsFolder stringByAppendingPathComponent:@"Failed Growl registrations"];
   767 		[mgr createDirectoryAtPath:failedTicketsFolder attributes:nil];
   768 		NSString *path = [failedTicketsFolder stringByAppendingPathComponent:filename];
   769 
   770 		//NSFileHandle will not create the file for us, so we must create it separately.
   771 		[mgr createFileAtPath:path contents:nil attributes:nil];
   772 
   773 		NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
   774 		[fh seekToEndOfFile];
   775 		if ([fh offsetInFile]) //we are not at the beginning of the file
   776 			[fh writeData:[@"\n---\n\n" dataUsingEncoding:NSUTF8StringEncoding]];
   777 		[fh writeData:[[[userInfo description] stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
   778 		[fh closeFile];
   779 
   780 		if (!appName) appName = @"with no name";
   781 
   782 		NSLog(@"Failed application registration for application %@; wrote failed registration dictionary %p to %@", appName, userInfo, path);
   783 		success = NO;
   784 	}
   785 
   786 	return success;
   787 }
   788 
   789 #pragma mark Version of Growl
   790 
   791 + (NSString *) growlVersion {
   792 	return [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
   793 }
   794 
   795 - (NSDictionary *) versionDictionary {
   796 	if (!versionInfo) {
   797 		if (version.releaseType == releaseType_svn)
   798 			version.development = strtoul(SVN_REVISION, /*endptr*/ NULL, 10);
   799 
   800 		NSNumber *major = [[NSNumber alloc] initWithUnsignedShort:version.major];
   801 		NSNumber *minor = [[NSNumber alloc] initWithUnsignedShort:version.minor];
   802 		NSNumber *incremental = [[NSNumber alloc] initWithUnsignedChar:version.incremental];
   803 		NSNumber *releaseType = [[NSNumber alloc] initWithUnsignedChar:version.releaseType];
   804 		NSNumber *development = [[NSNumber alloc] initWithUnsignedShort:version.development];
   805 
   806 		versionInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
   807 			[GrowlApplicationController growlVersion], (NSString *)kCFBundleVersionKey,
   808 
   809 			major,                                     @"Major version",
   810 			minor,                                     @"Minor version",
   811 			incremental,                               @"Incremental version",
   812 			releaseTypeNames[version.releaseType],     @"Release type name",
   813 			releaseType,                               @"Release type",
   814 			development,                               @"Development version",
   815 
   816 			nil];
   817 
   818 		[major       release];
   819 		[minor       release];
   820 		[incremental release];
   821 		[releaseType release];
   822 		[development release];
   823 	}
   824 	return versionInfo;
   825 }
   826 
   827 //this method could be moved to Growl.framework, I think.
   828 //pass nil to get GrowlHelperApp's version as a string.
   829 - (NSString *)stringWithVersionDictionary:(NSDictionary *)d {
   830 	if (!d)
   831 		d = [self versionDictionary];
   832 
   833 	//0.6
   834 	NSMutableString *result = [NSMutableString stringWithFormat:@"%@.%@",
   835 		[d objectForKey:@"Major version"],
   836 		[d objectForKey:@"Minor version"]];
   837 
   838 	//the .1 in 0.6.1
   839 	NSNumber *incremental = [d objectForKey:@"Incremental version"];
   840 	if ([incremental unsignedShortValue])
   841 		[result appendFormat:@".%@", incremental];
   842 
   843 	NSString *releaseTypeName = [d objectForKey:@"Release type name"];
   844 	if ([releaseTypeName length]) {
   845 		//"" (release), "b4", " SVN 900"
   846 		[result appendFormat:@"%@%@", releaseTypeName, [d objectForKey:@"Development version"]];
   847 	}
   848 
   849 	return result;
   850 }
   851 
   852 - (NSURL *) versionCheckURL {
   853 	if (!versionCheckURL)
   854 		versionCheckURL = [[NSURL URLWithString:@"http://growl.info/version.xml"] retain];
   855 	return versionCheckURL;
   856 }
   857 
   858 #pragma mark Accessors
   859 
   860 - (BOOL) quitAfterOpen {
   861 	return quitAfterOpen;
   862 }
   863 - (void) setQuitAfterOpen:(BOOL)flag {
   864 	quitAfterOpen = flag;
   865 }
   866 
   867 #pragma mark What NSThread should implement as a class method
   868 
   869 - (NSThread *)mainThread {
   870 	return mainThread;
   871 }
   872 
   873 #pragma mark Notifications (not the Growl kind)
   874 
   875 - (void) preferencesChanged:(NSNotification *) note {
   876 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   877 
   878 	//[note object] is the changed key. A nil key means reload our tickets.
   879 	id object = [note object];
   880 
   881 	if (!quitAfterOpen) {
   882 		if (!note || (object && [object isEqual:GrowlStartServerKey])) {
   883 			Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   884 			if (pathwayControllerClass)
   885 				[(id)[pathwayControllerClass sharedController] setServerEnabledFromPreferences];
   886 		}
   887 	}
   888 	if (!note || (object && [object isEqual:GrowlUserDefaultsKey]))
   889 		[[GrowlPreferencesController sharedController] synchronize];
   890 	if (!note || (object && [object isEqual:GrowlEnabledKey]))
   891 		growlIsEnabled = [[GrowlPreferencesController sharedController] boolForKey:GrowlEnabledKey];
   892 	if (!note || (object && [object isEqual:GrowlEnableForwardKey]))
   893 		enableForward = [[GrowlPreferencesController sharedController] isForwardingEnabled];
   894 	if (!note || (object && [object isEqual:GrowlForwardDestinationsKey])) {
   895 		[destinations release];
   896 		destinations = [[[GrowlPreferencesController sharedController] objectForKey:GrowlForwardDestinationsKey] retain];
   897 	}
   898 	if (!note || !object)
   899 		[ticketController loadAllSavedTickets];
   900 	if (!note || (object && [object isEqual:GrowlDisplayPluginKey]))
   901 		// force reload
   902 		[defaultDisplayPlugin release];
   903 		defaultDisplayPlugin = nil;
   904 	if (object) {
   905 		if ([object isEqual:@"GrowlTicketDeleted"]) {
   906 			NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
   907 			[ticketController removeTicketForApplicationName:ticketName];
   908 		} else if ([object isEqual:@"GrowlTicketChanged"]) {
   909 			NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
   910 			GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:ticketName];
   911 			if (newTicket) {
   912 				[ticketController addTicket:newTicket];
   913 				[newTicket release];
   914 			}
   915 		} else if ((!quitAfterOpen) && [object isEqual:GrowlUDPPortKey]) {
   916 			Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   917 			if (pathwayControllerClass) {
   918 				id pathwayController = [pathwayControllerClass sharedController];
   919 				[pathwayController setServerEnabled:NO];
   920 				[pathwayController setServerEnabled:YES];
   921 			}
   922 		}
   923 	}
   924 	
   925 	[pool release];
   926 }
   927 
   928 - (void) shutdown:(NSNotification *) note {
   929 #pragma unused(note)
   930 	[NSApp terminate:nil];
   931 }
   932 
   933 - (void) replyToPing:(NSNotification *) note {
   934 #pragma unused(note)
   935 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   936 
   937 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_PONG
   938 	                                                               object:nil
   939 	                                                             userInfo:nil
   940 	                                                   deliverImmediately:NO];
   941 	
   942 	[pool release];
   943 }
   944 
   945 #pragma mark NSApplication Delegate Methods
   946 
   947 - (BOOL) application:(NSApplication *)theApplication openFile:(NSString *)filename {
   948 #pragma unused(theApplication)
   949 	BOOL retVal = NO;
   950 	NSString *pathExtension = [filename pathExtension];
   951 
   952 	if ([pathExtension isEqualToString:GROWL_REG_DICT_EXTENSION]) {
   953 		//If the auto-quit flag is set, it's probably because we are not the real GHAÑwe're some other GHA that a broken (pre-1.1.3) GAB opened this file with. If that's the case, find the real one and open the file with it.
   954 		BOOL registerItOurselves = YES;
   955 		NSString *realHelperAppBundlePath = nil;
   956 
   957 		if (quitAfterOpen) {
   958 			//But, just to make sure we don't infinitely loop, make sure this isn't our own bundle.
   959 			NSString *ourBundlePath = [[NSBundle mainBundle] bundlePath];
   960 			realHelperAppBundlePath = [[GrowlPathUtilities runningHelperAppBundle] bundlePath];
   961 			if (![ourBundlePath isEqualToString:realHelperAppBundlePath])
   962 				registerItOurselves = NO;
   963 		}
   964 
   965 		if (registerItOurselves) {
   966 			//We are the real GHA.
   967 			//Have the property-list-file pathway process this registration dictionary file.
   968 			GrowlPropertyListFilePathway *pathway = [GrowlPropertyListFilePathway standardPathway];
   969 			[pathway application:theApplication openFile:filename];
   970 		} else {
   971 			//We're definitely not the real GHA, so pass it to the real GHA to be registered.
   972 			[[NSWorkspace sharedWorkspace] openFile:filename
   973 									withApplication:realHelperAppBundlePath];
   974 		}
   975 	} else {
   976 		GrowlPluginController *controller = [GrowlPluginController sharedController];
   977 		//the set returned by GrowlPluginController is case-insensitive. yay!
   978 		if ([[controller registeredPluginTypes] containsObject:pathExtension]) {
   979 			[controller installPluginFromPath:filename];
   980 
   981 			retVal = YES;
   982 		}
   983 	}
   984 
   985 	/*If Growl is not enabled and was not already running before
   986 	 *	(for example, via an autolaunch even though the user's last
   987 	 *	preference setting was to click "Stop Growl," setting enabled to NO),
   988 	 *	quit having registered; otherwise, remain running.
   989 	 */
   990 	if (!growlIsEnabled && !growlFinishedLaunching) {
   991 		//Terminate after one second to give us time to process any other openFile: messages.
   992 		[NSObject cancelPreviousPerformRequestsWithTarget:NSApp
   993 												 selector:@selector(terminate:)
   994 												   object:nil];
   995 		[NSApp performSelector:@selector(terminate:)
   996 					withObject:nil
   997 					afterDelay:1.0];
   998 	}
   999 
  1000 	return retVal;
  1001 }
  1002 
  1003 - (void) applicationWillFinishLaunching:(NSNotification *)aNotification {
  1004 #pragma unused(aNotification)
  1005 	mainThread = [[NSThread currentThread] retain];
  1006 
  1007 	BOOL printVersionAndExit = [[NSUserDefaults standardUserDefaults] boolForKey:@"PrintVersionAndExit"];
  1008 	if (printVersionAndExit) {
  1009 		printf("This is GrowlHelperApp version %s.\n"
  1010 			   "PrintVersionAndExit was set to %hhi, so GrowlHelperApp will now exit.\n",
  1011 			   [[self stringWithVersionDictionary:nil] UTF8String],
  1012 			   printVersionAndExit);
  1013 		[NSApp terminate:nil];
  1014 	}
  1015 
  1016 	NSFileManager *fs = [NSFileManager defaultManager];
  1017 
  1018 	NSString *destDir, *subDir;
  1019 	NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES);
  1020 
  1021 	destDir = [searchPath objectAtIndex:0U]; //first == last == ~/Library
  1022 	[fs createDirectoryAtPath:destDir attributes:nil];
  1023 	destDir = [destDir stringByAppendingPathComponent:@"Application Support"];
  1024 	[fs createDirectoryAtPath:destDir attributes:nil];
  1025 	destDir = [destDir stringByAppendingPathComponent:@"Growl"];
  1026 	[fs createDirectoryAtPath:destDir attributes:nil];
  1027 
  1028 	subDir  = [destDir stringByAppendingPathComponent:@"Tickets"];
  1029 	[fs createDirectoryAtPath:subDir attributes:nil];
  1030 	subDir  = [destDir stringByAppendingPathComponent:@"Plugins"];
  1031 	[fs createDirectoryAtPath:subDir attributes:nil];
  1032 }
  1033 
  1034 //Post a notification when we are done launching so the application bridge can inform participating applications
  1035 - (void) applicationDidFinishLaunching:(NSNotification *)aNotification {
  1036 #pragma unused(aNotification)
  1037 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_IS_READY
  1038 	                                                               object:nil
  1039 	                                                             userInfo:nil
  1040 	                                                   deliverImmediately:YES];
  1041 	growlFinishedLaunching = YES;
  1042 
  1043 	if (quitAfterOpen) {
  1044 		//We provide a delay of 1 second to give NSApp time to send us application:openFile: messages for any .growlRegDict files the GrowlPropertyListFilePathway needs to process.
  1045 		[NSApp performSelector:@selector(terminate:)
  1046 					withObject:nil
  1047 					afterDelay:1.0];
  1048 	} else {
  1049 		/*If Growl is not enabled and was not already running before
  1050 		 *	(for example, via an autolaunch even though the user's last
  1051 		 *	preference setting was to click "Stop Growl," setting enabled to NO),
  1052 		 *	quit having registered; otherwise, remain running.
  1053 		 */
  1054 		if (!growlIsEnabled)
  1055 			[NSApp terminate:nil];
  1056 	}
  1057 }
  1058 
  1059 //Same as applicationDidFinishLaunching, called when we are asked to reopen (that is, we are already running)
  1060 - (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
  1061 #pragma unused(theApplication, flag)
  1062 	return NO;
  1063 }
  1064 
  1065 - (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
  1066 #pragma unused(theApplication)
  1067 	return NO;
  1068 }
  1069 
  1070 - (void) applicationWillTerminate:(NSNotification *)notification {
  1071 #pragma unused(notification)
  1072 	[GrowlAbstractSingletonObject destroyAllSingletons];	//Release all our controllers
  1073 }
  1074 
  1075 #pragma mark Auto-discovery
  1076 
  1077 //called by NSWorkspace when an application launches.
  1078 - (void) applicationLaunched:(NSNotification *)notification {
  1079 	NSDictionary *userInfo = [notification userInfo];
  1080 
  1081 	if (!userInfo)
  1082 		return;
  1083 
  1084 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1085 	NSString *appPath = [userInfo objectForKey:@"NSApplicationPath"];
  1086 
  1087 	if (appPath) {
  1088 		NSString *ticketPath = [NSBundle pathForResource:@"Growl Registration Ticket" ofType:GROWL_REG_DICT_EXTENSION inDirectory:appPath];
  1089 		if (ticketPath) {
  1090 			CFURLRef ticketURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)ticketPath, kCFURLPOSIXPathStyle, false);
  1091 			NSMutableDictionary *ticket = (NSMutableDictionary *)createPropertyListFromURL((NSURL *)ticketURL, kCFPropertyListMutableContainers, NULL, NULL);
  1092 
  1093 			if (ticket) {
  1094 				NSString *appName = [userInfo objectForKey:@"NSApplicationName"];
  1095 
  1096 				//set the app's name in the dictionary, if it's not present already.
  1097 				if (![ticket objectForKey:GROWL_APP_NAME])
  1098 					[ticket setObject:appName forKey:GROWL_APP_NAME];
  1099 
  1100 				if ([GrowlApplicationTicket isValidTicketDictionary:ticket]) {
  1101 					NSLog(@"Auto-discovered registration ticket in %@ (located at %@)", appName, appPath);
  1102 
  1103 					/* set the app's location in the dictionary, avoiding costly
  1104 					 *	lookups later.
  1105 					 */
  1106 					NSURL *url = [[NSURL alloc] initFileURLWithPath:appPath];
  1107 					NSDictionary *file_data = createDockDescriptionWithURL(url);
  1108 					id location = file_data ? [NSDictionary dictionaryWithObject:file_data forKey:@"file-data"] : appPath;
  1109 					[file_data release];
  1110 					[ticket setObject:location forKey:GROWL_APP_LOCATION];
  1111 					[url release];
  1112 
  1113 					//write the new ticket to disk, and be sure to launch this ticket instead of the one in the app bundle.
  1114 					CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
  1115 					CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
  1116 					CFRelease(uuid);
  1117 					ticketPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:(NSString *)uuidString] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
  1118 					CFRelease(uuidString);
  1119 					[ticket writeToFile:ticketPath atomically:NO];
  1120 
  1121 					/* open the ticket with ourselves.
  1122 					 * we need to use LS in order to launch it with this specific
  1123 					 *	GHA, rather than some other.
  1124 					 */
  1125 					CFURLRef myURL      = (CFURLRef)copyCurrentProcessURL();
  1126 					NSArray *URLsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath:ticketPath]];
  1127 					struct LSLaunchURLSpec spec = {
  1128 						.appURL = myURL,
  1129 						.itemURLs = (CFArrayRef)URLsToOpen,
  1130 						.passThruParams = NULL,
  1131 						.launchFlags = kLSLaunchDontAddToRecents | kLSLaunchDontSwitch | kLSLaunchAsync,
  1132 						.asyncRefCon = NULL,
  1133 					};
  1134 					OSStatus err = LSOpenFromURLSpec(&spec, /*outLaunchedURL*/ NULL);
  1135 					if (err != noErr)
  1136 						NSLog(@"The registration ticket for %@ could not be opened (LSOpenFromURLSpec returned %li). Pathname for the ticket file: %@", appName, (long)err, ticketPath);
  1137 					CFRelease(myURL);
  1138 				} else if ([GrowlApplicationTicket isKnownTicketVersion:ticket]) {
  1139 					NSLog(@"%@ (located at %@) contains an invalid registration ticket - developer, please consult Growl developer documentation (http://growl.info/documentation/developer/)", appName, appPath);
  1140 				} else {
  1141 					NSNumber *versionNum = [ticket objectForKey:GROWL_TICKET_VERSION];
  1142 					if (versionNum)
  1143 						NSLog(@"%@ (located at %@) contains a ticket whose format version (%i) is unrecognised by this version (%@) of Growl", appName, appPath, [versionNum intValue], [self stringWithVersionDictionary:nil]);
  1144 					else
  1145 						NSLog(@"%@ (located at %@) contains a ticket with no format version number; Growl requires that a registration dictionary include a format version number, so that Growl knows whether it will understand the dictionary's format. This ticket will be ignored.", appName, appPath);
  1146 				}
  1147 				[ticket release];
  1148 			}
  1149 			CFRelease(ticketURL);
  1150 		}
  1151 	}
  1152 
  1153 	[pool release];
  1154 }
  1155 
  1156 #pragma mark Growl Application Bridge delegate
  1157 /*!
  1158  * @brief Returns the application name Growl will use
  1159  */
  1160 - (NSString *)applicationNameForGrowl
  1161 {
  1162 	return @"Growl";
  1163 }
  1164 
  1165 - (NSDictionary *)registrationDictionaryForGrowl
  1166 {	
  1167 	NSDictionary *descriptions = [NSDictionary dictionaryWithObjectsAndKeys:
  1168 		NSLocalizedString(@"A Growl update is available", nil), UPDATE_AVAILABLE_NOTIFICATION,
  1169 		NSLocalizedString(@"You are now considered idle by Growl", nil), USER_WENT_IDLE_NOTIFICATION,
  1170 		NSLocalizedString(@"You are no longer considered idle by Growl", nil), USER_RETURNED_NOTIFICATION,
  1171 		nil];
  1172 
  1173 	NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
  1174 		NSLocalizedString(@"Growl update available", nil), UPDATE_AVAILABLE_NOTIFICATION,
  1175 		NSLocalizedString(@"User went idle", nil), USER_WENT_IDLE_NOTIFICATION,
  1176 		NSLocalizedString(@"User returned", nil), USER_RETURNED_NOTIFICATION,
  1177 		nil];
  1178 	
  1179 	NSDictionary	*growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
  1180 		[NSArray arrayWithObjects:UPDATE_AVAILABLE_NOTIFICATION, USER_WENT_IDLE_NOTIFICATION, USER_RETURNED_NOTIFICATION, nil], GROWL_NOTIFICATIONS_ALL,
  1181 		[NSArray arrayWithObject:UPDATE_AVAILABLE_NOTIFICATION], GROWL_NOTIFICATIONS_DEFAULT,
  1182 		humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
  1183 		descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
  1184 		nil];
  1185 	
  1186 	return growlReg;
  1187 }
  1188 
  1189 - (NSImage *)applicationIconDataForGrowl
  1190 {
  1191 	return [NSImage imageNamed:@"growl-icon"];
  1192 }
  1193 
  1194 - (void)growlNotificationWasClicked:(id)clickContext
  1195 {
  1196 	if (clickContext && [clickContext isKindOfClass:[NSString class]]) {
  1197 		NSURL *downloadURL = [NSURL URLWithString:clickContext];
  1198 		[[NSWorkspace sharedWorkspace] openURL:downloadURL];
  1199 	}
  1200 }
  1201 
  1202 @end
  1203 
  1204 #pragma mark -
  1205 
  1206 @implementation GrowlApplicationController (PRIVATE)
  1207 
  1208 #pragma mark Click feedback from displays
  1209 
  1210 /*click feedback comes here first. GAB picks up the DN and calls our
  1211  *	-growlNotificationWasClicked:/-growlNotificationTimedOut: with it if it's a
  1212  *	GHA notification.
  1213  */
  1214 
  1215 - (void) notificationClicked:(NSNotification *)notification {
  1216 	NSString *appName, *growlNotificationClickedName;
  1217 	NSString *suffix;
  1218 	NSDictionary *clickInfo;
  1219 	NSDictionary *userInfo;
  1220 
  1221 	userInfo = [notification userInfo];
  1222 
  1223 	//Build the application-specific notification name
  1224 	appName = [notification object];
  1225 	if (getBooleanForKey(userInfo, @"ClickHandlerEnabled")) {
  1226 		suffix = GROWL_NOTIFICATION_CLICKED;
  1227 	} else {
  1228 		/*
  1229 		 * send GROWL_NOTIFICATION_TIMED_OUT instead, so that an application is
  1230 		 * guaranteed to receive feedback for every notification.
  1231 		 */
  1232 		suffix = GROWL_NOTIFICATION_TIMED_OUT;
  1233 	}
  1234 	NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
  1235 	if (pid)
  1236 		growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
  1237 			appName, pid, suffix];
  1238 	else
  1239 		growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@%@",
  1240 			appName, suffix];
  1241 	clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
  1242 		[userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
  1243 		nil];
  1244 
  1245 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationClickedName
  1246 	                                                               object:nil
  1247 	                                                             userInfo:clickInfo
  1248 	                                                   deliverImmediately:YES];
  1249 
  1250 	[clickInfo release];
  1251 	[growlNotificationClickedName release];
  1252 }
  1253 
  1254 - (void) notificationTimedOut:(NSNotification *)notification {
  1255 	NSString *appName, *growlNotificationTimedOutName;
  1256 	NSDictionary *clickInfo;
  1257 	NSDictionary *userInfo;
  1258 
  1259 	userInfo = [notification userInfo];
  1260 
  1261 	//Build the application-specific notification name
  1262 	appName = [notification object];
  1263 	NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
  1264 	if (pid)
  1265 		growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
  1266 			appName, pid, GROWL_NOTIFICATION_TIMED_OUT];
  1267 	else
  1268 		growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@%@",
  1269 			appName, GROWL_NOTIFICATION_TIMED_OUT];
  1270 	clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
  1271 		[userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
  1272 		nil];
  1273 
  1274 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationTimedOutName
  1275 	                                                               object:nil
  1276 	                                                             userInfo:clickInfo
  1277 	                                                   deliverImmediately:YES];
  1278 
  1279 	[clickInfo release];
  1280 	[growlNotificationTimedOutName release];
  1281 }
  1282 
  1283 @end
  1284 
  1285 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon) {
  1286 #pragma unused(refcon)
  1287 
  1288 	SystemSoundRemoveCompletionRoutine(actionID);
  1289 
  1290 	return SystemSoundRemoveActionID(actionID);
  1291 }