Add double-click protection to bubbles

Potentially dangerous situation is possible when user is performing
double-click on UI element. If bubble is being shown as a result of
the first click, the control under the mouse cursor (button on
the bubble) can be activated with the second click. This wasn't
the intention of the user as the time between clicks was too short
to read the contents in the bubble that appeared. For example, user
can accidentally click "Accept" button on the permission prompt
bubble.

This CL adds protection against such unintended clicks. Mouse and
touch events are ignored for a short period of time after bubble
has been shown.

Bug: 864530
Change-Id: I54d229bf39dd000079b9eabd8de1cfba5103a022
Reviewed-on: https://chromium-review.googlesource.com/1140307
Commit-Queue: Tomasz Moniuszko <tmoniuszko@opera.com>
Reviewed-by: Michael Wasserman <msw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#591324}
diff --git a/chrome/browser/ui/views/autofill/save_card_bubble_views_browsertest_base.cc b/chrome/browser/ui/views/autofill/save_card_bubble_views_browsertest_base.cc
index bb3f4a8..841ee5e 100644
--- a/chrome/browser/ui/views/autofill/save_card_bubble_views_browsertest_base.cc
+++ b/chrome/browser/ui/views/autofill/save_card_bubble_views_browsertest_base.cc
@@ -31,9 +31,12 @@
 #include "net/url_request/test_url_fetcher_factory.h"
 #include "services/device/public/cpp/test/scoped_geolocation_overrider.h"
 #include "ui/events/base_event_utils.h"
+#include "ui/views/bubble/bubble_frame_view.h"
 #include "ui/views/controls/button/button.h"
 #include "ui/views/test/widget_test.h"
+#include "ui/views/widget/widget.h"
 #include "ui/views/window/dialog_client_view.h"
+#include "ui/views/window/non_client_view.h"
 
 namespace autofill {
 
@@ -334,6 +337,16 @@
 }
 
 void SaveCardBubbleViewsBrowserTestBase::ClickOnDialogView(views::View* view) {
+  GetSaveCardBubbleViews()
+      ->GetDialogClientView()
+      ->ResetViewShownTimeStampForTesting();
+  views::BubbleFrameView* bubble_frame_view =
+      static_cast<views::BubbleFrameView*>(GetSaveCardBubbleViews()
+                                               ->GetWidget()
+                                               ->non_client_view()
+                                               ->frame_view());
+  bubble_frame_view->ResetViewShownTimeStampForTesting();
+
   DCHECK(view);
   ui::MouseEvent pressed(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
                          ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
diff --git a/chrome/browser/ui/views/sync/one_click_signin_dialog_view_unittest.cc b/chrome/browser/ui/views/sync/one_click_signin_dialog_view_unittest.cc
index f5c25bf..85e278e 100644
--- a/chrome/browser/ui/views/sync/one_click_signin_dialog_view_unittest.cc
+++ b/chrome/browser/ui/views/sync/one_click_signin_dialog_view_unittest.cc
@@ -137,6 +137,7 @@
 
 TEST_F(OneClickSigninDialogViewTest, OkButton) {
   OneClickSigninDialogView* view = ShowOneClickSigninDialog();
+  view->GetDialogClientView()->ResetViewShownTimeStampForTesting();
 
   gfx::Point center(10, 10);
   const ui::MouseEvent event(ui::ET_MOUSE_PRESSED, center, center,
@@ -153,6 +154,7 @@
 
 TEST_F(OneClickSigninDialogViewTest, UndoButton) {
   OneClickSigninDialogView* view = ShowOneClickSigninDialog();
+  view->GetDialogClientView()->ResetViewShownTimeStampForTesting();
 
   gfx::Point center(10, 10);
   const ui::MouseEvent event(ui::ET_MOUSE_PRESSED, center, center,
diff --git a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views_unittest.cc b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views_unittest.cc
index 617efea..10c31f5 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views_unittest.cc
+++ b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views_unittest.cc
@@ -79,6 +79,8 @@
   }
 
   void ClickButton(views::Button* button) {
+    bubble()->GetDialogClientView()->ResetViewShownTimeStampForTesting();
+
     ASSERT_TRUE(button);
     const gfx::Point point(10, 10);
     const ui::MouseEvent event(ui::ET_MOUSE_PRESSED, point, point,
diff --git a/ui/views/BUILD.gn b/ui/views/BUILD.gn
index 62b8973..31d1977 100644
--- a/ui/views/BUILD.gn
+++ b/ui/views/BUILD.gn
@@ -176,6 +176,7 @@
     "drag_utils.h",
     "event_monitor.h",
     "event_monitor_mac.h",
+    "event_utils.h",
     "focus/external_focus_tracker.h",
     "focus/focus_manager.h",
     "focus/focus_manager_delegate.h",
@@ -363,6 +364,7 @@
     "drag_utils.cc",
     "drag_utils_mac.mm",
     "event_monitor_mac.mm",
+    "event_utils.cc",
     "focus/external_focus_tracker.cc",
     "focus/focus_manager.cc",
     "focus/focus_manager_factory.cc",
diff --git a/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc b/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc
index 5de626a..453b0bd 100644
--- a/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc
+++ b/ui/views/bubble/bubble_dialog_delegate_view_unittest.cc
@@ -362,6 +362,7 @@
         BubbleDialogDelegateView::CreateBubble(bubble_delegate);
     bubble_widget->Show();
     BubbleFrameView* frame_view = bubble_delegate->GetBubbleFrameView();
+    frame_view->ResetViewShownTimeStampForTesting();
     Button* close_button = frame_view->close_;
     ASSERT_TRUE(close_button);
     frame_view->ButtonPressed(
diff --git a/ui/views/bubble/bubble_frame_view.cc b/ui/views/bubble/bubble_frame_view.cc
index f43e505..15830c0 100644
--- a/ui/views/bubble/bubble_frame_view.cc
+++ b/ui/views/bubble/bubble_frame_view.cc
@@ -28,6 +28,7 @@
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/button/image_button_factory.h"
 #include "ui/views/controls/image_view.h"
+#include "ui/views/event_utils.h"
 #include "ui/views/layout/box_layout.h"
 #include "ui/views/layout/layout_provider.h"
 #include "ui/views/paint_info.h"
@@ -407,6 +408,13 @@
   }
 }
 
+void BubbleFrameView::VisibilityChanged(View* starting_from, bool is_visible) {
+  NonClientFrameView::VisibilityChanged(starting_from, is_visible);
+
+  if (is_visible)
+    view_shown_time_stamp_ = base::TimeTicks::Now();
+}
+
 void BubbleFrameView::OnPaint(gfx::Canvas* canvas) {
   OnPaintBackground(canvas);
   // Border comes after children.
@@ -424,6 +432,9 @@
 }
 
 void BubbleFrameView::ButtonPressed(Button* sender, const ui::Event& event) {
+  if (IsPossiblyUnintendedInteraction(view_shown_time_stamp_, event))
+    return;
+
   if (sender == close_) {
     close_button_clicked_ = true;
     GetWidget()->Close();
@@ -477,6 +488,10 @@
   return bubble_border_->GetBounds(anchor_rect, size);
 }
 
+void BubbleFrameView::ResetViewShownTimeStampForTesting() {
+  view_shown_time_stamp_ = base::TimeTicks();
+}
+
 gfx::Rect BubbleFrameView::GetAvailableScreenBounds(
     const gfx::Rect& rect) const {
   // The bubble attempts to fit within the current screen bounds.
diff --git a/ui/views/bubble/bubble_frame_view.h b/ui/views/bubble/bubble_frame_view.h
index eef3fda..bfb407a 100644
--- a/ui/views/bubble/bubble_frame_view.h
+++ b/ui/views/bubble/bubble_frame_view.h
@@ -8,6 +8,7 @@
 #include "base/compiler_specific.h"
 #include "base/gtest_prod_util.h"
 #include "base/macros.h"
+#include "base/time/time.h"
 #include "ui/gfx/font_list.h"
 #include "ui/gfx/geometry/insets.h"
 #include "ui/views/controls/button/button.h"
@@ -65,6 +66,7 @@
   void OnNativeThemeChanged(const ui::NativeTheme* theme) override;
   void ViewHierarchyChanged(
       const ViewHierarchyChangedDetails& details) override;
+  void VisibilityChanged(View* starting_from, bool is_visible) override;
 
   // ButtonListener:
   void ButtonPressed(Button* sender, const ui::Event& event) override;
@@ -99,6 +101,11 @@
 
   Button* GetCloseButtonForTest() { return close_; }
 
+  // Resets the time when view has been shown. Tests may need to call this
+  // method if they use events that could be otherwise treated as unintended.
+  // See IsPossiblyUnintendedInteraction().
+  void ResetViewShownTimeStampForTesting();
+
  protected:
   // Returns the available screen bounds if the frame were to show in |rect|.
   virtual gfx::Rect GetAvailableScreenBounds(const gfx::Rect& rect) const;
@@ -115,6 +122,7 @@
   FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, GetBoundsForClientView);
   FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, RemoveFootnoteView);
   FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, LayoutWithIcon);
+  FRIEND_TEST_ALL_PREFIXES(BubbleFrameViewTest, IgnorePossiblyUnintendedClicks);
   FRIEND_TEST_ALL_PREFIXES(BubbleDelegateTest, CloseReasons);
   FRIEND_TEST_ALL_PREFIXES(BubbleDialogDelegateViewTest, CloseMethods);
 
@@ -180,6 +188,9 @@
   // Whether the close button was clicked.
   bool close_button_clicked_;
 
+  // Time when view has been shown.
+  base::TimeTicks view_shown_time_stamp_;
+
   DISALLOW_COPY_AND_ASSIGN(BubbleFrameView);
 };
 
diff --git a/ui/views/bubble/bubble_frame_view_unittest.cc b/ui/views/bubble/bubble_frame_view_unittest.cc
index 6af0826..38930c3 100644
--- a/ui/views/bubble/bubble_frame_view_unittest.cc
+++ b/ui/views/bubble/bubble_frame_view_unittest.cc
@@ -8,14 +8,19 @@
 
 #include "base/macros.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/time/time.h"
 #include "build/build_config.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/events/event.h"
 #include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/text_utils.h"
 #include "ui/views/bubble/bubble_border.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
 #include "ui/views/controls/button/label_button.h"
+#include "ui/views/metrics.h"
 #include "ui/views/test/test_layout_provider.h"
 #include "ui/views/test/test_views.h"
 #include "ui/views/test/views_test_base.h"
@@ -751,4 +756,28 @@
   EXPECT_EQ(title, title_label->GetDisplayTextForTesting());
 }
 
+// Ensures that clicks are ignored for short time after view has been shown.
+TEST_F(BubbleFrameViewTest, IgnorePossiblyUnintendedClicks) {
+  TestBubbleDialogDelegateView delegate;
+  TestAnchor anchor(CreateParams(Widget::InitParams::TYPE_WINDOW));
+  delegate.SetAnchorView(anchor.widget().GetContentsView());
+  Widget* bubble = BubbleDialogDelegateView::CreateBubble(&delegate);
+  bubble->Show();
+
+  BubbleFrameView* frame = delegate.GetBubbleFrameView();
+  frame->ButtonPressed(
+      frame->close_,
+      ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
+                     ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE));
+  EXPECT_FALSE(bubble->IsClosed());
+
+  frame->ButtonPressed(
+      frame->close_,
+      ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
+                     ui::EventTimeForNow() + base::TimeDelta::FromMilliseconds(
+                                                 GetDoubleClickInterval()),
+                     ui::EF_NONE, ui::EF_NONE));
+  EXPECT_TRUE(bubble->IsClosed());
+}
+
 }  // namespace views
diff --git a/ui/views/event_utils.cc b/ui/views/event_utils.cc
new file mode 100644
index 0000000..ecec6e0
--- /dev/null
+++ b/ui/views/event_utils.cc
@@ -0,0 +1,22 @@
+// Copyright (c) 2018 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 "ui/views/event_utils.h"
+
+#include "base/time/time.h"
+#include "ui/events/event.h"
+#include "ui/views/metrics.h"
+
+namespace views {
+
+bool IsPossiblyUnintendedInteraction(const base::TimeTicks& initial_timestamp,
+                                     const ui::Event& event) {
+  return (event.IsMouseEvent() || event.IsPointerEvent() ||
+          event.IsTouchEvent()) &&
+         event.time_stamp() <
+             initial_timestamp +
+                 base::TimeDelta::FromMilliseconds(GetDoubleClickInterval());
+}
+
+}  // namespace views
diff --git a/ui/views/event_utils.h b/ui/views/event_utils.h
new file mode 100644
index 0000000..cfc553f
--- /dev/null
+++ b/ui/views/event_utils.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2018 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 UI_VIEWS_EVENT_UTILS_H_
+#define UI_VIEWS_EVENT_UTILS_H_
+
+#include "ui/views/views_export.h"
+
+namespace base {
+class TimeTicks;
+}
+
+namespace ui {
+class Event;
+}
+
+namespace views {
+
+// Returns true if the event is a mouse, touch, or pointer event that took place
+// within the double-click time interval after the |initial_timestamp|.
+VIEWS_EXPORT bool IsPossiblyUnintendedInteraction(
+    const base::TimeTicks& initial_timestamp,
+    const ui::Event& event);
+
+}  // namespace views
+
+#endif  // UI_VIEWS_EVENT_UTILS_H_
diff --git a/ui/views/window/dialog_client_view.cc b/ui/views/window/dialog_client_view.cc
index 40cc9f3..7c5c56f 100644
--- a/ui/views/window/dialog_client_view.cc
+++ b/ui/views/window/dialog_client_view.cc
@@ -17,6 +17,7 @@
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/button/label_button.h"
 #include "ui/views/controls/button/md_text_button.h"
+#include "ui/views/event_utils.h"
 #include "ui/views/layout/grid_layout.h"
 #include "ui/views/layout/layout_provider.h"
 #include "ui/views/style/platform_style.h"
@@ -164,6 +165,13 @@
   return max_size;
 }
 
+void DialogClientView::VisibilityChanged(View* starting_from, bool is_visible) {
+  ClientView::VisibilityChanged(starting_from, is_visible);
+
+  if (is_visible)
+    view_shown_time_stamp_ = base::TimeTicks::Now();
+}
+
 void DialogClientView::Layout() {
   button_row_container_->SetSize(
       gfx::Size(width(), button_row_container_->GetHeightForWidth(width())));
@@ -234,6 +242,9 @@
   if (!GetDialogDelegate())
     return;
 
+  if (IsPossiblyUnintendedInteraction(view_shown_time_stamp_, event))
+    return;
+
   if (sender == ok_button_)
     AcceptWindow();
   else if (sender == cancel_button_)
@@ -242,6 +253,10 @@
     NOTREACHED();
 }
 
+void DialogClientView::ResetViewShownTimeStampForTesting() {
+  view_shown_time_stamp_ = base::TimeTicks();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // DialogClientView, private:
 
diff --git a/ui/views/window/dialog_client_view.h b/ui/views/window/dialog_client_view.h
index 108d977..3981e4f 100644
--- a/ui/views/window/dialog_client_view.h
+++ b/ui/views/window/dialog_client_view.h
@@ -7,6 +7,7 @@
 
 #include "base/gtest_prod_util.h"
 #include "base/macros.h"
+#include "base/time/time.h"
 #include "ui/base/ui_base_types.h"
 #include "ui/views/controls/button/button.h"
 #include "ui/views/window/client_view.h"
@@ -54,6 +55,7 @@
   gfx::Size CalculatePreferredSize() const override;
   gfx::Size GetMinimumSize() const override;
   gfx::Size GetMaximumSize() const override;
+  void VisibilityChanged(View* starting_from, bool is_visible) override;
 
   void Layout() override;
   bool AcceleratorPressed(const ui::Accelerator& accelerator) override;
@@ -66,6 +68,11 @@
 
   void set_minimum_size(const gfx::Size& size) { minimum_size_ = size; }
 
+  // Resets the time when view has been shown. Tests may need to call this
+  // method if they use events that could be otherwise treated as unintended.
+  // See IsPossiblyUnintendedInteraction().
+  void ResetViewShownTimeStampForTesting();
+
  private:
   enum {
     // The number of buttons that DialogClientView can support.
@@ -134,6 +141,9 @@
   // SetupLayout(). Everything will be manually updated afterwards.
   bool adding_or_removing_views_ = false;
 
+  // Time when view has been shown.
+  base::TimeTicks view_shown_time_stamp_;
+
   DISALLOW_COPY_AND_ASSIGN(DialogClientView);
 };
 
diff --git a/ui/views/window/dialog_client_view_unittest.cc b/ui/views/window/dialog_client_view_unittest.cc
index d3f0d56..4c4e67e 100644
--- a/ui/views/window/dialog_client_view_unittest.cc
+++ b/ui/views/window/dialog_client_view_unittest.cc
@@ -8,11 +8,16 @@
 
 #include "base/macros.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/time/time.h"
 #include "build/build_config.h"
 #include "ui/base/ui_base_types.h"
+#include "ui/events/base_event_utils.h"
+#include "ui/events/event.h"
+#include "ui/gfx/geometry/point.h"
 #include "ui/views/controls/button/checkbox.h"
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/button/label_button.h"
+#include "ui/views/metrics.h"
 #include "ui/views/style/platform_style.h"
 #include "ui/views/test/test_layout_provider.h"
 #include "ui/views/test/test_views.h"
@@ -505,4 +510,24 @@
   EXPECT_EQ(nullptr, focus_manager->GetFocusedView());
 }
 
+// Ensures that clicks are ignored for short time after view has been shown.
+TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks) {
+  widget()->Show();
+  SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
+
+  ui::MouseEvent mouse_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
+                             ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
+  client_view()->ButtonPressed(client_view()->ok_button(), mouse_event);
+  client_view()->ButtonPressed(client_view()->cancel_button(), mouse_event);
+  EXPECT_FALSE(widget()->IsClosed());
+
+  client_view()->ButtonPressed(
+      client_view()->cancel_button(),
+      ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
+                     ui::EventTimeForNow() + base::TimeDelta::FromMilliseconds(
+                                                 GetDoubleClickInterval()),
+                     ui::EF_NONE, ui::EF_NONE));
+  EXPECT_TRUE(widget()->IsClosed());
+}
+
 }  // namespace views