// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "android_webview/browser/network_service/aw_proxying_url_loader_factory.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "android_webview/browser/android_protocol_handler.h"
#include "android_webview/browser/aw_browser_context.h"
#include "android_webview/browser/aw_contents_client_bridge.h"
#include "android_webview/browser/aw_contents_io_thread_client.h"
#include "android_webview/browser/aw_contents_origin_matcher.h"
#include "android_webview/browser/aw_contents_statics.h"
#include "android_webview/browser/aw_cookie_access_policy.h"
#include "android_webview/browser/aw_settings.h"
#include "android_webview/browser/cookie_manager.h"
#include "android_webview/browser/http_headers/aw_origin_matched_header.h"
#include "android_webview/browser/network_service/aw_web_resource_intercept_response.h"
#include "android_webview/browser/network_service/net_helpers.h"
#include "android_webview/browser/prefetch/aw_prefetch_manager.h"
#include "android_webview/browser/renderer_host/auto_login_parser.h"
#include "android_webview/common/aw_features.h"
#include "android_webview/common/aw_switches.h"
#include "android_webview/common/url_constants.h"
#include "base/android/apk_info.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "components/embedder_support/android/util/input_stream.h"
#include "components/embedder_support/android/util/response_delegate_impl.h"
#include "components/embedder_support/android/util/web_resource_response.h"
#include "components/safe_browsing/core/common/safebrowsing_constants.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_request_id.h"
#include "content/public/browser/origin_trials_controller_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/url_utils.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/restricted_cookie_manager.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
#include "third_party/blink/public/mojom/origin_trials/origin_trial_feature.mojom-shared.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace android_webview {

namespace {

using PrivacySetting = net::NetworkDelegate::PrivacySetting;

using OptionalGetCookie = std::optional<base::RepeatingCallback<void(
    bool is_3pc_allowed,
    const network::ResourceRequest& request,
    base::OnceCallback<void(std::string)> callback)>>;

using OptionalSetCookie = std::optional<
    embedder_support::AndroidStreamReaderURLLoader::SetCookieHeader>;

std::unique_ptr<AwContentsIoThreadClient> GetIoThreadClient(
    std::optional<WebContentsKey> web_contents_key,
    content::FrameTreeNodeId frame_tree_node_id,
    AwBrowserContextIoThreadHandle* browser_context_handle) {
  // |frame_tree_node_id_| is set to be invalid for service workers.
  // |request_.originated_from_service_worker| is insufficient here because it
  // is not set to true on browser side requested main scripts.
  if (frame_tree_node_id.is_null()) {
    return browser_context_handle
               ? browser_context_handle->GetServiceWorkerIoThreadClient()
               : nullptr;
  }
  if (web_contents_key.has_value()) {
    return AwContentsIoThreadClient::FromKey(web_contents_key.value());
  }
  return AwContentsIoThreadClient::FromID(frame_tree_node_id);
}

const char kResponseHeaderViaShouldInterceptRequestName[] = "Client-Via";
const char kResponseHeaderViaShouldInterceptRequestValue[] =
    "shouldInterceptRequest";
const char kAutoLoginHeaderName[] = "X-Auto-Login";

// Handles intercepted, in-progress requests/responses, so that they can be
// controlled and modified accordingly.
//
// At a high level this class calls shouldInterceptRequest (when appropriate)
// then either:
// - loads the response provided by shouldInterceptRequest.
// - loads the response from an Android resource.
// - loads the response from the network.
//
// It also handles redirects and origin-matched headers.
class InterceptedRequest : public network::mojom::URLLoader,
                           public network::mojom::URLLoaderClient {
 public:
  InterceptedRequest(
      OptionalGetCookie get_cookie_header,
      OptionalSetCookie set_cookie_header,
      std::optional<WebContentsKey> web_contents_key,
      content::FrameTreeNodeId frame_tree_node_id,
      int32_t request_id,
      uint32_t options,
      network::ResourceRequest request,
      const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
      mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
      bool intercept_only,
      std::optional<AwProxyingURLLoaderFactory::SecurityOptions>
          security_options,
      std::vector<scoped_refptr<AwOriginMatchedHeader>> origin_matched_headers,
      scoped_refptr<AwBrowserContextIoThreadHandle> browser_context_handle);

  InterceptedRequest(const InterceptedRequest&) = delete;
  InterceptedRequest& operator=(const InterceptedRequest&) = delete;

  ~InterceptedRequest() override;

  // Main entry point for the request.
  void Restart();

  // network::mojom::URLLoaderClient
  void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override;
  void OnReceiveResponse(
      network::mojom::URLResponseHeadPtr head,
      mojo::ScopedDataPipeConsumerHandle body,
      std::optional<mojo_base::BigBuffer> cached_metadata) override;
  void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                         network::mojom::URLResponseHeadPtr head) override;
  void OnUploadProgress(int64_t current_position,
                        int64_t total_size,
                        OnUploadProgressCallback callback) override;
  void OnTransferSizeUpdated(int32_t transfer_size_diff) override;
  void OnComplete(const network::URLLoaderCompletionStatus& status) override;

  // network::mojom::URLLoader
  void FollowRedirect(
      network::HttpRequestHeadersUpdateParams headers_update_params,
      const std::optional<GURL>& new_url) override;
  void SetPriority(net::RequestPriority priority,
                   int32_t intra_priority_value) override;

  // Returns true if the request was restarted or completed.
  bool InputStreamFailed(bool restart_needed);

 private:
  void InterceptWithCookieHeader(std::string cookie);
  void InterceptResponseReceived(
      AwContentsIoThreadClient::InterceptResponseData async_result);

  // Called to progress the request without calling shouldInterceptRequest.
  void SendNoIntercept();

  void ContinueAfterIntercept();
  void ContinueAfterInterceptWithOverride(
      std::unique_ptr<embedder_support::WebResourceResponse> response,
      std::unique_ptr<embedder_support::InputStream> input_stream);

  // Applies the `AwOriginMatchedHeaders` that match the current
  // `request_.url`.
  // When called from `InterceptedRequest::FollowRedirect`, the caller should
  // pass in pointers to the vector of headers to remove, as well as the map of
  // headers to modify.
  void ApplyOriginMatchedHeaders(
      std::vector<std::string>* redirect_headers_to_remove,
      net::HttpRequestHeaders* redirect_headers_to_modify);

  std::unique_ptr<AwContentsIoThreadClient> GetIoThreadClient();

  // This is called when the original URLLoaderClient has a connection error.
  void OnURLLoaderClientError();

  // This is called when the original URLLoader has a connection error.
  void OnURLLoaderError(uint32_t custom_reason, const std::string& description);

  // Call OnComplete on |target_client_|. If |wait_for_loader_error| is true
  // then this object will wait for |proxied_loader_receiver_| to have a
  // connection error before destructing.
  void CallOnComplete(const network::URLLoaderCompletionStatus& status,
                      bool wait_for_loader_error);

  void SendErrorAndCompleteImmediately(int error_code);

  // TODO(timvolodine): consider factoring this out of this class.
  bool ShouldNotInterceptRequest();

  // Posts the error callback to the UI thread, ensuring that at most we send
  // only one.
  void SendErrorCallback(int error_code, bool safebrowsing_hit);

  OptionalGetCookie get_cookie_header_;
  OptionalSetCookie set_cookie_header_;
  const std::optional<WebContentsKey> web_contents_key_;
  const content::FrameTreeNodeId frame_tree_node_id_;
  const int32_t request_id_;
  const uint32_t options_;
  bool input_stream_previously_failed_ = false;
  bool request_was_redirected_ = false;

  // To avoid sending multiple OnReceivedError callbacks.
  bool sent_error_callback_ = false;

  // When true, the loader will not not proceed unless the
  // shouldInterceptRequest callback provided a non-null response.
  bool intercept_only_ = false;

  std::optional<AwProxyingURLLoaderFactory::SecurityOptions> security_options_;

  // If the |target_loader_| called OnComplete with an error this stores it.
  // That way the destructor can send it to OnReceivedError if safe browsing
  // error didn't occur.
  int error_status_ = net::OK;

  GURL last_url_;
  network::ResourceRequest request_;

  const net::MutableNetworkTrafficAnnotationTag traffic_annotation_;

  mojo::Receiver<network::mojom::URLLoader> proxied_loader_receiver_;
  mojo::Remote<network::mojom::URLLoaderClient> target_client_;

  mojo::Receiver<network::mojom::URLLoaderClient> proxied_client_receiver_{
      this};
  mojo::Remote<network::mojom::URLLoader> target_loader_;
  mojo::Remote<network::mojom::URLLoaderFactory> target_factory_;
  std::vector<scoped_refptr<AwOriginMatchedHeader>> origin_matched_headers_;
  std::vector<std::string> attached_origin_matched_headers_;
  scoped_refptr<AwBrowserContextIoThreadHandle> browser_context_handle_;

  base::WeakPtrFactory<InterceptedRequest> weak_factory_{this};
};

// A ResponseDelegate for responses returned by shouldInterceptRequest.
class InterceptResponseDelegate
    : public embedder_support::ResponseDelegateImpl {
 public:
  InterceptResponseDelegate(
      std::unique_ptr<embedder_support::WebResourceResponse> response,
      base::WeakPtr<InterceptedRequest> request)
      : ResponseDelegateImpl(std::move(response)), request_(request) {}

  // AndroidStreamReaderURLLoader::ResponseDelegate implementation:
  void AppendResponseHeaders(JNIEnv* env,
                             net::HttpResponseHeaders* headers) override {
    embedder_support::ResponseDelegateImpl::AppendResponseHeaders(env, headers);
    // Indicate that the response had been obtained via shouldInterceptRequest.
    headers->SetHeader(kResponseHeaderViaShouldInterceptRequestName,
                       kResponseHeaderViaShouldInterceptRequestValue);
  }

  bool OnInputStreamOpenFailed() override {
    // return true if there is no valid request, meaning it has completed or
    // deleted.
    return request_ ? request_->InputStreamFailed(false /* restart_needed */)
                    : true;
  }

 private:
  base::WeakPtr<InterceptedRequest> request_;
};

// A ResponseDelegate based on top of AndroidProtocolHandler for special
// protocols, such as content://, file:///android_asset, and file:///android_res
// URLs.
class ProtocolResponseDelegate
    : public embedder_support::AndroidStreamReaderURLLoader::ResponseDelegate {
 public:
  ProtocolResponseDelegate(const GURL& url,
                           base::WeakPtr<InterceptedRequest> request)
      : url_(url), request_(request) {}

  std::unique_ptr<embedder_support::InputStream> OpenInputStream(
      JNIEnv* env) override {
    return CreateInputStream(env, url_);
  }

  bool OnInputStreamOpenFailed() override {
    // return true if there is no valid request, meaning it has completed or has
    // been deleted.
    return request_ ? request_->InputStreamFailed(true /* restart_needed */)
                    : true;
  }

  bool GetMimeType(JNIEnv* env,
                   const GURL& url,
                   embedder_support::InputStream* stream,
                   std::string* mime_type) override {
    return GetInputStreamMimeType(env, url, stream, mime_type);
  }

  void GetCharset(JNIEnv* env,
                  const GURL& url,
                  embedder_support::InputStream* stream,
                  std::string* charset) override {
    // TODO: We should probably be getting this from the managed side.
  }

  void AppendResponseHeaders(JNIEnv* env,
                             net::HttpResponseHeaders* headers) override {
    // Indicate that the response had been obtained via shouldInterceptRequest.
    // TODO(jam): why is this added for protocol handler (e.g. content scheme
    // and file resources?). The old path does this as well.
    headers->SetHeader(kResponseHeaderViaShouldInterceptRequestName,
                       kResponseHeaderViaShouldInterceptRequestValue);
  }

 private:
  GURL url_;
  base::WeakPtr<InterceptedRequest> request_;
};

InterceptedRequest::InterceptedRequest(
    OptionalGetCookie get_cookie_header,
    OptionalSetCookie set_cookie_header,
    std::optional<WebContentsKey> web_contents_key,
    content::FrameTreeNodeId frame_tree_node_id,
    int32_t request_id,
    uint32_t options,
    network::ResourceRequest request,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
    mojo::PendingReceiver<network::mojom::URLLoader> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory,
    bool intercept_only,
    std::optional<AwProxyingURLLoaderFactory::SecurityOptions> security_options,
    std::vector<scoped_refptr<AwOriginMatchedHeader>> origin_matched_headers,
    scoped_refptr<AwBrowserContextIoThreadHandle> browser_context_handle)
    : get_cookie_header_(get_cookie_header),
      set_cookie_header_(set_cookie_header),
      web_contents_key_(web_contents_key),
      frame_tree_node_id_(frame_tree_node_id),
      request_id_(request_id),
      options_(options),
      intercept_only_(intercept_only),
      security_options_(security_options),
      last_url_(request.url),
      request_(std::move(request)),
      traffic_annotation_(traffic_annotation),
      proxied_loader_receiver_(this, std::move(loader_receiver)),
      target_client_(std::move(client)),
      target_factory_(std::move(target_factory)),
      origin_matched_headers_(std::move(origin_matched_headers)),
      browser_context_handle_(std::move(browser_context_handle)) {
  // If there is a client error, clean up the request.
  target_client_.set_disconnect_handler(base::BindOnce(
      &InterceptedRequest::OnURLLoaderClientError, base::Unretained(this)));
  proxied_loader_receiver_.set_disconnect_with_reason_handler(base::BindOnce(
      &InterceptedRequest::OnURLLoaderError, base::Unretained(this)));

  // Update the resource request with the socketTag
  request_.socket_tag = GetDefaultSocketTag();
}

InterceptedRequest::~InterceptedRequest() {
  if (error_status_ != net::OK)
    SendErrorCallback(error_status_, false);
}

void InterceptedRequest::Restart() {
  TRACE_EVENT0("android_webview", "InterceptedRequest::Restart");
  std::unique_ptr<AwContentsIoThreadClient> io_thread_client =
      GetIoThreadClient();

  if (ShouldBlockURL(request_.url, io_thread_client.get())) {
    SendErrorAndCompleteImmediately(net::ERR_ACCESS_DENIED);
    return;
  }

  if (!request_was_redirected_) {
    // Do not call this if the request has already been redirected, as it will
    // be called from `FollowRedirect` in that case.
    ApplyOriginMatchedHeaders(nullptr, nullptr);
  }

  request_.load_flags =
      UpdateLoadFlags(request_.load_flags, io_thread_client.get());

  if (!io_thread_client || ShouldNotInterceptRequest()) {
    SendNoIntercept();
  } else {
    if (request_.referrer.is_valid()) {
      // intentionally override if referrer header already exists
      request_.headers.SetHeader(net::HttpRequestHeaders::kReferer,
                                 request_.referrer.spec());
    }

    if (get_cookie_header_.has_value() &&
        io_thread_client->ShouldAcceptCookies()) {
      bool accept_third_party_cookies =
          io_thread_client->ShouldAcceptThirdPartyCookies();

      std::move(get_cookie_header_)
          ->Run(accept_third_party_cookies, request_,
                base::BindOnce(&InterceptedRequest::InterceptWithCookieHeader,
                               weak_factory_.GetWeakPtr()));
    } else {
      io_thread_client->ShouldInterceptRequestAsync(
          AwWebResourceRequest(request_),
          base::BindOnce(&InterceptedRequest::InterceptResponseReceived,
                         weak_factory_.GetWeakPtr()));
    }
  }
}

void InterceptedRequest::InterceptWithCookieHeader(std::string cookie) {
  std::unique_ptr<AwContentsIoThreadClient> io_thread_client =
      GetIoThreadClient();
  if (io_thread_client != nullptr) {
    // Attach cookies if we are intercepting.
    if (cookie != "") {
      request_.headers.SetHeader(net::HttpRequestHeaders::kCookie, cookie);
    }
    // TODO: verify the case when WebContents::RenderFrameDeleted is called
    // before network request is intercepted (i.e. if that's possible and
    // whether it can result in any issues).
    io_thread_client->ShouldInterceptRequestAsync(
        AwWebResourceRequest(request_),
        base::BindOnce(&InterceptedRequest::InterceptResponseReceived,
                       weak_factory_.GetWeakPtr()));
  } else {
    SendNoIntercept();
  }
}

void InterceptedRequest::InterceptResponseReceived(
    AwContentsIoThreadClient::InterceptResponseData async_result) {
  AwProxyingURLLoaderFactory::SetRequestedWithHeader(
      request_, request_.cors_exempt_headers);

  JNIEnv* env = base::android::AttachCurrentThread();
  if (async_result.response && async_result.response->RaisedException(env)) {
    // The JNI handler has already raised an exception. Fail the resource load
    // as it may be insecure to load on error.
    SendErrorAndCompleteImmediately(net::ERR_UNEXPECTED);
    return;
  }

  if (async_result.response && async_result.response->HasResponse(env)) {
    // non-null response: make sure to use it as an override for the
    // normal network data.
    ContinueAfterInterceptWithOverride(async_result.response->GetResponse(env),
                                       std::move(async_result.input_stream));
    return;
  }

  // Request was not intercepted/overridden. Proceed with loading from network,
  // unless this is a special |intercept_only_| loader, which happens for
  // external schemes: e.g. unsupported schemes and cid: schemes.
  if (intercept_only_) {
    SendErrorAndCompleteImmediately(net::ERR_UNKNOWN_URL_SCHEME);
    return;
  }

  ContinueAfterIntercept();
}

void InterceptedRequest::ContinueAfterIntercept() {
  // The Cookie header may have been set in `InterceptWithCookieHeader`. But in
  // cases where the application chose to not intercept, we should remove it
  // again, so we are sure that the correct header is then set by the lower
  // network layers.
  request_.headers.RemoveHeader(net::HttpRequestHeaders::kCookie);
  // For WebViewClassic compatibility this job can only accept URLs that can be
  // opened. URLs that cannot be opened should be resolved by the next handler.
  //
  // If a request is initially handled here but the job fails due to it being
  // unable to open the InputStream for that request the request is marked as
  // previously failed and restarted.
  // Restarting a request involves creating a new job for that request. This
  // handler will ignore requests known to have previously failed to 1) prevent
  // an infinite loop, 2) ensure that the next handler in line gets the
  // opportunity to create a job for the request.
  if (!input_stream_previously_failed_ &&
      (request_.url.SchemeIs(url::kContentScheme) ||
       android_webview::IsAndroidSpecialFileUrl(request_.url))) {
    embedder_support::AndroidStreamReaderURLLoader* loader =
        new embedder_support::AndroidStreamReaderURLLoader(
            request_, proxied_client_receiver_.BindNewPipeAndPassRemote(),
            traffic_annotation_,
            std::make_unique<ProtocolResponseDelegate>(
                request_.url, weak_factory_.GetWeakPtr()),
            security_options_, set_cookie_header_);
    loader->Start(nullptr);
    return;
  }

  if (!target_loader_ && target_factory_) {
    target_factory_->CreateLoaderAndStart(
        target_loader_.BindNewPipeAndPassReceiver(), request_id_, options_,
        request_, proxied_client_receiver_.BindNewPipeAndPassRemote(),
        traffic_annotation_);
  }
}

void InterceptedRequest::ContinueAfterInterceptWithOverride(
    std::unique_ptr<embedder_support::WebResourceResponse> response,
    std::unique_ptr<embedder_support::InputStream> input_stream) {
  embedder_support::AndroidStreamReaderURLLoader* loader =
      new embedder_support::AndroidStreamReaderURLLoader(
          request_, proxied_client_receiver_.BindNewPipeAndPassRemote(),
          traffic_annotation_,
          std::make_unique<InterceptResponseDelegate>(
              std::move(response), weak_factory_.GetWeakPtr()),
          std::nullopt, set_cookie_header_);
  loader->Start(std::move(input_stream));
}

void InterceptedRequest::ApplyOriginMatchedHeaders(
    std::vector<std::string>* redirect_headers_to_remove,
    net::HttpRequestHeaders* redirect_headers_to_modify) {
  // TODO(crbug.com/422368112): Figure out how to handle CORS requests.
  url::Origin request_origin = url::Origin::Create(request_.url);
  if (options_ & network::mojom::kURLLoadOptionAsCorsPreflight) {
    for (const auto& matched_header : origin_matched_headers_) {
      if (matched_header->MatchesOrigin(request_origin)) {
        base::UmaHistogramBoolean(
            "Android.WebView.AndroidX.Profile.ExtraHeaderTargetsCorsPreflight",
            true);
        break;
      }
    }
    // For now we simply omit the header on a CORS preflight, but otherwise
    // attach the header on the GET request.
    return;
  }

  if (redirect_headers_to_remove) {
    redirect_headers_to_remove->insert(redirect_headers_to_remove->end(),
                                       attached_origin_matched_headers_.begin(),
                                       attached_origin_matched_headers_.end());
  }

  // This request might be in the process of being redirected to a different
  // domain, where we need to apply different headers, so remove previously
  // attached headers from the canonical set of headers.
  for (const auto& header_name : attached_origin_matched_headers_) {
    request_.headers.RemoveHeader(header_name);
  }

  attached_origin_matched_headers_.clear();

  for (const auto& [header_name, header_value] :
       AwOriginMatchedHeader::GetCombinedMatchingHeaders(
           origin_matched_headers_, request_origin)) {
    // TODO(crbug.com/423581920): Follow up to determine what the best merging
    // strategy is. The current strategy is to only attach if there is no
    // collision, which is least likely to break existing web pages.
    bool should_attach = !request_.headers.HasHeader(header_name);
    base::UmaHistogramBoolean(
        "Android.WebView.AndroidX.Profile.ExtraHeaderAttached", should_attach);
    if (should_attach) {
      request_.headers.SetHeader(header_name, header_value);
      attached_origin_matched_headers_.emplace_back(header_name);
      if (redirect_headers_to_modify) {
        redirect_headers_to_modify->SetHeader(header_name, header_value);
      }
    }
  }
}

// logic for when not to invoke shouldInterceptRequest callback
bool InterceptedRequest::ShouldNotInterceptRequest() {
  if (request_was_redirected_) {
    return true;
  }

  bool should_skip_intercept_for_prefetch_enabled =
      base::FeatureList::IsEnabled(features::kWebViewSkipInterceptsForPrefetch);
  if (should_skip_intercept_for_prefetch_enabled) {
    // Only skip if the prefetch is not also a prerender.
    if (AwPrefetchManager::IsPrefetchRequest(request_) &&
        !AwPrefetchManager::IsPrerenderRequest(request_)) {
      // Only skip if the prefetch if it is not associated with any web
      // contents. This is required in order for browser context initiated
      // prefetch requests to skip shouldInterceptRequest while excluding
      // renderer initiated prefetches (e.g. speculation rules).
      return web_contents_key_ == std::nullopt;
    }
  }

  // Do not call shouldInterceptRequest callback for special android urls,
  // unless they fail to load on first attempt. Special android urls are urls
  // such as "file:///android_asset/", "file:///android_res/" urls or
  // "content:" scheme urls.
  return !input_stream_previously_failed_ &&
         (request_.url.SchemeIs(url::kContentScheme) ||
          android_webview::IsAndroidSpecialFileUrl(request_.url));
}

// returns true if the request has been restarted or was completed.
bool InterceptedRequest::InputStreamFailed(bool restart_needed) {
  DCHECK(!input_stream_previously_failed_);

  if (intercept_only_) {
    // This can happen for unsupported schemes, when no proper
    // response from shouldInterceptRequest() is received, i.e.
    // the provided input stream in response failed to load. In
    // this case we send and error and stop loading.
    SendErrorAndCompleteImmediately(net::ERR_UNKNOWN_URL_SCHEME);
    return true;  // request completed
  }

  if (!restart_needed) {
    // request will not be restarted, error reporting will be done
    // via other means e.g. setting appropriate response header status.
    return false;
  }

  input_stream_previously_failed_ = true;
  proxied_client_receiver_.reset();
  Restart();
  return true;  // request restarted
}

namespace {
// TODO(timvolodine): consider factoring this out of this file.

AwContentsClientBridge* GetAwContentsClientBridgeFromID(
    content::FrameTreeNodeId frame_tree_node_id) {
  content::WebContents* wc =
      content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
  return AwContentsClientBridge::FromWebContents(wc);
}

void OnReceivedHttpErrorOnUiThread(
    content::FrameTreeNodeId frame_tree_node_id,
    const AwWebResourceRequest& request,
    std::unique_ptr<AwContentsClientBridge::HttpErrorInfo> http_error_info) {
  auto* client = GetAwContentsClientBridgeFromID(frame_tree_node_id);
  if (!client) {
    DLOG(WARNING) << "client is null, onReceivedHttpError dropped for "
                  << request.url;
    return;
  }
  client->OnReceivedHttpError(request, std::move(http_error_info));
}

void OnReceivedErrorOnUiThread(content::FrameTreeNodeId frame_tree_node_id,
                               const AwWebResourceRequest& request,
                               int error_code,
                               bool safebrowsing_hit) {
  auto* client = GetAwContentsClientBridgeFromID(frame_tree_node_id);
  if (!client) {
    DLOG(WARNING) << "client is null, onReceivedError dropped for "
                  << request.url;
    return;
  }
  client->OnReceivedError(request, error_code, safebrowsing_hit, true);
}

void OnNewLoginRequestOnUiThread(content::FrameTreeNodeId frame_tree_node_id,
                                 const std::string& realm,
                                 const std::string& account,
                                 const std::string& args) {
  auto* client = GetAwContentsClientBridgeFromID(frame_tree_node_id);
  if (!client) {
    return;
  }
  client->NewLoginRequest(realm, account, args);
}

}  // namespace

// URLLoaderClient methods.

void InterceptedRequest::OnReceiveEarlyHints(
    network::mojom::EarlyHintsPtr early_hints) {
  target_client_->OnReceiveEarlyHints(std::move(early_hints));
}

void InterceptedRequest::OnReceiveResponse(
    network::mojom::URLResponseHeadPtr head,
    mojo::ScopedDataPipeConsumerHandle body,
    std::optional<mojo_base::BigBuffer> cached_metadata) {
  TRACE_EVENT0("android_webview", "InterceptedRequest::OnReceiveResponse");
  // intercept response headers here
  // pause/resume |proxied_client_receiver_| if necessary

  if (head->headers && head->headers->response_code() >= 400) {
    // In Android WebView the WebViewClient.onReceivedHttpError callback
    // is invoked for any resource (main page, iframe, image, etc.) with
    // status code >= 400.
    std::unique_ptr<AwContentsClientBridge::HttpErrorInfo> error_info =
        AwContentsClientBridge::ExtractHttpErrorInfo(head->headers.get());

    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&OnReceivedHttpErrorOnUiThread, frame_tree_node_id_,
                       AwWebResourceRequest(request_), std::move(error_info)));
  }

  if (request_.destination == network::mojom::RequestDestination::kDocument) {
    // Check for x-auto-login-header
    HeaderData header_data;
    if (head->headers) {
      std::optional<std::string> header_string =
          head->headers->GetNormalizedHeader(kAutoLoginHeaderName);
      if (header_string &&
          ParseHeader(*header_string, ALLOW_ANY_REALM, &header_data)) {
        // TODO(timvolodine): consider simplifying this and above callback
        // code, crbug.com/897149.
        content::GetUIThreadTaskRunner({})->PostTask(
            FROM_HERE, base::BindOnce(&OnNewLoginRequestOnUiThread,
                                      frame_tree_node_id_, header_data.realm,
                                      header_data.account, header_data.args));
      }
    }
  }

  target_client_->OnReceiveResponse(std::move(head), std::move(body),
                                    std::move(cached_metadata));
}

void InterceptedRequest::OnReceiveRedirect(
    const net::RedirectInfo& redirect_info,
    network::mojom::URLResponseHeadPtr head) {
  // TODO(timvolodine): handle redirect override.
  request_was_redirected_ = true;
  last_url_ = request_.url;
  target_client_->OnReceiveRedirect(redirect_info, std::move(head));
  request_.url = redirect_info.new_url;
  request_.method = redirect_info.new_method;
  request_.site_for_cookies = redirect_info.new_site_for_cookies;
  request_.referrer = GURL(redirect_info.new_referrer);
  request_.referrer_policy = redirect_info.new_referrer_policy;
}

void InterceptedRequest::OnUploadProgress(int64_t current_position,
                                          int64_t total_size,
                                          OnUploadProgressCallback callback) {
  target_client_->OnUploadProgress(current_position, total_size,
                                   std::move(callback));
}

void InterceptedRequest::OnTransferSizeUpdated(int32_t transfer_size_diff) {
  network::RecordOnTransferSizeUpdatedUMA(
      network::OnTransferSizeUpdatedFrom::kInterceptedRequest);
  target_client_->OnTransferSizeUpdated(transfer_size_diff);
}

void InterceptedRequest::OnComplete(
    const network::URLLoaderCompletionStatus& status) {
  // Only wait for the original loader to possibly have a custom error if the
  // target loader succeeded. If the target loader failed, then it was a race as
  // to whether that error or the safe browsing error would be reported.
  CallOnComplete(status, status.error_code == net::OK);
}

// URLLoader methods.

void InterceptedRequest::FollowRedirect(
    network::HttpRequestHeadersUpdateParams headers_update_params,
    const std::optional<GURL>& new_url) {
  GURL target_url = new_url.value_or(request_.url);
  if (request_was_redirected_ &&
      !content::IsSafeRedirectTarget(last_url_, target_url)) {
    target_loader_.reset();
    SendErrorAndCompleteImmediately(net::ERR_UNSAFE_REDIRECT);
    return;
  }

  if (target_loader_) {
    if (!origin_matched_headers_.empty()) {
      ApplyOriginMatchedHeaders(&headers_update_params.removed_headers,
                                &headers_update_params.modified_headers);
    }
    target_loader_->FollowRedirect(std::move(headers_update_params), new_url);
  } else {
    // Apply any potential headers to the canonical `request_.headers` to keep
    // it in sync.
    ApplyOriginMatchedHeaders(nullptr, nullptr);
  }

  // If |OnURLLoaderClientError| was called then we're just waiting for the
  // connection error handler of |proxied_loader_receiver_|. Don't restart the
  // job since that'll create another URLLoader
  if (!target_client_)
    return;

  Restart();
}

void InterceptedRequest::SetPriority(net::RequestPriority priority,
                                     int32_t intra_priority_value) {
  if (target_loader_)
    target_loader_->SetPriority(priority, intra_priority_value);
}

std::unique_ptr<AwContentsIoThreadClient>
InterceptedRequest::GetIoThreadClient() {
  return ::android_webview::GetIoThreadClient(
      web_contents_key_, frame_tree_node_id_, browser_context_handle_.get());
}

void InterceptedRequest::OnURLLoaderClientError() {
  // We set |wait_for_loader_error| to true because if the loader did have a
  // custom_reason error then the client would be reset as well and it would be
  // a race as to which connection error we saw first.
  CallOnComplete(network::URLLoaderCompletionStatus(net::ERR_ABORTED),
                 true /* wait_for_loader_error */);
}

void InterceptedRequest::OnURLLoaderError(uint32_t custom_reason,
                                          const std::string& description) {
  if (custom_reason == network::mojom::URLLoader::kClientDisconnectReason) {
    if (description == safe_browsing::kCustomCancelReasonForURLLoader) {
      SendErrorCallback(safe_browsing::kNetErrorCodeForSafeBrowsing, true);
    } else {
      int parsed_error_code;
      if (base::StringToInt(std::string_view(description),
                            &parsed_error_code)) {
        SendErrorCallback(parsed_error_code, false);
      }
    }
  }

  // If CallOnComplete was already called, then this object is ready to be
  // deleted.
  if (!target_client_)
    delete this;
}

void InterceptedRequest::CallOnComplete(
    const network::URLLoaderCompletionStatus& status,
    bool wait_for_loader_error) {
  // Save an error status so that we call onReceiveError at destruction if there
  // was no safe browsing error.
  if (status.error_code != net::OK)
    error_status_ = status.error_code;

  if (target_client_)
    target_client_->OnComplete(status);

  if (proxied_loader_receiver_.is_bound() && wait_for_loader_error) {
    // Since the original client is gone no need to continue loading the
    // request.
    proxied_client_receiver_.reset();
    target_loader_.reset();

    // Don't delete |this| yet, in case the |proxied_loader_receiver_|'s
    // error_handler is called with a reason to indicate an error which we want
    // to send to the client bridge. Also reset |target_client_| so we don't
    // get its error_handler called and then delete |this|.
    target_client_.reset();

    // In case there are pending checks as to whether this request should be
    // intercepted, we don't want that causing |target_client_| to be used
    // later.
    weak_factory_.InvalidateWeakPtrs();
  } else {
    delete this;
  }
}

void InterceptedRequest::SendErrorAndCompleteImmediately(int error_code) {
  auto status = network::URLLoaderCompletionStatus(error_code);
  SendErrorCallback(status.error_code, false);
  target_client_->OnComplete(status);
  delete this;
}

void InterceptedRequest::SendErrorCallback(int error_code,
                                           bool safebrowsing_hit) {
  // Ensure we only send one error callback, e.g. to avoid sending two if
  // there's both a networking error and safe browsing blocked the request.
  if (sent_error_callback_)
    return;

  // We can't get a |AwContentsClientBridge| based on the |render_frame_id| of
  // the |request_| initiated by the service worker, so interrupt it as soon as
  // possible.
  if (request_.originated_from_service_worker)
    return;

  sent_error_callback_ = true;
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&OnReceivedErrorOnUiThread, frame_tree_node_id_,
                                AwWebResourceRequest(request_), error_code,
                                safebrowsing_hit));
}

void InterceptedRequest::SendNoIntercept() {
  // equivalent to no interception
  InterceptResponseReceived(AwContentsIoThreadClient::InterceptResponseData());
}

}  // namespace

//============================
// AwProxyingURLLoaderFactory
//============================

AwProxyingURLLoaderFactory::AwProxyingURLLoaderFactory(
    std::optional<mojo::PendingRemote<network::mojom::CookieManager>>
        cookie_manager,
    AwCookieAccessPolicy* cookie_access_policy,
    std::optional<const net::IsolationInfo> isolation_info,
    std::optional<WebContentsKey> web_contents_key,
    content::FrameTreeNodeId frame_tree_node_id,
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote,
    bool intercept_only,
    std::optional<SecurityOptions> security_options,
    std::vector<scoped_refptr<AwOriginMatchedHeader>> origin_matched_headers,
    scoped_refptr<AwBrowserContextIoThreadHandle> browser_context_handle,
    std::optional<int64_t> navigation_id)
    : cookie_access_policy_(cookie_access_policy),
      isolation_info_(isolation_info),
      web_contents_key_(web_contents_key),
      frame_tree_node_id_(frame_tree_node_id),
      intercept_only_(intercept_only),
      security_options_(security_options),
      origin_matched_headers_(std::move(origin_matched_headers)),
      browser_context_handle_(std::move(browser_context_handle)),
      navigation_id_(navigation_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  DCHECK(!(intercept_only_ && target_factory_remote));
  if (target_factory_remote) {
    target_factory_.Bind(std::move(target_factory_remote));
    target_factory_.set_disconnect_handler(
        base::BindOnce(&AwProxyingURLLoaderFactory::OnTargetFactoryError,
                       base::Unretained(this)));
  }
  proxy_receivers_.Add(this, std::move(loader_receiver));
  proxy_receivers_.set_disconnect_handler(
      base::BindRepeating(&AwProxyingURLLoaderFactory::OnProxyBindingError,
                          base::Unretained(this)));

  if (cookie_manager.has_value() && cookie_manager->is_valid()) {
    cookie_manager_.Bind(std::move(cookie_manager.value()));
  }
}

AwProxyingURLLoaderFactory::~AwProxyingURLLoaderFactory() = default;

// static
void AwProxyingURLLoaderFactory::SetRequestedWithHeader(
    const network::ResourceRequest& request,
    net::HttpRequestHeaders& cors_exempt_headers) {
  // We send the application's package name in the X-Requested-With header for
  // compatibility with previous WebView versions. This should not be visible to
  // shouldInterceptRequest. It should also not trigger CORS preflight if
  // OOR-CORS is enabled.
  std::string header = content::GetCorsExemptRequestedWithHeaderName();

  // Only overwrite if the header hasn't already been set
  if (!request.headers.HasHeader(header)) {
    cors_exempt_headers.SetHeader(header,
                                  base::android::apk_info::host_package_name());
  }
}

// static
void AwProxyingURLLoaderFactory::CreateProxy(
    mojo::PendingRemote<network::mojom::CookieManager> cookie_manager,
    AwCookieAccessPolicy* cookie_access_policy,
    std::optional<const net::IsolationInfo> isolation_info,
    std::optional<WebContentsKey> web_contents_key,
    content::FrameTreeNodeId frame_tree_node_id,
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_remote,
    std::optional<SecurityOptions> security_options,
    std::vector<scoped_refptr<AwOriginMatchedHeader>> origin_matched_headers,
    scoped_refptr<AwBrowserContextIoThreadHandle> browser_context_handle,
    std::optional<int64_t> navigation_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  // will manage its own lifetime
  new AwProxyingURLLoaderFactory(
      std::move(cookie_manager), cookie_access_policy, isolation_info,
      web_contents_key, frame_tree_node_id, std::move(loader_receiver),
      std::move(target_factory_remote), false, security_options,
      std::move(origin_matched_headers), std::move(browser_context_handle),
      navigation_id);
}

void AwProxyingURLLoaderFactory::CreateLoaderAndStart(
    mojo::PendingReceiver<network::mojom::URLLoader> loader,
    int32_t request_id,
    uint32_t options,
    const network::ResourceRequest& request,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
  NOTREACHED() << "Non-const ref version of this method should be used as a "
                  "performance optimization.";
}

void AwProxyingURLLoaderFactory::CreateLoaderAndStart(
    mojo::PendingReceiver<network::mojom::URLLoader> loader,
    int32_t request_id,
    uint32_t options,
    network::ResourceRequest& request,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client,
    const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
  TRACE_EVENT0("android_webview",
               "AwProxyingURLLoaderFactory::CreateLoaderAndStart");
  // TODO(timvolodine): handle interception, modification (headers for
  // webview), blocking, callbacks etc..

  mojo::PendingRemote<network::mojom::URLLoaderFactory> target_factory_clone;
  if (target_factory_) {
    target_factory_->Clone(
        target_factory_clone.InitWithNewPipeAndPassReceiver());
  }

  std::unique_ptr<AwContentsIoThreadClient> io_thread_client =
      GetIoThreadClient(web_contents_key_, frame_tree_node_id_,
                        browser_context_handle_.get());

  // It is possible for us to receive a nullptr for the io_thread_client
  // from AwContentBrowserClient::HandleExternalProtocol.
  // This is because that method can be called while the RenderFrameHost is
  // shutting down. Since this behavior is only expected during shutdown, we
  // will take the safe default and assume cookies are not allowed to avoid
  // leaking data.
  bool global_cookie_policy = io_thread_client != nullptr
                                  ? io_thread_client->ShouldAcceptCookies()
                                  : false;

  bool third_party_cookie_policy =
      global_cookie_policy && io_thread_client->ShouldAcceptThirdPartyCookies();

  // If we are handling an external protocol, we skip providing the cookie
  // manager. In this case, it will not be bound so we move on.
  // We should also only provide cookies if cookies are enabled.
  bool include_cookies_on_intercept =
      cookie_manager_.is_bound() && global_cookie_policy &&
      io_thread_client->ShouldIncludeCookiesOnIntercept();

  if (!global_cookie_policy) {
    options |= network::mojom::kURLLoadOptionBlockAllCookies;
  } else if (!third_party_cookie_policy && !request.url.SchemeIsFile()) {
    // Special case: if the application has asked that we allow file:// scheme
    // URLs to set cookies, we need to avoid setting a cookie policy (as file://
    // scheme URLs are third-party to everything).
    options |= network::mojom::kURLLoadOptionBlockThirdPartyCookies;
  }

  OptionalGetCookie get_cookie_header;
  OptionalSetCookie set_cookie_header;
  if (include_cookies_on_intercept) {
    get_cookie_header =
        base::BindRepeating(&AwProxyingURLLoaderFactory::GetCookieHeader,
                            weak_factory_.GetWeakPtr());
    set_cookie_header =
        base::BindRepeating(&AwProxyingURLLoaderFactory::SetCookieHeader,
                            weak_factory_.GetWeakPtr());
  }

  // manages its own lifecycle
  // TODO(timvolodine): consider keeping track of requests.
  // TODO(crbug.com/332697604): Pass by non-const ref once mojo supports it.
  InterceptedRequest* req = new InterceptedRequest(
      std::move(get_cookie_header), std::move(set_cookie_header),
      web_contents_key_, frame_tree_node_id_, request_id, options,
      std::move(request), traffic_annotation, std::move(loader),
      std::move(client), std::move(target_factory_clone), intercept_only_,
      security_options_, origin_matched_headers_, browser_context_handle_);
  req->Restart();
}

void AwProxyingURLLoaderFactory::OnTargetFactoryError() {
  delete this;
}

void AwProxyingURLLoaderFactory::OnProxyBindingError() {
  if (proxy_receivers_.empty())
    delete this;
}

std::optional<net::CookiePartitionKey> GetPartitionKey(
    net::IsolationInfo& isolation_info,
    const network::ResourceRequest& request) {
  return net::CookiePartitionKey::FromNetworkIsolationKey(
      isolation_info.network_isolation_key(), isolation_info.site_for_cookies(),
      net::SchemefulSite(request.url), request.is_outermost_main_frame);
}

// We need to use this function to get the cookie header for Android apps
// because we are letting them intercept requests before we have even handed
// over the network request to the actual network stack.
void AwProxyingURLLoaderFactory::GetCookieHeader(
    bool is_3pc_allowed,
    const network::ResourceRequest& request,
    base::OnceCallback<void(std::string)> callback) {
  base::TimeTicks start = base::TimeTicks::Now();
  DCHECK(cookie_manager_.is_bound() && cookie_access_policy_ != nullptr);

  auto isolation_info = GetIsolationInfo(request);

  net::CookieOptions options;
  options.set_include_httponly();
  options.set_do_not_update_access_time();
  if (request.resource_type ==
          static_cast<int32_t>(blink::mojom::ResourceType::kMainFrame) ||
      request.resource_type ==
          static_cast<int32_t>(blink::mojom::ResourceType::kSubFrame)) {
    options.set_same_site_cookie_context(
        net::cookie_util::ComputeSameSiteContextForRequest(
            request.method, request.navigation_redirect_chain,
            request.site_for_cookies, request.request_initiator,
            request.resource_type ==
                static_cast<int32_t>(blink::mojom::ResourceType::kMainFrame),
            /*force_ignore_site_for_cookies=*/false,
            /*ignore_unsafe_method_for_same_site_lax=*/false));
  } else {
    options.set_same_site_cookie_context(
        net::cookie_util::ComputeSameSiteContextForSubresource(
            request.url, request.site_for_cookies,
            /*force_ignore_site_for_cookies=*/false));
  }

  PrivacySetting privacy_setting = cookie_access_policy_->CanAccessCookies(
      request.url, isolation_info.site_for_cookies(), is_3pc_allowed);

  // We should not bother retrieving the cookie list if cookies are not enabled.
  if (privacy_setting == PrivacySetting::kStateDisallowed) {
    std::move(callback).Run("");
    return;
  }

  cookie_manager_->GetCookieList(
      request.url, options,
      net::CookiePartitionKeyCollection(
          GetPartitionKey(isolation_info, request)),
      base::BindOnce(
          [](PrivacySetting privacy_setting, base::TimeTicks start,
             base::OnceCallback<void(std::string)> callback,
             const net::CookieAccessResultList& results,
             const net::CookieAccessResultList& excluded_cookies) {
            net::CookieList cookies;

            for (const net::CookieWithAccessResult& cookie : results) {
              if (privacy_setting == PrivacySetting::kStateAllowed ||
                  cookie.cookie.IsPartitioned()) {
                cookies.push_back(cookie.cookie);
              }
            }

            std::string cookie_line =
                net::CanonicalCookie::BuildCookieLine(cookies);
            std::move(callback).Run(cookie_line);
            UMA_HISTOGRAM_TIMES(
                "Android.WebView.ShouldInterceptRequest.GetCookieHeader."
                "PostMojo.TimeToRun",
                base::TimeTicks::Now() - start);
          },
          std::move(privacy_setting), start, std::move(callback)));
}

void AwProxyingURLLoaderFactory::SetCookieHeader(
    const network::ResourceRequest& request,
    std::string_view cookie_string,
    const std::optional<base::Time>& server_time) {
  base::TimeTicks start = base::TimeTicks::Now();
  DCHECK(cookie_manager_.is_bound());
  auto isolation_info = GetIsolationInfo(request);

  net::CookieInclusionStatus returned_status;

  std::unique_ptr<net::CanonicalCookie> cookie = net::CanonicalCookie::Create(
      request.url, cookie_string, base::Time::Now(), server_time,
      GetPartitionKey(isolation_info, request), net::CookieSourceType::kHTTP,
      &returned_status);

    cookie_manager_->SetCanonicalCookie(*cookie, request.url,
                                        net::CookieOptions::MakeAllInclusive(),
                                        base::DoNothing());

  UMA_HISTOGRAM_TIMES(
      "Android.WebView.ShouldInterceptRequest.SetCookieHeader.TimeToRun",
      base::TimeTicks::Now() - start);
}

net::IsolationInfo AwProxyingURLLoaderFactory::GetIsolationInfo(
    const network::ResourceRequest& request) {
  CHECK(isolation_info_.has_value());
  // If the factory is trusted, this will be included, otherwise we
  // receive the isolation info from WillCreateURLLoaderFactory when we
  // are being created.
  // See the WillCreateURLLoaderFactory doc block for more info on this.
  if (request.trusted_params.has_value()) {
    return request.trusted_params->isolation_info;
  }

  return isolation_info_.value();
}

void AwProxyingURLLoaderFactory::Clone(
    mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader_receiver) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  proxy_receivers_.Add(this, std::move(loader_receiver));
}

}  // namespace android_webview
