[Cast Receiver] Move WebContentsObserver Functionality To New Class

This CL updates the WebRuntimeApplication class by splitting half of its
WebContentsObserver functionality out to a new class, to be re-used by
other cast_receiver classes.

Bug: 1357135, 1359559, 1359578
Change-Id: If1f6c371f53b822dfb60d0cf314bdfbde020fdb2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3875999
Reviewed-by: Avi Drissman <avi@chromium.org>
Reviewed-by: Vigen Issahhanjan <vigeni@google.com>
Reviewed-by: Matt Mueller <mattm@chromium.org>
Commit-Queue: Ryan Keane <rwkeane@google.com>
Cr-Commit-Position: refs/heads/main@{#1044173}
diff --git a/chromecast/cast_core/runtime/browser/BUILD.gn b/chromecast/cast_core/runtime/browser/BUILD.gn
index d70919a..eba6523 100644
--- a/chromecast/cast_core/runtime/browser/BUILD.gn
+++ b/chromecast/cast_core/runtime/browser/BUILD.gn
@@ -180,6 +180,7 @@
     "//chromecast/browser:browser_base",
     "//chromecast/common:feature_constants",
     "//components/cast_receiver/browser",
+    "//components/cast_receiver/browser:page_state_observer",
     "//components/cast_streaming/public/mojom",
     "//components/url_rewrite/browser",
     "//third_party/abseil-cpp:absl",
diff --git a/chromecast/cast_core/runtime/browser/DEPS b/chromecast/cast_core/runtime/browser/DEPS
index 59e20a4..00458a5b 100644
--- a/chromecast/cast_core/runtime/browser/DEPS
+++ b/chromecast/cast_core/runtime/browser/DEPS
@@ -10,7 +10,7 @@
   "+chromecast/service",
   "+chromecast/shared",
   "+components/cast",
-  "+components/cast_receiver/browser/public",
+  "+components/cast_receiver/browser",
   "+components/cast_streaming/browser",
   "+components/cast_streaming/public",
   "+components/guest_view/browser",
diff --git a/chromecast/cast_core/runtime/browser/web_runtime_application.cc b/chromecast/cast_core/runtime/browser/web_runtime_application.cc
index 98fccb6e..8680da8 100644
--- a/chromecast/cast_core/runtime/browser/web_runtime_application.cc
+++ b/chromecast/cast_core/runtime/browser/web_runtime_application.cc
@@ -112,69 +112,6 @@
   NotifyMediaPlaybackChanged(false);
 }
 
-void WebRuntimeApplication::DidFinishLoad(
-    content::RenderFrameHost* render_frame_host,
-    const GURL& validated_url) {
-  // This logic is a subset of that for DidFinishLoad() in CastWebContentsImpl.
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  int http_status_code = 0;
-  content::NavigationEntry* nav_entry =
-      web_contents()->GetController().GetVisibleEntry();
-  if (nav_entry) {
-    http_status_code = nav_entry->GetHttpStatusCode();
-  }
-
-  if (http_status_code != 0 && http_status_code / 100 != 2) {
-    DLOG(INFO) << "Stopping after receiving http failure status code: "
-               << http_status_code;
-    StopApplication(cast::common::StopReason::HTTP_ERROR,
-                    net::ERR_HTTP_RESPONSE_CODE_FAILURE);
-    return;
-  }
-
-  OnPageLoaded();
-}
-
-void WebRuntimeApplication::DidFailLoad(
-    content::RenderFrameHost* render_frame_host,
-    const GURL& validated_url,
-    int error_code) {
-  // This logic is a subset of that for DidFailLoad() in CastWebContentsImpl.
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  if (render_frame_host->GetParent()) {
-    DLOG(ERROR) << "Got error on sub-iframe: url=" << validated_url.spec()
-                << ", error=" << error_code;
-    return;
-  }
-  if (error_code == net::ERR_ABORTED) {
-    // ERR_ABORTED means download was aborted by the app, typically this happens
-    // when flinging URL for direct playback, the initial URLRequest gets
-    // cancelled/aborted and then the same URL is requested via the buffered
-    // data source for media::Pipeline playback.
-    DLOG(INFO) << "Load canceled: url=" << validated_url.spec();
-
-    // We consider the page to be fully loaded in this case, since the app has
-    // intentionally entered this state. If the app wanted to stop, it would
-    // have called window.close() instead.
-    OnPageLoaded();
-    return;
-  }
-
-  StopApplication(cast::common::StopReason::HTTP_ERROR, error_code);
-}
-
-void WebRuntimeApplication::WebContentsDestroyed() {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  content::WebContentsObserver::Observe(nullptr);
-  StopApplication(cast::common::StopReason::APPLICATION_REQUEST, net::OK);
-}
-
-void WebRuntimeApplication::PrimaryMainFrameRenderProcessGone(
-    base::TerminationStatus status) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  StopApplication(cast::common::StopReason::HTTP_ERROR, net::ERR_UNEXPECTED);
-}
-
 void WebRuntimeApplication::OnAllBindingsReceived(
     cast::utils::GrpcStatusOr<cast::bindings::GetAllResponse> response_or) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -185,6 +122,8 @@
   }
 
   content::WebContentsObserver::Observe(GetCastWebContents()->web_contents());
+  cast_receiver::PageStateObserver::Observe(
+      GetCastWebContents()->web_contents());
   bindings_manager_ =
       std::make_unique<BindingsManagerWebRuntime>(core_message_port_app_stub());
   for (int i = 0; i < response_or->bindings_size(); ++i) {
@@ -198,4 +137,26 @@
   LoadPage(app_url_);
 }
 
+void WebRuntimeApplication::OnPageLoadComplete() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  OnPageLoaded();
+}
+
+void WebRuntimeApplication::OnPageStopped(StopReason reason,
+                                          int32_t error_code) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  switch (reason) {
+    case cast_receiver::PageStateObserver::StopReason::kUnknown:
+      StopApplication(cast::common::StopReason::RUNTIME_ERROR, error_code);
+      break;
+    case cast_receiver::PageStateObserver::StopReason::kApplicationRequest:
+      StopApplication(cast::common::StopReason::APPLICATION_REQUEST,
+                      error_code);
+      break;
+    case cast_receiver::PageStateObserver::StopReason::kHttpError:
+      StopApplication(cast::common::StopReason::HTTP_ERROR, error_code);
+      break;
+  }
+}
+
 }  // namespace chromecast
diff --git a/chromecast/cast_core/runtime/browser/web_runtime_application.h b/chromecast/cast_core/runtime/browser/web_runtime_application.h
index 959d8e19..6f5ab71 100644
--- a/chromecast/cast_core/runtime/browser/web_runtime_application.h
+++ b/chromecast/cast_core/runtime/browser/web_runtime_application.h
@@ -5,7 +5,9 @@
 #ifndef CHROMECAST_CAST_CORE_RUNTIME_BROWSER_WEB_RUNTIME_APPLICATION_H_
 #define CHROMECAST_CAST_CORE_RUNTIME_BROWSER_WEB_RUNTIME_APPLICATION_H_
 
+#include "chromecast/cast_core/runtime/browser/bindings_manager_web_runtime.h"
 #include "chromecast/cast_core/runtime/browser/runtime_application_base.h"
+#include "components/cast_receiver/browser/page_state_observer.h"
 #include "content/public/browser/web_contents_observer.h"
 
 namespace chromecast {
@@ -14,7 +16,8 @@
 class CastWebService;
 
 class WebRuntimeApplication final : public RuntimeApplicationBase,
-                                    public content::WebContentsObserver {
+                                    public content::WebContentsObserver,
+                                    public cast_receiver::PageStateObserver {
  public:
   // |web_service| is expected to exist for the lifetime of this instance.
   WebRuntimeApplication(std::string cast_session_id,
@@ -24,12 +27,19 @@
   ~WebRuntimeApplication() override;
 
  private:
+  void OnAllBindingsReceived(
+      cast::utils::GrpcStatusOr<cast::bindings::GetAllResponse> response_or);
+
   // RuntimeApplicationBase implementation:
   cast::utils::GrpcStatusOr<cast::web::MessagePortStatus> HandlePortMessage(
       cast::web::Message message) override;
   void LaunchApplication() override;
   bool IsStreamingApplication() const override;
 
+  // cast_receiver::PageStateObserver implementation:
+  void OnPageLoadComplete() override;
+  void OnPageStopped(StopReason reason, int32_t error_code) override;
+
   // content::WebContentsObserver implementation:
   void InnerWebContentsCreated(
       content::WebContents* inner_web_contents) override;
@@ -39,17 +49,6 @@
       const MediaPlayerInfo& video_type,
       const content::MediaPlayerId& id,
       content::WebContentsObserver::MediaStoppedReason reason) override;
-  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
-                     const GURL& validated_url) override;
-  void DidFailLoad(content::RenderFrameHost* render_frame_host,
-                   const GURL& validated_url,
-                   int error_code) override;
-  void WebContentsDestroyed() override;
-  void PrimaryMainFrameRenderProcessGone(
-      base::TerminationStatus status) override;
-
-  void OnAllBindingsReceived(
-      cast::utils::GrpcStatusOr<cast::bindings::GetAllResponse> response_or);
 
   const GURL app_url_;
   std::unique_ptr<BindingsManagerWebRuntime> bindings_manager_;
diff --git a/components/cast_receiver/browser/BUILD.gn b/components/cast_receiver/browser/BUILD.gn
index 15e2ddb..8affd5c 100644
--- a/components/cast_receiver/browser/BUILD.gn
+++ b/components/cast_receiver/browser/BUILD.gn
@@ -13,10 +13,24 @@
   friend = [ ":unit_tests" ]
 }
 
+# TODO(crbug.com/1359579): Limit visibility to just this component.
+source_set("page_state_observer") {
+  deps = [
+    "//base",
+    "//content",
+    "//net",
+  ]
+  sources = [
+    "page_state_observer.cc",
+    "page_state_observer.h",
+  ]
+}
+
 source_set("unit_tests") {
   testonly = true
   deps = [
     ":browser",
+    ":page_state_observer",
     "//media",
     "//testing/gmock",
     "//testing/gtest",
diff --git a/components/cast_receiver/browser/DEPS b/components/cast_receiver/browser/DEPS
index 1931639e9..656bcd8 100644
--- a/components/cast_receiver/browser/DEPS
+++ b/components/cast_receiver/browser/DEPS
@@ -1,4 +1,6 @@
 include_rules = [
+  "+content/public/browser",
   "+media",
+  "+net/base",
   "+ui/gfx/geometry/rect.h",
 ]
diff --git a/components/cast_receiver/browser/page_state_observer.cc b/components/cast_receiver/browser/page_state_observer.cc
new file mode 100644
index 0000000..dad1ed6
--- /dev/null
+++ b/components/cast_receiver/browser/page_state_observer.cc
@@ -0,0 +1,107 @@
+// Copyright 2022 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 "components/cast_receiver/browser/page_state_observer.h"
+
+#include "base/memory/raw_ref.h"
+#include "base/process/kill.h"
+#include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "net/base/net_errors.h"
+
+namespace cast_receiver {
+
+class PageStateObserver::WebContentsObserverWrapper
+    : public content::WebContentsObserver {
+ public:
+  using content::WebContentsObserver::Observe;
+
+  WebContentsObserverWrapper(PageStateObserver& wrapped,
+                             content::WebContents* web_contents)
+      : content::WebContentsObserver(web_contents), wrapped_(wrapped) {}
+
+  explicit WebContentsObserverWrapper(PageStateObserver& wrapped)
+      : wrapped_(wrapped) {}
+
+  ~WebContentsObserverWrapper() override { Observe(nullptr); }
+
+ private:
+  // content::WebContentsObserver implementation.
+  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
+                     const GURL& validated_url) override {
+    // This logic is a subset of that for DidFinishLoad() in
+    // CastWebContentsImpl.
+    int http_status_code = 0;
+    content::NavigationEntry* nav_entry =
+        web_contents()->GetController().GetVisibleEntry();
+    if (nav_entry) {
+      http_status_code = nav_entry->GetHttpStatusCode();
+    }
+
+    if (http_status_code != 0 && http_status_code / 100 != 2) {
+      DLOG(WARNING) << "Stopping after receiving http failure status code: "
+                    << http_status_code;
+      wrapped_->OnPageStopped(StopReason::kHttpError,
+                              net::ERR_HTTP_RESPONSE_CODE_FAILURE);
+      return;
+    }
+
+    wrapped_->OnPageLoadComplete();
+  }
+
+  void DidFailLoad(content::RenderFrameHost* render_frame_host,
+                   const GURL& validated_url,
+                   int error_code) override {
+    // This logic is a subset of that for DidFailLoad() in CastWebContentsImpl.
+    if (render_frame_host->GetParent()) {
+      DLOG(ERROR) << "Got error on sub-iframe: url=" << validated_url.spec()
+                  << ", error=" << error_code;
+      return;
+    }
+    if (error_code == net::ERR_ABORTED) {
+      // ERR_ABORTED means download was aborted by the app, typically this
+      // happens when flinging URL for direct playback, the initial URLRequest
+      // gets cancelled/aborted and then the same URL is requested via the
+      // buffered data source for media::Pipeline playback.
+      DLOG(WARNING) << "Load canceled: url=" << validated_url.spec();
+
+      // We consider the page to be fully loaded in this case, since the app has
+      // intentionally entered this state. If the app wanted to stop, it would
+      // have called window.close() instead.
+      wrapped_->OnPageLoadComplete();
+      return;
+    }
+
+    wrapped_->OnPageStopped(StopReason::kHttpError, error_code);
+  }
+
+  void WebContentsDestroyed() override {
+    content::WebContentsObserver::Observe(nullptr);
+    wrapped_->OnPageStopped(StopReason::kApplicationRequest, net::OK);
+  }
+
+  void PrimaryMainFrameRenderProcessGone(
+      base::TerminationStatus status) override {
+    content::WebContentsObserver::Observe(nullptr);
+    wrapped_->OnPageStopped(StopReason::kHttpError, net::ERR_UNEXPECTED);
+  }
+
+  base::raw_ref<PageStateObserver> wrapped_;
+};
+
+PageStateObserver::PageStateObserver()
+    : observer_wrapper_(std::make_unique<WebContentsObserverWrapper>(*this)) {}
+
+PageStateObserver::PageStateObserver(content::WebContents* web_contents)
+    : observer_wrapper_(
+          std::make_unique<WebContentsObserverWrapper>(*this, web_contents)) {}
+
+PageStateObserver::~PageStateObserver() = default;
+
+void PageStateObserver::Observe(content::WebContents* web_contents) {
+  observer_wrapper_->Observe(web_contents);
+}
+
+}  // namespace cast_receiver
diff --git a/components/cast_receiver/browser/page_state_observer.h b/components/cast_receiver/browser/page_state_observer.h
new file mode 100644
index 0000000..43adfd3
--- /dev/null
+++ b/components/cast_receiver/browser/page_state_observer.h
@@ -0,0 +1,56 @@
+// Copyright 2022 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 COMPONENTS_CAST_RECEIVER_BROWSER_PAGE_STATE_OBSERVER_H_
+#define COMPONENTS_CAST_RECEIVER_BROWSER_PAGE_STATE_OBSERVER_H_
+
+#include "content/public/browser/web_contents_observer.h"
+#include "url/gurl.h"
+
+namespace content {
+class WebContents;
+}  // namespace content
+
+namespace cast_receiver {
+
+// Wrapper around a WebContentsObserver to expose simple lifetime events for
+// a WebContents.
+class PageStateObserver {
+ public:
+  // Reason which a page load may have stopped. Used when calling
+  // OnPageStopped() below.
+  enum class StopReason { kUnknown = 0, kApplicationRequest, kHttpError };
+
+  virtual ~PageStateObserver();
+
+  // Called when the observed |web_contents| has completed loading of a page,
+  // either by completing loading of the page's contents or by user action to
+  // cancel the navigation.
+  virtual void OnPageLoadComplete() {}
+
+  // Called when the observed |web_contents| stops trying to load the page for
+  // reasons outside of the user's control - such as the page closing or an
+  // HTTP error.
+  virtual void OnPageStopped(StopReason reason, int32_t error_code) {}
+
+ protected:
+  PageStateObserver();
+  explicit PageStateObserver(content::WebContents* web_contents);
+
+  void Observe(content::WebContents* web_contents);
+
+ private:
+  // Implementation of the WebContentsObserver interface to call into the
+  // functions defined above. This extra later of indirection is used rather
+  // than directly observing the |web_contents| to avoid complexity associated
+  // with implementers of this class which wish to also implement
+  // content::WebContentsObserver.
+  class WebContentsObserverWrapper;
+
+  std::unique_ptr<WebContentsObserverWrapper> observer_wrapper_;
+};
+
+}  // namespace cast_receiver
+
+#endif  // COMPONENTS_CAST_RECEIVER_BROWSER_PAGE_STATE_OBSERVER_H_