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