File: programming/cocoa/NiftyFeatures.zip/NiftyFeatures/UKUpdateChecker.m


//
//  UKUpdateChecker.m
//  NiftyFeatures
//
//  Created by Uli Kusterer on Sun Nov 23 2003.
//  Copyright (c) 2003 M. Uli Kusterer. All rights reserved.
//
 
#import "UKUpdateChecker.h"
 
 
@implementation UKUpdateChecker
 
 
// -----------------------------------------------------------------------------
//	awakeFromNib:
//		This object has been created and loaded at startup. If this is first
//		launch, ask user whether we should check for updates periodically at
//		startup and adjust the prefs accurately.
//
//		If the user wants us to check for updates periodically, check whether
//		it is time and if so, initiate the check.
//
//	REVISIONS:
//		2004-03-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(void) awakeFromNib
{
	NSNumber	*   doCheck = [[NSUserDefaults standardUserDefaults] objectForKey: @"UKUpdateChecker:CheckAtStartup"];
	NSString	*   appName = [[NSFileManager defaultManager] displayNameAtPath: [[NSBundle mainBundle] bundlePath]]; 
	NSNumber	*   lastCheckDateNum = [[NSUserDefaults standardUserDefaults] objectForKey: @"UKUpdateChecker:LastCheckDate"];
	NSDate		*   lastCheckDate = nil;
	
	if( doCheck == nil )		// No setting in prefs yet? First launch! Ask!
	{
		if( NSRunAlertPanel( NSLocalizedStringFromTable(@"CHECK_FOR_UPDATES_TITLE", @"UKUpdateChecker", @"Asking whether to check for updates at startup - dialog title"),
							NSLocalizedStringFromTable(@"CHECK_FOR_UPDATES_TEXT", @"UKUpdateChecker", @"Asking whether to check for updates at startup - dialog text"),
							NSLocalizedString(@"Yes",nil), NSLocalizedString(@"No",nil), nil, appName ) == NSAlertDefaultReturn )
			doCheck = [NSNumber numberWithBool:YES];
		else
			doCheck = [NSNumber numberWithBool:NO];
		
		// Save user's preference to prefs file:
		[[NSUserDefaults standardUserDefaults] setObject: doCheck forKey: @"UKUpdateChecker:CheckAtStartup"];
	}
	
	[prefsButton setState: [doCheck boolValue]];	// Update prefs button, if we have one.
	
	// If user wants us to check for updates at startup, do so:
	if( [doCheck boolValue] )
	{
		NSTimeInterval  timeSinceLastCheck;
		
		// Determine how long since last check:
		if( lastCheckDateNum == nil )
			lastCheckDate = [NSDate distantPast];  // If there's no date in prefs, use something guaranteed to be past.
		else
			lastCheckDate = [NSDate dateWithTimeIntervalSinceReferenceDate: [lastCheckDateNum doubleValue]];
		timeSinceLastCheck = -[lastCheckDate timeIntervalSinceNow];
		
		// If last check was more than DAYS_BETWEEN_CHECKS days ago, check again now:
		if( timeSinceLastCheck > (3600 *24 *DAYS_BETWEEN_CHECKS) )
		{
			[NSThread detachNewThreadSelector: @selector(checkForUpdatesAndNotify:) toTarget: self withObject: [NSNumber numberWithBool: NO]];
			[[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithDouble: [NSDate timeIntervalSinceReferenceDate]] forKey: @"UKUpdateChecker:LastCheckDate"];
		}
	}
}
 
 
// -----------------------------------------------------------------------------
//	checkForUpdates:
//		IBAction to hook up to the "check for updates" menu item.
//
//	REVISIONS:
//		2004-03-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(IBAction) checkForUpdates: (id)sender
{
	[NSThread detachNewThreadSelector: @selector(checkForUpdatesAndNotify:) toTarget: self withObject: [NSNumber numberWithBool: YES]];
	// YES means we *also* tell the user about failure, since this is in response to a menu item.
}
 
 
// -----------------------------------------------------------------------------
//	latestVersionsDictionary:
//		Load a dictionary containing info on the latest versions of this app.
//
//		This first tries to get MacPAD-compatible version information. If the
//		developer didn't provide that, it will try the old UKUpdateChecker
//		scheme instead.
//
//	REVISIONS:
//		2004-03-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(NSDictionary*)	latestVersionsDictionary
{
	NSString*   fpath = [[NSBundle mainBundle] pathForResource: UKUpdateCheckerURLFilename ofType: @"url"];
	
	// Do we have a MacPAD.url file?
	if( [[NSFileManager defaultManager] fileExistsAtPath: fpath] )  // MacPAD-compatible!
	{
		NSString*		urlfile = [NSString stringWithContentsOfFile: fpath];
		NSArray*		lines = [urlfile componentsSeparatedByString: @"\n"];
		NSString*		urlString = [lines lastObject];   // Either this is the only line, or the line following [InternetShortcut]
		
		if( [urlString characterAtIndex: [urlString length] -1] == '/'		// Directory path? Append bundle identifier and .plist to get an actual file path to download.
			|| [urlString characterAtIndex: [urlString length] -1] == '=' ) // CGI parameter?
			urlString = [[urlString stringByAppendingString: [[NSBundle mainBundle] bundleIdentifier]] stringByAppendingString: @".plist"];
	
		return [NSDictionary dictionaryWithContentsOfURL: [NSURL URLWithString: urlString]];	// Download info from that URL.
	}
	else	// Old-style UKUpdateChecker stuff:
	{
		NSURL*			versDictURL = [NSURL URLWithString: NSLocalizedString(@"UPDATE_PLIST_URL", @"URL where the plist with the latest version numbers is.")];
		NSDictionary*   allVersionsDict = [NSDictionary dictionaryWithContentsOfURL: versDictURL];
		return [allVersionsDict objectForKey: [[NSBundle mainBundle] bundleIdentifier]];
	}
}
 
 
// -----------------------------------------------------------------------------
//	latestVersionsDictionary:
//		This does the actual update checking. This is called in a new thread
//		usually to make sure the user doesn't have to wait to work with their
//		app until this has succeeded or even worse timed out with an error.
//
//	REVISIONS:
//		2004-10-19	witness	Documented, made to run in another thread,
//							extracted actual notification into method
//							notifyAboutUpdateToNewVersion:.
// -----------------------------------------------------------------------------
 
-(void)		checkForUpdatesAndNotify: (NSNumber*)doNotifyBool
{
	NSAutoreleasePool*	pool = [[NSAutoreleasePool alloc] init];
	BOOL			doNotify = [doNotifyBool boolValue];
	// Load a .plist of application version info from a web URL:
    NSDictionary *  appVersionDict = [self latestVersionsDictionary];
	BOOL			succeeded = NO;
    
    if( appVersionDict != nil )		// We were able to download a dictionary?
	{
		// Extract version number and URL from dictionary:
		NSString *newVersion = [appVersionDict valueForKey: UKUpdateCheckerVersionPlistKey];
        NSString *newUrl = [appVersionDict valueForKey: UKUpdateCheckerURLPlistKey];
        
		if( !newVersion || !newUrl )	// Dictionary doesn't contain new MacPAD stuff? Use old UKUpdateChecker stuff instead.
		{
			newVersion = [appVersionDict valueForKey:UKUpdateCheckerOldVersionPlistKey];
			newUrl = [appVersionDict valueForKey:UKUpdateCheckerOldURLPlistKey];
		}
		
		// Is it current? Then tell the user, or just quietly go on, depending on doNotify:
        if( [newVersion isEqualToString: [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]] )
		{
            if( doNotify )
				[self performSelectorOnMainThread: @selector(notifyAboutUpdateToNewVersion:)
							withObject:[NSDictionary dictionaryWithObjectsAndKeys: nil] waitUntilDone: YES];
			succeeded = YES;
        }
		else if( newVersion != nil )	// If there's an entry for this app:
		{
			// Ask user whether they'd like to open the URL for the new version:
			[self performSelectorOnMainThread: @selector(notifyAboutUpdateToNewVersion:)
						 withObject: [NSDictionary dictionaryWithObjectsAndKeys:
															newVersion, UKUpdateCheckerVersionPlistKey,
															newUrl, UKUpdateCheckerURLPlistKey,
															nil] waitUntilDone: YES];
 
            succeeded = YES;	// Otherwise, it's still a success.
        }
    }
	
	// Failed? File not found, no internet, there is no entry for our app?
	if( !succeeded && doNotify )
		[self performSelectorOnMainThread: @selector(notifyAboutUpdateToNewVersion:)
			withObject: [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool: YES], @"isError", nil] waitUntilDone: YES];
 
	[pool release];
}
 
 
// -----------------------------------------------------------------------------
//	notifyAboutUpdateToNewVersion:
//		This actually tells the user about new updates, and is therefore called
//		on the main thread.
//
//	REVISIONS:
//		2004-10-19	witness	Documented, extracted from checkForUpdatesAndNotify:.
// -----------------------------------------------------------------------------
 
-(void)	notifyAboutUpdateToNewVersion: (NSDictionary*)info
{
	NSString*	appName = [[NSFileManager defaultManager] displayNameAtPath: [[NSBundle mainBundle] bundlePath]];
	NSString*	newVersion = [info objectForKey: UKUpdateCheckerVersionPlistKey];
	NSString*	newUrl = [info objectForKey: UKUpdateCheckerURLPlistKey];
	BOOL		isError = [info objectForKey: @"isError"];
	
	if( newVersion == nil && !isError )
		NSRunAlertPanel(NSLocalizedStringFromTable(@"UP_TO_DATE_TITLE", @"UKUpdateChecker", @"When soft is up-to-date - dialog title"),
				NSLocalizedStringFromTable(@"UP_TO_DATE_TEXT", @"UKUpdateChecker", @"When soft is up-to-date - dialog text"),
				NSLocalizedStringFromTable(@"OK", @"UKUpdateChecker", @""), nil, nil, appName );
	else if( newVersion != nil && !isError )
	{
		int button = NSRunAlertPanel(
				NSLocalizedStringFromTable(@"NEW_VERSION_TITLE", @"UKUpdateChecker", @"A New Version is Available - dialog title"),
				NSLocalizedStringFromTable(@"NEW_VERSION_TEXT", @"UKUpdateChecker", @"A New Version is Available - dialog text"),
				NSLocalizedStringFromTable(@"OK", @"UKUpdateChecker", @""), NSLocalizedStringFromTable(@"Cancel", @"UKUpdateChecker", @""), nil,
				appName, newVersion );
		if( NSOKButton == button )	// Yes?
			[[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:newUrl]];   //Open!
	}
	else
	{
		NSRunAlertPanel( NSLocalizedStringFromTable(@"UPDATE_ERROR_TITLE", @"UKUpdateChecker", @"When update test failed - dialog title"),
						 NSLocalizedStringFromTable(@"UPDATE_ERROR_TEXT", @"UKUpdateChecker", @"When update test failed - dialog text"),
						 @"OK", nil, nil, appName );
	}
}
 
 
// -----------------------------------------------------------------------------
//	takeBoolFromObject:
//		Action for the "check at startup" checkbox in your preferences.
//
//	REVISIONS:
//		2004-10-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(IBAction)		takeBoolFromObject: (id)sender
{
	if( [sender respondsToSelector: @selector(boolValue)] )
		[self setCheckAtStartup: [sender boolValue]];
	else
		[self setCheckAtStartup: [sender state]];
}
 
 
// -----------------------------------------------------------------------------
//	setCheckAtStartup:
//		Mutator for startup check (de)activation.
//
//	REVISIONS:
//		2004-10-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(void)			setCheckAtStartup: (BOOL)shouldCheck
{
	NSNumber*		doCheck = [NSNumber numberWithBool: shouldCheck];
	[[NSUserDefaults standardUserDefaults] setObject: doCheck forKey: @"UKUpdateChecker:CheckAtStartup"];
	
	[prefsButton setState: shouldCheck];
	[[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithDouble: 0] forKey: @"UKUpdateChecker:LastCheckDate"];
}
 
 
// -----------------------------------------------------------------------------
//	checkAtStartup:
//		Accessor for finding out whether this will check at startup.
//
//	REVISIONS:
//		2004-10-19	witness	Documented.
// -----------------------------------------------------------------------------
 
-(BOOL)			checkAtStartup
{
	NSNumber	*   doCheck = [[NSUserDefaults standardUserDefaults] objectForKey: @"UKUpdateChecker:CheckAtStartup"];
	
	if( doCheck )
		return [doCheck boolValue];
	else
		return YES;
}
 
 
@end

This code uses the PclZip Zip File reading code, which is subject to the GNU LGPL. It also uses the GeSHi syntax highlighter, subject to the GPL. Ask if you want this for your own web site, it's free.