Merge "Add SpeculativeLoadingConfig API" into androidx-main
diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java
new file mode 100644
index 0000000..5f09ac3
--- /dev/null
+++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/PrefetchTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.webkit.test.common.WebkitUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PrefetchTest {
+
+ /**
+ * Test setting valid values for
+ * {@link SpeculativeLoadingConfig.Builder#setPrefetchTtlSeconds(int)}
+ */
+ @Test
+ public void testTTLValidValues() {
+ WebkitUtils.checkFeature(WebViewFeature.SPECULATIVE_LOADING_CONFIG);
+ SpeculativeLoadingConfig.Builder builder = new SpeculativeLoadingConfig.Builder();
+ // lower values
+ builder.setPrefetchTtlSeconds(1);
+ assertEquals(1, builder.build().getPrefetchTtlSeconds());
+
+ builder.setPrefetchTtlSeconds(Integer.MAX_VALUE - 1);
+ assertEquals(Integer.MAX_VALUE - 1, builder.build().getMaxPrefetches());
+
+ builder.setPrefetchTtlSeconds(5685);
+ assertEquals(5685, builder.build().getMaxPrefetches());
+ }
+
+ /**
+ * Test setting valid values for {@link SpeculativeLoadingConfig.Builder#setMaxPrefetches(int)}
+ */
+ @Test
+ public void testMaxPrefetchesValidValues() {
+ WebkitUtils.checkFeature(WebViewFeature.SPECULATIVE_LOADING_CONFIG);
+ SpeculativeLoadingConfig.Builder builder = new SpeculativeLoadingConfig.Builder();
+ builder.setMaxPrefetches(1);
+ assertEquals(1, builder.build().getMaxPrefetches());
+
+ builder.setPrefetchTtlSeconds(SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES);
+ assertEquals(SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES,
+ builder.build().getMaxPrefetches());
+ }
+
+ /**
+ * Test setting out-of-range values for
+ * {@link SpeculativeLoadingConfig.Builder#setPrefetchTtlSeconds(int)}
+ */
+ @Test
+ public void testTTLLimit() {
+ WebkitUtils.checkFeature(WebViewFeature.SPECULATIVE_LOADING_CONFIG);
+ SpeculativeLoadingConfig.Builder builder = new SpeculativeLoadingConfig.Builder();
+
+ IllegalArgumentException expectedException = assertThrows(IllegalArgumentException.class,
+ () -> builder.setPrefetchTtlSeconds(0));
+ assertEquals("Prefetch TTL must be greater than 0", expectedException.getMessage());
+ }
+
+ /**
+ * Test setting out-of-range values for
+ * {@link SpeculativeLoadingConfig.Builder#setMaxPrefetches(int)}
+ */
+ @Test
+ public void testMaxPrefetchesLimit() {
+ WebkitUtils.checkFeature(WebViewFeature.SPECULATIVE_LOADING_CONFIG);
+ SpeculativeLoadingConfig.Builder builder = new SpeculativeLoadingConfig.Builder();
+
+ // lower bound
+ IllegalArgumentException expectedException = assertThrows(IllegalArgumentException.class,
+ () -> builder.setMaxPrefetches(0));
+ assertEquals("Max prefetches must be greater than 0", expectedException.getMessage());
+
+ // upper bound
+ expectedException = assertThrows(IllegalArgumentException.class,
+ () -> builder.setMaxPrefetches(
+ SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES + 1));
+ assertEquals("Max prefetches cannot exceed"
+ + SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES, expectedException.getMessage());
+ }
+
+}
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 77fa0ba..23ee69f 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -80,6 +80,7 @@
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.WebStorage getWebStorage();
method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.SPECULATIVE_LOADING_CONFIG, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void setSpeculativeLoadingConfig(androidx.webkit.SpeculativeLoadingConfig);
field public static final String DEFAULT_PROFILE_NAME = "Default";
}
@@ -162,6 +163,21 @@
method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
}
+ @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public class SpeculativeLoadingConfig {
+ method @IntRange(from=1, to=androidx.webkit.SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES) public int getMaxPrefetches();
+ method @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) public int getPrefetchTtlSeconds();
+ field public static final int ABSOLUTE_MAX_PREFETCHES = 20; // 0x14
+ field public static final int DEFAULT_MAX_PREFETCHES = 10; // 0xa
+ field public static final int DEFAULT_TTL_SECS = 60; // 0x3c
+ }
+
+ @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final class SpeculativeLoadingConfig.Builder {
+ ctor public SpeculativeLoadingConfig.Builder();
+ method @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public androidx.webkit.SpeculativeLoadingConfig build();
+ method public androidx.webkit.SpeculativeLoadingConfig.Builder setMaxPrefetches(@IntRange(from=1, to=androidx.webkit.SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES) int);
+ method public androidx.webkit.SpeculativeLoadingConfig.Builder setPrefetchTtlSeconds(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+ }
+
@SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public final class SpeculativeLoadingParameters {
method public java.util.Map<java.lang.String!,java.lang.String!> getAdditionalHeaders();
method public androidx.webkit.NoVarySearchHeader? getExpectedNoVarySearchData();
@@ -484,6 +500,7 @@
field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
field public static final String SPECULATIVE_LOADING = "SPECULATIVE_LOADING_STATUS";
+ field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String SPECULATIVE_LOADING_CONFIG = "SPECULATIVE_LOADING_CONFIG";
field public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES = "STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
field public static final String STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = "STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 77fa0ba..23ee69f 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -80,6 +80,7 @@
method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROFILE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public android.webkit.WebStorage getWebStorage();
method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void prefetchUrlAsync(String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.OutcomeReceiverCompat<java.lang.Void!,androidx.webkit.PrefetchException!>);
+ method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.SPECULATIVE_LOADING_CONFIG, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.Profile.ExperimentalUrlPrefetch public void setSpeculativeLoadingConfig(androidx.webkit.SpeculativeLoadingConfig);
field public static final String DEFAULT_PROFILE_NAME = "Default";
}
@@ -162,6 +163,21 @@
method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
}
+ @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public class SpeculativeLoadingConfig {
+ method @IntRange(from=1, to=androidx.webkit.SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES) public int getMaxPrefetches();
+ method @IntRange(from=1, to=java.lang.Integer.MAX_VALUE) public int getPrefetchTtlSeconds();
+ field public static final int ABSOLUTE_MAX_PREFETCHES = 20; // 0x14
+ field public static final int DEFAULT_MAX_PREFETCHES = 10; // 0xa
+ field public static final int DEFAULT_TTL_SECS = 60; // 0x3c
+ }
+
+ @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final class SpeculativeLoadingConfig.Builder {
+ ctor public SpeculativeLoadingConfig.Builder();
+ method @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public androidx.webkit.SpeculativeLoadingConfig build();
+ method public androidx.webkit.SpeculativeLoadingConfig.Builder setMaxPrefetches(@IntRange(from=1, to=androidx.webkit.SpeculativeLoadingConfig.ABSOLUTE_MAX_PREFETCHES) int);
+ method public androidx.webkit.SpeculativeLoadingConfig.Builder setPrefetchTtlSeconds(@IntRange(from=1, to=java.lang.Integer.MAX_VALUE) int);
+ }
+
@SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PROFILE_URL_PREFETCH, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.Profile.ExperimentalUrlPrefetch public final class SpeculativeLoadingParameters {
method public java.util.Map<java.lang.String!,java.lang.String!> getAdditionalHeaders();
method public androidx.webkit.NoVarySearchHeader? getExpectedNoVarySearchData();
@@ -484,6 +500,7 @@
field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
field public static final String SPECULATIVE_LOADING = "SPECULATIVE_LOADING_STATUS";
+ field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String SPECULATIVE_LOADING_CONFIG = "SPECULATIVE_LOADING_CONFIG";
field public static final String STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES = "STARTUP_FEATURE_CONFIGURE_PARTITIONED_COOKIES";
field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
field public static final String STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = "STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/Profile.java b/webkit/webkit/src/main/java/androidx/webkit/Profile.java
index ec6fcb1..370e930 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/Profile.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/Profile.java
@@ -236,4 +236,22 @@
@NonNull Executor callbackExecutor,
@NonNull OutcomeReceiverCompat<Void, PrefetchException> operationCallback);
+ /**
+ * Sets the {@link SpeculativeLoadingConfig} for the current profile session.
+ * These configurations will be applied to any Prefetch requests made after they are set;
+ * they will not be applied to in-flight requests.
+ * <p>
+ * These configurations will be applied to any prefetch requests initiated by
+ * a prerender request. This applies specifically to WebViews that are
+ * associated with this Profile.
+ * <p>
+ * @param speculativeLoadingConfig the config to set for this profile session.
+ */
+ @RequiresFeature(name = WebViewFeature.SPECULATIVE_LOADING_CONFIG,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ @UiThread
+ @ExperimentalUrlPrefetch
+ void setSpeculativeLoadingConfig(@NonNull SpeculativeLoadingConfig
+ speculativeLoadingConfig);
+
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/SpeculativeLoadingConfig.java b/webkit/webkit/src/main/java/androidx/webkit/SpeculativeLoadingConfig.java
new file mode 100644
index 0000000..51ef8bd
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/SpeculativeLoadingConfig.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.IntRange;
+
+import org.jspecify.annotations.NonNull;
+
+/**
+ * Represents a configuration for speculative loading in a {@link Profile} instance. This should
+ * be set using {@link Profile#setSpeculativeLoadingConfig(SpeculativeLoadingConfig)}
+ */
[email protected]
+public class SpeculativeLoadingConfig {
+
+ /**
+ * The absolute maximum number of prefetches allowed in cache.
+ */
+ public static final int ABSOLUTE_MAX_PREFETCHES = 20;
+
+ /**
+ * The default Time-to-Live (TTL) in seconds for prefetched data.
+ */
+ public static final int DEFAULT_TTL_SECS = 60;
+
+ /**
+ * The default number of prefetches allowed in cache.
+ */
+ public static final int DEFAULT_MAX_PREFETCHES = 10;
+
+ private final int mPrefetchTTLSeconds;
+
+ private final int mMaxPrefetches;
+
+ /**
+ * Private constructors, the application will need to use
+ * {@link Builder} for constructing instances of
+ * this class.
+ */
+ private SpeculativeLoadingConfig(int ttlSecs, int max) {
+ mPrefetchTTLSeconds = ttlSecs;
+ mMaxPrefetches = max;
+ }
+
+ /**
+ * The "time to live" for a prefetch inside of the prefetch cache.
+ * This is representative of the maximum time that a prefetch is considered
+ * valid and can be served to a navigation. This value is in seconds and
+ * defaults to {@link SpeculativeLoadingConfig#DEFAULT_TTL_SECS}.
+ */
+ @IntRange(from = 1, to = Integer.MAX_VALUE)
+ public int getPrefetchTtlSeconds() {
+ return mPrefetchTTLSeconds;
+ }
+
+ /**
+ * The max amount of prefetches that can live in the cache. Defaults to
+ * {@link SpeculativeLoadingConfig#DEFAULT_MAX_PREFETCHES}.
+ * <p>
+ * Cannot exceed {@link SpeculativeLoadingConfig#ABSOLUTE_MAX_PREFETCHES}.
+ */
+ @IntRange(from = 1, to = ABSOLUTE_MAX_PREFETCHES)
+ public int getMaxPrefetches() {
+ return mMaxPrefetches;
+ }
+
+ @Profile.ExperimentalUrlPrefetch
+ public static final class Builder {
+ private int mPrefetchTTLSeconds = DEFAULT_TTL_SECS;
+ private int mMaxPrefetches = DEFAULT_MAX_PREFETCHES;
+
+ public Builder() {
+ }
+
+ /**
+ * Sets the Time-to-Live (TTL) in seconds for prefetched data.
+ * <p>
+ * This value determines how long prefetched data will be considered valid before it is
+ * refreshed.
+ *
+ * @param ttlSeconds The TTL value in seconds. Must be a positive integer.
+ * @return This builder instance for method chaining.
+ * @throws IllegalArgumentException If {@code ttlSeconds} is less than 1.
+ * @see Builder#build()
+ */
+ @NonNull
+ public Builder setPrefetchTtlSeconds(
+ @IntRange(from = 1, to = Integer.MAX_VALUE) int ttlSeconds) {
+ if (ttlSeconds <= 0) {
+ throw new IllegalArgumentException("Prefetch TTL must be greater than 0");
+ }
+ mPrefetchTTLSeconds = ttlSeconds;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of allowed prefetches.
+ *
+ * <p>
+ * This value limits the number of prefetch data that can live in the cache.
+ *
+ * @param max The maximum number of prefetches. Must be a positive integer and not exceed
+ * {@link SpeculativeLoadingConfig#ABSOLUTE_MAX_PREFETCHES}.
+ * @return This builder instance for method chaining.
+ * @throws IllegalArgumentException If {@code max} is less than 1 or greater than
+ * {@link SpeculativeLoadingConfig#ABSOLUTE_MAX_PREFETCHES}.
+ * @see Builder#build()
+ */
+ @NonNull
+ public Builder setMaxPrefetches(@IntRange(from = 1, to = ABSOLUTE_MAX_PREFETCHES) int max) {
+ if (max > ABSOLUTE_MAX_PREFETCHES) {
+ String error = "Max prefetches cannot exceed" + ABSOLUTE_MAX_PREFETCHES;
+ throw new IllegalArgumentException(error);
+ }
+
+ if (max < 1) {
+ throw new IllegalArgumentException("Max prefetches must be greater than 0");
+ }
+ mMaxPrefetches = max;
+ return this;
+ }
+
+ /**
+ * Builds a new {@link SpeculativeLoadingConfig} instance.
+ * <p>
+ * This method creates a new {@link SpeculativeLoadingConfig} object using the parameters
+ * that have been set in this builder.
+ *
+ * @return A new {@link SpeculativeLoadingConfig} instance.
+ */
+ @Profile.ExperimentalUrlPrefetch
+ @NonNull
+ public SpeculativeLoadingConfig build() {
+ return new SpeculativeLoadingConfig(mPrefetchTTLSeconds, mMaxPrefetches);
+ }
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 2ff0751..18747e3 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -660,6 +660,14 @@
public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
/**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link Profile#setSpeculativeLoadingConfig(SpeculativeLoadingConfig)}
+ */
+ @Profile.ExperimentalUrlPrefetch
+ public static final String SPECULATIVE_LOADING_CONFIG = "SPECULATIVE_LOADING_CONFIG";
+
+ /**
* Return whether a feature is supported at run-time. This will check whether a feature is
* supported, depending on the combination of the desired feature, the Android version of
* device, and the WebView APK on the device.
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
index dd61055..47c026a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
@@ -25,6 +25,7 @@
import androidx.webkit.OutcomeReceiverCompat;
import androidx.webkit.PrefetchException;
import androidx.webkit.Profile;
+import androidx.webkit.SpeculativeLoadingConfig;
import androidx.webkit.SpeculativeLoadingParameters;
import org.chromium.support_lib_boundary.ProfileBoundaryInterface;
@@ -152,4 +153,18 @@
}
}
+ @Override
+ public void setSpeculativeLoadingConfig(
+ @NonNull SpeculativeLoadingConfig speculativeLoadingConfig) {
+ ApiFeature.NoFramework feature = WebViewFeatureInternal.SPECULATIVE_LOADING_CONFIG;
+ if (feature.isSupportedByWebView()) {
+ InvocationHandler configInvocation =
+ BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+ new SpeculativeLoadingConfigAdapter(speculativeLoadingConfig));
+ mProfileImpl.setSpeculativeLoadingConfig(configInvocation);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/SpeculativeLoadingConfigAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/SpeculativeLoadingConfigAdapter.java
new file mode 100644
index 0000000..a2a0501
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/SpeculativeLoadingConfigAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import androidx.webkit.SpeculativeLoadingConfig;
+
+import org.chromium.support_lib_boundary.SpeculativeLoadingConfigBoundaryInterface;
+import org.jspecify.annotations.NonNull;
+
+public class SpeculativeLoadingConfigAdapter implements SpeculativeLoadingConfigBoundaryInterface {
+ private final SpeculativeLoadingConfig mSpeculativeLoadingConfig;
+
+ public SpeculativeLoadingConfigAdapter(@NonNull SpeculativeLoadingConfig config) {
+ mSpeculativeLoadingConfig = config;
+ }
+
+ @Override
+ public int getMaxPrefetches() {
+ return mSpeculativeLoadingConfig.getMaxPrefetches();
+ }
+
+ @Override
+ public int getPrefetchTTLSeconds() {
+ return mSpeculativeLoadingConfig.getPrefetchTtlSeconds();
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index dc818ef..fd040be 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -37,6 +37,7 @@
import androidx.webkit.ProxyController;
import androidx.webkit.SafeBrowsingResponseCompat;
import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.SpeculativeLoadingConfig;
import androidx.webkit.SpeculativeLoadingParameters;
import androidx.webkit.TracingConfig;
import androidx.webkit.TracingController;
@@ -695,6 +696,13 @@
new ApiFeature.NoFramework(WebViewFeature.PRERENDER_WITH_URL,
Features.PRERENDER_WITH_URL);
+ /**
+ * Feature for {@link WebViewFeature#isFeatureSupported(String)}.
+ * This feature covers {@link Profile#setSpeculativeLoadingConfig(SpeculativeLoadingConfig)}
+ */
+ public static final ApiFeature.NoFramework SPECULATIVE_LOADING_CONFIG =
+ new ApiFeature.NoFramework(WebViewFeature.SPECULATIVE_LOADING_CONFIG,
+ Features.SPECULATIVE_LOADING_CONFIG);
// --- Add new feature constants above this line ---