Core/Source/GrowlApplicationController.m
author Rudy Richter
Sat Aug 01 20:50:32 2009 -0400 (2009-08-01)
changeset 4261 48b7c994f6c8
parent 4246 4f52d1d98978
child 4267 59b4e4f61882
permissions -rw-r--r--
PrefPane: clang warnings and setup for Sparkle
     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 "HgRevision.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, 2U, 0U, releaseType_development, 1U, };
    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 					break;
   543 				}
   544 			}
   545 		}
   546 	}
   547 
   548 	free(filenameBuf);
   549 
   550 	return err;
   551 }
   552 
   553 #pragma mark Dispatching notifications
   554 
   555 - (void) dispatchNotificationWithDictionary:(NSDictionary *) dict {
   556 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   557 
   558 	[[GrowlLog sharedController] writeNotificationDictionaryToLog:dict];
   559 
   560 	// Make sure this notification is actually registered
   561 	NSString *appName = [dict objectForKey:GROWL_APP_NAME];
   562 	GrowlApplicationTicket *ticket = [ticketController ticketForApplicationName:appName];
   563 	NSString *notificationName = [dict objectForKey:GROWL_NOTIFICATION_NAME];
   564 	if (!ticket || ![ticket isNotificationAllowed:notificationName]) {
   565 		// Either the app isn't registered or the notification is turned off
   566 		// We should do nothing
   567 		[pool release];
   568 		return;
   569 	}
   570 
   571 	NSMutableDictionary *aDict = [dict mutableCopy];
   572 
   573 	// Check icon
   574 	Class NSImageClass = [NSImage class];
   575 	Class NSDataClass  = [NSData  class];
   576 	NSImage *icon = nil;
   577 	id image = [aDict objectForKey:GROWL_NOTIFICATION_ICON];
   578 	if (image) {
   579 		if ([image isKindOfClass:NSImageClass])
   580 			icon = [image copy];
   581 		else if ([image isKindOfClass:NSDataClass])
   582 			icon = [[NSImage alloc] initWithData:image];
   583 	}
   584 	if (!icon)
   585 		icon = [[ticket icon] copy];
   586 
   587 	if (icon) {
   588 		[aDict setObject:icon forKey:GROWL_NOTIFICATION_ICON];
   589 		[icon release];
   590 	} else {
   591 		//We get here when no image existed, and if an NSData existed, an image could not be created from it.
   592 		//In the latter case, we don't need to keep that non-image NSData around.
   593 		[aDict removeObjectForKey:GROWL_NOTIFICATION_ICON];
   594 	}
   595 
   596 	// If app icon present, convert to NSImage
   597 	icon = nil;
   598 	image = [aDict objectForKey:GROWL_NOTIFICATION_APP_ICON];
   599 	if (image) {
   600 		if ([image isKindOfClass:NSImageClass])
   601 			icon = [image copy];
   602 		else if ([image isKindOfClass:NSDataClass])
   603 			icon = [[NSImage alloc] initWithData:image];
   604 	}
   605 	if (icon) {
   606 		[aDict setObject:icon forKey:GROWL_NOTIFICATION_APP_ICON];
   607 		[icon release];
   608 	} else
   609 		[aDict removeObjectForKey:GROWL_NOTIFICATION_APP_ICON];
   610 
   611 	// To avoid potential exceptions, make sure we have both text and title
   612 	if (![aDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION])
   613 		[aDict setObject:@"" forKey:GROWL_NOTIFICATION_DESCRIPTION];
   614 	if (![aDict objectForKey:GROWL_NOTIFICATION_TITLE])
   615 		[aDict setObject:@"" forKey:GROWL_NOTIFICATION_TITLE];
   616 
   617 	//Retrieve and set the the priority of the notification
   618 	GrowlNotificationTicket *notification = [ticket notificationTicketForName:notificationName];
   619 	int priority = [notification priority];
   620 	NSNumber *value;
   621 	if (priority == GrowlPriorityUnset) {
   622 		value = [dict objectForKey:GROWL_NOTIFICATION_PRIORITY];
   623 		if (!value)
   624 			value = [NSNumber numberWithInt:0];
   625 	} else
   626 		value = [NSNumber numberWithInt:priority];
   627 	[aDict setObject:value forKey:GROWL_NOTIFICATION_PRIORITY];
   628 
   629 	GrowlPreferencesController *preferences = [GrowlPreferencesController sharedController];
   630 
   631 	// Retrieve and set the sticky bit of the notification
   632 	int sticky = [notification sticky];
   633 	if (sticky >= 0)
   634 		setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, sticky);
   635 	else if ([preferences stickyWhenAway] && !getBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY))
   636 		setBooleanForKey(aDict, GROWL_NOTIFICATION_STICKY, GrowlIdleStatusController_isIdle());
   637 
   638 	BOOL saveScreenshot = [[NSUserDefaults standardUserDefaults] boolForKey:GROWL_SCREENSHOT_MODE];
   639 	setBooleanForKey(aDict, GROWL_SCREENSHOT_MODE, saveScreenshot);
   640 	setBooleanForKey(aDict, @"ClickHandlerEnabled", [ticket clickHandlersEnabled]);
   641 
   642 	if (![preferences squelchMode]) {
   643 		GrowlDisplayPlugin *display = [notification displayPlugin];
   644 
   645 		if (!display)
   646 			display = [ticket displayPlugin];
   647 
   648 		if (!display) {
   649 			if (!defaultDisplayPlugin) {
   650 				NSString *displayPluginName = [[GrowlPreferencesController sharedController] defaultDisplayPluginName];
   651 				defaultDisplayPlugin = [(GrowlDisplayPlugin *)[[GrowlPluginController sharedController] displayPluginInstanceWithName:displayPluginName author:nil version:nil type:nil] retain];
   652 				if (!defaultDisplayPlugin) {
   653 					//User's selected default display has gone AWOL. Change to the default default.
   654 					NSString *file = [[NSBundle mainBundle] pathForResource:@"GrowlDefaults" ofType:@"plist"];
   655 					NSURL *fileURL = [NSURL fileURLWithPath:file];
   656 					NSDictionary *defaultDefaults = (NSDictionary *)createPropertyListFromURL((NSURL *)fileURL, kCFPropertyListImmutable, NULL, NULL);
   657 					if (defaultDefaults) {
   658 						displayPluginName = [defaultDefaults objectForKey:GrowlDisplayPluginKey];
   659 						if (!displayPluginName)
   660 							GrowlLog_log(@"No default display specified in default preferences! Perhaps your Growl installation is corrupted?");
   661 						else {
   662 							defaultDisplayPlugin = (GrowlDisplayPlugin *)[[[GrowlPluginController sharedController] displayPluginDictionaryWithName:displayPluginName author:nil version:nil type:nil] pluginInstance];
   663 
   664 							//Now fix the user's preferences to forget about the missing display plug-in.
   665 							[preferences setObject:displayPluginName forKey:GrowlDisplayPluginKey];
   666 						}
   667 
   668 						[defaultDefaults release];
   669 					}
   670 				}
   671 			}
   672 			display = defaultDisplayPlugin;
   673 		}
   674 
   675 		GrowlApplicationNotification *appNotification = [[GrowlApplicationNotification alloc] initWithDictionary:aDict];
   676 		[display displayNotification:appNotification];
   677 		[appNotification release];
   678 
   679 		NSString *soundName = [notification sound];
   680 		if (soundName) {
   681 			NSError *error = nil;
   682 			NSDictionary *userInfo;
   683 
   684 			FSRef soundRef;
   685 			OSStatus err = [self getFSRef:&soundRef forSoundNamed:soundName];
   686 			if (err == noErr) {
   687 				SystemSoundActionID actionID;
   688 				err = SystemSoundGetActionID(&soundRef, &actionID);
   689 				if (err == noErr) {
   690 					err = SystemSoundSetCompletionRoutine(actionID, CFRunLoopGetCurrent(), /*runLoopMode*/ NULL, soundCompletionCallback, /*refcon*/ NULL);
   691 					SystemSoundPlay(actionID);
   692 					userInfo = nil;
   693 				} else {
   694 					userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
   695 						[NSString stringWithFormat:NSLocalizedString(@"Could not load and play sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
   696 						nil];
   697 				}					
   698 			} else {
   699 				userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
   700 					[NSString stringWithFormat:NSLocalizedString(@"Could not find sound file named \"%@\": %s", /*comment*/ nil), soundName, GetMacOSStatusCommentString(err)], NSLocalizedDescriptionKey,
   701 					nil];
   702 			}
   703 
   704 			if (err != noErr) {
   705 				error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:userInfo];
   706 				[NSApp presentError:error];
   707 			}
   708 		}
   709 	}
   710 
   711 	// send to DO observers
   712 	[growlNotificationCenter notifyObservers:aDict];
   713 
   714 	[aDict release];
   715 
   716 	// forward to remote destinations
   717 	if (enableForward && ![dict objectForKey:GROWL_REMOTE_ADDRESS]) {
   718 		if ([NSThread currentThread] == mainThread)
   719 			[NSThread detachNewThreadSelector:@selector(forwardNotification:)
   720 									 toTarget:self
   721 								   withObject:dict];
   722 		else
   723 			[self forwardNotification:dict];
   724 	}
   725 
   726 	[pool release];
   727 }
   728 
   729 - (BOOL) registerApplicationWithDictionary:(NSDictionary *)userInfo {
   730 	[[GrowlLog sharedController] writeRegistrationDictionaryToLog:userInfo];
   731 
   732 	NSString *appName = [userInfo objectForKey:GROWL_APP_NAME];
   733 
   734 	GrowlApplicationTicket *newApp = [ticketController ticketForApplicationName:appName];
   735 
   736 	if (newApp) {
   737 		[newApp reregisterWithDictionary:userInfo];
   738 	} else {
   739 		newApp = [[[GrowlApplicationTicket alloc] initWithDictionary:userInfo] autorelease];
   740 	}
   741 
   742 	BOOL success = YES;
   743 
   744 	if (appName && newApp) {
   745 		if ([newApp hasChanged])
   746 			[newApp saveTicket];
   747 		[ticketController addTicket:newApp];
   748 
   749 		if (enableForward && ![userInfo objectForKey:GROWL_REMOTE_ADDRESS]) {
   750 			if ([NSThread currentThread] == mainThread)
   751 				[NSThread detachNewThreadSelector:@selector(forwardRegistration:)
   752 										 toTarget:self
   753 									   withObject:userInfo];
   754 			else
   755 				[self forwardRegistration:userInfo];
   756 		}
   757 	} else { //!(appName && newApp)
   758 		NSString *filename = [(appName ? appName : @"unknown-application") stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
   759 
   760 		//We'll be writing the file to ~/Library/Logs/Failed Growl registrations.
   761 		NSFileManager *mgr = [NSFileManager defaultManager];
   762 		NSString *userLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES) lastObject];
   763 		NSString *logsFolder = [userLibraryFolder stringByAppendingPathComponent:@"Logs"];
   764 		[mgr createDirectoryAtPath:logsFolder attributes:nil];
   765 		NSString *failedTicketsFolder = [logsFolder stringByAppendingPathComponent:@"Failed Growl registrations"];
   766 		[mgr createDirectoryAtPath:failedTicketsFolder attributes:nil];
   767 		NSString *path = [failedTicketsFolder stringByAppendingPathComponent:filename];
   768 
   769 		//NSFileHandle will not create the file for us, so we must create it separately.
   770 		[mgr createFileAtPath:path contents:nil attributes:nil];
   771 
   772 		NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
   773 		[fh seekToEndOfFile];
   774 		if ([fh offsetInFile]) //we are not at the beginning of the file
   775 			[fh writeData:[@"\n---\n\n" dataUsingEncoding:NSUTF8StringEncoding]];
   776 		[fh writeData:[[[userInfo description] stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
   777 		[fh closeFile];
   778 
   779 		if (!appName) appName = @"with no name";
   780 
   781 		NSLog(@"Failed application registration for application %@; wrote failed registration dictionary %p to %@", appName, userInfo, path);
   782 		success = NO;
   783 	}
   784 
   785 	return success;
   786 }
   787 
   788 #pragma mark Version of Growl
   789 
   790 + (NSString *) growlVersion {
   791 	return [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
   792 }
   793 
   794 - (NSDictionary *) versionDictionary {
   795 	if (!versionInfo) {
   796 		if (version.releaseType == releaseType_svn)
   797 			version.development = (u_int32_t)strtoul(HG_REVISION, /*endptr*/ NULL, 10);
   798 
   799 		NSNumber *major = [[NSNumber alloc] initWithUnsignedShort:version.major];
   800 		NSNumber *minor = [[NSNumber alloc] initWithUnsignedShort:version.minor];
   801 		NSNumber *incremental = [[NSNumber alloc] initWithUnsignedChar:version.incremental];
   802 		NSNumber *releaseType = [[NSNumber alloc] initWithUnsignedChar:version.releaseType];
   803 		NSNumber *development = [[NSNumber alloc] initWithUnsignedShort:version.development];
   804 
   805 		versionInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
   806 			[GrowlApplicationController growlVersion], (NSString *)kCFBundleVersionKey,
   807 
   808 			major,                                     @"Major version",
   809 			minor,                                     @"Minor version",
   810 			incremental,                               @"Incremental version",
   811 			releaseTypeNames[version.releaseType],     @"Release type name",
   812 			releaseType,                               @"Release type",
   813 			development,                               @"Development version",
   814 
   815 			nil];
   816 
   817 		[major       release];
   818 		[minor       release];
   819 		[incremental release];
   820 		[releaseType release];
   821 		[development release];
   822 	}
   823 	return versionInfo;
   824 }
   825 
   826 //this method could be moved to Growl.framework, I think.
   827 //pass nil to get GrowlHelperApp's version as a string.
   828 - (NSString *)stringWithVersionDictionary:(NSDictionary *)d {
   829 	if (!d)
   830 		d = [self versionDictionary];
   831 
   832 	//0.6
   833 	NSMutableString *result = [NSMutableString stringWithFormat:@"%@.%@",
   834 		[d objectForKey:@"Major version"],
   835 		[d objectForKey:@"Minor version"]];
   836 
   837 	//the .1 in 0.6.1
   838 	NSNumber *incremental = [d objectForKey:@"Incremental version"];
   839 	if ([incremental unsignedShortValue])
   840 		[result appendFormat:@".%@", incremental];
   841 
   842 	NSString *releaseTypeName = [d objectForKey:@"Release type name"];
   843 	if ([releaseTypeName length]) {
   844 		//"" (release), "b4", " SVN 900"
   845 		[result appendFormat:@"%@%@", releaseTypeName, [d objectForKey:@"Development version"]];
   846 	}
   847 
   848 	return result;
   849 }
   850 
   851 - (NSURL *) versionCheckURL {
   852 	if (!versionCheckURL)
   853 		versionCheckURL = [[NSURL URLWithString:@"http://growl.info/version.xml"] retain];
   854 	return versionCheckURL;
   855 }
   856 
   857 #pragma mark Accessors
   858 
   859 - (BOOL) quitAfterOpen {
   860 	return quitAfterOpen;
   861 }
   862 - (void) setQuitAfterOpen:(BOOL)flag {
   863 	quitAfterOpen = flag;
   864 }
   865 
   866 #pragma mark What NSThread should implement as a class method
   867 
   868 - (NSThread *)mainThread {
   869 	return mainThread;
   870 }
   871 
   872 #pragma mark Notifications (not the Growl kind)
   873 
   874 - (void) preferencesChanged:(NSNotification *) note {
   875 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   876 
   877 	//[note object] is the changed key. A nil key means reload our tickets.
   878 	id object = [note object];
   879 
   880 	if (!quitAfterOpen) {
   881 		if (!note || (object && [object isEqual:GrowlStartServerKey])) {
   882 			Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   883 			if (pathwayControllerClass)
   884 				[(id)[pathwayControllerClass sharedController] setServerEnabledFromPreferences];
   885 		}
   886 	}
   887 	if (!note || (object && [object isEqual:GrowlUserDefaultsKey]))
   888 		[[GrowlPreferencesController sharedController] synchronize];
   889 	if (!note || (object && [object isEqual:GrowlEnabledKey]))
   890 		growlIsEnabled = [[GrowlPreferencesController sharedController] boolForKey:GrowlEnabledKey];
   891 	if (!note || (object && [object isEqual:GrowlEnableForwardKey]))
   892 		enableForward = [[GrowlPreferencesController sharedController] isForwardingEnabled];
   893 	if (!note || (object && [object isEqual:GrowlForwardDestinationsKey])) {
   894 		[destinations release];
   895 		destinations = [[[GrowlPreferencesController sharedController] objectForKey:GrowlForwardDestinationsKey] retain];
   896 	}
   897 	if (!note || !object)
   898 		[ticketController loadAllSavedTickets];
   899 	if (!note || (object && [object isEqual:GrowlDisplayPluginKey]))
   900 		// force reload
   901 		[defaultDisplayPlugin release];
   902 		defaultDisplayPlugin = nil;
   903 	if (object) {
   904 		if ([object isEqual:@"GrowlTicketDeleted"]) {
   905 			NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
   906 			[ticketController removeTicketForApplicationName:ticketName];
   907 		} else if ([object isEqual:@"GrowlTicketChanged"]) {
   908 			NSString *ticketName = [[note userInfo] objectForKey:@"TicketName"];
   909 			GrowlApplicationTicket *newTicket = [[GrowlApplicationTicket alloc] initTicketForApplication:ticketName];
   910 			if (newTicket) {
   911 				[ticketController addTicket:newTicket];
   912 				[newTicket release];
   913 			}
   914 		} else if ((!quitAfterOpen) && [object isEqual:GrowlUDPPortKey]) {
   915 			Class pathwayControllerClass = NSClassFromString(@"GrowlPathwayController");
   916 			if (pathwayControllerClass) {
   917 				id pathwayController = [pathwayControllerClass sharedController];
   918 				[pathwayController setServerEnabled:NO];
   919 				[pathwayController setServerEnabled:YES];
   920 			}
   921 		}
   922 	}
   923 	
   924 	[pool release];
   925 }
   926 
   927 - (void) shutdown:(NSNotification *) note {
   928 #pragma unused(note)
   929 	[NSApp terminate:nil];
   930 }
   931 
   932 - (void) replyToPing:(NSNotification *) note {
   933 #pragma unused(note)
   934 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   935 
   936 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_PONG
   937 	                                                               object:nil
   938 	                                                             userInfo:nil
   939 	                                                   deliverImmediately:NO];
   940 	
   941 	[pool release];
   942 }
   943 
   944 #pragma mark NSApplication Delegate Methods
   945 
   946 - (BOOL) application:(NSApplication *)theApplication openFile:(NSString *)filename {
   947 #pragma unused(theApplication)
   948 	BOOL retVal = NO;
   949 	NSString *pathExtension = [filename pathExtension];
   950 
   951 	if ([pathExtension isEqualToString:GROWL_REG_DICT_EXTENSION]) {
   952 		//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.
   953 		BOOL registerItOurselves = YES;
   954 		NSString *realHelperAppBundlePath = nil;
   955 
   956 		if (quitAfterOpen) {
   957 			//But, just to make sure we don't infinitely loop, make sure this isn't our own bundle.
   958 			NSString *ourBundlePath = [[NSBundle mainBundle] bundlePath];
   959 			realHelperAppBundlePath = [[GrowlPathUtilities runningHelperAppBundle] bundlePath];
   960 			if (![ourBundlePath isEqualToString:realHelperAppBundlePath])
   961 				registerItOurselves = NO;
   962 		}
   963 
   964 		if (registerItOurselves) {
   965 			//We are the real GHA.
   966 			//Have the property-list-file pathway process this registration dictionary file.
   967 			GrowlPropertyListFilePathway *pathway = [GrowlPropertyListFilePathway standardPathway];
   968 			[pathway application:theApplication openFile:filename];
   969 		} else {
   970 			//We're definitely not the real GHA, so pass it to the real GHA to be registered.
   971 			[[NSWorkspace sharedWorkspace] openFile:filename
   972 									withApplication:realHelperAppBundlePath];
   973 		}
   974 	} else {
   975 		GrowlPluginController *controller = [GrowlPluginController sharedController];
   976 		//the set returned by GrowlPluginController is case-insensitive. yay!
   977 		if ([[controller registeredPluginTypes] containsObject:pathExtension]) {
   978 			[controller installPluginFromPath:filename];
   979 
   980 			retVal = YES;
   981 		}
   982 	}
   983 
   984 	/*If Growl is not enabled and was not already running before
   985 	 *	(for example, via an autolaunch even though the user's last
   986 	 *	preference setting was to click "Stop Growl," setting enabled to NO),
   987 	 *	quit having registered; otherwise, remain running.
   988 	 */
   989 	if (!growlIsEnabled && !growlFinishedLaunching) {
   990 		//Terminate after one second to give us time to process any other openFile: messages.
   991 		[NSObject cancelPreviousPerformRequestsWithTarget:NSApp
   992 												 selector:@selector(terminate:)
   993 												   object:nil];
   994 		[NSApp performSelector:@selector(terminate:)
   995 					withObject:nil
   996 					afterDelay:1.0];
   997 	}
   998 
   999 	return retVal;
  1000 }
  1001 
  1002 - (void) applicationWillFinishLaunching:(NSNotification *)aNotification {
  1003 #pragma unused(aNotification)
  1004 	mainThread = [[NSThread currentThread] retain];
  1005 
  1006 	BOOL printVersionAndExit = [[NSUserDefaults standardUserDefaults] boolForKey:@"PrintVersionAndExit"];
  1007 	if (printVersionAndExit) {
  1008 		printf("This is GrowlHelperApp version %s.\n"
  1009 			   "PrintVersionAndExit was set to %hhi, so GrowlHelperApp will now exit.\n",
  1010 			   [[self stringWithVersionDictionary:nil] UTF8String],
  1011 			   printVersionAndExit);
  1012 		[NSApp terminate:nil];
  1013 	}
  1014 
  1015 	NSFileManager *fs = [NSFileManager defaultManager];
  1016 
  1017 	NSString *destDir, *subDir;
  1018 	NSArray *searchPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, /*expandTilde*/ YES);
  1019 
  1020 	destDir = [searchPath objectAtIndex:0U]; //first == last == ~/Library
  1021 	[fs createDirectoryAtPath:destDir attributes:nil];
  1022 	destDir = [destDir stringByAppendingPathComponent:@"Application Support"];
  1023 	[fs createDirectoryAtPath:destDir attributes:nil];
  1024 	destDir = [destDir stringByAppendingPathComponent:@"Growl"];
  1025 	[fs createDirectoryAtPath:destDir attributes:nil];
  1026 
  1027 	subDir  = [destDir stringByAppendingPathComponent:@"Tickets"];
  1028 	[fs createDirectoryAtPath:subDir attributes:nil];
  1029 	subDir  = [destDir stringByAppendingPathComponent:@"Plugins"];
  1030 	[fs createDirectoryAtPath:subDir attributes:nil];
  1031 }
  1032 
  1033 //Post a notification when we are done launching so the application bridge can inform participating applications
  1034 - (void) applicationDidFinishLaunching:(NSNotification *)aNotification {
  1035 #pragma unused(aNotification)
  1036 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:GROWL_IS_READY
  1037 	                                                               object:nil
  1038 	                                                             userInfo:nil
  1039 	                                                   deliverImmediately:YES];
  1040 	growlFinishedLaunching = YES;
  1041 
  1042 	if (quitAfterOpen) {
  1043 		//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.
  1044 		[NSApp performSelector:@selector(terminate:)
  1045 					withObject:nil
  1046 					afterDelay:1.0];
  1047 	} else {
  1048 		/*If Growl is not enabled and was not already running before
  1049 		 *	(for example, via an autolaunch even though the user's last
  1050 		 *	preference setting was to click "Stop Growl," setting enabled to NO),
  1051 		 *	quit having registered; otherwise, remain running.
  1052 		 */
  1053 		if (!growlIsEnabled)
  1054 			[NSApp terminate:nil];
  1055 	}
  1056 }
  1057 
  1058 //Same as applicationDidFinishLaunching, called when we are asked to reopen (that is, we are already running)
  1059 - (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
  1060 #pragma unused(theApplication, flag)
  1061 	return NO;
  1062 }
  1063 
  1064 - (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
  1065 #pragma unused(theApplication)
  1066 	return NO;
  1067 }
  1068 
  1069 - (void) applicationWillTerminate:(NSNotification *)notification {
  1070 #pragma unused(notification)
  1071 	[GrowlAbstractSingletonObject destroyAllSingletons];	//Release all our controllers
  1072 }
  1073 
  1074 #pragma mark Auto-discovery
  1075 
  1076 //called by NSWorkspace when an application launches.
  1077 - (void) applicationLaunched:(NSNotification *)notification {
  1078 	NSDictionary *userInfo = [notification userInfo];
  1079 
  1080 	if (!userInfo)
  1081 		return;
  1082 
  1083 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1084 	NSString *appPath = [userInfo objectForKey:@"NSApplicationPath"];
  1085 
  1086 	if (appPath) {
  1087 		NSString *ticketPath = [NSBundle pathForResource:@"Growl Registration Ticket" ofType:GROWL_REG_DICT_EXTENSION inDirectory:appPath];
  1088 		if (ticketPath) {
  1089 			CFURLRef ticketURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)ticketPath, kCFURLPOSIXPathStyle, false);
  1090 			NSMutableDictionary *ticket = (NSMutableDictionary *)createPropertyListFromURL((NSURL *)ticketURL, kCFPropertyListMutableContainers, NULL, NULL);
  1091 
  1092 			if (ticket) {
  1093 				NSString *appName = [userInfo objectForKey:@"NSApplicationName"];
  1094 
  1095 				//set the app's name in the dictionary, if it's not present already.
  1096 				if (![ticket objectForKey:GROWL_APP_NAME])
  1097 					[ticket setObject:appName forKey:GROWL_APP_NAME];
  1098 
  1099 				if ([GrowlApplicationTicket isValidTicketDictionary:ticket]) {
  1100 					NSLog(@"Auto-discovered registration ticket in %@ (located at %@)", appName, appPath);
  1101 
  1102 					/* set the app's location in the dictionary, avoiding costly
  1103 					 *	lookups later.
  1104 					 */
  1105 					NSURL *url = [[NSURL alloc] initFileURLWithPath:appPath];
  1106 					NSDictionary *file_data = createDockDescriptionWithURL(url);
  1107 					id location = file_data ? [NSDictionary dictionaryWithObject:file_data forKey:@"file-data"] : appPath;
  1108 					[file_data release];
  1109 					[ticket setObject:location forKey:GROWL_APP_LOCATION];
  1110 					[url release];
  1111 
  1112 					//write the new ticket to disk, and be sure to launch this ticket instead of the one in the app bundle.
  1113 					CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
  1114 					CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuid);
  1115 					CFRelease(uuid);
  1116 					ticketPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:(NSString *)uuidString] stringByAppendingPathExtension:GROWL_REG_DICT_EXTENSION];
  1117 					CFRelease(uuidString);
  1118 					[ticket writeToFile:ticketPath atomically:NO];
  1119 
  1120 					/* open the ticket with ourselves.
  1121 					 * we need to use LS in order to launch it with this specific
  1122 					 *	GHA, rather than some other.
  1123 					 */
  1124 					CFURLRef myURL      = (CFURLRef)copyCurrentProcessURL();
  1125 					NSArray *URLsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath:ticketPath]];
  1126 					struct LSLaunchURLSpec spec = {
  1127 						.appURL = myURL,
  1128 						.itemURLs = (CFArrayRef)URLsToOpen,
  1129 						.passThruParams = NULL,
  1130 						.launchFlags = kLSLaunchDontAddToRecents | kLSLaunchDontSwitch | kLSLaunchAsync,
  1131 						.asyncRefCon = NULL,
  1132 					};
  1133 					OSStatus err = LSOpenFromURLSpec(&spec, /*outLaunchedURL*/ NULL);
  1134 					if (err != noErr)
  1135 						NSLog(@"The registration ticket for %@ could not be opened (LSOpenFromURLSpec returned %li). Pathname for the ticket file: %@", appName, (long)err, ticketPath);
  1136 					CFRelease(myURL);
  1137 				} else if ([GrowlApplicationTicket isKnownTicketVersion:ticket]) {
  1138 					NSLog(@"%@ (located at %@) contains an invalid registration ticket - developer, please consult Growl developer documentation (http://growl.info/documentation/developer/)", appName, appPath);
  1139 				} else {
  1140 					NSNumber *versionNum = [ticket objectForKey:GROWL_TICKET_VERSION];
  1141 					if (versionNum)
  1142 						NSLog(@"%@ (located at %@) contains a ticket whose format version (%i) is unrecognised by this version (%@) of Growl", appName, appPath, [versionNum intValue], [self stringWithVersionDictionary:nil]);
  1143 					else
  1144 						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);
  1145 				}
  1146 				[ticket release];
  1147 			}
  1148 			CFRelease(ticketURL);
  1149 		}
  1150 	}
  1151 
  1152 	[pool release];
  1153 }
  1154 
  1155 #pragma mark Growl Application Bridge delegate
  1156 /*!
  1157  * @brief Returns the application name Growl will use
  1158  */
  1159 - (NSString *)applicationNameForGrowl
  1160 {
  1161 	return @"Growl";
  1162 }
  1163 
  1164 - (NSDictionary *)registrationDictionaryForGrowl
  1165 {	
  1166 	NSDictionary *descriptions = [NSDictionary dictionaryWithObjectsAndKeys:
  1167 		NSLocalizedString(@"A Growl update is available", nil), UPDATE_AVAILABLE_NOTIFICATION,
  1168 		NSLocalizedString(@"You are now considered idle by Growl", nil), USER_WENT_IDLE_NOTIFICATION,
  1169 		NSLocalizedString(@"You are no longer considered idle by Growl", nil), USER_RETURNED_NOTIFICATION,
  1170 		nil];
  1171 
  1172 	NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
  1173 		NSLocalizedString(@"Growl update available", nil), UPDATE_AVAILABLE_NOTIFICATION,
  1174 		NSLocalizedString(@"User went idle", nil), USER_WENT_IDLE_NOTIFICATION,
  1175 		NSLocalizedString(@"User returned", nil), USER_RETURNED_NOTIFICATION,
  1176 		nil];
  1177 	
  1178 	NSDictionary	*growlReg = [NSDictionary dictionaryWithObjectsAndKeys:
  1179 		[NSArray arrayWithObjects:UPDATE_AVAILABLE_NOTIFICATION, USER_WENT_IDLE_NOTIFICATION, USER_RETURNED_NOTIFICATION, nil], GROWL_NOTIFICATIONS_ALL,
  1180 		[NSArray arrayWithObject:UPDATE_AVAILABLE_NOTIFICATION], GROWL_NOTIFICATIONS_DEFAULT,
  1181 		humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
  1182 		descriptions, GROWL_NOTIFICATIONS_DESCRIPTIONS,
  1183 		nil];
  1184 	
  1185 	return growlReg;
  1186 }
  1187 
  1188 - (NSImage *)applicationIconDataForGrowl
  1189 {
  1190 	return [NSImage imageNamed:@"growl-icon"];
  1191 }
  1192 
  1193 - (void)growlNotificationWasClicked:(id)clickContext
  1194 {
  1195 	if (clickContext && [clickContext isKindOfClass:[NSString class]]) {
  1196 		NSURL *downloadURL = [NSURL URLWithString:clickContext];
  1197 		[[NSWorkspace sharedWorkspace] openURL:downloadURL];
  1198 	}
  1199 }
  1200 
  1201 @end
  1202 
  1203 #pragma mark -
  1204 
  1205 @implementation GrowlApplicationController (PRIVATE)
  1206 
  1207 #pragma mark Click feedback from displays
  1208 
  1209 /*click feedback comes here first. GAB picks up the DN and calls our
  1210  *	-growlNotificationWasClicked:/-growlNotificationTimedOut: with it if it's a
  1211  *	GHA notification.
  1212  */
  1213 
  1214 - (void) notificationClicked:(NSNotification *)notification {
  1215 	NSString *appName, *growlNotificationClickedName;
  1216 	NSString *suffix;
  1217 	NSDictionary *clickInfo;
  1218 	NSDictionary *userInfo;
  1219 
  1220 	userInfo = [notification userInfo];
  1221 
  1222 	//Build the application-specific notification name
  1223 	appName = [notification object];
  1224 	if (getBooleanForKey(userInfo, @"ClickHandlerEnabled")) {
  1225 		suffix = GROWL_NOTIFICATION_CLICKED;
  1226 	} else {
  1227 		/*
  1228 		 * send GROWL_NOTIFICATION_TIMED_OUT instead, so that an application is
  1229 		 * guaranteed to receive feedback for every notification.
  1230 		 */
  1231 		suffix = GROWL_NOTIFICATION_TIMED_OUT;
  1232 	}
  1233 	NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
  1234 	if (pid)
  1235 		growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
  1236 			appName, pid, suffix];
  1237 	else
  1238 		growlNotificationClickedName = [[NSString alloc] initWithFormat:@"%@%@",
  1239 			appName, suffix];
  1240 	clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
  1241 		[userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
  1242 		nil];
  1243 
  1244 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationClickedName
  1245 	                                                               object:nil
  1246 	                                                             userInfo:clickInfo
  1247 	                                                   deliverImmediately:YES];
  1248 
  1249 	[clickInfo release];
  1250 	[growlNotificationClickedName release];
  1251 }
  1252 
  1253 - (void) notificationTimedOut:(NSNotification *)notification {
  1254 	NSString *appName, *growlNotificationTimedOutName;
  1255 	NSDictionary *clickInfo;
  1256 	NSDictionary *userInfo;
  1257 
  1258 	userInfo = [notification userInfo];
  1259 
  1260 	//Build the application-specific notification name
  1261 	appName = [notification object];
  1262 	NSNumber *pid = [userInfo objectForKey:GROWL_APP_PID];
  1263 	if (pid)
  1264 		growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@-%@-%@",
  1265 			appName, pid, GROWL_NOTIFICATION_TIMED_OUT];
  1266 	else
  1267 		growlNotificationTimedOutName = [[NSString alloc] initWithFormat:@"%@%@",
  1268 			appName, GROWL_NOTIFICATION_TIMED_OUT];
  1269 	clickInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
  1270 		[userInfo objectForKey:GROWL_KEY_CLICKED_CONTEXT], GROWL_KEY_CLICKED_CONTEXT,
  1271 		nil];
  1272 
  1273 	[[NSDistributedNotificationCenter defaultCenter] postNotificationName:growlNotificationTimedOutName
  1274 	                                                               object:nil
  1275 	                                                             userInfo:clickInfo
  1276 	                                                   deliverImmediately:YES];
  1277 
  1278 	[clickInfo release];
  1279 	[growlNotificationTimedOutName release];
  1280 }
  1281 
  1282 @end
  1283 
  1284 static OSStatus soundCompletionCallbackProc(SystemSoundActionID actionID, void *refcon) 
  1285 {
  1286 #pragma unused(refcon)
  1287 
  1288 	SystemSoundRemoveCompletionRoutine(actionID);
  1289 
  1290 	return SystemSoundRemoveActionID(actionID);
  1291 }