diff options
Diffstat (limited to 'chrome')
-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 |
8 files changed, 699 insertions, 433 deletions
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]; |