Extras/GrowlSafari/GrowlSafari.m
author Peter Hosey <hg@boredzo.org>
Thu Jul 02 06:31:09 2009 -0700 (2009-07-02)
changeset 4226 a6eb28fcc0b7
parent 4225 22ffbf7ae378
child 4237 d092b0dd14a5
permissions -rw-r--r--
Fix GrowlSafari under Safari 4 by updating the download stage numbers. For simplicity and ease of fixage, I've ripped out all the Safari 2 and 3 support—GrowlSafari requires Safari 4 now. For clarity, I've also added an enumeration to name all the stages.
     1 /*
     2  Copyright (c) The Growl Project, 2004-2005
     3  All rights reserved.
     4 
     5 
     6  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
     7 
     8 
     9  1. Redistributions of source code must retain the above copyright
    10  notice, this list of conditions and the following disclaimer.
    11  2. Redistributions in binary form must reproduce the above copyright
    12  notice, this list of conditions and the following disclaimer in the
    13  documentation and/or other materials provided with the distribution.
    14  3. Neither the name of Growl nor the names of its contributors
    15  may be used to endorse or promote products derived from this software
    16  without specific prior written permission.
    17 
    18 
    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.
    20 
    21  */
    22 
    23 //
    24 //  GrowlSafari.m
    25 //  GrowlSafari
    26 //
    27 //  Created by Kevin Ballard on 10/29/04.
    28 //  Copyright 2004 Kevin Ballard. All rights reserved.
    29 //
    30 
    31 #import "GrowlSafari.h"
    32 #import "GSWebBookmark.h"
    33 #import <Growl/Growl.h>
    34 #import <objc/objc-runtime.h>
    35 
    36 
    37 #define SAFARI_VERSION_4_0  5530
    38 
    39 enum {
    40 	GrowlSafariDownloadStageActive = 1,
    41 	GrowlSafariDownloadStageDecompressing = 2,
    42 	GrowlSafariDownloadStageDiskImagePreparing = 4,
    43 	GrowlSafariDownloadStageDiskImageVerifying = 7,
    44 	GrowlSafariDownloadStageDiskImageVerified = 8,
    45 	GrowlSafariDownloadStageDiskImageMounting = 9,
    46 	GrowlSafariDownloadStageDiskImageCleanup = 12,
    47 	GrowlSafariDownloadStageInactive = 13,
    48 	GrowlSafariDownloadStageFinished = 14
    49 };
    50 
    51 // How long should we wait (in seconds) before it's a long download?
    52 static double longDownload = 15.0;
    53 static int safariVersion;
    54 static NSMutableDictionary *dates = nil;
    55 
    56 // Using method swizzling as outlined here:
    57 // http://www.cocoadev.com/index.pl?MethodSwizzling
    58 // A couple of modifications made to support swizzling class methods
    59 
    60 static BOOL PerformSwizzle(Class aClass, SEL orig_sel, SEL alt_sel, BOOL forInstance) {
    61     // First, make sure the class isn't nil
    62 	if (aClass) {
    63 		Method orig_method = nil, alt_method = nil;
    64 
    65 		// Next, look for the methods
    66 		if (forInstance) {
    67 			orig_method = class_getInstanceMethod(aClass, orig_sel);
    68 			alt_method = class_getInstanceMethod(aClass, alt_sel);
    69 		} else {
    70 			orig_method = class_getClassMethod(aClass, orig_sel);
    71 			alt_method = class_getClassMethod(aClass, alt_sel);
    72 		}
    73 
    74 		// If both are found, swizzle them
    75 		if (orig_method && alt_method) {
    76 			IMP temp;
    77 
    78 			temp = orig_method->method_imp;
    79 			orig_method->method_imp = alt_method->method_imp;
    80 			alt_method->method_imp = temp;
    81 
    82 			return YES;
    83 		} else {
    84 			// This bit stolen from SubEthaFari's source
    85 			NSLog(@"GrowlSafari Error: Original (selector %s) %@, Alternate (selector %s) %@",
    86 				  orig_sel,
    87 				  orig_method ? @"was found" : @"not found",
    88 				  alt_sel,
    89 				  alt_method ? @"was found" : @"not found");
    90 		}
    91 	} else {
    92 		NSLog(@"%@", @"GrowlSafari Error: No class to swizzle methods in");
    93 	}
    94 
    95 	return NO;
    96 }
    97 
    98 static void setDownloadStarted(id dl) {
    99 	if (!dates)
   100 		dates = [[NSMutableDictionary alloc] init];
   101 
   102 	[dates setObject:[NSDate date] forKey:[dl identifier]];
   103 }
   104 
   105 static NSDate *dateStarted(id dl) {
   106 	if (dates)
   107 		return [dates objectForKey:[dl identifier]];
   108 
   109 	return nil;
   110 }
   111 
   112 static BOOL isLongDownload(id dl) {
   113 	NSDate *date = dateStarted(dl);
   114 	return (date && -[date timeIntervalSinceNow] > longDownload);
   115 }
   116 
   117 static void setDownloadFinished(id dl) {
   118 	[dates removeObjectForKey:[dl identifier]];
   119 }
   120 
   121 @implementation GrowlSafari
   122 + (NSBundle *) bundle {
   123 	return [NSBundle bundleWithIdentifier:@"com.growl.GrowlSafari"];
   124 }
   125 
   126 + (NSString *) bundleVersion {
   127 	return [[[GrowlSafari bundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey];
   128 }
   129 
   130 + (void) initialize {
   131 	NSString *growlPath = [[[GrowlSafari bundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
   132 	NSBundle *growlBundle = [NSBundle bundleWithPath:growlPath];
   133 
   134 	if (growlBundle && [growlBundle load]) {
   135 		// Register ourselves as a Growl delegate
   136 		[GrowlApplicationBridge setGrowlDelegate:self];
   137 
   138 		safariVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleVersionKey] intValue];
   139 		//  NSLog(@"%d",safariVersion);
   140 
   141 		if (safariVersion >= SAFARI_VERSION_4_0) {
   142 			//	NSLog(@"Patching DownloadProgressEntry...");
   143 			Class class = NSClassFromString(@"DownloadProgressEntry");
   144 			PerformSwizzle(class, @selector(setDownloadStage:), @selector(mySetDownloadStage:), YES);
   145 					
   146 			PerformSwizzle(class, @selector(_updateDiskImageStatus:), @selector(myUpdateDiskImageStatus:), YES);
   147 			
   148 			PerformSwizzle(class, @selector(initWithDownload:mayOpenWhenDone:allowOverwrite:),
   149 						   @selector(myInitWithDownload:mayOpenWhenDone:allowOverwrite:),
   150 						   YES);
   151 			
   152 			Class webBookmarkClass = NSClassFromString(@"WebBookmark");
   153 			if (webBookmarkClass)
   154 				[[GSWebBookmark class] poseAsClass:webBookmarkClass];
   155 
   156 			NSLog(@"Loaded GrowlSafari %@", [GrowlSafari bundleVersion]);
   157 			NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
   158 			NSLog(@"Using Growl.framework %@ (%@)",
   159 				  [infoDictionary objectForKey:@"CFBundleShortVersionString"],
   160 				  [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
   161 		} else {
   162 			NSLog(@"Safari too old (4.0 required); GrowlSafari disabled.");
   163 		}
   164 	} else {
   165 		NSLog(@"Could not load Growl.framework, GrowlSafari disabled");
   166 	}
   167 	
   168 }
   169 
   170 #pragma mark GrowlApplicationBridge delegate methods
   171 
   172 + (NSString *) applicationNameForGrowl {
   173 	return @"GrowlSafari";
   174 }
   175 
   176 + (NSImage *) applicationIconForGrowl {
   177 	return [NSImage imageNamed:@"NSApplicationIcon"];
   178 }
   179 
   180 + (NSDictionary *) registrationDictionaryForGrowl {
   181 	NSBundle *bundle = [GrowlSafari bundle];
   182 	NSArray *array = [[NSArray alloc] initWithObjects:
   183 		NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @""),
   184 		NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @""),
   185 		NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @""),
   186 		NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @""),
   187 		NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @""),
   188 		nil];
   189 	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
   190 		array, GROWL_NOTIFICATIONS_DEFAULT,
   191 		array, GROWL_NOTIFICATIONS_ALL,
   192 		nil];
   193 	[array release];
   194 
   195 	return dict;
   196 }
   197 
   198 + (void) growlNotificationWasClicked:(id)clickContext {
   199 	NSURL *url = [[NSURL alloc] initWithString:clickContext];
   200 	[[NSWorkspace sharedWorkspace] openURL:url];
   201 	[url release];
   202 }
   203 
   204 + (void) notifyRSSUpdate:(WebBookmark *)bookmark newEntries:(int)newEntries {
   205 	NSBundle *bundle = [GrowlSafari bundle];
   206 	NSMutableString	*description = [[NSMutableString alloc]
   207 		initWithFormat:newEntries == 1 ? NSLocalizedStringFromTableInBundle(@"%d new entry", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"%d new entries", nil, bundle, @""),
   208 		newEntries,
   209 		[bookmark unreadRSSCount]];
   210 	if (newEntries != [bookmark unreadRSSCount])
   211 		[description appendFormat:NSLocalizedStringFromTableInBundle(@" (%d unread)", nil, bundle, @""), [bookmark unreadRSSCount]];
   212 
   213 	NSString *title = [bookmark title];
   214 	[GrowlApplicationBridge notifyWithTitle:(title ? title : [bookmark URLString])
   215 								description:description
   216 						   notificationName:NSLocalizedStringFromTableInBundle(@"New feed entry", nil, bundle, @"")
   217 								   iconData:nil
   218 								   priority:0
   219 								   isSticky:NO
   220 							   clickContext:[bookmark URLString]];
   221 	[description release];
   222 }
   223 @end
   224 
   225 @implementation NSObject (GrowlSafariPatch)
   226 - (void) mySetDownloadStage:(int)stage {
   227 	//int oldStage = [self downloadStage];
   228 	
   229 	//NSLog(@"mySetDownloadStage:%d -> %d", oldStage, stage);
   230 	[self mySetDownloadStage:stage];
   231 	if (dateStarted(self)) {
   232 		if ( stage == GrowlSafariDownloadStageDecompressing ) {
   233 			NSBundle *bundle = [GrowlSafari bundle];
   234 			NSString *description = [[NSString alloc] initWithFormat:
   235 				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   236 				[[self gsDownloadPath] lastPathComponent]];
   237 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Decompressing File", nil, bundle, @"")
   238 										description:description
   239 								   notificationName:NSLocalizedStringFromTableInBundle(@"Compression Status", nil, bundle, @"")
   240 										   iconData:nil
   241 										   priority:0
   242 										   isSticky:NO
   243 									   clickContext:nil];
   244 			[description release];
   245 		} else if ( stage == GrowlSafariDownloadStageDiskImageVerifying ) {
   246 			NSBundle *bundle = [GrowlSafari bundle];
   247 			NSString *description = [[NSString alloc] initWithFormat:
   248 									 NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   249 									 [[self gsDownloadPath] lastPathComponent]];
   250 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Verifying Disk Image", nil, bundle, @"")
   251 										description:description
   252 								   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
   253 										   iconData:nil
   254 										   priority:0
   255 										   isSticky:NO
   256 									   clickContext:nil];
   257 			[description release];
   258 		} else if ( stage == GrowlSafariDownloadStageFinished ) {
   259 			NSBundle *bundle = [GrowlSafari bundle];
   260 			NSString *notificationName = isLongDownload(self) ? NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"") : NSLocalizedStringFromTableInBundle(@"Short Download Complete", nil, bundle, @"");
   261 			setDownloadFinished(self);
   262 			NSString *description = [[NSString alloc] initWithFormat:
   263 				NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, "Message shown when a download is complete, where %@ becomes the filename"),
   264 				[self filename]];
   265 			[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Download Complete", nil, bundle, @"")
   266 										description:description
   267 								   notificationName:notificationName
   268 										   iconData:nil
   269 										   priority:0
   270 										   isSticky:NO
   271 									   clickContext:nil];
   272 			[description release];
   273 		}
   274 	} else if (stage == GrowlSafariDownloadStageActive) {
   275 		setDownloadStarted(self);
   276 	}
   277 }
   278 
   279 - (void) myUpdateDiskImageStatus:(NSDictionary *)status {
   280 	int oldStage = [self downloadStage];
   281 	[self myUpdateDiskImageStatus:status];
   282 	//NSLog(@"myUpdateDiskImageStatus:%@ stage=%d -> %d", status, oldStage, [self downloadStage]);
   283 
   284 	if (dateStarted(self)
   285 			&& oldStage == GrowlSafariDownloadStageDiskImageVerified
   286 			&& [self downloadStage] == GrowlSafariDownloadStageDiskImageMounting
   287 			&& [[status objectForKey:@"status-stage"] isEqualToString:@"attach"]) {
   288 		NSBundle *bundle = [GrowlSafari bundle];
   289 		NSString *description = [[NSString alloc] initWithFormat:
   290 			NSLocalizedStringFromTableInBundle(@"%@", nil, bundle, @""),
   291 			[[self gsDownloadPath] lastPathComponent]];
   292 		[GrowlApplicationBridge notifyWithTitle:NSLocalizedStringFromTableInBundle(@"Mounting Disk Image", nil, bundle, @"")
   293 									description:description
   294 							   notificationName:NSLocalizedStringFromTableInBundle(@"Disk Image Status", nil, bundle, @"")
   295 									   iconData:nil
   296 									   priority:0
   297 									   isSticky:NO
   298 								   clickContext:nil];
   299 		[description release];
   300 	}
   301 }
   302 
   303 // This is to make sure we're done with the pre-saved downloads
   304 - (id) myInitWithDownload:(id)fp8 mayOpenWhenDone:(BOOL)fp12 allowOverwrite:(BOOL)fp16 {
   305 	id retval = [self myInitWithDownload:fp8 mayOpenWhenDone:fp12 allowOverwrite:fp16];
   306 	setDownloadStarted(self);
   307 	return retval;
   308 }
   309 
   310 - (NSString*) gsDownloadPath {
   311 	return [self currentPath];
   312 }
   313 
   314 @end