Extras/GrowlSafari/GrowlSafari.m
author Peter Hosey <hg@boredzo.org>
Thu Jul 02 06:30:46 2009 -0700 (2009-07-02)
changeset 4225 22ffbf7ae378
parent 4077 a571d8dd2fb0
child 4226 a6eb28fcc0b7
permissions -rw-r--r--
Fix an apparent leak: We put the download-started date into our dictionary under the download's identifier, but then tried to remove the date from the dictionary under the download itself. Thus, we leaked both the date and the copy of the identifier.
tick@794
     1
/*
ingmarstein@1568
     2
 Copyright (c) The Growl Project, 2004-2005
tick@794
     3
 All rights reserved.
ingmarstein@1905
     4
ingmarstein@1905
     5
tick@794
     6
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
ingmarstein@1905
     7
ingmarstein@1905
     8
tick@794
     9
 1. Redistributions of source code must retain the above copyright
tick@794
    10
 notice, this list of conditions and the following disclaimer.
tick@794
    11
 2. Redistributions in binary form must reproduce the above copyright
tick@794
    12
 notice, this list of conditions and the following disclaimer in the
tick@794
    13
 documentation and/or other materials provided with the distribution.
tick@794
    14
 3. Neither the name of Growl nor the names of its contributors
tick@794
    15
 may be used to endorse or promote products derived from this software
tick@794
    16
 without specific prior written permission.
ingmarstein@1905
    17
ingmarstein@1905
    18
tick@794
    19
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ingmarstein@1905
    20
tick@794
    21
 */
tick@794
    22
aranor@568
    23
//
aranor@568
    24
//  GrowlSafari.m
aranor@568
    25
//  GrowlSafari
aranor@568
    26
//
aranor@568
    27
//  Created by Kevin Ballard on 10/29/04.
aranor@568
    28
//  Copyright 2004 Kevin Ballard. All rights reserved.
aranor@568
    29
//
aranor@568
    30
aranor@568
    31
#import "GrowlSafari.h"
ingmarstein@2112
    32
#import "GSWebBookmark.h"
ingmarstein@1943
    33
#import <Growl/Growl.h>
aranor@568
    34
#import <objc/objc-runtime.h>
aranor@568
    35
ingmarstein@2162
    36
ingmarstein@2162
    37
#define SAFARI_VERSION_2_0	412
evands@4077
    38
#define SAFARI_VERSION_3_0  523
ingmarstein@2162
    39
ingmarstein@2162
    40
// How long should we wait (in seconds) before it's a long download?
ingmarstein@2162
    41
static double longDownload = 15.0;
ingmarstein@2162
    42
static int safariVersion;
ingmarstein@2162
    43
static NSMutableDictionary *dates = nil;
ingmarstein@2162
    44
aranor@568
    45
// Using method swizzling as outlined here:
aranor@568
    46
// http://www.cocoadev.com/index.pl?MethodSwizzling
aranor@568
    47
// A couple of modifications made to support swizzling class methods
aranor@568
    48
ingmarstein@1764
    49
static BOOL PerformSwizzle(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
aranor@568
    50
    // First, make sure the class isn't nil
ingmarstein@1469
    51
	if (aClass) {
aranor@568
    52
		Method orig_method = nil, alt_method = nil;
ingmarstein@1469
    53
aranor@568
    54
		// Next, look for the methods
eevyl@590
    55
		if (forInstance) {
eevyl@590
    56
			orig_method = class_getInstanceMethod(aClass, orig_sel);
eevyl@590
    57
			alt_method = class_getInstanceMethod(aClass, alt_sel);
aranor@568
    58
		} else {
eevyl@590
    59
			orig_method = class_getClassMethod(aClass, orig_sel);
eevyl@590
    60
			alt_method = class_getClassMethod(aClass, alt_sel);
aranor@568
    61
		}
ingmarstein@1472
    62
aranor@568
    63
		// If both are found, swizzle them
ingmarstein@1469
    64
		if (orig_method && alt_method) {
aranor@568
    65
			IMP temp;
ingmarstein@1472
    66
aranor@568
    67
			temp = orig_method->method_imp;
aranor@568
    68
			orig_method->method_imp = alt_method->method_imp;
aranor@568
    69
			alt_method->method_imp = temp;
ingmarstein@1473
    70
ingmarstein@1473
    71
			return YES;
aranor@568
    72
		} else {
aranor@568
    73
			// This bit stolen from SubEthaFari's source
boredzo@1788
    74
			NSLog(@"GrowlSafari Error: Original (selector %s) %@, Alternate (selector %s) %@",
diggory@1810
    75
				  orig_sel,
diggory@1810
    76
				  orig_method ? @"was found" : @"not found",
diggory@1810
    77
				  alt_sel,
diggory@1810
    78
				  alt_method ? @"was found" : @"not found");
aranor@568
    79
		}
aranor@568
    80
	} else {
boredzo@1788
    81
		NSLog(@"%@", @"GrowlSafari Error: No class to swizzle methods in");
aranor@568
    82
	}
ingmarstein@1473
    83
ingmarstein@1473
    84
	return NO;
aranor@568
    85
}
aranor@568
    86
ingmarstein@1764
    87
static void setDownloadStarted(id dl) {
ingmarstein@1764
    88
	if (!dates)
ingmarstein@1764
    89
		dates = [[NSMutableDictionary alloc] init];
ingmarstein@1764
    90
ingmarstein@2162
    91
	[dates setObject:[NSDate date] forKey:[dl identifier]];
ingmarstein@1764
    92
}
ingmarstein@1764
    93
ingmarstein@1764
    94
static NSDate *dateStarted(id dl) {
ingmarstein@1764
    95
	if (dates)
ingmarstein@2162
    96
		return [dates objectForKey:[dl identifier]];
ingmarstein@1764
    97
ingmarstein@1764
    98
	return nil;
ingmarstein@1764
    99
}
ingmarstein@1764
   100
ingmarstein@1764
   101
static BOOL isLongDownload(id dl) {
ingmarstein@1764
   102
	NSDate *date = dateStarted(dl);
ingmarstein@1764
   103
	return (date && -[date timeIntervalSinceNow] > longDownload);
ingmarstein@1764
   104
}
ingmarstein@1764
   105
ingmarstein@1764
   106
static void setDownloadFinished(id dl) {
hg@4225
   107
	[dates removeObjectForKey:[dl identifier]];
ingmarstein@1764
   108
}
aranor@900
   109
aranor@568
   110
@implementation GrowlSafari
ingmarstein@1764
   111
+ (NSBundle *) bundle {
ingmarstein@2324
   112
	return [NSBundle bundleWithIdentifier:@"com.growl.GrowlSafari"];
ingmarstein@1764
   113
}
ingmarstein@1764
   114
ingmarstein@2113
   115
+ (NSString *) bundleVersion {
ingmarstein@2113
   116
	return [[[GrowlSafari bundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
ingmarstein@2113
   117
}
ingmarstein@2113
   118
ingmarstein@1764
   119
+ (void) initialize {
ingmarstein@1943
   120
	NSString *growlPath = [[[GrowlSafari bundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
ingmarstein@1943
   121
	NSBundle *growlBundle = [NSBundle bundleWithPath:growlPath];
ingmarstein@2103
   122
ingmarstein@2212
   123
	if (growlBundle && [growlBundle load]) {
ingmarstein@1943
   124
		// Register ourselves as a Growl delegate
ingmarstein@2103
   125
		[GrowlApplicationBridge setGrowlDelegate:self];
ingmarstein@2298
   126
evands@4077
   127
		safariVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey] intValue];
evands@4077
   128
		//  NSLog(@"%d",safariVersion);
evands@4077
   129
ingmarstein@2298
   130
		//	NSLog(@"Patching DownloadProgressEntry...");
ingmarstein@2298
   131
		Class class = NSClassFromString(@"DownloadProgressEntry");
ingmarstein@2298
   132
		PerformSwizzle(class, @selector(setDownloadStage:), @selector(mySetDownloadStage:), YES);
evands@4077
   133
				
evands@4077
   134
		if (safariVersion<SAFARI_VERSION_3_0) 
evands@4077
   135
			PerformSwizzle(class, @selector(updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
evands@4077
   136
		else
evands@4077
   137
			PerformSwizzle(class, @selector(_updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
evands@4077
   138
		
ingmarstein@2298
   139
		PerformSwizzle(class, @selector(initWithDownload:mayOpenWhenDone:allowOverwrite:),
ingmarstein@2298
   140
					   @selector(myInitWithDownload:mayOpenWhenDone:allowOverwrite:),
ingmarstein@2298
   141
					   YES);
ingmarstein@2298
   142
		
ingmarstein@2298
   143
		Class webBookmarkClass = NSClassFromString(@"WebBookmark");
ingmarstein@2298
   144
		if (webBookmarkClass)
ingmarstein@2298
   145
			[[GSWebBookmark class] poseAsClass:webBookmarkClass];
ingmarstein@2298
   146
ingmarstein@2113
   147
		NSLog(@"Loaded GrowlSafari %@", [GrowlSafari bundleVersion]);
ingmarstein@2369
   148
		NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
ingmarstein@2369
   149
		NSLog(@"Using Growl.framework %@ (%@)",
ingmarstein@2369
   150
			  [infoDictionary objectForKey:@"CFBundleShortVersionString"],
ingmarstein@2369
   151
			  [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
ingmarstein@2212
   152
	} else {
ingmarstein@2212
   153
		NSLog(@"Could not load Growl.framework, GrowlSafari disabled");
ingmarstein@1943
   154
	}
evands@4077
   155
	
ingmarstein@1943
   156
}
ingmarstein@1943
   157
ingmarstein@1943
   158
#pragma mark GrowlApplicationBridge delegate methods
ingmarstein@1943
   159
ingmarstein@1943
   160
+ (NSString *) applicationNameForGrowl {
ingmarstein@1943
   161
	return @"GrowlSafari";
ingmarstein@1943
   162
}
ingmarstein@1943
   163
ingmarstein@2245
   164
+ (NSImage *) applicationIconForGrowl {
ingmarstein@2324
   165
	return [NSImage imageNamed:@"NSApplicationIcon"];
ingmarstein@1943
   166
}
ingmarstein@1943
   167
ingmarstein@1943
   168
+ (NSDictionary *) registrationDictionaryForGrowl {
ingmarstein@642
   169
	NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@1764
   170
	NSArray *array = [[NSArray alloc] initWithObjects:
ingmarstein@1764
   171
		NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @""),
ingmarstein@642
   172
		NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @""),
ingmarstein@642
   173
		NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @""),
ingmarstein@642
   174
		NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @""),
ingmarstein@2112
   175
		NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @""),
ingmarstein@611
   176
		nil];
aranor@568
   177
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
aranor@568
   178
		array, GROWL_NOTIFICATIONS_DEFAULT,
aranor@568
   179
		array, GROWL_NOTIFICATIONS_ALL,
aranor@568
   180
		nil];
ingmarstein@1764
   181
	[array release];
ingmarstein@1943
   182
ingmarstein@1943
   183
	return dict;
aranor@568
   184
}
ingmarstein@2112
   185
ingmarstein@2112
   186
+ (void) growlNotificationWasClicked:(id)clickContext {
ingmarstein@2112
   187
	NSURL *url = [[NSURL alloc] initWithString:clickContext];
ingmarstein@2112
   188
	[[NSWorkspace sharedWorkspace] openURL:url];
ingmarstein@2112
   189
	[url release];
ingmarstein@2112
   190
}
ingmarstein@2112
   191
ingmarstein@2245
   192
+ (void) notifyRSSUpdate:(WebBookmark *)bookmark newEntries:(int)newEntries {
ingmarstein@2112
   193
	NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2113
   194
	NSMutableString	*description = [[NSMutableString alloc]
ingmarstein@2113
   195
		initWithFormat:newEntries == 1 ? NSLocalizedStringFromTableInBundle(@"%d new entry", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"%d new entries", nil, bundle, @""),
ingmarstein@2113
   196
		newEntries,
ingmarstein@2112
   197
		[bookmark unreadRSSCount]];
ingmarstein@2112
   198
	if (newEntries != [bookmark unreadRSSCount])
ingmarstein@2113
   199
		[description appendFormat:NSLocalizedStringFromTableInBundle(@" (%d unread)", nil, bundle, @""), [bookmark unreadRSSCount]];
ingmarstein@2112
   200
ingmarstein@2112
   201
	NSString *title = [bookmark title];
ingmarstein@2112
   202
	[GrowlApplicationBridge notifyWithTitle:(title ? title : [bookmark URLString])
ingmarstein@2112
   203
								description:description
ingmarstein@2112
   204
						   notificationName:NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @"")
evands@4077
   205
								   iconData:nil
ingmarstein@2112
   206
								   priority:0
ingmarstein@2112
   207
								   isSticky:NO
ingmarstein@2112
   208
							   clickContext:[bookmark URLString]];
ingmarstein@2113
   209
	[description release];
ingmarstein@2112
   210
}
aranor@568
   211
@end
aranor@568
   212
aranor@568
   213
@implementation NSObject (GrowlSafariPatch)
ingmarstein@1764
   214
- (void) mySetDownloadStage:(int)stage {
ingmarstein@2162
   215
	int oldStage = [self downloadStage];
evands@4077
   216
	
ingmarstein@2162
   217
	//NSLog(@"mySetDownloadStage:%d -> %d", oldStage, stage);
aranor@568
   218
	[self mySetDownloadStage:stage];
ingmarstein@1764
   219
	if (dateStarted(self)) {
evands@4077
   220
		if ( (safariVersion < SAFARI_VERSION_3_0 && stage == 2) || (safariVersion >= SAFARI_VERSION_3_0 && stage == 1) ) {
ingmarstein@1943
   221
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   222
			NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   223
				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
evands@4077
   224
				[[self gsDownloadPath] lastPathComponent]];
ingmarstein@2103
   225
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Decompressing File", nil, bundle, @"")
ingmarstein@2162
   226
										description:description
ingmarstein@1943
   227
								   notificationName:NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @"")
ingmarstein@1943
   228
										   iconData:nil
ingmarstein@1943
   229
										   priority:0
ingmarstein@1943
   230
										   isSticky:NO
ingmarstein@1943
   231
									   clickContext:nil];
ingmarstein@2162
   232
			[description release];
evands@4077
   233
		} else if ( (safariVersion < SAFARI_VERSION_3_0 && stage == 9 && oldStage != 9) || 
evands@4077
   234
			        (safariVersion >= SAFARI_VERSION_3_0 && stage == 8 && oldStage != 8) ) {
ingmarstein@1943
   235
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   236
			NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   237
				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
evands@4077
   238
				[[self gsDownloadPath] lastPathComponent]];
ingmarstein@2103
   239
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Copying Disk Image", nil, bundle, @"")
ingmarstein@2162
   240
										description:description
ingmarstein@1943
   241
								   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
ingmarstein@1943
   242
										   iconData:nil
ingmarstein@1943
   243
										   priority:0
ingmarstein@1943
   244
										   isSticky:NO
ingmarstein@1943
   245
									   clickContext:nil];
ingmarstein@2162
   246
			[description release];
evands@4077
   247
		} else if ( (safariVersion < SAFARI_VERSION_2_0 && stage == 13) || 
evands@4077
   248
		            ((safariVersion >= SAFARI_VERSION_2_0 && safariVersion < SAFARI_VERSION_3_0) && stage == 15) ||
evands@4077
   249
				    (safariVersion >= SAFARI_VERSION_3_0 && stage == 13) ) {
aranor@900
   250
			NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@1764
   251
			NSString *notificationName = isLongDownload(self) ? NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @"");
ingmarstein@1764
   252
			setDownloadFinished(self);
ingmarstein@2162
   253
			NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   254
				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, "Message shown when a download is complete, where %@ becomes the filename"),
ingmarstein@2162
   255
				[self filename]];
ingmarstein@2103
   256
			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"")
ingmarstein@2162
   257
										description:description
ingmarstein@1943
   258
								   notificationName:notificationName
ingmarstein@1943
   259
										   iconData:nil
ingmarstein@1943
   260
										   priority:0
ingmarstein@1943
   261
										   isSticky:NO
ingmarstein@1943
   262
									   clickContext:nil];
ingmarstein@2162
   263
			[description release];
aranor@900
   264
		}
ingmarstein@2162
   265
	} else if (stage == 0) {
ingmarstein@1764
   266
		setDownloadStarted(self);
ingmarstein@1764
   267
	}
ingmarstein@1764
   268
}
ingmarstein@1764
   269
ingmarstein@1764
   270
- (void) myUpdateDiskImageStatus:(NSDictionary *)status {
ingmarstein@2162
   271
	int oldStage = [self downloadStage];
ingmarstein@642
   272
	[self myUpdateDiskImageStatus:status];
ingmarstein@2162
   273
	//NSLog(@"myUpdateDiskImageStatus:%@ stage=%d -> %d", status, oldStage, [self downloadStage]);
ingmarstein@2162
   274
ingmarstein@2162
   275
	if (dateStarted(self)
evands@4077
   276
			&& ( (safariVersion < SAFARI_VERSION_3_0 && oldStage == 3) || (safariVersion >= SAFARI_VERSION_3_0 && oldStage == 7) )
ingmarstein@2162
   277
			&& [self downloadStage] == 8
ingmarstein@2162
   278
			&& [[status objectForKey:@"status-stage"] isEqualToString:@"attach"]) {
ingmarstein@2162
   279
		NSBundle *bundle = [GrowlSafari bundle];
ingmarstein@2162
   280
		NSString *description = [[NSString alloc] initWithFormat:
evands@4077
   281
			NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
evands@4077
   282
			[[self gsDownloadPath] lastPathComponent]];
ingmarstein@2162
   283
		[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Mounting Disk Image", nil, bundle, @"")
ingmarstein@2162
   284
									description:description
ingmarstein@2162
   285
							   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
ingmarstein@2162
   286
									   iconData:nil
ingmarstein@2162
   287
									   priority:0
ingmarstein@2162
   288
									   isSticky:NO
ingmarstein@2162
   289
								   clickContext:nil];
ingmarstein@2162
   290
		[description release];
aranor@568
   291
	}
aranor@568
   292
}
aranor@900
   293
aranor@900
   294
// This is to make sure we're done with the pre-saved downloads
ingmarstein@1764
   295
- (id) myInitWithDownload:(id)fp8 mayOpenWhenDone:(BOOL)fp12 allowOverwrite:(BOOL)fp16 {
ingmarstein@1764
   296
	id retval = [self myInitWithDownload:fp8 mayOpenWhenDone:fp12 allowOverwrite:fp16];
ingmarstein@1764
   297
	setDownloadStarted(self);
ingmarstein@1764
   298
	return retval;
ingmarstein@1473
   299
}
evands@4077
   300
evands@4077
   301
- (NSString*) gsDownloadPath {
evands@4077
   302
	if (safariVersion<SAFARI_VERSION_3_0)
evands@4077
   303
		return [self downloadPath];
evands@4077
   304
	else
evands@4077
   305
		return [self currentPath];
evands@4077
   306
}
evands@4077
   307
aranor@568
   308
@end