diff options
-rw-r--r-- | chrome/app/keystone_glue.h | 80 | ||||
-rw-r--r-- | chrome/app/keystone_glue.m | 180 | ||||
-rw-r--r-- | chrome/app/keystone_glue_unittest.mm | 150 | ||||
-rw-r--r-- | chrome/browser/browser_main_mac.mm | 2 |
4 files changed, 375 insertions, 37 deletions
diff --git a/chrome/app/keystone_glue.h b/chrome/app/keystone_glue.h index e51f16d..d9d0fac 100644 --- a/chrome/app/keystone_glue.h +++ b/chrome/app/keystone_glue.h @@ -7,6 +7,27 @@ #import <Foundation/Foundation.h> +// Objects which request callbacks from KeystoneGlue (e.g. information +// on update availability) should implement this protocol. All callbacks +// require the caller to be spinning in the runloop to happen. +@protocol KeystoneGlueCallbacks + +// Callback when a checkForUpdate completes. +// |latestVersion| may be nil if not returned from the server. +// |latestVersion| is not a localizable string. +- (void)upToDateCheckCompleted:(BOOL)upToDate + latestVersion:(NSString*)latestVersion; + +// Callback when a startUpdate completes. +// |successful| tells if the *check* was successful. This does not +// necessarily mean updates installed successfully. +// |installs| tells the number of updates that installed successfully +// (typically 0 or 1). +- (void)updateCompleted:(BOOL)successful installs:(int)installs; + +@end // protocol KeystoneGlueCallbacks + + // KeystoneGlue is an adapter around the KSRegistration class, allowing it to // be used without linking directly against its containing KeystoneRegistration // framework. This is used in an environment where most builds (such as @@ -19,15 +40,64 @@ // and that it contain a string identifying the update URL to be used by // Keystone. -@interface KeystoneGlue : NSObject +@class KSRegistration; + +@interface KeystoneGlue : NSObject { + @protected + + // Data for Keystone registration + NSString* url_; + NSString* productID_; + NSString* version_; + + // And the Keystone registration itself, with the active timer + KSRegistration* registration_; // strong + NSTimer* timer_; // strong + + // Data for callbacks, all strong. Deallocated (if needed) in a + // NSNotificationCenter callback. + NSObject<KeystoneGlueCallbacks>* startTarget_; + NSObject<KeystoneGlueCallbacks>* checkTarget_; +} + +// Return the default Keystone Glue object. ++ (id)defaultKeystoneGlue; // Load KeystoneRegistration.framework if present, call into it to register // with Keystone, and set up periodic activity pings. -+ (void)registerWithKeystone; +- (void)registerWithKeystone; + +// Check if updates are available. +// upToDateCheckCompleted:: called on target when done. +// Return NO if we could not start the check. +- (BOOL)checkForUpdate:(NSObject<KeystoneGlueCallbacks>*)target; + +// Start an update. +// updateCompleted:: called on target when done. +// This cannot be cancelled. +// Return NO if we could not start the check. +- (BOOL)startUpdate:(NSObject<KeystoneGlueCallbacks>*)target; + +@end // KeystoneGlue + + +@interface KeystoneGlue (ExposedForTesting) + +// Load any params we need for configuring Keystone. +- (void)loadParameters; + +// Load the Keystone registration object. +// Return NO on failure. +- (BOOL)loadKeystoneRegistration; + +- (void)stopTimer; + +// Called when a checkForUpdate: notification completes. +- (void)checkComplete:(NSNotification *)notification; -// Called periodically to announce activity by pinging the Keystone server. -+ (void)markActive:(NSTimer*)timer; +// Called when a startUpdate: notification completes. +- (void)startUpdateComplete:(NSNotification *)notification; -@end +@end // KeystoneGlue (ExposedForTesting) #endif // CHROME_APP_KEYSTONE_GLUE_H_ diff --git a/chrome/app/keystone_glue.m b/chrome/app/keystone_glue.m index 0a01374..56a8e5f 100644 --- a/chrome/app/keystone_glue.m +++ b/chrome/app/keystone_glue.m @@ -4,9 +4,28 @@ #import "keystone_glue.h" +@interface KeystoneGlue(Private) + +// Called periodically to announce activity by pinging the Keystone server. +- (void)markActive:(NSTimer*)timer; + +@end + + // Provide declarations of the Keystone registration bits needed here. From // KSRegistration.h. typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType; + +NSString *KSRegistrationCheckForUpdateNotification = + @"KSRegistrationCheckForUpdateNotification"; +NSString *KSRegistrationStatusKey = @"Status"; +NSString *KSRegistrationVersionKey = @"Version"; + +NSString *KSRegistrationStartUpdateNotification = + @"KSRegistrationStartUpdateNotification"; +NSString *KSUpdateCheckSuccessfulKey = @"CheckSuccessful"; +NSString *KSUpdateCheckSuccessfullyInstalledKey = @"SuccessfullyInstalled"; + @interface KSRegistration : NSObject + (id)registrationWithProductID:(NSString*)productID; - (BOOL)registerWithVersion:(NSString*)version @@ -14,28 +33,65 @@ typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType; existenceCheckerString:(NSString*)xc serverURLString:(NSString*)serverURLString; - (void)setActive; +- (void)checkForUpdate; +- (void)startUpdate; @end + @implementation KeystoneGlue -// TODO(mmentovai): Determine if the app is writable, and don't register for -// updates if not - but keep the periodic activity pings. -+ (void)registerWithKeystone { - // Figure out who we are. - NSBundle* mainBundle = [NSBundle mainBundle]; - NSDictionary* infoDictionary = [mainBundle infoDictionary]; ++ (id)defaultKeystoneGlue { + // TODO(jrg): rename this file to .mm so I can use C++ and + // make this type a base::SingletonObjC<KeystoneGlue>. + static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked + + if (sDefaultKeystoneGlue == nil) { + sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; + [sDefaultKeystoneGlue loadParameters]; + if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { + [sDefaultKeystoneGlue release]; + sDefaultKeystoneGlue = nil; + } + } + return sDefaultKeystoneGlue; +} + +- (void)dealloc { + [url_ release]; + [productID_ release]; + [version_ release]; + [registration_ release]; + [super dealloc]; +} + +- (NSDictionary*)infoDictionary { + return [[NSBundle mainBundle] infoDictionary]; +} + +- (void)loadParameters { + NSDictionary* infoDictionary = [self infoDictionary]; NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; - NSString* bundleIdentifier = [infoDictionary objectForKey:@"KSProductID"]; - if (bundleIdentifier == nil) { - bundleIdentifier = [mainBundle bundleIdentifier]; + NSString* product = [infoDictionary objectForKey:@"KSProductID"]; + if (product == nil) { + product = [[NSBundle mainBundle] bundleIdentifier]; } NSString* version = [infoDictionary objectForKey:@"KSVersion"]; - if (!bundleIdentifier || !url || !version) { + if (!product || !url || !version) { // If parameters required for Keystone are missing, don't use it. return; } + url_ = [url retain]; + productID_ = [product retain]; + version_ = [version retain]; +} + +- (BOOL)loadKeystoneRegistration { + if (!productID_ || !url_ || !version_) + return NO; + // Load the KeystoneRegistration framework bundle. + NSBundle* mainBundle = [NSBundle mainBundle]; NSString* ksrPath = [[mainBundle privateFrameworksPath] stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; @@ -44,36 +100,98 @@ typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType; // Harness the KSRegistration class. Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; - KSRegistration* ksr = [ksrClass registrationWithProductID:bundleIdentifier]; - if (!ksr) { - // Strictly speaking, this isn't necessary, because it's harmless to send - // messages to nil. However, if there really isn't a - // KeystoneRegistration.framework or KSRegistration class, bailing out here - // avoids setting up the timer that will only be able to perform no-ops. - return; - } + KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; + if (!ksr) + return NO; + + registration_ = [ksr retain]; + return YES; +} - // Keystone will asynchronously handle installation and registration as - // needed. - [ksr registerWithVersion:version - existenceCheckerType:kKSPathExistenceChecker - existenceCheckerString:[mainBundle bundlePath] - serverURLString:url]; +- (void)registerWithKeystone { + [registration_ registerWithVersion:version_ + existenceCheckerType:kKSPathExistenceChecker + existenceCheckerString:[[NSBundle mainBundle] bundlePath] + serverURLString:url_]; // Mark an active RIGHT NOW; don't wait an hour for the first one. - [ksr setActive]; + [registration_ setActive]; // Set up hourly activity pings. - [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour - target:self - selector:@selector(markActive:) - userInfo:ksr - repeats:YES]; + timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour + target:self + selector:@selector(markActive:) + userInfo:registration_ + repeats:YES]; +} + +- (void)stopTimer { + [timer_ invalidate]; } -+ (void)markActive:(NSTimer*)timer { +- (void)markActive:(NSTimer*)timer { KSRegistration* ksr = [timer userInfo]; [ksr setActive]; } +- (void)checkComplete:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + BOOL updatesAvailable = [[userInfo objectForKey:KSRegistrationStatusKey] + boolValue]; + NSString *latestVersion = [userInfo objectForKey:KSRegistrationVersionKey]; + + [checkTarget_ upToDateCheckCompleted:updatesAvailable + latestVersion:latestVersion]; + [checkTarget_ release]; + checkTarget_ = nil; + + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:KSRegistrationCheckForUpdateNotification + object:nil]; +} + +- (BOOL)checkForUpdate:(NSObject<KeystoneGlueCallbacks>*)target { + if (registration_ == nil) + return NO; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(checkComplete:) + name:KSRegistrationCheckForUpdateNotification + object:nil]; + checkTarget_ = [target retain]; + [registration_ checkForUpdate]; + return YES; +} + +- (void)startUpdateComplete:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + BOOL checkSuccessful = [[userInfo objectForKey:KSUpdateCheckSuccessfulKey] + boolValue]; + int installs = [[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] + intValue]; + + [startTarget_ updateCompleted:checkSuccessful installs:installs]; + [startTarget_ release]; + startTarget_ = nil; + + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self + name:KSRegistrationStartUpdateNotification + object:nil]; +} + +- (BOOL)startUpdate:(NSObject<KeystoneGlueCallbacks>*)target { + if (registration_ == nil) + return NO; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(startUpdateComplete:) + name:KSRegistrationStartUpdateNotification + object:nil]; + startTarget_ = [target retain]; + [registration_ startUpdate]; + return YES; +} + @end diff --git a/chrome/app/keystone_glue_unittest.mm b/chrome/app/keystone_glue_unittest.mm new file mode 100644 index 0000000..614bdbc --- /dev/null +++ b/chrome/app/keystone_glue_unittest.mm @@ -0,0 +1,150 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Foundation/Foundation.h> +#import <objc/objc-class.h> + +#import "chrome/app/keystone_glue.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface FakeGlueRegistration : NSObject +@end + + +@implementation FakeGlueRegistration +- (void)checkForUpdate { } +- (void)startUpdate { } +@end + + +@interface FakeKeystoneGlue : KeystoneGlue<KeystoneGlueCallbacks> { + @public + BOOL upToDate_; + NSString *latestVersion_; + BOOL successful_; + int installs_; +} +@end + + +@implementation FakeKeystoneGlue + +- (id)init { + if ((self = [super init])) { + // some lies + upToDate_ = YES; + latestVersion_ = @"foo bar"; + successful_ = YES; + installs_ = 1010101010; + } + return self; +} + +// For mocking +- (NSDictionary*)infoDictionary { + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: + @"http://foo.bar", @"KSUpdateURL", + @"com.google.whatever", @"KSProductID", + @"0.0.0.1", @"KSVersion", + nil]; + return dict; +} + +// For mocking +- (BOOL)loadKeystoneRegistration { + return YES; +} + +// Confirms certain things are happy +- (BOOL)dictReadCorrectly { + return ([url_ isEqual:@"http://foo.bar"] && + [productID_ isEqual:@"com.google.whatever"] && + [version_ isEqual:@"0.0.0.1"]); +} + +// Confirms certain things are happy +- (BOOL)hasATimer { + return timer_ ? YES : NO; +} + +- (void)upToDateCheckCompleted:(BOOL)upToDate + latestVersion:(NSString*)latestVersion { + upToDate_ = upToDate; + latestVersion_ = latestVersion; +} + +- (void)updateCompleted:(BOOL)successful installs:(int)installs { + successful_ = successful; + installs_ = installs; +} + +- (void)addFakeRegistration { + registration_ = [[FakeGlueRegistration alloc] init]; +} + +// Confirm we look like callbacks with nil NSNotifications +- (BOOL)confirmCallbacks { + return (!upToDate_ && + (latestVersion_ == nil) && + !successful_ && + (installs_ == 0)); +} + +@end + + +namespace { + +class KeystoneGlueTest : public PlatformTest { +}; + +TEST_F(KeystoneGlueTest, BasicGlobalCreate) { + // Allow creation of a KeystoneGlue by mocking out a few calls + SEL ids = @selector(infoDictionary); + IMP oldInfoImp_ = [[KeystoneGlue class] instanceMethodForSelector:ids]; + IMP newInfoImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:ids]; + Method infoMethod_ = class_getInstanceMethod([KeystoneGlue class], ids); + method_setImplementation(infoMethod_, newInfoImp_); + + SEL lks = @selector(loadKeystoneRegistration); + IMP oldLoadImp_ = [[KeystoneGlue class] instanceMethodForSelector:lks]; + IMP newLoadImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:lks]; + Method loadMethod_ = class_getInstanceMethod([KeystoneGlue class], lks); + method_setImplementation(loadMethod_, newLoadImp_); + + KeystoneGlue *glue = [KeystoneGlue defaultKeystoneGlue]; + ASSERT_TRUE(glue); + + // Fix back up the class to the way we found it. + method_setImplementation(infoMethod_, oldInfoImp_); + method_setImplementation(loadMethod_, oldLoadImp_); +} + +TEST_F(KeystoneGlueTest, BasicUse) { + FakeKeystoneGlue* glue = [[[FakeKeystoneGlue alloc] init] autorelease]; + [glue loadParameters]; + ASSERT_TRUE([glue dictReadCorrectly]); + + // Likely returns NO in the unit test, but call it anyway to make + // sure it doesn't crash. + [glue loadKeystoneRegistration]; + + // Confirm we start up an active timer + [glue registerWithKeystone]; + ASSERT_TRUE([glue hasATimer]); + [glue stopTimer]; + + ASSERT_TRUE(![glue checkForUpdate:glue] && ![glue startUpdate:glue]); + + // Brief exercise of callbacks + [glue addFakeRegistration]; + ASSERT_TRUE([glue checkForUpdate:glue]); + [glue checkComplete:nil]; + ASSERT_TRUE([glue startUpdate:glue]); + [glue startUpdateComplete:nil]; + ASSERT_TRUE([glue confirmCallbacks]); +} + +} // namespace diff --git a/chrome/browser/browser_main_mac.mm b/chrome/browser/browser_main_mac.mm index 458d54d..7a43cdc 100644 --- a/chrome/browser/browser_main_mac.mm +++ b/chrome/browser/browser_main_mac.mm @@ -28,7 +28,7 @@ void WillInitializeMainMessageLoop(const CommandLine & command_line) { // Doesn't need to be in a GOOGLE_CHROME_BUILD since this references // a framework only distributed with Google Chrome. - [KeystoneGlue registerWithKeystone]; + [[KeystoneGlue defaultKeystoneGlue] registerWithKeystone]; // TODO(port): Use of LSUIElement=1 is a temporary fix. The right // answer is to fix the renderer to not use Cocoa. |