blob: 0f812b2a834769819e6bd2573eb3eb72ca7e909b [file]
// Copyright 2025 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/prefetch/aw_prefetch_manager.h"
#include <jni.h>
#include <optional>
#include "android_webview/browser/metrics/aw_metrics_service_accessor.h"
#include "android_webview/browser/metrics/aw_metrics_service_client.h"
#include "android_webview/browser/prefetch/aw_prefetch_manager_data.h"
#include "android_webview/browser/prefetch/aw_preloading_utils.h"
#include "android_webview/common/aw_features.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/check_is_test.h"
#include "base/notimplemented.h"
#include "base/trace_event/trace_event.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/prefetch_deduplication_utils.h"
#include "content/public/browser/preload_pipeline_info.h"
#include "content/public/common/content_features.h"
// Has to come after all the FromJniType() / ToJniType() headers.
#include "android_webview/browser_jni_headers/AwPrefetchManager_jni.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
using content::BrowserThread;
namespace android_webview {
BASE_FEATURE(kWebViewPrefetchDisableBlockUntilHeadTimeout,
base::FEATURE_ENABLED_BY_DEFAULT);
// Listens to the status of a prefetch request and propagates it to Java
// callbacks.
//
// Thread model:
//
// Instances of this class can be invoked on any thread. Since
// `JNIEnv` is thread-local, we use `AttachCurrentThread()` to safely acquire a
// valid `JNIEnv` and `ScopedJavaGlobalRef` to safely hold Java objects across
// different threads.
//
// TODO(crbug.com/496807663): Ideally the callback should be invoked as async
// task on a created thread environment as a guardrail not to invoke any
// navigation/prefetch reentrancy.
class AwPrefetchRequestStatusListener
: public content::PrefetchRequestStatusListener {
public:
AwPrefetchRequestStatusListener(
const base::android::ScopedJavaGlobalRef<jobject>
prefetch_manager_java_object,
const base::android::JavaRef<jobject>& callback,
const base::android::JavaRef<jobject>& callback_executor)
: prefetch_manager_java_object_(prefetch_manager_java_object),
prefetch_java_callback_(callback),
prefetch_java_callback_executor_(callback_executor) {
CHECK(prefetch_manager_java_object_);
CHECK(prefetch_java_callback_);
CHECK(prefetch_java_callback_executor_);
}
~AwPrefetchRequestStatusListener() override = default;
void OnPrefetchStartFailedGeneric() override {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AwPrefetchManager_onPrefetchStartFailedGeneric(
env, prefetch_manager_java_object_, prefetch_java_callback_,
prefetch_java_callback_executor_);
}
void OnPrefetchStartFailedDuplicate() override {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AwPrefetchManager_onPrefetchStartFailedDuplicate(
env, prefetch_manager_java_object_, prefetch_java_callback_,
prefetch_java_callback_executor_);
}
void OnPrefetchResponseCompleted() override {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AwPrefetchManager_onPrefetchResponseCompleted(
env, prefetch_manager_java_object_, prefetch_java_callback_,
prefetch_java_callback_executor_);
}
void OnPrefetchResponseError() override {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AwPrefetchManager_onPrefetchResponseError(
env, prefetch_manager_java_object_, prefetch_java_callback_,
prefetch_java_callback_executor_);
}
void OnPrefetchResponseServerError(int response_code) override {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AwPrefetchManager_onPrefetchResponseServerError(
env, prefetch_manager_java_object_, prefetch_java_callback_,
prefetch_java_callback_executor_, response_code);
}
private:
base::android::ScopedJavaGlobalRef<jobject> prefetch_manager_java_object_;
base::android::ScopedJavaGlobalRef<jobject> prefetch_java_callback_;
base::android::ScopedJavaGlobalRef<jobject> prefetch_java_callback_executor_;
};
AwPrefetchManager::AwPrefetchManager(content::BrowserContext* browser_context)
: browser_context_(*browser_context),
aw_pre_prefetch_service_(
base::FeatureList::IsEnabled(
features::kWebViewPrefetchOffTheMainThread)
? content::PrePrefetchService::Create(&browser_context_.get())
: nullptr) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT_INSTANT("android_webview",
"AwPrefetchManager::AwPrefetchManager");
}
AwPrefetchManager::~AwPrefetchManager() = default;
// static
bool AwPrefetchManager::IsPrefetchRequest(
const network::ResourceRequest& resource_request) {
return AwPrefetchManager::IsSecPurposeForPrefetch(
resource_request.headers.GetHeader(blink::kSecPurposeHeaderName));
}
// static
bool AwPrefetchManager::IsPrerenderRequest(
const network::ResourceRequest& resource_request) {
return blink::IsSecPurposeForPrerender(
resource_request.headers.GetHeader(blink::kSecPurposeHeaderName));
}
// static
bool AwPrefetchManager::IsSecPurposeForPrefetch(
std::optional<std::string> sec_purpose_header_value) {
return blink::IsSecPurposeForPrefetch(sec_purpose_header_value);
}
// static
void AwPrefetchManager::SetOrClearExternalPrefetchExperiment(
std::optional<int> variations_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::vector<int> experiment_ids;
if (variations_id.has_value()) {
experiment_ids.push_back(variations_id.value());
}
// Always invoke registration to ensure the metrics state is synchronized
// with the current request. Providing an empty ID list is necessary to
// clear state from previous requests if the current one lacks a
// Variations ID.
AwMetricsServiceAccessor::RegisterExternalExperiment(experiment_ids);
}
AwPrefetchKey AwPrefetchManager::StartPrefetchRequest(
JNIEnv* env,
const std::string& url,
const base::android::JavaRef<jobject>& prefetch_params,
const base::android::JavaRef<jobject>& callback,
const base::android::JavaRef<jobject>& callback_executor) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT0("android_webview", "AwPrefetchManager::StartPrefetchRequest");
return StartRequest(
env, url, /*is_pre_prefetch=*/false,
content::PreloadPipelineInfo::Create(
/*planned_max_preloading_type=*/content::PreloadingType::kPrefetch),
prefetch_params, callback, callback_executor);
}
AwPrefetchKey AwPrefetchManager::StartPrefetchRequestAheadOfPrerender(
base::PassKey<AwContents>,
JNIEnv* env,
const std::string& url,
const base::android::JavaRef<jobject>& prefetch_params,
scoped_refptr<content::PreloadPipelineInfo> preload_pipeline_info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT0("android_webview",
"AwPrefetchManager::StartPrefetchRequestAheadOfPrerender");
return StartRequest(
env, url, /*is_pre_prefetch=*/false, std::move(preload_pipeline_info),
prefetch_params,
// Callbacks are not set for prefetch ahead of prerender. Callers are
// expected to observe failures etc. via prerender.
/*callback=*/base::android::JavaRef(),
/*callback_executor=*/base::android::JavaRef());
}
int AwPrefetchManager::StartPrePrefetchRequest(
JNIEnv* env,
const std::string& url,
const base::android::JavaRef<jobject>& prefetch_params,
const base::android::JavaRef<jobject>& callback,
const base::android::JavaRef<jobject>& callback_executor) {
DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
TRACE_EVENT0("android_webview", "AwPrefetchManager::StartPrePrefetchRequest");
CHECK(
base::FeatureList::IsEnabled(features::kWebViewPrefetchOffTheMainThread));
return StartRequest(env, url, /*is_pre_prefetch=*/true,
/*preload_pipeline_info=*/nullptr, prefetch_params,
callback, callback_executor);
}
int AwPrefetchManager::StartPrefetchFromPrePrefetch(JNIEnv* env,
int32_t prefetch_key) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT0("android_webview",
"AwPrefetchManager::StartPrefetchFromPrePrefetch");
CHECK(
base::FeatureList::IsEnabled(features::kWebViewPrefetchOffTheMainThread));
// TODO(crbug.com/452406598, crbug.com/452389538): Start Prefetch from
// PrePrefetch.
return prefetch_key;
}
int AwPrefetchManager::StartRequest(
JNIEnv* env,
const std::string& url,
bool is_pre_prefetch,
scoped_refptr<content::PreloadPipelineInfo> preload_pipeline_info,
const base::android::JavaRef<jobject>& prefetch_params,
const base::android::JavaRef<jobject>& callback,
const base::android::JavaRef<jobject>& callback_executor) {
GURL pf_url = GURL(url);
net::HttpRequestHeaders additional_headers =
GetAdditionalHeadersFromPrefetchParameters(env, prefetch_params);
// TODO(crbug.com/455296998): Remove this code for M145.
bool should_bypass_http_cache = false;
if (base::FeatureList::IsEnabled(
android_webview::features::
kWebViewBypassHttpCacheForPrefetchFromHeader)) {
should_bypass_http_cache = GetShouldBypassHttpCacheFromHeaders(
additional_headers, /*remove_header=*/true);
}
std::optional<net::HttpNoVarySearchData> expected_no_vary_search =
GetExpectedNoVarySearchFromPrefetchParameters(env, prefetch_params);
std::optional<int> variations_id =
GetVariationsIdFromPrefetchParameters(env, prefetch_params);
if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&AwPrefetchManager::SetOrClearExternalPrefetchExperiment,
variations_id));
} else {
AwPrefetchManager::SetOrClearExternalPrefetchExperiment(variations_id);
}
std::unique_ptr<content::PrefetchRequestStatusListener>
request_status_listener;
if (java_obj_ && callback && callback_executor) {
request_status_listener = std::make_unique<AwPrefetchRequestStatusListener>(
java_obj_, callback, callback_executor);
}
// For WebView we will check if there is already a duplicate prefetch
// request based on the URL and the No-Vary-Search hint. This is for the
// purpose of deduping prefetch requests on the application's behalf.
// TODO(crbug.com/393344309): Apply deduping to all prefetch requests (not
// just WebView).
bool is_prefetch_duplicate = [&]() {
if (!base::FeatureList::IsEnabled(
features::kWebViewPrefetchOffTheMainThread)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return browser_context_->IsPrefetchDuplicate(pf_url,
expected_no_vary_search);
} else {
return aw_prefetch_manager_data_.IsPrefetchDuplicate(
pf_url, expected_no_vary_search);
}
}();
if (is_prefetch_duplicate) {
if (request_status_listener) {
request_status_listener->OnPrefetchStartFailedDuplicate();
}
return NO_PREFETCH_KEY;
}
// Make room for the new prefetch request by evicting the older ones to
// respect the `max_prefetches_` limit.
//
// We intentionally do this **before** starting prefetch instead of after.
// Due to current //content `PrefetchScheduler` restrictions of its
// sequential async scheduling, if an evicted prefetch is still running,
// canceling it before starting a next one reduces one PostTask, which is
// good from performance perspective. Please see
// https://docs.google.com/document/d/1OylSDdS_RTOkG_E_PXJ0aPI1QrygMjGkgSs5JcTrFlE/edit?tab=t.0#bookmark=id.rcr0rfweiz90
// for more information.
// TODO(crbug.com/426404355): After parallel prefetching being enabled for
// WV.prefetch, perhaps we no longer need this. Revisit and verify.
aw_prefetch_manager_data_.MayEvictOldestPrefetchHandleForANewRequest();
std::unique_ptr<content::PrefetchHandle> prefetch_handle;
std::unique_ptr<content::PrePrefetchHandle> pre_prefetch_handle;
if (is_pre_prefetch) {
DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
CHECK(base::FeatureList::IsEnabled(
features::kWebViewPrefetchOffTheMainThread));
CHECK(aw_pre_prefetch_service_);
pre_prefetch_handle = aw_pre_prefetch_service_->StartPrePrefetchRequest(
pf_url, AW_PREFETCH_METRICS_SUFFIX,
GetIsJavaScriptEnabledFromPrefetchParameters(env, prefetch_params),
expected_no_vary_search,
base::FeatureList::IsEnabled(
::features::kWebViewPrefetchHighestPrefetchPriority)
? std::optional(content::PrefetchPriority::kHighest)
: std::nullopt,
additional_headers, std::move(request_status_listener),
base::Seconds(aw_prefetch_manager_data_.GetTtlInSec()),
/*should_append_variations_header=*/false,
base::FeatureList::IsEnabled(
kWebViewPrefetchDisableBlockUntilHeadTimeout),
should_bypass_http_cache);
} else {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
prefetch_handle = browser_context_->StartBrowserPrefetchRequest(
pf_url, AW_PREFETCH_METRICS_SUFFIX,
GetIsJavaScriptEnabledFromPrefetchParameters(env, prefetch_params),
expected_no_vary_search,
base::FeatureList::IsEnabled(
::features::kWebViewPrefetchHighestPrefetchPriority)
? std::optional(content::PrefetchPriority::kHighest)
: std::nullopt,
std::move(preload_pipeline_info), additional_headers,
std::move(request_status_listener),
base::Seconds(aw_prefetch_manager_data_.GetTtlInSec()),
/*should_append_variations_header=*/false,
base::FeatureList::IsEnabled(
kWebViewPrefetchDisableBlockUntilHeadTimeout),
should_bypass_http_cache);
}
// TODO(crbug.com/452406598): Here we actually insert the handle to
// `aw_prefetch_manager_data_`, after checking the limit and deduping
// independently, but it can potentially cause a race condition if
// `kWebViewPrefetchOffTheMainThread` is enabled. See
// `AwPrefetchManagerData::AddPrefetchHandle` for more details.
if (pre_prefetch_handle) {
return aw_prefetch_manager_data_.AddNewPrefetchHandleWrapper(
std::make_unique<AwPrefetchHandleWrapper>(
pf_url, expected_no_vary_search, std::move(pre_prefetch_handle)));
} else if (prefetch_handle) {
return aw_prefetch_manager_data_.AddNewPrefetchHandleWrapper(
std::make_unique<AwPrefetchHandleWrapper>(
pf_url, expected_no_vary_search, std::move(prefetch_handle)));
} else {
return NO_PREFETCH_KEY;
}
}
void AwPrefetchManager::CancelPrefetch(JNIEnv* env,
AwPrefetchKey prefetch_key) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
TRACE_EVENT0("android_webview", "AwPrefetchManager::CancelPrefetch");
if (prefetch_key == NO_PREFETCH_KEY) {
// no-op.
return;
}
aw_prefetch_manager_data_.CancelPrefetch(prefetch_key);
}
void AwPrefetchManager::SetTtlInSec(JNIEnv* env, int ttl_in_sec) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK_GT(ttl_in_sec, 0);
aw_prefetch_manager_data_.SetTtlInSec(ttl_in_sec);
}
void AwPrefetchManager::SetMaxPrefetches(JNIEnv* env, int max_prefetches) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK_GT(max_prefetches, 0);
size_t sanitized_max_prefetches = static_cast<size_t>(max_prefetches);
aw_prefetch_manager_data_.SetMaxPrefetches(
std::min(sanitized_max_prefetches, kAbsoluteMaxPrefetches));
}
int AwPrefetchManager::GetTtlInSec(JNIEnv* env) const {
return aw_prefetch_manager_data_.GetTtlInSec();
}
size_t AwPrefetchManager::GetMaxPrefetches(JNIEnv* env) const {
return aw_prefetch_manager_data_.GetMaxPrefetches();
}
void AwPrefetchManager::ClearTtl(JNIEnv* env) {
aw_prefetch_manager_data_.SetTtlInSec(kDefaultTtlInSec);
}
void AwPrefetchManager::ClearMaxPrefetches(JNIEnv* env) {
aw_prefetch_manager_data_.SetMaxPrefetches(kDefaultMaxPrefetches);
}
std::vector<AwPrefetchKey>
AwPrefetchManager::GetAllPrefetchKeysForTesting() // IN-TEST
const {
return aw_prefetch_manager_data_.GetAllPrefetchKeysForTesting(); // IN-TEST
}
AwPrefetchKey AwPrefetchManager::GetLastPrefetchKeyForTesting() const {
return aw_prefetch_manager_data_.GetLastPrefetchKeyForTesting(); // IN-TEST
}
bool AwPrefetchManager::GetIsPrefetchInCacheForTesting( // IN-TEST
JNIEnv* env,
AwPrefetchKey prefetch_key) const {
return aw_prefetch_manager_data_.GetIsPrefetchInCacheForTesting( // IN-TEST
prefetch_key);
}
static AwPrefetchKey JNI_AwPrefetchManager_GetNoPrefetchKey(JNIEnv* env) {
return NO_PREFETCH_KEY;
}
static bool JNI_AwPrefetchManager_IsSecPurposeForPrefetch(
JNIEnv* env,
const std::string& sec_purpose_header_value) {
return AwPrefetchManager::IsSecPurposeForPrefetch(sec_purpose_header_value);
}
base::android::ScopedJavaLocalRef<jobject>
AwPrefetchManager::GetJavaPrefetchManager() {
if (!java_obj_) {
JNIEnv* env = base::android::AttachCurrentThread();
java_obj_ =
Java_AwPrefetchManager_create(env, reinterpret_cast<intptr_t>(this));
}
return base::android::ScopedJavaLocalRef<jobject>(java_obj_);
}
} // namespace android_webview
DEFINE_JNI(AwPrefetchManager)