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