diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-26 19:58:13 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-10-26 19:58:13 +0000 |
commit | 49aeee5080135d65173fb06f117cbb19f87ae867 (patch) | |
tree | 13ea17786c1cd4c59c833906f3d26ef92a50b091 | |
parent | 8dd8ea73b9fc2571777d841a85f0b27f82530455 (diff) | |
download | chromium_src-49aeee5080135d65173fb06f117cbb19f87ae867.zip chromium_src-49aeee5080135d65173fb06f117cbb19f87ae867.tar.gz chromium_src-49aeee5080135d65173fb06f117cbb19f87ae867.tar.bz2 |
About box auto-update improvements.
The About box now knows how to check to see if updates have been installed
in the background without anyone having to click the Update button in the box.
The About box no longer gets stuck in the "installed" state. Even if an
update has been installed, the About box will still check for new updates when
reopened.
BUG=13165, 20488
TEST=Play with the about box and auto-update a whole lot
Review URL: http://codereview.chromium.org/338012
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@30078 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | base/base.gyp | 1 | ||||
-rw-r--r-- | base/worker_pool_mac.h | 26 | ||||
-rw-r--r-- | base/worker_pool_mac.mm | 45 | ||||
-rw-r--r-- | chrome/app/keystone_glue.h | 90 | ||||
-rw-r--r-- | chrome/app/keystone_glue.mm | 283 | ||||
-rw-r--r-- | chrome/app/keystone_glue_unittest.mm | 76 | ||||
-rw-r--r-- | chrome/browser/app_controller_mac.mm | 23 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller.h | 30 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller.mm | 554 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller_unittest.mm | 74 | ||||
-rw-r--r-- | chrome/browser/cocoa/restart_browser.mm | 2 |
11 files changed, 758 insertions, 446 deletions
diff --git a/base/base.gyp b/base/base.gyp index 6f6790c..d948e84 100644 --- a/base/base.gyp +++ b/base/base.gyp @@ -349,6 +349,7 @@ 'worker_pool.h', 'worker_pool_linux.cc', 'worker_pool_linux.h', + 'worker_pool_mac.h', 'worker_pool_mac.mm', 'worker_pool_win.cc', ], diff --git a/base/worker_pool_mac.h b/base/worker_pool_mac.h new file mode 100644 index 0000000..7c67e45 --- /dev/null +++ b/base/worker_pool_mac.h @@ -0,0 +1,26 @@ +// 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. + +#ifndef BASE_WORKER_POOL_MAC_H_ +#define BASE_WORKER_POOL_MAC_H_ + +#include "base/worker_pool.h" + +#import <Foundation/Foundation.h> + +// WorkerPoolObjC provides an Objective-C interface to the same WorkerPool +// used by the rest of the application. +@interface WorkerPoolObjC : NSObject + +// Returns the underlying NSOperationQueue back end that WorkerPool::PostTask +// would post tasks to. This can be used to add NSOperation subclasses +// directly to the same NSOperationQueue, by calling -[NSOperationQueue +// addOperation:]. Most Objective-C code wishing to dispatch tasks to the +// WorkerPool will find it handy to add an operation of type +// NSInvocationOperation. ++ (NSOperationQueue*)sharedOperationQueue; + +@end // @interface WorkerPoolObjC + +#endif // BASE_WORKER_POOL_MAC_H_ diff --git a/base/worker_pool_mac.mm b/base/worker_pool_mac.mm index 1147110..162509c 100644 --- a/base/worker_pool_mac.mm +++ b/base/worker_pool_mac.mm @@ -2,18 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "base/worker_pool.h" - -#import <Foundation/Foundation.h> +#include "base/worker_pool_mac.h" #include "base/logging.h" +#import "base/scoped_nsautorelease_pool.h" +#include "base/scoped_ptr.h" #import "base/singleton_objc.h" #include "base/task.h" +@implementation WorkerPoolObjC + ++ (NSOperationQueue*)sharedOperationQueue { + return SingletonObjC<NSOperationQueue>::get(); +} + +@end // @implementation WorkerPoolObjC + // TaskOperation adapts Task->Run() for use in an NSOperationQueue. @interface TaskOperation : NSOperation { @private - Task* task_; // (strong) + scoped_ptr<Task> task_; } // Returns an autoreleased instance of TaskOperation. See -initWithTask: for @@ -24,7 +32,7 @@ // this operation will call when executed. - (id)initWithTask:(Task*)task; -@end +@end // @interface TaskOperation @implementation TaskOperation @@ -38,34 +46,45 @@ - (id)initWithTask:(Task*)task { if ((self = [super init])) { - task_ = task; + task_.reset(task); } return self; } - (void)main { - DCHECK(task_) << "-[TaskOperation main] called with no task"; + DCHECK(task_.get()) << "-[TaskOperation main] called with no task"; + if (!task_.get()) { + return; + } + + base::ScopedNSAutoreleasePool autoreleasePool; + task_->Run(); - delete task_; - task_ = NULL; + task_.reset(NULL); } - (void)dealloc { - DCHECK(!task_) << "-[TaskOperation dealloc] called on unused TaskOperation"; - delete task_; + DCHECK(!task_.get()) + << "-[TaskOperation dealloc] called without running task"; + [super dealloc]; } -@end +@end // @implementation TaskOperation bool WorkerPool::PostTask(const tracked_objects::Location& from_here, Task* task, bool task_is_slow) { // Ignore |task_is_slow|, it doesn't map directly to any tunable aspect of // an NSOperation. + DCHECK(task) << "WorkerPool::PostTask called with no task"; + if (!task) { + return false; + } + task->SetBirthPlace(from_here); - NSOperationQueue* operation_queue = SingletonObjC<NSOperationQueue>::get(); + NSOperationQueue* operation_queue = [WorkerPoolObjC sharedOperationQueue]; [operation_queue addOperation:[TaskOperation taskOperationWithTask:task]]; return true; diff --git a/chrome/app/keystone_glue.h b/chrome/app/keystone_glue.h index 9a8eb01..f8d2a53 100644 --- a/chrome/app/keystone_glue.h +++ b/chrome/app/keystone_glue.h @@ -6,27 +6,30 @@ #define CHROME_APP_KEYSTONE_GLUE_H_ #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 - +#import <base/scoped_nsobject.h> + +// Possible outcomes of -checkForUpdate and -installUpdate. A version may +// accompany some of these, but beware: a version is never required. For +// statuses that can be accompanied by a version, the comment indicates what +// version is referenced. +enum AutoupdateStatus { + kAutoupdateCurrent = 0, // version of the running application + kAutoupdateAvailable, // version of the update that is available + kAutoupdateInstalled, // version of the update that was installed + kAutoupdateCheckFailed, // no version + kAutoupdateInstallFailed // no version +}; + +// kAutoupdateStatusNotification is the name of the notification posted when +// -checkForUpdate and -installUpdate complete. This notification will be +// sent with with its sender object set to the KeystoneGlue instance sending +// the notification. Its userInfo dictionary will contain an AutoupdateStatus +// value as an intValue at key kAutoupdateStatusStatus. If a version is +// available (see AutoupdateStatus), it will be present at key +// kAutoupdateStatusVersion. +extern const NSString* const kAutoupdateStatusNotification; +extern const NSString* const kAutoupdateStatusStatus; +extern const NSString* const kAutoupdateStatusVersion; // KeystoneGlue is an adapter around the KSRegistration class, allowing it to // be used without linking directly against its containing KeystoneRegistration @@ -55,10 +58,11 @@ KSRegistration* registration_; // strong NSTimer* timer_; // strong - // Data for callbacks, all strong. Deallocated (if needed) in a - // NSNotificationCenter callback. - NSObject<KeystoneGlueCallbacks>* startTarget_; - NSObject<KeystoneGlueCallbacks>* checkTarget_; + // The most recent kAutoupdateStatusNotification notification posted. + scoped_nsobject<NSNotification> recentNotification_; + + // YES if an update was ever successfully installed by -installUpdate. + BOOL updateSuccessfullyInstalled_; } // Return the default Keystone Glue object. @@ -68,21 +72,27 @@ // with Keystone, and set up periodic activity pings. - (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; +// -checkForUpdate launches a check for updates, and -installUpdate begins +// installing an available update. For each, status will be communicated via +// a kAutoupdateStatusNotification notification, and will also be available +// through -recentUpdateStatus. +- (void)checkForUpdate; +- (void)installUpdate; + +// Accessor for recentNotification_. Returns an autoreleased NSNotification. +- (NSNotification*)recentNotification; -// 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; +// Clears the saved recentNotification_. +- (void)clearRecentNotification; -@end // KeystoneGlue +@end // @interface KeystoneGlue +@interface KeystoneGlue(ExposedForTesting) -@interface KeystoneGlue (ExposedForTesting) +// Release the shared instance. Use this in tests to reset the shared +// instance in case strange things are done to it for testing purposes. Never +// call this from non-test code. ++ (void)releaseDefaultKeystoneGlue; // Load any params we need for configuring Keystone. - (void)loadParameters; @@ -94,11 +104,11 @@ - (void)stopTimer; // Called when a checkForUpdate: notification completes. -- (void)checkComplete:(NSNotification *)notification; +- (void)checkForUpdateComplete:(NSNotification*)notification; -// Called when a startUpdate: notification completes. -- (void)startUpdateComplete:(NSNotification *)notification; +// Called when an installUpdate: notification completes. +- (void)installUpdateComplete:(NSNotification*)notification; -@end // KeystoneGlue (ExposedForTesting) +@end // @interface KeystoneGlue(ExposedForTesting) #endif // CHROME_APP_KEYSTONE_GLUE_H_ diff --git a/chrome/app/keystone_glue.mm b/chrome/app/keystone_glue.mm index 736972c..689cfdb 100644 --- a/chrome/app/keystone_glue.mm +++ b/chrome/app/keystone_glue.mm @@ -2,16 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "base/mac_util.h" #import "chrome/app/keystone_glue.h" -@interface KeystoneGlue(Private) - -// Called periodically to announce activity by pinging the Keystone server. -- (void)markActive:(NSTimer*)timer; - -@end - +#include "base/logging.h" +#include "base/mac_util.h" +#import "base/worker_pool_mac.h" +#include "chrome/common/chrome_constants.h" // Provide declarations of the Keystone registration bits needed here. From // KSRegistration.h. @@ -31,7 +27,9 @@ NSString *KSRegistrationRemoveExistingTag = @""; #define KSRegistrationPreserveExistingTag nil @interface KSRegistration : NSObject + + (id)registrationWithProductID:(NSString*)productID; + // Older API - (BOOL)registerWithVersion:(NSString*)version existenceCheckerType:(KSExistenceCheckerType)xctype @@ -43,20 +41,65 @@ NSString *KSRegistrationRemoveExistingTag = @""; existenceCheckerString:(NSString*)xc serverURLString:(NSString*)serverURLString preserveTTToken:(BOOL)preserveToken - tag:(NSString *)tag; + tag:(NSString*)tag; + - (void)setActive; - (void)checkForUpdate; - (void)startUpdate; -@end +@end // @interface KSRegistration + +@interface KeystoneGlue(Private) + +// Called periodically to announce activity by pinging the Keystone server. +- (void)markActive:(NSTimer*)timer; + +// Called when an update check or update installation is complete. Posts the +// kAutoupdateStatusNotification notification to the default notification +// center. +- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; + +// These three methods are used to determine the version of the application +// currently installed on disk, compare that to the currently-running version, +// decide whether any updates have been installed, and call +// -updateStatus:version:. +// +// In order to check the version on disk, the installed application's +// Info.plist dictionary must be read; in order to see changes as updates are +// applied, the dictionary must be read each time, bypassing any caches such +// as the one that NSBundle might be maintaining. Reading files can be a +// blocking operation, and blocking operations are to be avoided on the main +// thread. I'm not quite sure what jank means, but I bet that a blocked main +// thread would cause some of it. +// +// -determineUpdateStatusAsync is called on the main thread to initiate the +// operation. It performs initial set-up work that must be done on the main +// thread and arranges for -determineUpdateStatusAtPath: to be called on a +// work queue thread managed by NSOperationQueue. +// -determineUpdateStatusAtPath: then reads the Info.plist, gets the version +// from the CFBundleShortVersionString key, and performs +// -determineUpdateStatusForVersion: on the main thread. +// -determineUpdateStatusForVersion: does the actual comparison of the version +// on disk with the running version and calls -updateStatus:version: with the +// results of its analysis. +- (void)determineUpdateStatusAsync; +- (void)determineUpdateStatusAtPath:(NSString*)appPath; +- (void)determineUpdateStatusForVersion:(NSString*)version; + +@end // @interface KeystoneGlue(Private) + +const NSString* const kAutoupdateStatusNotification = + @"AutoupdateStatusNotification"; +const NSString* const kAutoupdateStatusStatus = @"status"; +const NSString* const kAutoupdateStatusVersion = @"version"; @implementation KeystoneGlue -+ (id)defaultKeystoneGlue { - // TODO(jrg): use base::SingletonObjC<KeystoneGlue> - static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked +// TODO(jrg): use base::SingletonObjC<KeystoneGlue> +static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked - if (sDefaultKeystoneGlue == nil) { ++ (id)defaultKeystoneGlue { + if (!sDefaultKeystoneGlue) { sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; [sDefaultKeystoneGlue loadParameters]; if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { @@ -67,6 +110,29 @@ NSString *KSRegistrationRemoveExistingTag = @""; return sDefaultKeystoneGlue; } ++ (void)releaseDefaultKeystoneGlue { + [sDefaultKeystoneGlue release]; + sDefaultKeystoneGlue = nil; +} + +- (id)init { + if ((self = [super init])) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + + [center addObserver:self + selector:@selector(checkForUpdateComplete:) + name:KSRegistrationCheckForUpdateNotification + object:nil]; + + [center addObserver:self + selector:@selector(installUpdateComplete:) + name:KSRegistrationStartUpdateNotification + object:nil]; + } + + return self; +} + - (void)dealloc { [url_ release]; [productID_ release]; @@ -174,64 +240,155 @@ NSString *KSRegistrationRemoveExistingTag = @""; [ksr setActive]; } -- (void)checkComplete:(NSNotification *)notification { - NSDictionary *userInfo = [notification userInfo]; - BOOL updatesAvailable = [[userInfo objectForKey:KSRegistrationStatusKey] - boolValue]; - NSString *latestVersion = [userInfo objectForKey:KSRegistrationVersionKey]; +- (void)checkForUpdate { + if (!registration_) { + [self updateStatus:kAutoupdateCheckFailed version:nil]; + return; + } - [checkTarget_ upToDateCheckCompleted:updatesAvailable - latestVersion:latestVersion]; - [checkTarget_ release]; - checkTarget_ = nil; + [registration_ checkForUpdate]; - NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; - [center removeObserver:self - name:KSRegistrationCheckForUpdateNotification - object:nil]; + // Upon completion, KSRegistrationCheckForUpdateNotification will be posted, + // and -checkForUpdateComplete: will be called. } -- (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)checkForUpdateComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + BOOL updatesAvailable = + [[userInfo objectForKey:KSRegistrationStatusKey] boolValue]; + + if (updatesAvailable) { + // If an update is known to be available, go straight to + // -updateStatus:version:. It doesn't matter what's currently on disk. + NSString* version = [userInfo objectForKey:KSRegistrationVersionKey]; + [self updateStatus:kAutoupdateAvailable version:version]; + } else { + // If no updates are available, check what's on disk, because an update + // may have already been installed. This check happens on another thread, + // and -updateStatus:version: will be called on the main thread when done. + [self determineUpdateStatusAsync]; + } } -- (void)startUpdateComplete:(NSNotification *)notification { - NSDictionary *userInfo = [notification userInfo]; - BOOL checkSuccessful = [[userInfo objectForKey:KSUpdateCheckSuccessfulKey] - boolValue]; - int installs = [[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] - intValue]; +- (void)installUpdate { + if (!registration_) { + [self updateStatus:kAutoupdateInstallFailed version:nil]; + return; + } - [startTarget_ updateCompleted:checkSuccessful installs:installs]; - [startTarget_ release]; - startTarget_ = nil; + [registration_ startUpdate]; - NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; - [center removeObserver:self - name:KSRegistrationStartUpdateNotification - object:nil]; + // Upon completion, KSRegistrationStartUpdateNotification will be posted, + // and -installUpdateComplete: will be called. } -- (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; +- (void)installUpdateComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + BOOL checkSuccessful = + [[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue]; + int installs = + [[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] intValue]; + + if (!checkSuccessful || !installs) { + [self updateStatus:kAutoupdateInstallFailed version:nil]; + } else { + updateSuccessfullyInstalled_ = YES; + + // Nothing in the notification dictionary reports the version that was + // installed. Figure it out based on what's on disk. + [self determineUpdateStatusAsync]; + } +} + +// Runs on the main thread. +- (void)determineUpdateStatusAsync { + // NSBundle is not documented as being thread-safe. Do NSBundle operations + // on the main thread before jumping over to a NSOperationQueue-managed + // thread to do blocking file input. + DCHECK([NSThread isMainThread]); + + SEL selector = @selector(determineUpdateStatusAtPath:); + NSString* appPath = [[NSBundle mainBundle] bundlePath]; + NSInvocationOperation* operation = + [[[NSInvocationOperation alloc] initWithTarget:self + selector:selector + object:appPath] autorelease]; + + NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue]; + [operationQueue addOperation:operation]; +} + +// Runs on a thread managed by NSOperationQueue. +- (void)determineUpdateStatusAtPath:(NSString*)appPath { + DCHECK(![NSThread isMainThread]); + + NSString* appInfoPlistPath = + [[appPath stringByAppendingPathComponent:@"Contents"] + stringByAppendingPathComponent:@"Info.plist"]; + NSDictionary* infoPlist = + [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; + NSString* version = [infoPlist objectForKey:@"CFBundleShortVersionString"]; + + [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) + withObject:version + waitUntilDone:NO]; +} + +// Runs on the main thread. +- (void)determineUpdateStatusForVersion:(NSString*)version { + DCHECK([NSThread isMainThread]); + + AutoupdateStatus status; + if (updateSuccessfullyInstalled_) { + // If an update was successfully installed and this object saw it happen, + // then don't even bother comparing versions. + status = kAutoupdateInstalled; + } else { + NSString* currentVersion = + [NSString stringWithUTF8String:chrome::kChromeVersion]; + if (!version) { + // If the version on disk could not be determined, assume that + // whatever's running is current. + version = currentVersion; + status = kAutoupdateCurrent; + } else if ([version isEqualToString:currentVersion]) { + status = kAutoupdateCurrent; + } else { + // If the version on disk doesn't match what's currently running, an + // update must have been applied in the background, without this app's + // direct participation. Leave updateSuccessfullyInstalled_ alone + // because there's no direct knowledge of what actually happened. + status = kAutoupdateInstalled; + } + } + + [self updateStatus:status version:version]; +} + +- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { + NSNumber* statusNumber = [NSNumber numberWithInt:status]; + NSMutableDictionary* dictionary = + [NSMutableDictionary dictionaryWithObject:statusNumber + forKey:kAutoupdateStatusStatus]; + if (version) { + [dictionary setObject:version forKey:kAutoupdateStatusVersion]; + } + + NSNotification* notification = + [NSNotification notificationWithName:kAutoupdateStatusNotification + object:self + userInfo:dictionary]; + recentNotification_.reset([notification retain]); + + [[NSNotificationCenter defaultCenter] postNotification:notification]; +} + +- (NSNotification*)recentNotification { + return [[recentNotification_ retain] autorelease]; +} + +- (void)clearRecentNotification { + recentNotification_.reset(nil); } -@end +@end // @implementation KeystoneGlue diff --git a/chrome/app/keystone_glue_unittest.mm b/chrome/app/keystone_glue_unittest.mm index 614bdbc..90a2f40 100644 --- a/chrome/app/keystone_glue_unittest.mm +++ b/chrome/app/keystone_glue_unittest.mm @@ -14,18 +14,38 @@ @implementation FakeGlueRegistration -- (void)checkForUpdate { } -- (void)startUpdate { } + +// Send the notifications that a real KeystoneGlue object would send. + +- (void)checkForUpdate { + NSNumber* yesNumber = [NSNumber numberWithBool:YES]; + NSString* statusKey = @"Status"; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:yesNumber + forKey:statusKey]; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationCheckForUpdateNotification" + object:nil + userInfo:dictionary]; +} + +- (void)startUpdate { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationStartUpdateNotification" + object:nil]; +} + @end -@interface FakeKeystoneGlue : KeystoneGlue<KeystoneGlueCallbacks> { +@interface FakeKeystoneGlue : KeystoneGlue { @public BOOL upToDate_; NSString *latestVersion_; BOOL successful_; int installs_; } + +- (void)fakeAboutWindowCallback:(NSNotification*)notification; @end @@ -38,10 +58,23 @@ latestVersion_ = @"foo bar"; successful_ = YES; installs_ = 1010101010; + + // Set up an observer that takes the notification that the About window + // listens for. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(fakeAboutWindowCallback:) + name:kAutoupdateStatusNotification + object:nil]; } return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + // For mocking - (NSDictionary*)infoDictionary { NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: @@ -69,21 +102,24 @@ 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]; } +- (void)fakeAboutWindowCallback:(NSNotification*)notification { + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + if (status == kAutoupdateAvailable) { + upToDate_ = NO; + latestVersion_ = [dictionary objectForKey:kAutoupdateStatusVersion]; + } else if (status == kAutoupdateInstallFailed) { + successful_ = NO; + installs_ = 0; + } +} + // Confirm we look like callbacks with nil NSNotifications - (BOOL)confirmCallbacks { return (!upToDate_ && @@ -114,12 +150,16 @@ TEST_F(KeystoneGlueTest, BasicGlobalCreate) { Method loadMethod_ = class_getInstanceMethod([KeystoneGlue class], lks); method_setImplementation(loadMethod_, newLoadImp_); + // Dump any existing KeystoneGlue shared instance so that a new one can be + // created with the mocked methods. + [KeystoneGlue releaseDefaultKeystoneGlue]; 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_); + [KeystoneGlue releaseDefaultKeystoneGlue]; } TEST_F(KeystoneGlueTest, BasicUse) { @@ -136,14 +176,10 @@ TEST_F(KeystoneGlueTest, BasicUse) { 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]; + [glue checkForUpdate]; + [glue installUpdate]; ASSERT_TRUE([glue confirmCallbacks]); } diff --git a/chrome/browser/app_controller_mac.mm b/chrome/browser/app_controller_mac.mm index 434f61e..db96e05 100644 --- a/chrome/browser/app_controller_mac.mm +++ b/chrome/browser/app_controller_mac.mm @@ -665,26 +665,17 @@ // window controller. - (void)aboutWindowClosed:(NSNotification*)notify { [[NSNotificationCenter defaultCenter] - removeObserver:self - name:kUserClosedAboutNotification - object:aboutController_.get()]; - aboutController_.reset(NULL); + removeObserver:self + name:kUserClosedAboutNotification + object:aboutController_.get()]; + aboutController_.reset(nil); } - (IBAction)orderFrontStandardAboutPanel:(id)sender { - // Otherwise bring up our special dialog (e.g. with an auto-update button). if (!aboutController_) { aboutController_.reset([[AboutWindowController alloc] - initWithProfile:[self defaultProfile]]); - if (!aboutController_) { - // If we get here something is wacky. I managed to do it when - // testing by explicitly forcing an auto-update to an older - // version then trying to open the about box again (missing - // nib). This shouldn't be possible in general but let's try - // hard to not do nothing. - [NSApp orderFrontStandardAboutPanel:sender]; - return; - } + initWithProfile:[self defaultProfile]]); + // Watch for a notification of when it goes away so that we can destroy // the controller. [[NSNotificationCenter defaultCenter] @@ -693,8 +684,10 @@ name:kUserClosedAboutNotification object:aboutController_.get()]; } + if (![[aboutController_ window] isVisible]) [[aboutController_ window] center]; + [aboutController_ showWindow:self]; } diff --git a/chrome/browser/cocoa/about_window_controller.h b/chrome/browser/cocoa/about_window_controller.h index d3117bb..23e76e0 100644 --- a/chrome/browser/cocoa/about_window_controller.h +++ b/chrome/browser/cocoa/about_window_controller.h @@ -5,22 +5,17 @@ #ifndef CHROME_BROWSER_COCOA_ABOUT_WINDOW_CONTROLLER_H_ #define CHROME_BROWSER_COCOA_ABOUT_WINDOW_CONTROLLER_H_ -#import <Cocoa/Cocoa.h> -#include "base/scoped_nsobject.h" -#import "chrome/app/keystone_glue.h" +#import <AppKit/AppKit.h> @class BackgroundTileView; class Profile; -// Returns an NSAttributedString that contains the locale specific legal text. -NSAttributedString* BuildAboutWindowLegalTextBlock(); +// kUserClosedAboutNotification is the name of the notification posted when +// the About window is closed. +extern const NSString* const kUserClosedAboutNotification; -// A window controller that handles the branded (Chrome.app) about -// window. The branded about window has a few features beyond the -// standard Cocoa about panel. For example, opening the about window -// will check to see if this version is current and tell the user. -// There is also an "update me now" button with a progress spinner. -@interface AboutWindowController : NSWindowController<KeystoneGlueCallbacks> { +// A window controller that handles the About box. +@interface AboutWindowController : NSWindowController { @private IBOutlet NSTextField* version_; IBOutlet BackgroundTileView* backgroundView_; @@ -35,9 +30,6 @@ NSAttributedString* BuildAboutWindowLegalTextBlock(); BOOL updateTriggered_; // Has an update ever been triggered? Profile* profile_; // Weak, probably the default profile. - - // The version we got told about by Keystone - scoped_nsobject<NSString> newVersionAvailable_; } // Initialize the controller with the given profile, but does not show it. @@ -47,17 +39,17 @@ NSAttributedString* BuildAboutWindowLegalTextBlock(); // Trigger an update right now, as initiated by a button. - (IBAction)updateNow:(id)sender; -@end +@end // @interface AboutWindowController +@interface AboutWindowController(JustForTesting) -@interface AboutWindowController (JustForTesting) - (NSTextView*)legalText; - (NSButton*)updateButton; - (NSTextField*)updateText; -@end +// Returns an NSAttributedString that contains locale-specific legal text. ++ (NSAttributedString*)legalTextBlock; -// NSNotification sent when the about window is closed. -extern NSString* const kUserClosedAboutNotification; +@end // @interface AboutWindowController(JustForTesting) #endif // CHROME_BROWSER_COCOA_ABOUT_WINDOW_CONTROLLER_H_ diff --git a/chrome/browser/cocoa/about_window_controller.mm b/chrome/browser/cocoa/about_window_controller.mm index 936dc8a..1fb2496 100644 --- a/chrome/browser/cocoa/about_window_controller.mm +++ b/chrome/browser/cocoa/about_window_controller.mm @@ -4,7 +4,6 @@ #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" -#include "base/file_version_info.h" #include "base/logging.h" #include "base/mac_util.h" #include "base/string_util.h" @@ -20,23 +19,8 @@ #include "grit/locale_settings.h" #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" -NSString* const kUserClosedAboutNotification = - @"kUserClosedAboutNotification"; - -@interface AboutWindowController (Private) -- (KeystoneGlue*)defaultKeystoneGlue; -- (void)startProgressMessageID:(uint32_t)messageID; -- (void)startProgressMessage:(NSString*)message; -- (void)stopProgressMessage:(NSString*)message imageID:(uint32_t)imageID; -@end - namespace { -// Keystone doesn't give us error numbers on some results, so we just make -// our own for reporting in the UI. -const int kUpdateInstallFailed = 128; -const int kUpdateInstallFailedToStart = 129; - void AttributedStringAppendString(NSMutableAttributedString* attr_str, NSString* str) { // You might think doing [[attr_str mutableString] appendString:str] would @@ -49,7 +33,6 @@ void AttributedStringAppendString(NSMutableAttributedString* attr_str, void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, NSString* text, NSString* url_str) { - // Figure out the range of the text we're adding and add the text. NSRange range = NSMakeRange([attr_str length], [text length]); AttributedStringAppendString(attr_str, text); @@ -71,7 +54,287 @@ void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, } // namespace -NSAttributedString* BuildAboutWindowLegalTextBlock() { +@interface AboutWindowController(Private) + +// Launches a check for available updates. +- (void)checkForUpdate; + +// Notification callback, called with the status of asynchronous +// -checkForUpdate and -updateNow: operations. +- (void)updateStatus:(NSNotification*)notification; + +// These methods maintain the image (or throbber) and text displayed regarding +// update status. -setUpdateThrobberMessage: starts a progress throbber and +// sets the text. -setUpdateImage:message: displays an image and sets the +// text. +- (void)setUpdateThrobberMessage:(NSString*)message; +- (void)setUpdateImage:(int)imageID message:(NSString*)message; + +@end // @interface AboutWindowController(Private) + +const NSString* const kUserClosedAboutNotification = + @"UserClosedAboutNotification"; + +@implementation AboutWindowController + +- (id)initWithProfile:(Profile*)profile { + NSString* nibPath = [mac_util::MainAppBundle() pathForResource:@"About" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + profile_ = profile; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(updateStatus:) + name:kAutoupdateStatusNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (void)awakeFromNib { + NSBundle* bundle = mac_util::MainAppBundle(); + NSString* chromeVersion = + [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + +#if defined(GOOGLE_CHROME_BUILD) + NSString* version = chromeVersion; +#else // GOOGLE_CHROME_BUILD + // The format string is not localized, but this is how the displayed version + // is built on Windows too. + NSString* svnRevision = [bundle objectForInfoDictionaryKey:@"SVNRevision"]; + NSString* version = + [NSString stringWithFormat:@"%@ (%@)", chromeVersion, svnRevision]; +#endif // GOOGLE_CHROME_BUILD + + [version_ setStringValue:version]; + + // Put the two images into the UI. + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + NSImage* backgroundImage = rb.GetNSImageNamed(IDR_ABOUT_BACKGROUND_COLOR); + DCHECK(backgroundImage); + [backgroundView_ setTileImage:backgroundImage]; + NSImage* logoImage = rb.GetNSImageNamed(IDR_ABOUT_BACKGROUND); + DCHECK(logoImage); + [logoView_ setImage:logoImage]; + + [[legalText_ textStorage] setAttributedString:[[self class] legalTextBlock]]; + + // Resize our text view now so that the |updateShift| below is set + // correctly. The About box has its controls manually positioned, so we need + // to calculate how much larger (or smaller) our text box is and store that + // difference in |legalShift|. We do something similar with |updateShift| + // below, which is either 0, or the amount of space to offset the window size + // because the view that contains the update button has been removed because + // this build doesn't have KeyStone. + NSRect oldLegalRect = [legalBlock_ frame]; + [legalText_ sizeToFit]; + NSRect newRect = oldLegalRect; + newRect.size.height = [legalText_ frame].size.height; + [legalBlock_ setFrame:newRect]; + CGFloat legalShift = newRect.size.height - oldLegalRect.size.height; + + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + CGFloat updateShift; + if (keystoneGlue) { + NSNotification* recentNotification = [keystoneGlue recentNotification]; + NSDictionary* recentDictionary = [recentNotification userInfo]; + AutoupdateStatus recentStatus = static_cast<AutoupdateStatus>( + [[recentDictionary objectForKey:kAutoupdateStatusStatus] intValue]); + if (recentStatus == kAutoupdateInstallFailed) { + // A previous update attempt was unsuccessful, but no About box was + // around to report status. Use the saved notification to set up the + // About box with the error message, and to allow another chance to + // install the update. + [self updateStatus:recentNotification]; + } else { + [self checkForUpdate]; + } + + updateShift = 0.0; + } else { + // Hide all the update UI + [updateBlock_ setHidden:YES]; + + // Figure out the amount being removed by taking out the update block + // and its spacing. + updateShift = NSMinY([legalBlock_ frame]) - NSMinY([updateBlock_ frame]); + + NSRect legalFrame = [legalBlock_ frame]; + legalFrame.origin.y -= updateShift; + [legalBlock_ setFrame:legalFrame]; + } + + NSRect backgroundFrame = [backgroundView_ frame]; + backgroundFrame.origin.y += legalShift - updateShift; + [backgroundView_ setFrame:backgroundFrame]; + + NSSize windowDelta = NSMakeSize(0.0, legalShift - updateShift); + + [GTMUILocalizerAndLayoutTweaker + resizeWindowWithoutAutoResizingSubViews:[self window] + delta:windowDelta]; +} + +- (void)windowWillClose:(NSNotification*)notification { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:kUserClosedAboutNotification object:self]; +} + +- (void)setUpdateThrobberMessage:(NSString*)message { + [updateStatusIndicator_ setHidden:YES]; + + [spinner_ setHidden:NO]; + [spinner_ startAnimation:self]; + + [updateText_ setStringValue:message]; +} + +- (void)setUpdateImage:(int)imageID message:(NSString*)message { + [spinner_ stopAnimation:self]; + [spinner_ setHidden:YES]; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + NSImage* statusImage = rb.GetNSImageNamed(imageID); + DCHECK(statusImage); + [updateStatusIndicator_ setImage:statusImage]; + [updateStatusIndicator_ setHidden:NO]; + + [updateText_ setStringValue:message]; +} + +- (void)checkForUpdate { + [self setUpdateThrobberMessage: + l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED)]; + [[KeystoneGlue defaultKeystoneGlue] checkForUpdate]; + + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called. +} + +- (IBAction)updateNow:(id)sender { + updateTriggered_ = YES; + + // Don't let someone click "Update Now" twice! + [updateNowButton_ setEnabled:NO]; + [self setUpdateThrobberMessage: + l10n_util::GetNSStringWithFixup(IDS_UPGRADE_STARTED)]; + [[KeystoneGlue defaultKeystoneGlue] installUpdate]; + + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called. +} + +- (void)updateStatus:(NSNotification*)notification { + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + // Don't assume |version| is a real string. It may be nil. + NSString* version = [dictionary objectForKey:kAutoupdateStatusVersion]; + + int imageID; + NSString* message; + + switch (status) { + case kAutoupdateCurrent: + imageID = IDR_UPDATE_UPTODATE; + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_ALREADY_UP_TO_DATE, + l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), + base::SysNSStringToUTF16(version)); + + break; + + case kAutoupdateAvailable: + imageID = IDR_UPDATE_AVAILABLE; + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_AVAILABLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); + [updateNowButton_ setEnabled:YES]; + + break; + + case kAutoupdateInstalled: + { + imageID = IDR_UPDATE_UPTODATE; + string16 productName = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); + if (version) { + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_SUCCESSFUL, + productName, + base::SysNSStringToUTF16(version)); + } else { + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_SUCCESSFUL_NOVERSION, productName); + } + + // TODO(mark): Turn the button in the dialog into a restart button + // instead of springing this sheet or dialog. + NSWindow* window = [self window]; + NSWindow* restartDialogParent = [window isVisible] ? window : nil; + restart_browser::RequestRestart(restartDialogParent); + } + + break; + + case kAutoupdateInstallFailed: + // Allow another chance. + [updateNowButton_ setEnabled:YES]; + + // Fall through. + + case kAutoupdateCheckFailed: + // TODO(mark): Keystone doesn't currently indicate when a check for + // updates failed. Fix that. + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); + + break; + + default: + NOTREACHED(); + return; + } + + [self setUpdateImage:imageID message:message]; + + // Since the update status is now displayed in an About box, the saved state + // can be cleared. If the About box is closed and then reopened, this will + // let it start out with a clean slate and not be affected by past failures. + [[KeystoneGlue defaultKeystoneGlue] clearRecentNotification]; +} + +- (BOOL)textView:(NSTextView *)aTextView + clickedOnLink:(id)link + atIndex:(NSUInteger)charIndex { + // We always create a new window, so there's no need to try to re-use + // an existing one just to pass in the NEW_WINDOW disposition. + Browser* browser = Browser::Create(profile_); + if (browser) { + browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_WINDOW, + PageTransition::LINK); + } + return YES; +} + +- (NSTextView*)legalText { + return legalText_; +} + +- (NSButton*)updateButton { + return updateNowButton_; +} + +- (NSTextField*)updateText { + return updateText_; +} + ++ (NSAttributedString*)legalTextBlock { // Windows builds this up in a very complex way, we're just trying to model // it the best we can to get all the information in (they actually do it // but created Labels and Links that they carefully place to make it appear @@ -82,7 +345,8 @@ NSAttributedString* BuildAboutWindowLegalTextBlock() { [[[NSMutableAttributedString alloc] init] autorelease]; [legal_block beginEditing]; - NSString* copyright = l10n_util::GetNSString(IDS_ABOUT_VERSION_COPYRIGHT); + NSString* copyright = + l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_COPYRIGHT); AttributedStringAppendString(legal_block, copyright); // These are the markers directly in IDS_ABOUT_VERSION_LICENSE @@ -97,7 +361,8 @@ NSAttributedString* BuildAboutWindowLegalTextBlock() { // Now fetch the license string and deal with the markers - NSString* license = l10n_util::GetNSString(IDS_ABOUT_VERSION_LICENSE); + NSString* license = + l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_LICENSE); NSRange begin_chr = [license rangeOfString:kBeginLinkChr]; NSRange begin_oss = [license rangeOfString:kBeginLinkOss]; @@ -166,7 +431,8 @@ NSAttributedString* BuildAboutWindowLegalTextBlock() { &url_offsets); DCHECK_EQ(url_offsets.size(), 1U); NSString* about_terms = base::SysWideToNSString(w_about_terms); - NSString* terms_link_text = l10n_util::GetNSString(IDS_TERMS_OF_SERVICE); + NSString* terms_link_text = + l10n_util::GetNSStringWithFixup(IDS_TERMS_OF_SERVICE); AttributedStringAppendString(legal_block, @"\n\n"); sub_str = [about_terms substringToIndex:url_offsets[0]]; @@ -174,7 +440,7 @@ NSAttributedString* BuildAboutWindowLegalTextBlock() { AttributedStringAppendHyperlink(legal_block, terms_link_text, kTOS); sub_str = [about_terms substringFromIndex:url_offsets[0]]; AttributedStringAppendString(legal_block, sub_str); -#endif // defined(GOOGLE_CHROME_BUILD) +#endif // GOOGLE_CHROME_BUILD // We need to explicitly select Lucida Grande because once we click on // the NSTextView, it changes to Helvetica 12 otherwise. @@ -187,246 +453,4 @@ NSAttributedString* BuildAboutWindowLegalTextBlock() { return legal_block; } -@implementation AboutWindowController - -- (id)initWithProfile:(Profile*)profile { - NSString* nibpath = [mac_util::MainAppBundle() pathForResource:@"About" - ofType:@"nib"]; - self = [super initWithWindowNibPath:nibpath owner:self]; - if (self) { - profile_ = profile; - } - return self; -} - -- (void)awakeFromNib { - // Set our current version. - scoped_ptr<FileVersionInfo> version_info( - FileVersionInfo::CreateFileVersionInfoForCurrentModule()); - std::wstring version(version_info->product_version()); -#if !defined(GOOGLE_CHROME_BUILD) - // Yes, Windows does this raw since it is only in Chromium builds - // src/chrome/browser/views/about_chrome_view.cc AboutChromeView::Init() - version += L" ("; - version += version_info->last_change(); - version += L")"; -#endif - NSString* nsversion = base::SysWideToNSString(version); - [version_ setStringValue:nsversion]; - - // Put the two images into the ui - ResourceBundle& rb = ResourceBundle::GetSharedInstance(); - NSImage* backgroundImage = rb.GetNSImageNamed(IDR_ABOUT_BACKGROUND_COLOR); - DCHECK(backgroundImage); - [backgroundView_ setTileImage:backgroundImage]; - NSImage* logoImage = rb.GetNSImageNamed(IDR_ABOUT_BACKGROUND); - DCHECK(logoImage); - [logoView_ setImage:logoImage]; - - [[legalText_ textStorage] - setAttributedString:BuildAboutWindowLegalTextBlock()]; - - // Resize our text view now so that the |updateShift| below is set - // correctly. The about box has its controls manually positioned, so we need - // to calculate how much larger (or smaller) our text box is and store that - // difference in |legalShift|. We do something similar with |updateShift| - // below, which is either 0, or the amount of space to offset the window size - // because the view that contains the update button has been removed because - // this build doesn't have KeyStone. - NSRect oldLegalRect = [legalBlock_ frame]; - [legalText_ sizeToFit]; - NSRect newRect = oldLegalRect; - newRect.size.height = [legalText_ frame].size.height; - [legalBlock_ setFrame:newRect]; - CGFloat legalShift = newRect.size.height - oldLegalRect.size.height; - - KeystoneGlue* keystone = [self defaultKeystoneGlue]; - CGFloat updateShift = 0.0; - if (keystone) { - // Initiate an update check. - if ([keystone checkForUpdate:self]) { - [self startProgressMessageID:IDS_UPGRADE_CHECK_STARTED]; - } - } else { - // Hide all the update UI - [updateBlock_ setHidden:YES]; - // Figure out the amount we're removing by taking about the update block - // (and it's spacing). - updateShift = NSMinY([legalBlock_ frame]) - NSMinY([updateBlock_ frame]); - } - - // Adjust the sizes/locations. - NSRect rect = [legalBlock_ frame]; - rect.origin.y -= updateShift; - [legalBlock_ setFrame:rect]; - - rect = [backgroundView_ frame]; - rect.origin.y = rect.origin.y - updateShift + legalShift; - [backgroundView_ setFrame:rect]; - - NSSize windowDelta = NSMakeSize(0, (legalShift - updateShift)); - [GTMUILocalizerAndLayoutTweaker - resizeWindowWithoutAutoResizingSubViews:[self window] - delta:windowDelta]; -} - -- (KeystoneGlue*)defaultKeystoneGlue { - return [KeystoneGlue defaultKeystoneGlue]; -} - -- (void)startProgressMessageID:(uint32_t)messageID { - NSString* message = l10n_util::GetNSStringWithFixup(messageID); - [self startProgressMessage:message]; -} - -- (void)startProgressMessage:(NSString*)message { - [updateStatusIndicator_ setHidden:YES]; - [spinner_ setHidden:NO]; - [spinner_ startAnimation:self]; - - [updateText_ setStringValue:message]; -} - -- (void)stopProgressMessage:(NSString*)message imageID:(uint32_t)imageID { - [spinner_ stopAnimation:self]; - [spinner_ setHidden:YES]; - if (imageID) { - [updateStatusIndicator_ setHidden:NO]; - ResourceBundle& rb = ResourceBundle::GetSharedInstance(); - NSImage* statusImage = rb.GetNSImageNamed(imageID); - DCHECK(statusImage); - [updateStatusIndicator_ setImage:statusImage]; - } - - [updateText_ setStringValue:message]; -} - -// Callback from KeystoneGlue; implementation of KeystoneGlueCallbacks protocol. -// Warning: latest version may be nil if not set in server config. -- (void)upToDateCheckCompleted:(BOOL)updatesAvailable - latestVersion:(NSString*)latestVersion { - uint32_t imageID; - NSString* message; - if (updatesAvailable) { - newVersionAvailable_.reset([latestVersion copy]); - - // Window UI doesn't put the version number in the string. - imageID = IDR_UPDATE_AVAILABLE; - message = - l10n_util::GetNSStringF(IDS_UPGRADE_AVAILABLE, - l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); - [updateNowButton_ setEnabled:YES]; - } else { - // NOTE: This is can be a lie, Keystone does not provide us with an error if - // it was not able to reach the server. So we can't completely map to the - // Windows UI. - - // Keystone does not provide the version number when we are up to date so to - // maintain the UI, we just go fetch our version and call it good. - scoped_ptr<FileVersionInfo> version_info( - FileVersionInfo::CreateFileVersionInfoForCurrentModule()); - std::wstring version(version_info->product_version()); - - // TODO: We really should check to see if what is on disk is newer then what - // is running and report it as such. (Windows has some messages that can - // help with this.) http://crbug.com/13165 - - imageID = IDR_UPDATE_UPTODATE; - message = - l10n_util::GetNSStringF(IDS_UPGRADE_ALREADY_UP_TO_DATE, - l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), - WideToUTF16(version)); - } - [self stopProgressMessage:message imageID:imageID]; -} - -- (void)windowWillClose:(NSNotification*)notification { - // If an update has ever been triggered, we force reuse of the same About Box. - // This gives us 2 things: - // 1. If an update is ongoing and the window was closed we would have - // no way of getting status. - // 2. If we have a "Please restart" message we want it to stay there. - if (updateTriggered_) - return; - - [[NSNotificationCenter defaultCenter] - postNotificationName:kUserClosedAboutNotification - object:self]; -} - -// Callback from KeystoneGlue; implementation of KeystoneGlueCallbacks protocol. -- (void)updateCompleted:(BOOL)successful installs:(int)installs { - uint32_t imageID; - NSString* message; - if (successful && installs) { - imageID = IDR_UPDATE_UPTODATE; - if ([newVersionAvailable_.get() length]) { - message = - l10n_util::GetNSStringF(IDS_UPGRADE_SUCCESSFUL, - l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), - base::SysNSStringToUTF16( - newVersionAvailable_.get())); - } else { - message = - l10n_util::GetNSStringF(IDS_UPGRADE_SUCCESSFUL_NOVERSION, - l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); - } - - // Tell the user to restart their browser. - restart_browser::RequestRestart(nil); - - } else { - imageID = IDR_UPDATE_FAIL; - message = - l10n_util::GetNSStringF(IDS_UPGRADE_ERROR, - IntToString16(kUpdateInstallFailed)); - - // Allow a second chance. - [updateNowButton_ setEnabled:YES]; - } - - [self stopProgressMessage:message imageID:imageID]; -} - -- (IBAction)updateNow:(id)sender { - updateTriggered_ = YES; - - // Don't let someone click "Update Now" twice! - [updateNowButton_ setEnabled:NO]; - if ([[self defaultKeystoneGlue] startUpdate:self]) { - // Clear any previous error message from the throbber area. - [self startProgressMessageID:IDS_UPGRADE_STARTED]; - } else { - NSString* message = - l10n_util::GetNSStringF(IDS_UPGRADE_ERROR, - IntToString16(kUpdateInstallFailedToStart)); - [self stopProgressMessage:message imageID:IDR_UPDATE_FAIL]; - } -} - -- (BOOL)textView:(NSTextView *)aTextView - clickedOnLink:(id)link - atIndex:(NSUInteger)charIndex { - // We always create a new window, so there's no need to try to re-use - // an existing one just to pass in the NEW_WINDOW disposition. - Browser* browser = Browser::Create(profile_); - if (browser) - browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_WINDOW, - PageTransition::LINK); - return YES; -} - -- (NSTextView*)legalText { - return legalText_; -} - -- (NSButton*)updateButton { - return updateNowButton_; -} - -- (NSTextField*)updateText { - return updateText_; -} - -@end - +@end // @implementation AboutWindowController diff --git a/chrome/browser/cocoa/about_window_controller_unittest.mm b/chrome/browser/cocoa/about_window_controller_unittest.mm index 3117b08..c7b65fe 100644 --- a/chrome/browser/cocoa/about_window_controller_unittest.mm +++ b/chrome/browser/cocoa/about_window_controller_unittest.mm @@ -5,6 +5,7 @@ #import <Cocoa/Cocoa.h> #import "base/scoped_nsobject.h" +#import "chrome/app/keystone_glue.h" #import "chrome/browser/cocoa/about_window_controller.h" #include "chrome/browser/cocoa/browser_test_helper.h" #include "chrome/browser/cocoa/cocoa_test_helper.h" @@ -13,6 +14,23 @@ namespace { +void PostAutoupdateStatusNotification(AutoupdateStatus status, + NSString* version) { + NSNumber* statusNumber = [NSNumber numberWithInt:status]; + NSMutableDictionary* dictionary = + [NSMutableDictionary dictionaryWithObjects:&statusNumber + forKeys:&kAutoupdateStatusStatus + count:1]; + if (version) { + [dictionary setObject:version forKey:kAutoupdateStatusVersion]; + } + + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:kAutoupdateStatusNotification + object:nil + userInfo:dictionary]; +} + class AboutWindowControllerTest : public PlatformTest { public: virtual void SetUp() { @@ -27,7 +45,7 @@ class AboutWindowControllerTest : public PlatformTest { }; TEST_F(AboutWindowControllerTest, TestCopyright) { - NSString* text = [BuildAboutWindowLegalTextBlock() string]; + NSString* text = [[AboutWindowController legalTextBlock] string]; // Make sure we have the word "Copyright" in it, which is present in all // locales. @@ -36,7 +54,7 @@ TEST_F(AboutWindowControllerTest, TestCopyright) { } TEST_F(AboutWindowControllerTest, RemovesLinkAnchors) { - NSString* text = [BuildAboutWindowLegalTextBlock() string]; + NSString* text = [[AboutWindowController legalTextBlock] string]; // Make sure that we removed the "BEGIN_LINK" and "END_LINK" anchors. NSRange range = [text rangeOfString:@"BEGIN_LINK"]; @@ -47,7 +65,7 @@ TEST_F(AboutWindowControllerTest, RemovesLinkAnchors) { } TEST_F(AboutWindowControllerTest, AwakeNibSetsString) { - NSAttributedString* legal_text = BuildAboutWindowLegalTextBlock(); + NSAttributedString* legal_text = [AboutWindowController legalTextBlock]; NSAttributedString* text_storage = [[about_window_controller_ legalText] textStorage]; @@ -60,7 +78,7 @@ TEST_F(AboutWindowControllerTest, TestButton) { // Not enabled until we know if updates are available. ASSERT_FALSE([button isEnabled]); - [about_window_controller_ upToDateCheckCompleted:YES latestVersion:nil]; + PostAutoupdateStatusNotification(kAutoupdateAvailable, nil); ASSERT_TRUE([button isEnabled]); // Make sure the button is hooked up @@ -75,16 +93,52 @@ TEST_F(AboutWindowControllerTest, TestButton) { // Doesn't confirm correctness, but does confirm something happens. TEST_F(AboutWindowControllerTest, TestCallbacks) { NSString *lastText = [[about_window_controller_ updateText] - stringValue]; - [about_window_controller_ upToDateCheckCompleted:NO latestVersion:@"foo"]; + stringValue]; + PostAutoupdateStatusNotification(kAutoupdateCurrent, @"foo"); + ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateCurrent, @"foo"); + ASSERT_TRUE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateCurrent, @"bar"); + ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateAvailable, nil); ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] - stringValue]]); + stringValue]]); lastText = [[about_window_controller_ updateText] stringValue]; - [about_window_controller_ updateCompleted:NO installs:0]; + PostAutoupdateStatusNotification(kAutoupdateCheckFailed, nil); + ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); + +#if 0 + // TODO(mark): The kAutoupdateInstalled portion of the test is disabled + // because it leaks restart dialogs. If the About box is revised to use + // a button within the box to advise a restart instead of popping dialogs, + // these tests should be enabled. + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateInstalled, @"ver"); + ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateInstalled, nil); + ASSERT_FALSE([lastText isEqual:[[about_window_controller_ updateText] + stringValue]]); +#endif + + lastText = [[about_window_controller_ updateText] stringValue]; + PostAutoupdateStatusNotification(kAutoupdateInstallFailed, nil); ASSERT_FALSE([lastText isEqual:[[about_window_controller_ - updateText] stringValue]]); + updateText] stringValue]]); } } // namespace - diff --git a/chrome/browser/cocoa/restart_browser.mm b/chrome/browser/cocoa/restart_browser.mm index 9a17f04..726fe65 100644 --- a/chrome/browser/cocoa/restart_browser.mm +++ b/chrome/browser/cocoa/restart_browser.mm @@ -53,7 +53,7 @@ void RequestRestart(NSWindow* parent) { RestartHelper* helper = [[RestartHelper alloc] init]; NSAlert* alert = [helper alert]; - [alert setAlertStyle:NSCriticalAlertStyle]; + [alert setAlertStyle:NSInformationalAlertStyle]; [alert setMessageText:title]; [alert setInformativeText:text]; [alert addButtonWithTitle:okBtn]; |