chromium/components/cast_receiver/browser/page_state_observer.cc

// Copyright 2022 The Chromium Authors
// 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/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.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:
  void TryCallOnPageStopped(StopReason reason, net::Error error_code) {
    if (!navigation_handle_) {
      return;
    }

    navigation_handle_ = nullptr;
    wrapped_->OnPageStopped(reason, error_code);
  }

  void TryCallOnPageLoadComplete() {
    if (!navigation_handle_) {
      return;
    }

    navigation_handle_ = nullptr;
    wrapped_->OnPageLoadComplete();
  }

  // content::WebContentsObserver implementation.
  void DidStartNavigation(
      content::NavigationHandle* navigation_handle) override {
    if (!navigation_handle->IsInPrimaryMainFrame() ||
        navigation_handle->IsSameDocument()) {
      return;
    }

    navigation_handle_ = navigation_handle;
  }

  void DidFinishNavigation(
      content::NavigationHandle* navigation_handle) override {
    // Ignore sub-frame and non-current main frame navigation.
    if (navigation_handle != navigation_handle_) {
      return;
    }

    // If the navigation was not committed, it means either the page was a
    // download or error 204/205, or the navigation never left the previous
    // URL. Ignore these navigations.
    if (!navigation_handle->HasCommitted()) {
      LOG(WARNING) << "Navigation did not commit: url="
                   << navigation_handle->GetURL();
      navigation_handle_ = nullptr;
      return;
    }

    if (navigation_handle->IsErrorPage()) {
      const net::Error error_code = navigation_handle->GetNetErrorCode();
      LOG(ERROR) << "Got error on navigation: url="
                 << navigation_handle->GetURL() << ", error_code=" << error_code
                 << ", description=" << net::ErrorToShortString(error_code);
      TryCallOnPageStopped(StopReason::kHttpError, error_code);
      return;
    }

    // Notifies observers that the navigation of the main frame has finished
    // with no errors.
    TryCallOnPageLoadComplete();
  }

  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
                     const GURL& validated_url) override {
    if (render_frame_host != web_contents()->GetPrimaryMainFrame()) {
      return;
    }

    // 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;
      TryCallOnPageStopped(StopReason::kHttpError,
                           net::ERR_HTTP_RESPONSE_CODE_FAILURE);
      return;
    }

    TryCallOnPageLoadComplete();
  }

  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.
      TryCallOnPageLoadComplete();
      return;
    }

    TryCallOnPageStopped(StopReason::kHttpError,
                         static_cast<net::Error>(error_code));
  }

  void WebContentsDestroyed() override {
    content::WebContentsObserver::Observe(nullptr);
    TryCallOnPageStopped(StopReason::kApplicationRequest, net::OK);
  }

  void PrimaryMainFrameRenderProcessGone(
      base::TerminationStatus status) override {
    content::WebContentsObserver::Observe(nullptr);
    TryCallOnPageStopped(StopReason::kHttpError, net::ERR_UNEXPECTED);
  }

  content::NavigationHandle* navigation_handle_ = nullptr;
  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