summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/app/keystone_glue.h80
-rw-r--r--chrome/app/keystone_glue.m180
-rw-r--r--chrome/app/keystone_glue_unittest.mm150
-rw-r--r--chrome/browser/browser_main_mac.mm2
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.