1. Perry Metzger
  2. growl


boredzo  committed a6a92fb

We now record all notifications from all pathways in a central location: ~/Library/Application Support/Growl/Saved Notifications. That directory contains one subdirectory per day, and each of those subdirectories contains plist files which are named in the pattern “Growl saved notification #NUMBER.plist” (where NUMBER increases monotonically from 0).

Naturally, this is restricted to the main thread, in order to avoid two notifications from different sources running into each other in attempting to save their respective notification files.

  • Participants
  • Parent commits a8bcd69
  • Branches Growl-NotificationRecording

Comments (0)

Files changed (3)

File Common/Source/GrowlPathUtilities.h

View file
  • Ignore whitespace
+	GrowlSavedNotificationsDirectory,
 typedef NSSearchPathDomainMask GrowlSearchPathDomainMask; //consistency

File Common/Source/GrowlPathUtilities.m

View file
  • Ignore whitespace
 #define NAME_OF_SCREENSHOTS_DIRECTORY           @"Screenshots"
 #define NAME_OF_TICKETS_DIRECTORY               @"Tickets"
 #define NAME_OF_PLUGINS_DIRECTORY               @"Plugins"
+#define NAME_OF_SAVED_NOTIFICATIONS_DIRECTORY   @"Saved Notifications"
 @implementation GrowlPathUtilities
+			case GrowlSavedNotificationsDirectory:
+				break;
 				NSLog(@"ERROR: GrowlPathUtil was asked for directory 0x%x, but it doesn't know what directory that is. Please tell the Growl developers.", directory);
 				return nil;

File Core/Source/GrowlPathway.m

View file
  • Ignore whitespace
 #import "GrowlPathway.h"
 #import "GrowlApplicationController.h"
+#import "GrowlPathUtilities.h"
+static NSString *lastSavedNotificationsSubdirectoryName = nil;
+static unsigned long long notificationCounter = 0U;
 @implementation GrowlPathway
 	return self;
+#pragma mark Recording notifications
++ (void) recordNotificationWithDictionary:(NSDictionary *)dict {
+	NSFileManager *mgr = [NSFileManager defaultManager];
+	NSString *directory = nil;
+	NSArray *searchPath = [GrowlPathUtilities searchPathForDirectory: GrowlSavedNotificationsDirectory
+														   inDomains: NSAllDomainsMask
+													  mustBeWritable: YES];
+	if (searchPath && [searchPath count])
+		directory = [searchPath objectAtIndex:0U];
+	else {
+		directory = [GrowlPathUtilities growlSupportDirectory];
+		if (directory) {
+#warning XXX We really should make GrowlPathUtilities able to create the subdirectories for us. That would clean up several of its own methods, too.
+			directory = [directory stringByAppendingPathComponent:@"Saved Notifications"];
+			[mgr createDirectoryAtPath:directory attributes:nil];
+		}
+	}
+	//Go through the search-path method again in order to benefit again from its permissions checks.
+	searchPath = [GrowlPathUtilities searchPathForDirectory: GrowlSavedNotificationsDirectory
+												  inDomains: NSAllDomainsMask
+											 mustBeWritable: YES];
+	if (searchPath && [searchPath count])
+		directory = [searchPath objectAtIndex:0U];
+	else {
+		NSLog(@"Could not create directory: %@", directory);
+		directory = nil;
+	}
+	if (directory) {
+		//The Saved Notifications directory's contents are subdirectories, named by date (YYYY-MM-DD).
+		NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];
+		[formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+		[formatter setDateFormat:@"yyyy-MM-dd"];
+		NSString *subdirectoryName = [formatter stringFromDate:[NSDate date]];
+		directory = [directory stringByAppendingPathComponent:subdirectoryName];
+		/*If lastSavedNotificationsSubdirectoryName is nil, then this is the first notification that this GHA process has received, so we have not yet set that variable.
+		 *If the two names are not equal, then the date has rolled over. In that case, this subdirectory doesn't exist yet, so we must create it.
+		 *Following De Morgan's Law: If lastSavedEtc is not nil, and is equal to the current subdirectory name, then this is not the first notification and the date has not rolled over, so we don't need to create the subdirectory or set lastSavedEtc.
+		 *If, on the other hand, that proposition is false, then we do need to create the subdirectory or set lastSavedEtc (or both).
+		 *In any case, when we *do* do those things, we also reset the notification counter, because it's a new day.
+		 */
+		if (!(lastSavedNotificationsSubdirectoryName && [lastSavedNotificationsSubdirectoryName isEqualToString:subdirectoryName])) {
+			[lastSavedNotificationsSubdirectoryName release];
+			lastSavedNotificationsSubdirectoryName = [subdirectoryName retain];
+			[mgr createDirectoryAtPath:directory attributes:nil];
+			notificationCounter = 0U;
+		}
+		NSString *path;
+		//If the user *re*started Growl, blindly using this value of notificationCounter could result in overwriting already-recorded notifications from earlier today.
+		//So, we loop until either we find a filename that doesn't already exist or we run out of numbers.
+		while ((path = [directory stringByAppendingPathComponent:[[NSString stringWithFormat:@"Growl saved notification #%llu", notificationCounter] stringByAppendingPathExtension:@"plist"]])) {
+			//If the file doesn't already exist, then we have found what we're looking for.
+			if (![mgr fileExistsAtPath:path])
+				break;
+			//If it does already exist, and the next number is 0, then we have run out of numbers.
+			if (++notificationCounter == 0U) {
+				NSLog(@"%s: Ran out of notification numbers! Can't record this notification.", __PRETTY_FUNCTION__);
+				return;
+			}
+		}
+		NSError *error = nil;
+		NSString *errorString = nil;
+		//OK. We have a filename for a file that doesn't already exist. Now, finally, we write the dictionary to the file.
+		NSData *data = [NSPropertyListSerialization dataFromPropertyList:dict
+																  format:NSPropertyListXMLFormat_v1_0
+														errorDescription:&errorString];
+		if ([data writeToFile:path options:0 error:&error])
+			NSLog(@"Wrote notification to file: %@", path);
+		else
+			NSLog(@"Could not write notification to file %@ because %@", path, error ? (id)error : (id)errorString);
+	}
+//This method is a trampoline. We only have it because NSObject doesn't have performSelectorOnMainThread: as a class method, only as an instance method.
+- (void) recordNotificationWithDictionary:(NSDictionary *)dict {
+	[[self class] recordNotificationWithDictionary:dict];
+#pragma mark Notification-receptor methods
 - (void) registerApplicationWithDictionary:(NSDictionary *)dict {
 	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
 	[applicationController performSelectorOnMainThread:@selector(registerApplicationWithDictionary:)
 	[applicationController performSelectorOnMainThread:@selector(dispatchNotificationWithDictionary:)
+	[self performSelectorOnMainThread:@selector(recordNotificationWithDictionary:)
+						   withObject:dict
+						waitUntilDone:NO];
 	[pool release];