Reland "[serial] Implement tab indicator triggered by open()"

This is a reland of 0ca38034e8e6daca423130b7643dae169a60692b

The tests were flaking because the tab indicator is only removed after the
browser process receives the Mojo connection error. To fix this the tests are
now unit tests where we can make sure to run the necessary message loop to make
sure that this event has been delivered before checking.

Original change's description:
> [serial] Implement tab indicator triggered by open()
>
> As long as there is an active connection between a frame in a tab and a
> serial port the tab should display an indicator.
>
> This is accomplished by adding an additional Mojo pipe that is kept open
> between the device service and the browser processes for as long as the
> render process has a connection to the port.
>
> Bug: 917204
> Change-Id: I6441070f3473bd6b4a98dae98844a89ee4eb2329
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1546502
> Reviewed-by: Scott Violet <[email protected]>
> Reviewed-by: Dominick Ng <[email protected]>
> Reviewed-by: Yuri Wiitala <[email protected]>
> Commit-Queue: Reilly Grant <[email protected]>
> Cr-Commit-Position: refs/heads/master@{#647109}

Bug: 917204
Change-Id: I26fbb51845ae6b7ce2fe54bad2cc0a271ce0cbd0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1555508
Reviewed-by: Dominick Ng <[email protected]>
Reviewed-by: Scott Violet <[email protected]>
Commit-Queue: Reilly Grant <[email protected]>
Cr-Commit-Position: refs/heads/master@{#649154}
diff --git a/content/browser/serial/serial_browsertest.cc b/content/browser/serial/serial_browsertest.cc
index 361bd9d7..9c6c3368 100644
--- a/content/browser/serial/serial_browsertest.cc
+++ b/content/browser/serial/serial_browsertest.cc
@@ -7,6 +7,7 @@
 
 #include "base/command_line.h"
 #include "base/unguessable_token.h"
+#include "content/browser/serial/serial_test_utils.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/serial_chooser.h"
 #include "content/public/browser/serial_delegate.h"
@@ -14,6 +15,7 @@
 #include "content/public/test/content_browser_test.h"
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/shell/browser/shell.h"
+#include "services/device/public/cpp/test/fake_serial_port_manager.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -25,73 +27,6 @@
 
 namespace {
 
-class FakeSerialPortManager : public device::mojom::SerialPortManager {
- public:
-  FakeSerialPortManager() = default;
-  ~FakeSerialPortManager() override = default;
-
-  void AddPort(device::mojom::SerialPortInfoPtr port) {
-    base::UnguessableToken token = port->token;
-    ports_[token] = std::move(port);
-  }
-
-  // device::mojom::SerialPortManager
-  void GetDevices(GetDevicesCallback callback) override {
-    std::vector<device::mojom::SerialPortInfoPtr> ports;
-    for (const auto& map_entry : ports_)
-      ports.push_back(map_entry.second.Clone());
-    std::move(callback).Run(std::move(ports));
-  }
-
-  void GetPort(const base::UnguessableToken& token,
-               device::mojom::SerialPortRequest request) override {}
-
- private:
-  std::map<base::UnguessableToken, device::mojom::SerialPortInfoPtr> ports_;
-
-  DISALLOW_COPY_AND_ASSIGN(FakeSerialPortManager);
-};
-
-class MockSerialDelegate : public SerialDelegate {
- public:
-  MockSerialDelegate() = default;
-  ~MockSerialDelegate() override = default;
-
-  std::unique_ptr<SerialChooser> RunChooser(
-      RenderFrameHost* frame,
-      std::vector<blink::mojom::SerialPortFilterPtr> filters,
-      SerialChooser::Callback callback) override {
-    std::move(callback).Run(RunChooserInternal());
-    return nullptr;
-  }
-
-  MOCK_METHOD0(RunChooserInternal, device::mojom::SerialPortInfoPtr());
-  MOCK_METHOD2(HasPortPermission,
-               bool(content::RenderFrameHost* frame,
-                    const device::mojom::SerialPortInfo& port));
-  MOCK_METHOD1(
-      GetPortManager,
-      device::mojom::SerialPortManager*(content::RenderFrameHost* frame));
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(MockSerialDelegate);
-};
-
-class TestContentBrowserClient : public ContentBrowserClient {
- public:
-  TestContentBrowserClient() = default;
-  ~TestContentBrowserClient() override = default;
-
-  MockSerialDelegate& delegate() { return delegate_; }
-
-  SerialDelegate* GetSerialDelegate() override { return &delegate_; }
-
- private:
-  MockSerialDelegate delegate_;
-
-  DISALLOW_COPY_AND_ASSIGN(TestContentBrowserClient);
-};
-
 class SerialTest : public ContentBrowserTest {
  public:
   SerialTest() {
@@ -115,12 +50,12 @@
   }
 
   MockSerialDelegate& delegate() { return test_client_.delegate(); }
-  FakeSerialPortManager* port_manager() { return &port_manager_; }
+  device::FakeSerialPortManager* port_manager() { return &port_manager_; }
 
  private:
-  TestContentBrowserClient test_client_;
+  SerialTestContentBrowserClient test_client_;
   ContentBrowserClient* original_client_ = nullptr;
-  FakeSerialPortManager port_manager_;
+  device::FakeSerialPortManager port_manager_;
 };
 
 }  // namespace
@@ -140,15 +75,9 @@
       .WillOnce(Return(false))
       .WillOnce(Return(true));
 
-  int result;
-  EXPECT_TRUE(ExecuteScriptAndExtractInt(
-      shell(),
-      "navigator.serial.getPorts()"
-      "    .then(ports => {"
-      "        domAutomationController.send(ports.length);"
-      "    });",
-      &result));
-  EXPECT_EQ(2, result);
+  EXPECT_EQ(
+      2, EvalJs(shell(),
+                R"(navigator.serial.getPorts().then(ports => ports.length))"));
 }
 
 IN_PROC_BROWSER_TEST_F(SerialTest, RequestPort) {
@@ -159,17 +88,11 @@
   EXPECT_CALL(delegate(), RunChooserInternal)
       .WillOnce(Return(ByMove(std::move(port))));
 
-  bool result;
-  EXPECT_TRUE(
-      ExecuteScriptAndExtractBool(shell(),
-                                  "navigator.serial.requestPort({})"
-                                  "    .then(port => {"
-                                  "        domAutomationController.send(true);"
-                                  "    }, error => {"
-                                  "        domAutomationController.send(false);"
-                                  "    });",
-                                  &result));
-  EXPECT_TRUE(result);
+  EXPECT_EQ(true, EvalJs(shell(),
+                         R"((async () => {
+                           let port = await navigator.serial.requestPort({});
+                           return port instanceof SerialPort;
+                         })())"));
 }
 
 }  // namespace content
diff --git a/content/browser/serial/serial_service.cc b/content/browser/serial/serial_service.cc
index d62e9ece..e4b7c9d0 100644
--- a/content/browser/serial/serial_service.cc
+++ b/content/browser/serial/serial_service.cc
@@ -6,11 +6,15 @@
 
 #include <utility>
 
+#include "base/bind.h"
 #include "base/callback.h"
+#include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/serial_chooser.h"
 #include "content/public/browser/serial_delegate.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
 #include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom.h"
 
 namespace content {
@@ -36,9 +40,15 @@
     : render_frame_host_(render_frame_host) {
   DCHECK(render_frame_host_->IsFeatureEnabled(
       blink::mojom::FeaturePolicyFeature::kSerial));
+  watchers_.set_connection_error_handler(base::BindRepeating(
+      &SerialService::OnWatcherConnectionError, base::Unretained(this)));
 }
 
-SerialService::~SerialService() = default;
+SerialService::~SerialService() {
+  // The remaining watchers will be closed from this end.
+  if (!watchers_.empty())
+    DecrementActiveFrameCount();
+}
 
 void SerialService::Bind(blink::mojom::SerialServiceRequest request) {
   bindings_.AddBinding(this, std::move(request));
@@ -72,6 +82,24 @@
                      weak_factory_.GetWeakPtr(), std::move(callback)));
 }
 
+void SerialService::GetPort(const base::UnguessableToken& token,
+                            device::mojom::SerialPortRequest request) {
+  SerialDelegate* delegate = GetContentClient()->browser()->GetSerialDelegate();
+  if (!delegate)
+    return;
+
+  if (watchers_.empty()) {
+    auto* web_contents_impl = static_cast<WebContentsImpl*>(
+        WebContents::FromRenderFrameHost(render_frame_host_));
+    web_contents_impl->IncrementSerialActiveFrameCount();
+  }
+
+  device::mojom::SerialPortConnectionWatcherPtr watcher;
+  watchers_.AddBinding(this, mojo::MakeRequest(&watcher));
+  delegate->GetPortManager(render_frame_host_)
+      ->GetPort(token, std::move(request), std::move(watcher));
+}
+
 void SerialService::FinishGetPorts(
     GetPortsCallback callback,
     std::vector<device::mojom::SerialPortInfoPtr> ports) {
@@ -101,4 +129,15 @@
   std::move(callback).Run(ToBlinkType(*port));
 }
 
+void SerialService::OnWatcherConnectionError() {
+  if (watchers_.empty())
+    DecrementActiveFrameCount();
+}
+
+void SerialService::DecrementActiveFrameCount() {
+  auto* web_contents_impl = static_cast<WebContentsImpl*>(
+      WebContents::FromRenderFrameHost(render_frame_host_));
+  web_contents_impl->DecrementSerialActiveFrameCount();
+}
+
 }  // namespace content
diff --git a/content/browser/serial/serial_service.h b/content/browser/serial/serial_service.h
index 81c8048..12378db 100644
--- a/content/browser/serial/serial_service.h
+++ b/content/browser/serial/serial_service.h
@@ -19,7 +19,8 @@
 class RenderFrameHost;
 class SerialChooser;
 
-class SerialService : public blink::mojom::SerialService {
+class SerialService : public blink::mojom::SerialService,
+                      public device::mojom::SerialPortConnectionWatcher {
  public:
   explicit SerialService(RenderFrameHost* render_frame_host);
   ~SerialService() override;
@@ -30,12 +31,16 @@
   void GetPorts(GetPortsCallback callback) override;
   void RequestPort(std::vector<blink::mojom::SerialPortFilterPtr> filters,
                    RequestPortCallback callback) override;
+  void GetPort(const base::UnguessableToken& token,
+               device::mojom::SerialPortRequest request) override;
 
  private:
   void FinishGetPorts(GetPortsCallback callback,
                       std::vector<device::mojom::SerialPortInfoPtr> ports);
   void FinishRequestPort(RequestPortCallback callback,
                          device::mojom::SerialPortInfoPtr port);
+  void OnWatcherConnectionError();
+  void DecrementActiveFrameCount();
 
   RenderFrameHost* const render_frame_host_;
   mojo::BindingSet<blink::mojom::SerialService> bindings_;
@@ -43,6 +48,10 @@
   // The last shown serial port chooser UI.
   std::unique_ptr<SerialChooser> chooser_;
 
+  // Each pipe here watches a connection created by GetPort() in order to notify
+  // the WebContentsImpl when an active connection indicator should be shown.
+  mojo::BindingSet<device::mojom::SerialPortConnectionWatcher> watchers_;
+
   base::WeakPtrFactory<SerialService> weak_factory_{this};
 
   DISALLOW_COPY_AND_ASSIGN(SerialService);
diff --git a/content/browser/serial/serial_test_utils.cc b/content/browser/serial/serial_test_utils.cc
new file mode 100644
index 0000000..4b7f103
--- /dev/null
+++ b/content/browser/serial/serial_test_utils.cc
@@ -0,0 +1,33 @@
+// Copyright 2019 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 "content/browser/serial/serial_test_utils.h"
+
+#include <utility>
+
+#include "base/callback.h"
+
+namespace content {
+
+MockSerialDelegate::MockSerialDelegate() = default;
+
+MockSerialDelegate::~MockSerialDelegate() = default;
+
+std::unique_ptr<SerialChooser> MockSerialDelegate::RunChooser(
+    RenderFrameHost* frame,
+    std::vector<blink::mojom::SerialPortFilterPtr> filters,
+    SerialChooser::Callback callback) {
+  std::move(callback).Run(RunChooserInternal());
+  return nullptr;
+}
+
+SerialTestContentBrowserClient::SerialTestContentBrowserClient() = default;
+
+SerialTestContentBrowserClient::~SerialTestContentBrowserClient() = default;
+
+SerialDelegate* SerialTestContentBrowserClient::GetSerialDelegate() {
+  return &delegate_;
+}
+
+}  // namespace content
diff --git a/content/browser/serial/serial_test_utils.h b/content/browser/serial/serial_test_utils.h
new file mode 100644
index 0000000..cdc08c6
--- /dev/null
+++ b/content/browser/serial/serial_test_utils.h
@@ -0,0 +1,54 @@
+// Copyright 2019 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 CONTENT_BROWSER_SERIAL_SERIAL_TEST_UTILS_H_
+#define CONTENT_BROWSER_SERIAL_SERIAL_TEST_UTILS_H_
+
+#include "content/public/browser/content_browser_client.h"
+#include "content/public/browser/serial_delegate.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace content {
+
+class MockSerialDelegate : public SerialDelegate {
+ public:
+  MockSerialDelegate();
+  ~MockSerialDelegate() override;
+
+  std::unique_ptr<SerialChooser> RunChooser(
+      RenderFrameHost* frame,
+      std::vector<blink::mojom::SerialPortFilterPtr> filters,
+      SerialChooser::Callback callback) override;
+
+  MOCK_METHOD0(RunChooserInternal, device::mojom::SerialPortInfoPtr());
+  MOCK_METHOD2(HasPortPermission,
+               bool(content::RenderFrameHost* frame,
+                    const device::mojom::SerialPortInfo& port));
+  MOCK_METHOD1(
+      GetPortManager,
+      device::mojom::SerialPortManager*(content::RenderFrameHost* frame));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MockSerialDelegate);
+};
+
+class SerialTestContentBrowserClient : public ContentBrowserClient {
+ public:
+  SerialTestContentBrowserClient();
+  ~SerialTestContentBrowserClient() override;
+
+  MockSerialDelegate& delegate() { return delegate_; }
+
+  // ContentBrowserClient:
+  SerialDelegate* GetSerialDelegate() override;
+
+ private:
+  MockSerialDelegate delegate_;
+
+  DISALLOW_COPY_AND_ASSIGN(SerialTestContentBrowserClient);
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_SERIAL_SERIAL_TEST_UTILS_H_
diff --git a/content/browser/serial/serial_unittest.cc b/content/browser/serial/serial_unittest.cc
new file mode 100644
index 0000000..c29428dd
--- /dev/null
+++ b/content/browser/serial/serial_unittest.cc
@@ -0,0 +1,104 @@
+// Copyright 2019 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 "content/browser/serial/serial_test_utils.h"
+#include "content/public/common/content_client.h"
+#include "content/test/test_render_view_host.h"
+#include "content/test/test_web_contents.h"
+#include "services/device/public/cpp/test/fake_serial_port_manager.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+const char kTestUrl[] = "https://www.google.com";
+const char kCrossOriginTestUrl[] = "https://www.chromium.org";
+
+class SerialTest : public RenderViewHostImplTestHarness {
+ public:
+  SerialTest() {
+    ON_CALL(test_client_.delegate(), GetPortManager)
+        .WillByDefault(testing::Return(&port_manager_));
+  }
+
+  ~SerialTest() override = default;
+
+  void SetUp() override {
+    original_client_ = SetBrowserClientForTesting(&test_client_);
+    RenderViewHostTestHarness::SetUp();
+  }
+
+  void TearDown() override {
+    RenderViewHostTestHarness::TearDown();
+    if (original_client_)
+      SetBrowserClientForTesting(original_client_);
+  }
+
+  device::FakeSerialPortManager* port_manager() { return &port_manager_; }
+
+ private:
+  SerialTestContentBrowserClient test_client_;
+  ContentBrowserClient* original_client_ = nullptr;
+  device::FakeSerialPortManager port_manager_;
+
+  DISALLOW_COPY_AND_ASSIGN(SerialTest);
+};
+
+}  // namespace
+
+TEST_F(SerialTest, OpenAndClosePort) {
+  NavigateAndCommit(GURL(kTestUrl));
+
+  blink::mojom::SerialServicePtr service;
+  contents()->GetMainFrame()->BinderRegistryForTesting().BindInterface(
+      blink::mojom::SerialService::Name_,
+      mojo::MakeRequest(&service).PassMessagePipe());
+
+  auto token = base::UnguessableToken::Create();
+  auto port_info = device::mojom::SerialPortInfo::New();
+  port_info->token = token;
+  port_manager()->AddPort(std::move(port_info));
+
+  EXPECT_FALSE(contents()->IsConnectedToSerialPort());
+
+  device::mojom::SerialPortPtr port;
+  service->GetPort(token, mojo::MakeRequest(&port));
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(contents()->IsConnectedToSerialPort());
+
+  port.reset();
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(contents()->IsConnectedToSerialPort());
+}
+
+TEST_F(SerialTest, OpenAndNavigateCrossOrigin) {
+  NavigateAndCommit(GURL(kTestUrl));
+
+  blink::mojom::SerialServicePtr service;
+  contents()->GetMainFrame()->BinderRegistryForTesting().BindInterface(
+      blink::mojom::SerialService::Name_,
+      mojo::MakeRequest(&service).PassMessagePipe());
+
+  auto token = base::UnguessableToken::Create();
+  auto port_info = device::mojom::SerialPortInfo::New();
+  port_info->token = token;
+  port_manager()->AddPort(std::move(port_info));
+
+  EXPECT_FALSE(contents()->IsConnectedToSerialPort());
+
+  device::mojom::SerialPortPtr port;
+  service->GetPort(token, mojo::MakeRequest(&port));
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(contents()->IsConnectedToSerialPort());
+
+  NavigateAndCommit(GURL(kCrossOriginTestUrl));
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(contents()->IsConnectedToSerialPort());
+  port.FlushForTesting();
+  EXPECT_TRUE(port.encountered_error());
+}
+
+}  // namespace content
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index a012751..c319c1a 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -583,7 +583,6 @@
           GetContentClient()->browser()->GetAXModeForBrowserContext(
               browser_context)),
       audio_stream_monitor_(this),
-      bluetooth_connected_device_count_(0),
       media_web_contents_observer_(
           std::make_unique<MediaWebContentsObserver>(this)),
       media_device_group_id_salt_base_(
@@ -1542,6 +1541,10 @@
   return bluetooth_connected_device_count_ > 0;
 }
 
+bool WebContentsImpl::IsConnectedToSerialPort() const {
+  return serial_active_frame_count_ > 0;
+}
+
 bool WebContentsImpl::HasPictureInPictureVideo() {
   return has_picture_in_picture_video_;
 }
@@ -6713,6 +6716,31 @@
   }
 }
 
+void WebContentsImpl::IncrementSerialActiveFrameCount() {
+  // Trying to invalidate the tab state while being destroyed could result in a
+  // use after free.
+  if (IsBeingDestroyed())
+    return;
+
+  // Notify for UI updates if the state changes.
+  serial_active_frame_count_++;
+  if (serial_active_frame_count_ == 1)
+    NotifyNavigationStateChanged(INVALIDATE_TYPE_TAB);
+}
+
+void WebContentsImpl::DecrementSerialActiveFrameCount() {
+  // Trying to invalidate the tab state while being destroyed could result in a
+  // use after free.
+  if (IsBeingDestroyed())
+    return;
+
+  // Notify for UI updates if the state changes.
+  DCHECK_NE(0u, serial_active_frame_count_);
+  serial_active_frame_count_--;
+  if (serial_active_frame_count_ == 0)
+    NotifyNavigationStateChanged(INVALIDATE_TYPE_TAB);
+}
+
 void WebContentsImpl::SetHasPersistentVideo(bool has_persistent_video) {
   if (has_persistent_video_ == has_persistent_video)
     return;
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index f2eeeea..d2c789a 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -344,6 +344,7 @@
   void SetAudioMuted(bool mute) override;
   bool IsCurrentlyAudible() override;
   bool IsConnectedToBluetoothDevice() override;
+  bool IsConnectedToSerialPort() const override;
   bool HasPictureInPictureVideo() override;
   bool IsCrashed() override;
   void SetIsCrashed(base::TerminationStatus status, int error_code) override;
@@ -935,6 +936,11 @@
   void IncrementBluetoothConnectedDeviceCount();
   void DecrementBluetoothConnectedDeviceCount();
 
+  // Modify the counter of frames in this WebContents actively using serial
+  // ports.
+  void IncrementSerialActiveFrameCount();
+  void DecrementSerialActiveFrameCount();
+
   // Called when the WebContents gains or loses a persistent video.
   void SetHasPersistentVideo(bool has_persistent_video);
 
@@ -1754,7 +1760,8 @@
   // Created on-demand to mute all audio output from this WebContents.
   std::unique_ptr<WebContentsAudioMuter> audio_muter_;
 
-  size_t bluetooth_connected_device_count_;
+  size_t bluetooth_connected_device_count_ = 0;
+  size_t serial_active_frame_count_ = 0;
 
   bool has_picture_in_picture_video_ = false;