Show bubbles for ExtensionsToolbarContainer

Fixes loading of extensions that require activation and page reload to
work. Showing a confirmation bubble is necessary for a user to be able
to accept reloading the page so that the extension can inject on page
load.

Bug: chromium:959930
Change-Id: I7a0563eb5cb5fe46496b8ea12ce30d68d1467641
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1641550
Commit-Queue: Peter Boström <[email protected]>
Reviewed-by: Devlin <[email protected]>
Reviewed-by: Elly Fong-Jones <[email protected]>
Cr-Commit-Position: refs/heads/master@{#669367}
diff --git a/chrome/browser/extensions/extension_action_runner.cc b/chrome/browser/extensions/extension_action_runner.cc
index eb0d4128..ced1a58b 100644
--- a/chrome/browser/extensions/extension_action_runner.cc
+++ b/chrome/browser/extensions/extension_action_runner.cc
@@ -373,18 +373,18 @@
     const base::Callback<void(ToolbarActionsBarBubbleDelegate::CloseAction)>&
         callback) {
   Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
-  ToolbarActionsBar* toolbar_actions_bar =
-      browser ? browser->window()->GetToolbarActionsBar() : nullptr;
-  if (toolbar_actions_bar) {
-    if (default_bubble_close_action_for_testing_) {
-      base::ThreadTaskRunnerHandle::Get()->PostTask(
-          FROM_HERE,
-          base::BindOnce(callback, *default_bubble_close_action_for_testing_));
-    } else {
-      toolbar_actions_bar->ShowToolbarActionBubble(
-          std::make_unique<BlockedActionBubbleDelegate>(callback,
-                                                        extension->id()));
-    }
+  ExtensionsContainer* const extensions_container =
+      browser ? browser->window()->GetExtensionsContainer() : nullptr;
+  if (!extensions_container)
+    return;
+  if (default_bubble_close_action_for_testing_) {
+    base::ThreadTaskRunnerHandle::Get()->PostTask(
+        FROM_HERE,
+        base::BindOnce(callback, *default_bubble_close_action_for_testing_));
+  } else {
+    extensions_container->ShowToolbarActionBubble(
+        std::make_unique<BlockedActionBubbleDelegate>(callback,
+                                                      extension->id()));
   }
 }
 
diff --git a/chrome/browser/ui/browser_window.h b/chrome/browser/ui/browser_window.h
index 3acdf53..f0c296f 100644
--- a/chrome/browser/ui/browser_window.h
+++ b/chrome/browser/ui/browser_window.h
@@ -35,6 +35,7 @@
 class Browser;
 class DownloadShelf;
 class ExclusiveAccessContext;
+class ExtensionsContainer;
 class FindBar;
 class GURL;
 class LocationBar;
@@ -276,8 +277,13 @@
   virtual void FocusToolbar() = 0;
 
   // Returns the ToolbarActionsBar associated with the window, if any.
+  // TODO(pbos): Replace usages of |GetToolbarActionsBar| with
+  // |GetExtensionsContainer|.
   virtual ToolbarActionsBar* GetToolbarActionsBar() = 0;
 
+  // Returns the ExtensionsContainer associated with the window, if any.
+  virtual ExtensionsContainer* GetExtensionsContainer() = 0;
+
   // Called from toolbar subviews during their show/hide animations.
   virtual void ToolbarSizeChanged(bool is_animating) = 0;
 
diff --git a/chrome/browser/ui/extensions/extensions_container.h b/chrome/browser/ui/extensions/extensions_container.h
index 1f54184e0..46d643e 100644
--- a/chrome/browser/ui/extensions/extensions_container.h
+++ b/chrome/browser/ui/extensions/extensions_container.h
@@ -10,6 +10,7 @@
 #include "base/callback_forward.h"
 
 class ToolbarActionViewController;
+class ToolbarActionsBarBubbleDelegate;
 
 // An interface for containers in the toolbar that host extensions.
 class ExtensionsContainer {
@@ -48,6 +49,14 @@
   virtual void PopOutAction(ToolbarActionViewController* action,
                             bool is_sticky,
                             const base::Closure& closure) = 0;
+
+  // Displays the given |bubble| once the toolbar is no longer animating.
+  virtual void ShowToolbarActionBubble(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) = 0;
+
+  // Same as above, but uses PostTask() in all cases.
+  virtual void ShowToolbarActionBubbleAsync(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) = 0;
 };
 
 #endif  // CHROME_BROWSER_UI_EXTENSIONS_EXTENSIONS_CONTAINER_H_
diff --git a/chrome/browser/ui/toolbar/toolbar_actions_bar.h b/chrome/browser/ui/toolbar/toolbar_actions_bar.h
index e46bf85..8ceb350c 100644
--- a/chrome/browser/ui/toolbar/toolbar_actions_bar.h
+++ b/chrome/browser/ui/toolbar/toolbar_actions_bar.h
@@ -193,13 +193,6 @@
   void AddObserver(ToolbarActionsBarObserver* observer);
   void RemoveObserver(ToolbarActionsBarObserver* observer);
 
-  // Displays the given |bubble| once the toolbar is no longer animating.
-  void ShowToolbarActionBubble(
-      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble);
-  // Same as above, but uses PostTask() in all cases.
-  void ShowToolbarActionBubbleAsync(
-      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble);
-
   // Returns the underlying toolbar actions, but does not order them. Primarily
   // for use in testing.
   const ToolbarActions& toolbar_actions_unordered() const {
@@ -248,6 +241,10 @@
   void PopOutAction(ToolbarActionViewController* action,
                     bool is_sticky,
                     const base::Closure& closure) override;
+  void ShowToolbarActionBubble(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) override;
+  void ShowToolbarActionBubbleAsync(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) override;
 
  private:
   // Returns the insets by which the icon area bounds (See GetIconAreaRect())
diff --git a/chrome/browser/ui/views/extensions/extensions_menu_view_browsertest.cc b/chrome/browser/ui/views/extensions/extensions_menu_view_browsertest.cc
index 64368f8..50c92f8 100644
--- a/chrome/browser/ui/views/extensions/extensions_menu_view_browsertest.cc
+++ b/chrome/browser/ui/views/extensions/extensions_menu_view_browsertest.cc
@@ -9,17 +9,24 @@
 #include "base/path_service.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/extensions/chrome_test_extension_loader.h"
+#include "chrome/browser/extensions/extension_action_runner.h"
+#include "chrome/browser/extensions/scripting_permissions_modifier.h"
 #include "chrome/browser/ui/test/test_browser_dialog.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/extensions/extensions_menu_button.h"
 #include "chrome/browser/ui/views/extensions/extensions_toolbar_button.h"
 #include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h"
 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/webui_url_constants.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
+#include "net/dns/mock_host_resolver.h"
 #include "ui/views/test/button_test_api.h"
 #include "ui/views/test/widget_test.h"
+#include "ui/views/window/dialog_client_view.h"
 
 class ExtensionsMenuViewBrowserTest : public DialogBrowserTest {
  protected:
@@ -36,6 +43,11 @@
     DialogBrowserTest::SetUp();
   }
 
+  void SetUpOnMainThread() override {
+    DialogBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("*", "127.0.0.1");
+  }
+
   void ShowUi(const std::string& name) override {
     ui::MouseEvent click_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
                                base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, 0);
@@ -130,7 +142,7 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionsMenuViewBrowserTest,
-                       TriggeringExtensionClosesMenu) {
+                       ActivationWithReloadNeeded_Accept) {
   LoadTestExtension("extensions/trigger_actions/browser_action");
   ShowUi("");
   VerifyUi();
@@ -186,5 +198,68 @@
       browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL());
 }
 
+class ActivateWithReloadExtensionsMenuBrowserTest
+    : public ExtensionsMenuViewBrowserTest,
+      public ::testing::WithParamInterface<bool> {};
+
+IN_PROC_BROWSER_TEST_P(ActivateWithReloadExtensionsMenuBrowserTest,
+                       ActivateWithReload) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  LoadTestExtension("extensions/blocked_actions/content_scripts");
+  auto extension = extensions_.back();
+  extensions::ScriptingPermissionsModifier modifier(browser()->profile(),
+                                                    extension);
+  modifier.SetWithholdHostPermissions(true);
+
+  ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("example.com", "/empty.html"));
+
+  ShowUi("");
+  VerifyUi();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  extensions::ExtensionActionRunner* action_runner =
+      extensions::ExtensionActionRunner::GetForWebContents(web_contents);
+
+  EXPECT_TRUE(action_runner->WantsToRun(extension.get()));
+
+  TriggerSingleExtensionButton();
+
+  auto* const action_bubble = BrowserView::GetBrowserViewForBrowser(browser())
+                                  ->toolbar()
+                                  ->extensions_container()
+                                  ->action_bubble_public_for_testing();
+  ASSERT_TRUE(action_bubble);
+
+  views::DialogClientView* const dialog_client_view =
+      action_bubble->GetDialogClientView();
+
+  const bool accept_reload_dialog = GetParam();
+  if (accept_reload_dialog) {
+    content::TestNavigationObserver observer(web_contents);
+    dialog_client_view->AcceptWindow();
+    EXPECT_TRUE(web_contents->IsLoading());
+    // Wait for reload to finish.
+    observer.WaitForNavigationFinished();
+    EXPECT_TRUE(observer.last_navigation_succeeded());
+    // After reload the extension should be allowed to run.
+    EXPECT_FALSE(action_runner->WantsToRun(extension.get()));
+  } else {
+    dialog_client_view->CancelWindow();
+    EXPECT_FALSE(web_contents->IsLoading());
+    EXPECT_TRUE(action_runner->WantsToRun(extension.get()));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(AcceptDialog,
+                         ActivateWithReloadExtensionsMenuBrowserTest,
+                         testing::Values(true));
+
+INSTANTIATE_TEST_SUITE_P(CancelDialog,
+                         ActivateWithReloadExtensionsMenuBrowserTest,
+                         testing::Values(false));
+
 // TODO(pbos): Add test coverage that makes sure removing popped-out extensions
 // properly disposes of the popup.
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_container.cc b/chrome/browser/ui/views/extensions/extensions_toolbar_container.cc
index b8676d1..d963686 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_container.cc
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_container.cc
@@ -9,6 +9,8 @@
 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
 #include "chrome/browser/ui/views/extensions/extensions_menu_view.h"
 #include "chrome/browser/ui/views/extensions/extensions_toolbar_button.h"
+#include "chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h"
+#include "ui/views/widget/widget_observer.h"
 
 ExtensionsToolbarContainer::ExtensionsToolbarContainer(Browser* browser)
     : ToolbarIconContainerView(/*uses_highlight=*/true),
@@ -21,7 +23,13 @@
   CreateActions();
 }
 
-ExtensionsToolbarContainer::~ExtensionsToolbarContainer() = default;
+ExtensionsToolbarContainer::~ExtensionsToolbarContainer() {
+  if (active_bubble_)
+    active_bubble_->GetWidget()->Close();
+  // We should synchronously receive the OnWidgetClosing() event, so we should
+  // always have cleared the active bubble by now.
+  DCHECK(!active_bubble_);
+}
 
 void ExtensionsToolbarContainer::UpdateAllIcons() {
   extensions_button_->UpdateIcon();
@@ -44,7 +52,9 @@
 bool ExtensionsToolbarContainer::IsActionVisibleOnToolbar(
     const ToolbarActionViewController* action) const {
   return model_->IsActionPinned(action->GetId()) ||
-         action == popped_out_action_;
+         action == popped_out_action_ ||
+         (active_bubble_ &&
+          action->GetId() == active_bubble_->GetAnchorActionId());
 }
 
 void ExtensionsToolbarContainer::UndoPopOut() {
@@ -92,6 +102,34 @@
   closure.Run();
 }
 
+void ExtensionsToolbarContainer::ShowToolbarActionBubble(
+    std::unique_ptr<ToolbarActionsBarBubbleDelegate> controller) {
+  // TODO(pbos): Make sure we finish animations before showing the bubble.
+
+  auto iter = icons_.find(controller->GetAnchorActionId());
+
+  views::View* const anchor_view = iter != icons_.end()
+                                       ? static_cast<View*>(iter->second.get())
+                                       : extensions_button_;
+
+  anchor_view->SetVisible(true);
+
+  active_bubble_ = new ToolbarActionsBarBubbleViews(
+      anchor_view, gfx::Point(), anchor_view != extensions_button_,
+      std::move(controller));
+  views::BubbleDialogDelegateView::CreateBubble(active_bubble_)
+      ->AddObserver(this);
+  active_bubble_->Show();
+}
+
+void ExtensionsToolbarContainer::ShowToolbarActionBubbleAsync(
+    std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) {
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&ExtensionsToolbarContainer::ShowToolbarActionBubble,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(bubble)));
+}
+
 void ExtensionsToolbarContainer::OnToolbarActionAdded(
     const ToolbarActionsModel::ActionId& action_id,
     int index) {
@@ -219,3 +257,27 @@
   // TODO(pbos): Implement
   return false;
 }
+
+void ExtensionsToolbarContainer::OnWidgetClosing(views::Widget* widget) {
+  ClearActiveBubble(widget);
+}
+
+void ExtensionsToolbarContainer::OnWidgetDestroying(views::Widget* widget) {
+  ClearActiveBubble(widget);
+}
+
+void ExtensionsToolbarContainer::ClearActiveBubble(views::Widget* widget) {
+  DCHECK(active_bubble_);
+  DCHECK_EQ(active_bubble_->GetWidget(), widget);
+  ToolbarActionViewController* const action =
+      GetActionForId(active_bubble_->GetAnchorActionId());
+  // TODO(pbos): Note that this crashes if a bubble anchors to the menu and not
+  // to an extension that gets popped out. This should be fixed, but a test
+  // should first be added to make sure that it's covered.
+  CHECK(action);
+  active_bubble_ = nullptr;
+  widget->RemoveObserver(this);
+  // Note that we only hide this view if it's not visible for other reasons
+  // than displaying the bubble.
+  icons_[action->GetId()]->SetVisible(IsActionVisibleOnToolbar(action));
+}
diff --git a/chrome/browser/ui/views/extensions/extensions_toolbar_container.h b/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
index 919e980..3998230 100644
--- a/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
+++ b/chrome/browser/ui/views/extensions/extensions_toolbar_container.h
@@ -9,6 +9,7 @@
 #include <memory>
 #include <vector>
 
+#include "base/optional.h"
 #include "chrome/browser/ui/extensions/extensions_container.h"
 #include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
 #include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"
@@ -17,6 +18,7 @@
 class Browser;
 class ExtensionsToolbarButton;
 class ToolbarActionViewController;
+class ToolbarActionsBarBubbleViews;
 
 // Container for extensions shown in the toolbar. These include pinned
 // extensions and extensions that are 'popped out' transitively to show dialogs
@@ -29,7 +31,8 @@
 class ExtensionsToolbarContainer : public ToolbarIconContainerView,
                                    public ExtensionsContainer,
                                    public ToolbarActionsModel::Observer,
-                                   public ToolbarActionView::Delegate {
+                                   public ToolbarActionView::Delegate,
+                                   public views::WidgetObserver {
  public:
   explicit ExtensionsToolbarContainer(Browser* browser);
   ~ExtensionsToolbarContainer() override;
@@ -38,6 +41,10 @@
     return extensions_button_;
   }
 
+  ToolbarActionsBarBubbleViews* action_bubble_public_for_testing() {
+    return active_bubble_;
+  }
+
   // ToolbarIconContainerView:
   void UpdateAllIcons() override;
 
@@ -54,6 +61,9 @@
   // popped out actions, extensions button).
   void ReorderViews();
 
+  // Clears the |active_bubble_|, and unregisters the container as an observer.
+  void ClearActiveBubble(views::Widget* widget);
+
   // ExtensionsContainer:
   ToolbarActionViewController* GetActionForId(
       const std::string& action_id) override;
@@ -67,6 +77,10 @@
   void PopOutAction(ToolbarActionViewController* action,
                     bool is_sticky,
                     const base::Closure& closure) override;
+  void ShowToolbarActionBubble(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) override;
+  void ShowToolbarActionBubbleAsync(
+      std::unique_ptr<ToolbarActionsBarBubbleDelegate> bubble) override;
 
   // ToolbarActionsModel::Observer:
   void OnToolbarActionAdded(const ToolbarActionsModel::ActionId& action_id,
@@ -97,6 +111,10 @@
                            const gfx::Point& press_pt,
                            const gfx::Point& p) override;
 
+  // views::WidgetObserver:
+  void OnWidgetClosing(views::Widget* widget) override;
+  void OnWidgetDestroying(views::Widget* widget) override;
+
   Browser* const browser_;
   ToolbarActionsModel* const model_;
   ScopedObserver<ToolbarActionsModel, ToolbarActionsModel::Observer>
@@ -116,6 +134,11 @@
   // The action that triggered the current popup, if any.
   ToolbarActionViewController* popup_owner_ = nullptr;
 
+  // The extension bubble that is actively showing, if any.
+  ToolbarActionsBarBubbleViews* active_bubble_ = nullptr;
+
+  base::WeakPtrFactory<ExtensionsToolbarContainer> weak_ptr_factory_{this};
+
   DISALLOW_COPY_AND_ASSIGN(ExtensionsToolbarContainer);
 };
 
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index c90d55c..77939df 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -83,6 +83,7 @@
 #include "chrome/browser/ui/views/download/download_shelf_view.h"
 #include "chrome/browser/ui/views/exclusive_access_bubble_views.h"
 #include "chrome/browser/ui/views/extensions/extension_keybinding_registry_views.h"
+#include "chrome/browser/ui/views/extensions/extensions_toolbar_container.h"
 #include "chrome/browser/ui/views/find_bar_host.h"
 #include "chrome/browser/ui/views/frame/app_menu_button.h"
 #include "chrome/browser/ui/views/frame/browser_view_layout.h"
@@ -1167,6 +1168,12 @@
   return container ? container->toolbar_actions_bar() : nullptr;
 }
 
+ExtensionsContainer* BrowserView::GetExtensionsContainer() {
+  if (toolbar_ && toolbar_->extensions_container())
+    return toolbar_->extensions_container();
+  return GetToolbarActionsBar();
+}
+
 void BrowserView::ToolbarSizeChanged(bool is_animating) {
   if (is_animating)
     contents_web_view_->SetFastResize(true);
diff --git a/chrome/browser/ui/views/frame/browser_view.h b/chrome/browser/ui/views/frame/browser_view.h
index fcfaa4fa..b2ddd63 100644
--- a/chrome/browser/ui/views/frame/browser_view.h
+++ b/chrome/browser/ui/views/frame/browser_view.h
@@ -347,6 +347,7 @@
   void ResetToolbarTabState(content::WebContents* contents) override;
   void FocusToolbar() override;
   ToolbarActionsBar* GetToolbarActionsBar() override;
+  ExtensionsContainer* GetExtensionsContainer() override;
   void ToolbarSizeChanged(bool is_animating) override;
   void TabDraggingStatusChanged(bool is_dragging) override;
   void FocusAppMenu() override;
diff --git a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.cc b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.cc
index f6e7ff6..741dc25 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.cc
+++ b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.cc
@@ -54,6 +54,10 @@
   GetWidget()->Show();
 }
 
+std::string ToolbarActionsBarBubbleViews::GetAnchorActionId() {
+  return delegate_->GetAnchorActionId();
+}
+
 std::unique_ptr<views::View> ToolbarActionsBarBubbleViews::CreateExtraView() {
   std::unique_ptr<ToolbarActionsBarBubbleDelegate::ExtraViewInfo>
       extra_view_info = delegate_->GetExtraViewInfo();
diff --git a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h
index 89cb1c8..1e105f7 100644
--- a/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h
+++ b/chrome/browser/ui/views/toolbar/toolbar_actions_bar_bubble_views.h
@@ -32,6 +32,7 @@
   ~ToolbarActionsBarBubbleViews() override;
 
   void Show();
+  std::string GetAnchorActionId();
 
   const views::Label* body_text() const { return body_text_; }
   const views::Label* item_list() const { return item_list_; }