[Mac] Create CrTrackingArea and use it in TabStripController.

CrTrackingArea can prevent messages from being sent to the tracking area's
owner, which is the source of a significant number of zombie crashes. This hopes
to prevent that.

BUG=48709
TEST=Crash reports go down.

Review URL: http://codereview.chromium.org/6486002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@74940 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h
index 96a8e6b..69ffd41 100644
--- a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h
+++ b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.h
@@ -15,6 +15,7 @@
 #import "chrome/browser/ui/cocoa/url_drop_target.h"
 #import "third_party/GTM/AppKit/GTMWindowSheetController.h"
 
+@class CrTrackingArea;
 @class NewTabButton;
 @class TabContentsController;
 @class TabView;
@@ -71,7 +72,7 @@
   NewTabButton* newTabButton_;  // weak, obtained from the nib.
 
   // Tracks the newTabButton_ for rollovers.
-  scoped_nsobject<NSTrackingArea> newTabTrackingArea_;
+  scoped_nsobject<CrTrackingArea> newTabTrackingArea_;
   scoped_ptr<TabStripModelObserverBridge> bridge_;
   Browser* browser_;  // weak
   TabStripModel* tabStripModel_;  // weak
@@ -123,7 +124,7 @@
   float availableResizeWidth_;
   // A tracking area that's the size of the tab strip used to be notified
   // when the mouse moves in the tab strip
-  scoped_nsobject<NSTrackingArea> trackingArea_;
+  scoped_nsobject<CrTrackingArea> trackingArea_;
   TabView* hoveredTab_;  // weak. Tab that the mouse is hovering over
 
   // Array of subviews which are permanent (and which should never be removed),
diff --git a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm
index 93048d4..eb49281 100644
--- a/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm
+++ b/chrome/browser/ui/cocoa/tabs/tab_strip_controller.mm
@@ -38,6 +38,7 @@
 #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
 #import "chrome/browser/ui/cocoa/tabs/throbber_view.h"
+#import "chrome/browser/ui/cocoa/tracking_area.h"
 #include "chrome/browser/ui/find_bar/find_bar.h"
 #include "chrome/browser/ui/find_bar/find_bar_controller.h"
 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
@@ -122,7 +123,6 @@
 }  // namespace
 
 @interface TabStripController (Private)
-- (void)installTrackingArea;
 - (void)addSubviewToPermanentList:(NSView*)aView;
 - (void)regenerateSubviewList;
 - (NSInteger)indexForContentsView:(NSView*)view;
@@ -296,6 +296,7 @@
     bridge_.reset(new TabStripModelObserverBridge(tabStripModel_, self));
     tabContentsArray_.reset([[NSMutableArray alloc] init]);
     tabArray_.reset([[NSMutableArray alloc] init]);
+    NSWindow* browserWindow = [view window];
 
     // Important note: any non-tab subviews not added to |permanentSubviews_|
     // (see |-addSubviewToPermanentList:|) will be wiped out.
@@ -321,11 +322,13 @@
         app::mac::GetCachedImageWithName(kNewTabPressedImage)];
     newTabButtonShowingHoverImage_ = NO;
     newTabTrackingArea_.reset(
-        [[NSTrackingArea alloc] initWithRect:[newTabButton_ bounds]
+        [[CrTrackingArea alloc] initWithRect:[newTabButton_ bounds]
                                      options:(NSTrackingMouseEnteredAndExited |
                                               NSTrackingActiveAlways)
                                        owner:self
                                     userInfo:nil]);
+    if (browserWindow)  // Nil for Browsers without a tab strip (e.g. popups).
+      [newTabTrackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
     [newTabButton_ addTrackingArea:newTabTrackingArea_.get()];
     targetFrames_.reset([[NSMutableDictionary alloc] init]);
 
@@ -350,7 +353,7 @@
                name:NSViewFrameDidChangeNotification
              object:tabStripView_];
 
-    trackingArea_.reset([[NSTrackingArea alloc]
+    trackingArea_.reset([[CrTrackingArea alloc]
         initWithRect:NSZeroRect  // Ignored by NSTrackingInVisibleRect
              options:NSTrackingMouseEnteredAndExited |
                      NSTrackingMouseMoved |
@@ -358,6 +361,8 @@
                      NSTrackingInVisibleRect
                owner:self
             userInfo:nil]);
+    if (browserWindow)  // Nil for Browsers without a tab strip (e.g. popups).
+      [trackingArea_ clearOwnerWhenWindowWillClose:browserWindow];
     [tabStripView_ addTrackingArea:trackingArea_.get()];
 
     // Check to see if the mouse is currently in our bounds so we can
diff --git a/chrome/browser/ui/cocoa/tracking_area.h b/chrome/browser/ui/cocoa/tracking_area.h
new file mode 100644
index 0000000..26cb2ef
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tracking_area.h
@@ -0,0 +1,37 @@
+// Copyright (c) 2011 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 CHROME_BROWSER_UI_COCOA_TRACKING_AREA_H_
+#define CHROME_BROWSER_UI_COCOA_TRACKING_AREA_H_
+
+#import <AppKit/AppKit.h>
+
+#include "base/scoped_nsobject.h"
+
+@class CrTrackingAreaOwnerProxy;
+
+// The CrTrackingArea can be used in place of an NSTrackingArea to shut off
+// messaging to the |owner| at a specific point in time.
+@interface CrTrackingArea : NSTrackingArea {
+ @private
+  scoped_nsobject<CrTrackingAreaOwnerProxy> ownerProxy_;
+}
+
+// Designated initializer. Forwards all arguments to the superclass, but wraps
+// |owner| in a proxy object.
+- (id)initWithRect:(NSRect)rect
+           options:(NSTrackingAreaOptions)options
+             owner:(id)owner
+          userInfo:(NSDictionary*)userInfo;
+
+// Prevents any future messages from being delivered to the |owner|.
+- (void)clearOwner;
+
+// Watches |window| for its NSWindowWillCloseNotification and calls
+// |-clearOwner| when the notification is observed.
+- (void)clearOwnerWhenWindowWillClose:(NSWindow*)window;
+
+@end
+
+#endif  // CHROME_BROWSER_UI_COCOA_TRACKING_AREA_H_
diff --git a/chrome/browser/ui/cocoa/tracking_area.mm b/chrome/browser/ui/cocoa/tracking_area.mm
new file mode 100644
index 0000000..692515f
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tracking_area.mm
@@ -0,0 +1,107 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "chrome/browser/ui/cocoa/tracking_area.h"
+
+#include "base/logging.h"
+
+// NSTrackingArea does not retain its |owner| so CrTrackingArea wraps the real
+// owner in this proxy, which can stop forwarding messages to the owner when
+// it is no longer |alive_|.
+@interface CrTrackingAreaOwnerProxy : NSObject {
+ @private
+  // Whether or not the owner is "alive" and should forward calls to the real
+  // owner object.
+  BOOL alive_;
+
+  // The real object for which this is a proxy. Weak.
+  id owner_;
+
+  // The Class of |owner_|. When the actual object is no longer alive (and could
+  // be zombie), this allows for introspection.
+  Class ownerClass_;
+}
+@property(nonatomic, assign) BOOL alive;
+- (id)initWithOwner:(id)owner;
+@end
+
+@implementation CrTrackingAreaOwnerProxy
+
+@synthesize alive = alive_;
+
+- (id)initWithOwner:(id)owner {
+  if ((self = [super init])) {
+    alive_ = YES;
+    owner_ = owner;
+    ownerClass_ = [owner class];
+  }
+  return self;
+}
+
+- (void)forwardInvocation:(NSInvocation*)invocation {
+  if (!alive_)
+    return;
+  [invocation invokeWithTarget:owner_];
+}
+
+- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
+  // This can be called if |owner_| is not |alive_|, so use the Class to
+  // generate the signature. |-forwardInvocation:| will block the actual call.
+  return [ownerClass_ instanceMethodSignatureForSelector:sel];
+}
+
+- (BOOL)respondsToSelector:(SEL)aSelector {
+  return [ownerClass_ instancesRespondToSelector:aSelector];
+}
+
+@end
+
+// Private Interface ///////////////////////////////////////////////////////////
+
+@interface CrTrackingArea (Private)
+- (void)windowWillClose:(NSNotification*)notif;
+@end
+
+////////////////////////////////////////////////////////////////////////////////
+
+@implementation CrTrackingArea
+
+- (id)initWithRect:(NSRect)rect
+           options:(NSTrackingAreaOptions)options
+             owner:(id)owner
+          userInfo:(NSDictionary*)userInfo {
+  scoped_nsobject<CrTrackingAreaOwnerProxy> ownerProxy(
+      [[CrTrackingAreaOwnerProxy alloc] initWithOwner:owner]);
+  if ((self = static_cast<id>([super initWithRect:rect
+                                          options:options
+                                            owner:ownerProxy.get()
+                                         userInfo:userInfo]))) {
+    ownerProxy_.swap(ownerProxy);
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+  [super dealloc];
+}
+
+- (void)clearOwner {
+  [ownerProxy_ setAlive:NO];
+}
+
+- (void)clearOwnerWhenWindowWillClose:(NSWindow*)window {
+  DCHECK(window);
+  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+  [center addObserver:self
+             selector:@selector(windowWillClose:)
+                 name:NSWindowWillCloseNotification
+               object:window];
+}
+
+- (void)windowWillClose:(NSNotification*)notif {
+  [self clearOwner];
+}
+
+@end
diff --git a/chrome/browser/ui/cocoa/tracking_area_unittest.mm b/chrome/browser/ui/cocoa/tracking_area_unittest.mm
new file mode 100644
index 0000000..151d033
--- /dev/null
+++ b/chrome/browser/ui/cocoa/tracking_area_unittest.mm
@@ -0,0 +1,86 @@
+// Copyright (c) 2011 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.
+
+#include "base/scoped_nsobject.h"
+#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
+#include "chrome/browser/ui/cocoa/objc_zombie.h"
+#import "chrome/browser/ui/cocoa/tracking_area.h"
+
+// A test object that counts the number of times a message is sent to it.
+@interface TestTrackingAreaOwner : NSObject {
+ @private
+  NSUInteger messageCount_;
+}
+@property(nonatomic, assign) NSUInteger messageCount;
+- (void)performMessage;
+@end
+
+@implementation TestTrackingAreaOwner
+@synthesize messageCount = messageCount_;
+- (void)performMessage {
+  ++messageCount_;
+}
+@end
+
+class CrTrackingAreaTest : public CocoaTest {
+ public:
+  CrTrackingAreaTest()
+      : owner_([[TestTrackingAreaOwner alloc] init]),
+        trackingArea_([[CrTrackingArea alloc]
+            initWithRect:NSMakeRect(0, 0, 100, 100)
+                 options:NSTrackingMouseMoved | NSTrackingActiveInKeyWindow
+                   owner:owner_.get()
+                userInfo:nil]) {
+  }
+
+  scoped_nsobject<TestTrackingAreaOwner> owner_;
+  scoped_nsobject<CrTrackingArea> trackingArea_;
+};
+
+TEST_F(CrTrackingAreaTest, OwnerForwards) {
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(2U, [owner_ messageCount]);
+}
+
+TEST_F(CrTrackingAreaTest, OwnerStopsForwarding) {
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+
+  [trackingArea_ clearOwner];
+
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+}
+
+TEST_F(CrTrackingAreaTest, OwnerAutomaticallyStopsForwardingOnClose) {
+  [test_window() orderFront:nil];
+  [trackingArea_ clearOwnerWhenWindowWillClose:test_window()];
+
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+
+  [test_window() close];
+
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+}
+
+TEST_F(CrTrackingAreaTest, ZombieOwner) {
+  EXPECT_TRUE(ObjcEvilDoers::ZombieEnable(NO, 20));
+
+  [[trackingArea_ owner] performMessage];
+  EXPECT_EQ(1U, [owner_ messageCount]);
+
+  [owner_ shouldBecomeCrZombie];
+  owner_.reset();
+  [trackingArea_ clearOwner];
+
+  [[trackingArea_ owner] performMessage];
+  // Don't crash!
+
+  ObjcEvilDoers::ZombieDisable();
+}