Merge "Stop trying to draw ripples while unattached" into androidx-main
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index de994f6..8bcdb8c 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -23,6 +23,7 @@
 
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.internal.util.TestUtil;
+import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.ImageAnalysis;
@@ -88,7 +89,8 @@
         mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
         mCameraControl = TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
         mTorchControl = mCameraControl.getTorchControl();
-        mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel() > 1;
+        mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel()
+                != CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     @After
@@ -118,7 +120,7 @@
 
     @Test(timeout = 5000L)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOnLevel()
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOn()
             throws ExecutionException, InterruptedException {
         assumeTrue(mIsTorchStrengthSupported);
 
@@ -132,7 +134,7 @@
 
     @Test(timeout = 5000L)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOffLevel()
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOff()
             throws ExecutionException, InterruptedException {
         assumeTrue(mIsTorchStrengthSupported);
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 8a2a439..d6321f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -516,6 +516,10 @@
             return Futures.immediateFailedFuture(
                     new OperationCanceledException("Camera is not active."));
         }
+        if (!mCameraCharacteristics.isTorchStrengthLevelSupported()) {
+            return Futures.immediateFailedFuture(new UnsupportedOperationException(
+                    "The device doesn't support configuring torch strength level."));
+        }
         if (torchStrengthLevel < 1
                 || torchStrengthLevel > mCameraCharacteristics.getMaxTorchStrengthLevel()) {
             return Futures.immediateFailedFuture(new IllegalArgumentException(
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index aeb7058..4804b521 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -719,9 +719,11 @@
     }
 
     @Override
-    @IntRange(from = 1)
+    @IntRange(from = 0)
     public int getMaxTorchStrengthLevel() {
-        return mCameraCharacteristicsCompat.getMaxTorchStrengthLevel();
+        return mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+                ? mCameraCharacteristicsCompat.getMaxTorchStrengthLevel()
+                : TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     @Override
@@ -730,7 +732,9 @@
             if (mCamera2CameraControlImpl == null) {
                 if (mRedirectTorchStrengthLiveData == null) {
                     mRedirectTorchStrengthLiveData = new RedirectableLiveData<>(
-                            mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel());
+                            mCameraCharacteristicsCompat.isTorchStrengthLevelSupported()
+                                    ? mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel()
+                                    : TORCH_STRENGTH_LEVEL_UNSUPPORTED);
                 }
                 return mRedirectTorchStrengthLiveData;
             }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7ea9db9..1bb729d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -103,10 +103,10 @@
         mExecutor = executor;
 
         mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
-        mIsTorchStrengthSupported =
-                mHasFlashUnit && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
-                        && cameraCharacteristics.getMaxTorchStrengthLevel() > 1;
-        mDefaultTorchStrength = cameraCharacteristics.getDefaultTorchStrengthLevel();
+        mIsTorchStrengthSupported = cameraCharacteristics.isTorchStrengthLevelSupported();
+        mDefaultTorchStrength = mHasFlashUnit && mIsTorchStrengthSupported
+                ? cameraCharacteristics.getDefaultTorchStrengthLevel()
+                : Camera2CameraInfoImpl.TORCH_STRENGTH_LEVEL_UNSUPPORTED;
         mTargetTorchStrength = mDefaultTorchStrength;
         mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
         mTorchStrength = new MutableLiveData<>(mDefaultTorchStrength);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index 1f2cc60..766cfec 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -22,6 +22,7 @@
 import android.os.Build;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.internal.compat.workaround.OutputSizesCorrector;
 
@@ -135,23 +136,40 @@
      */
     public int getDefaultTorchStrengthLevel() {
         Integer defaultLevel = null;
-        if (Build.VERSION.SDK_INT >= 35) {
+        if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
             defaultLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL);
         }
+        // The framework returns 1 when the device doesn't support configuring torch strength. So
+        // also return 1 if the device doesn't have flash unit or is unable to provide the
+        // information.
         return defaultLevel == null ? 1 : defaultLevel;
     }
 
     /**
      * Returns the maximum torch strength level.
      */
+    @IntRange(from = 1)
     public int getMaxTorchStrengthLevel() {
         Integer maxLevel = null;
-        if (Build.VERSION.SDK_INT >= 35) {
+        if (hasFlashUnit() && Build.VERSION.SDK_INT >= 35) {
             maxLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL);
         }
+        // The framework returns 1 when the device doesn't support configuring torch strength. So
+        // also return 1 if the device doesn't have flash unit or is unable to provide the
+        // information.
         return maxLevel == null ? 1 : maxLevel;
     }
 
+    public boolean isTorchStrengthLevelSupported() {
+        return hasFlashUnit() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+                && getMaxTorchStrengthLevel() > 1;
+    }
+
+    private boolean hasFlashUnit() {
+        Boolean flashInfoAvailable = get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+        return flashInfoAvailable != null && flashInfoAvailable;
+    }
+
     /**
      * Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
      * workarounds in it.
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index e7fcc70..1d87d00 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -916,6 +916,30 @@
         assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(CAMERA0_MAX_TORCH_STRENGTH);
     }
 
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnMaxTorchStrengthUnsupported()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnTorchStrengthUnsupported()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
+    }
+
     @Config(minSdk = 33)
     @Test
     public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
@@ -944,13 +968,14 @@
 
     @Config(maxSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM - 1)
     @Test
-    public void apiVersionNotMet_returnMaxTorchStrengthOne()
+    public void apiVersionNotMet_returnMaxTorchStrengthUnsupported()
             throws CameraAccessExceptionCompat {
         init(/* hasAvailableCapabilities = */ true);
 
         final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
 
-        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(1);
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(
+                CameraInfo.TORCH_STRENGTH_LEVEL_UNSUPPORTED);
     }
 
     @Config(maxSdk = 32)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
index d725f7f..9f45cd9 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/TorchControlTest.java
@@ -155,6 +155,17 @@
     }
 
     @Test
+    public void setTorchStrengthLevel_throwExceptionWhenNoFlashUnit() throws InterruptedException {
+        Throwable cause = null;
+        try {
+            mNoFlashUnitTorchControl.setTorchStrengthLevel(1).get();
+        } catch (ExecutionException e) {
+            cause = e.getCause();
+        }
+        assertThat(cause).isInstanceOf(UnsupportedOperationException.class);
+    }
+
+    @Test
     public void enableTorch_whenInactive() throws InterruptedException {
         mTorchControl.setActive(false);
         ListenableFuture<Void> listenableFuture = mTorchControl.enableTorch(true);
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -49,7 +49,7 @@
     method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
     method public default int getLensFacing();
     method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
-    method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+    method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
     method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
     method public int getSensorRotationDegrees();
     method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
     method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
     method public static boolean mustPlayShutterSound();
     method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+    field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
   }
 
   public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 5c0933e..893b313 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -49,7 +49,7 @@
     method @FloatRange(from=0, fromInclusive=false) public default float getIntrinsicZoomRatio();
     method public default int getLensFacing();
     method public default androidx.lifecycle.LiveData<java.lang.Integer!> getLowLightBoostState();
-    method @IntRange(from=1) public default int getMaxTorchStrengthLevel();
+    method @IntRange(from=0) public default int getMaxTorchStrengthLevel();
     method public default java.util.Set<androidx.camera.core.CameraInfo!> getPhysicalCameraInfos();
     method public int getSensorRotationDegrees();
     method public int getSensorRotationDegrees(int);
@@ -64,6 +64,7 @@
     method @SuppressCompatibility @androidx.camera.core.ExperimentalZeroShutterLag public default boolean isZslSupported();
     method public static boolean mustPlayShutterSound();
     method public default java.util.Set<androidx.camera.core.DynamicRange!> querySupportedDynamicRanges(java.util.Set<androidx.camera.core.DynamicRange!>);
+    field public static final int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0; // 0x0
   }
 
   public final class CameraInfoUnavailableException extends java.lang.Exception {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index ac335d1..2332ad6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -245,6 +245,10 @@
      * {@link ListenableFuture} will fail with an {@link IllegalArgumentException} and it won't
      * modify the torch strength.
      *
+     * <p>If the device doesn't have a flash unit or doesn't support configuring torch strength
+     * level, the returned {@link ListenableFuture} will fail with an
+     * {@link UnsupportedOperationException}.
+     *
      * @param torchStrengthLevel The desired torch strength level.
      * @return a {@link ListenableFuture} that is completed when the torch strength has been
      * applied.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 52a744f..cd2f8a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -50,6 +50,12 @@
 public interface CameraInfo {
 
     /**
+     * The torch strength level when the device doesn't have a flash unit or doesn't support
+     * adjusting torch strength.
+     */
+    int TORCH_STRENGTH_LEVEL_UNSUPPORTED = 0;
+
+    /**
      * An unknown intrinsic zoom ratio. Usually to indicate the camera is unable to provide
      * necessary information to resolve its intrinsic zoom ratio.
      *
@@ -429,12 +435,12 @@
     /**
      * Returns the maximum torch strength level.
      *
-     * @return The maximum strength level, or {code 1} if the device doesn't have a flash unit or
-     * doesn't support configuring torch strength.
+     * @return The maximum strength level, or {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if the
+     * device doesn't have a flash unit or doesn't support configuring torch strength.
      */
-    @IntRange(from = 1)
+    @IntRange(from = 0)
     default int getMaxTorchStrengthLevel() {
-        return 1;
+        return TORCH_STRENGTH_LEVEL_UNSUPPORTED;
     }
 
     /**
@@ -442,9 +448,12 @@
      *
      * <p>The value of the {@link LiveData} will be the default torch strength level of this
      * device if {@link CameraControl#setTorchStrengthLevelAsync(int)} hasn't been called.
+     *
+     * <p>The value of the {@link LiveData} will be {@link #TORCH_STRENGTH_LEVEL_UNSUPPORTED} if
+     * the device doesn't have a flash unit or doesn't support configuring torch strength.
      */
     default @NonNull LiveData<Integer> getTorchStrengthLevel() {
-        return new MutableLiveData<>(1);
+        return new MutableLiveData<>(TORCH_STRENGTH_LEVEL_UNSUPPORTED);
     }
 
     @StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 10d896a..de7943c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -19,6 +19,7 @@
 import android.util.Range;
 import android.util.Size;
 
+import androidx.annotation.IntRange;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraState;
@@ -78,6 +79,7 @@
     }
 
     @Override
+    @IntRange(from = 0)
     public int getMaxTorchStrengthLevel() {
         return mCameraInfoInternal.getMaxTorchStrengthLevel();
     }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index befe1c0..c3aa6d1 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -365,13 +365,13 @@
     fun getResolutionInfo_shouldMatchRecordedVideoResolution() {
         // Arrange.
         checkAndBindUseCases(preview, videoCapture)
+        val resolutionInfo = videoCapture.resolutionInfo!!
 
         // Act.
         val result = recordingSession.createRecording().recordAndVerify()
 
         // Assert: the resolution of the video file should match the resolution calculated by
         // rotating the cropRect specified in the ResolutionInfo.
-        val resolutionInfo = videoCapture.resolutionInfo!!
         val expectedResolution =
             rotateSize(rectToSize(resolutionInfo.cropRect), resolutionInfo.rotationDegrees)
         verifyVideoResolution(context, result.file, expectedResolution)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
index a90d80a..50eb946 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
 import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
@@ -854,6 +855,38 @@
         }
     }
 
+    @Test
+    fun textField_keyEvent_functionReference() {
+        val state = mutableIntStateOf(0)
+        var handled = -1
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            val stateValue = state.value
+
+            @Suppress("UNUSED_PARAMETER")
+            fun handle(key: KeyEvent): Boolean {
+                handled = stateValue
+                return true
+            }
+
+            BasicTextField(
+                value = "text",
+                onValueChange = {},
+                modifier = Modifier.focusRequester(focusRequester).testTag(tag).onKeyEvent(::handle)
+            )
+        }
+
+        rule.runOnIdle { focusRequester.requestFocus() }
+        rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+        rule.runOnIdle {
+            assertThat(handled).isEqualTo(0)
+            state.value += 1
+        }
+
+        rule.onNodeWithTag(tag).performKeyInput { pressKey(Key.A) }
+        rule.runOnIdle { assertThat(handled).isEqualTo(1) }
+    }
+
     private inner class SequenceScope(
         val state: TextFieldState,
         val clipboard: Clipboard,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index cba5ff1..d60c1b5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -46,7 +46,7 @@
 fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier =
     this then KeyInputElement(onKeyEvent = null, onPreKeyEvent = onPreviewKeyEvent)
 
-private data class KeyInputElement(
+private class KeyInputElement(
     val onKeyEvent: ((KeyEvent) -> Boolean)?,
     val onPreKeyEvent: ((KeyEvent) -> Boolean)?
 ) : ModifierNodeElement<KeyInputNode>() {
@@ -67,6 +67,21 @@
             properties["onPreviewKeyEvent"] = it
         }
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is KeyInputElement) return false
+
+        if (onKeyEvent !== other.onKeyEvent) return false
+        if (onPreKeyEvent !== other.onPreKeyEvent) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = onKeyEvent?.hashCode() ?: 0
+        result = 31 * result + (onPreKeyEvent?.hashCode() ?: 0)
+        return result
+    }
 }
 
 private class KeyInputNode(
diff --git a/libraryversions.toml b/libraryversions.toml
index 4158de0..59c4949 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -168,7 +168,7 @@
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
-WEAR_PHONE_INTERACTIONS = "1.1.0-alpha05"
+WEAR_PHONE_INTERACTIONS = "1.1.0-beta01"
 WEAR_PROTOLAYOUT = "1.3.0-alpha07"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-rc01"
 WEAR_TILES = "1.5.0-alpha07"
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 3d44721..1dc4afe 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -853,9 +853,9 @@
     private fun resetViewsAndModels(fileUri: Uri) {
         if (pdfLoader != null) {
             pdfLoaderCallbacks?.uri = fileUri
-            paginatedView?.resetModels()
             destroyContentModel()
         }
+        paginatedView?.resetModels()
         fastScrollView?.resetContents()
         findInFileView?.resetFindInFile()
     }
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
index 222e6bd..8467ece 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/MainActivity.kt
@@ -299,7 +299,15 @@
                 } else {
                     switchContentFragment(ResizeFragment(), menuItem.title)
                 }
-            R.id.item_scroll -> switchContentFragment(ScrollFragment(), menuItem.title)
+            R.id.item_scroll ->
+                if (useCompose) {
+                    switchContentFragment(
+                        ScrollComposeFragment(),
+                        "${menuItem.title} ${getString(R.string.compose)}"
+                    )
+                } else {
+                    switchContentFragment(ScrollFragment(), menuItem.title)
+                }
             R.id.item_pooling_container ->
                 switchContentFragment(PoolingContainerFragment(), menuItem.title)
             R.id.item_fullscreen ->
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
new file mode 100644
index 0000000..6acfb9c
--- /dev/null
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollComposeFragment.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.privacysandbox.ui.integration.testapp
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
+import androidx.privacysandbox.ui.client.view.SandboxedSdkUi
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+class ScrollComposeFragment : BaseFragment() {
+
+    private var bottomBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+    private var scrollBannerAdapter: SandboxedUiAdapter? by mutableStateOf(null)
+
+    override fun handleLoadAdFromDrawer(
+        adType: Int,
+        mediationOption: Int,
+        drawViewabilityLayer: Boolean
+    ) {
+        currentAdType = adType
+        currentMediationOption = mediationOption
+        shouldDrawViewabilityLayer = drawViewabilityLayer
+        setAdapter()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        setAdapter()
+        return ComposeView(requireContext()).apply {
+            // Dispose of the Composition when the view's LifecycleOwner is destroyed
+            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+            setContent {
+                Column(
+                    modifier = Modifier.fillMaxSize().padding(16.dp),
+                    verticalArrangement = Arrangement.Top,
+                    horizontalAlignment = Alignment.Start
+                ) {
+                    Column(modifier = Modifier.weight(0.8f).verticalScroll(rememberScrollState())) {
+                        scrollBannerAdapter?.let {
+                            SandboxedSdkUi(
+                                it,
+                                Modifier.fillMaxWidth().height(200.dp),
+                                providerUiOnTop = providerUiOnTop
+                            )
+                        }
+                        Text(stringResource(R.string.long_text), Modifier.padding(vertical = 16.dp))
+                    }
+                    bottomBannerAdapter?.let {
+                        SandboxedSdkUi(it, Modifier.weight(0.2f), providerUiOnTop = providerUiOnTop)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun setAdapter() {
+        val coroutineScope = MainScope()
+        coroutineScope.launch {
+            bottomBannerAdapter =
+                SandboxedUiAdapterFactory.createFromCoreLibInfo(
+                    getSdkApi()
+                        .loadBannerAd(
+                            currentAdType,
+                            currentMediationOption,
+                            false,
+                            shouldDrawViewabilityLayer,
+                        )
+                )
+            scrollBannerAdapter =
+                SandboxedUiAdapterFactory.createFromCoreLibInfo(
+                    getSdkApi()
+                        .loadBannerAd(
+                            currentAdType,
+                            currentMediationOption,
+                            false,
+                            shouldDrawViewabilityLayer,
+                        )
+                )
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
index 7e04e8a..edfbff0 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/resources.proto
@@ -155,6 +155,9 @@
   //
   // If not set, the animation will play on load.</setter>
   androidx.wear.protolayout.expression.proto.DynamicFloat progress = 2;
+
+  // The trigger to start the animation.
+  Trigger start_trigger = 3;
 }
 
 // An image resource, which can be used by layouts. This holds multiple
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1309,12 +1309,14 @@
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
     method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
     method @RawRes public int getRawResourceId();
+    method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
   }
 
   public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
     ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
     method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
   }
 
   @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
   public final class TriggerBuilders {
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
   }
 
+  public final class BorderKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+  }
+
   public final class ClickableKt {
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
     method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
   }
 
+  public final class OpacityKt {
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+  }
+
   public final class PaddingKt {
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
     method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
   }
 
+  public final class VisibilityKt {
+    method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+  }
+
 }
 
 package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 21886f5..da4e766 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1309,12 +1309,14 @@
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public static final class ResourceBuilders.AndroidLottieResourceByResId {
     method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? getProgress();
     method @RawRes public int getRawResourceId();
+    method public androidx.wear.protolayout.TriggerBuilders.Trigger? getStartTrigger();
   }
 
   public static final class ResourceBuilders.AndroidLottieResourceByResId.Builder {
     ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public ResourceBuilders.AndroidLottieResourceByResId.Builder(@RawRes int);
     method public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setProgress(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=500) public androidx.wear.protolayout.ResourceBuilders.AndroidLottieResourceByResId.Builder setStartTrigger(androidx.wear.protolayout.TriggerBuilders.Trigger);
   }
 
   @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class ResourceBuilders.AndroidSeekableAnimatedImageResourceByResId {
@@ -1434,6 +1436,24 @@
   public final class TriggerBuilders {
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnConditionMetTrigger(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnLoadTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleOnceTrigger();
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static androidx.wear.protolayout.TriggerBuilders.Trigger createOnVisibleTrigger();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleOnceTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleOnceTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleOnceTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleOnceTrigger build();
+  }
+
+  @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static final class TriggerBuilders.OnVisibleTrigger implements androidx.wear.protolayout.TriggerBuilders.Trigger {
+  }
+
+  public static final class TriggerBuilders.OnVisibleTrigger.Builder {
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public TriggerBuilders.OnVisibleTrigger.Builder();
+    method public androidx.wear.protolayout.TriggerBuilders.OnVisibleTrigger build();
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface TriggerBuilders.Trigger {
@@ -1527,6 +1547,10 @@
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier clipTopRight(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float x, optional @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float y);
   }
 
+  public final class BorderKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier border(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float width, androidx.wear.protolayout.types.LayoutColor color);
+  }
+
   public final class ClickableKt {
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable();
     method public static androidx.wear.protolayout.ModifiersBuilders.Clickable clickable(optional androidx.wear.protolayout.ActionBuilders.Action action);
@@ -1557,6 +1581,10 @@
     method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
   }
 
+  public final class OpacityKt {
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) public static androidx.wear.protolayout.modifiers.LayoutModifier opacity(androidx.wear.protolayout.modifiers.LayoutModifier, @FloatRange(from=0.0, to=1.0) float staticValue, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat? dynamicValue);
+  }
+
   public final class PaddingKt {
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, androidx.wear.protolayout.ModifiersBuilders.Padding padding);
     method public static androidx.wear.protolayout.modifiers.LayoutModifier padding(androidx.wear.protolayout.modifiers.LayoutModifier, @Dimension(unit=androidx.annotation.Dimension.Companion.DP) float all);
@@ -1572,6 +1600,10 @@
     method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
   }
 
+  public final class VisibilityKt {
+    method @SuppressCompatibility @androidx.wear.protolayout.expression.ProtoLayoutExperimental @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) public static androidx.wear.protolayout.modifiers.LayoutModifier visibility(androidx.wear.protolayout.modifiers.LayoutModifier, boolean staticVisibility, optional androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool? dynamicVisibility);
+  }
+
 }
 
 package androidx.wear.protolayout.types {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
index 0fff0a2..a51c996 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ResourceBuilders.java
@@ -520,6 +520,15 @@
             }
         }
 
+        /** Gets the trigger to start the animation. */
+        public @Nullable Trigger getStartTrigger() {
+            if (mImpl.hasStartTrigger()) {
+                return TriggerBuilders.triggerFromProto(mImpl.getStartTrigger());
+            } else {
+                return null;
+            }
+        }
+
         /** Creates a new wrapper instance from the proto. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         public static @NonNull AndroidLottieResourceByResId fromProto(
@@ -540,6 +549,8 @@
                     + getRawResourceId()
                     + ", progress="
                     + getProgress()
+                    + ", startTrigger="
+                    + getStartTrigger()
                     + "}";
         }
 
@@ -590,6 +601,13 @@
                 return this;
             }
 
+            /** Sets the trigger to start the animation. */
+            @RequiresSchemaVersion(major = 1, minor = 500)
+            public @NonNull Builder setStartTrigger(@NonNull Trigger startTrigger) {
+                mImpl.setStartTrigger(startTrigger.toTriggerProto());
+                return this;
+            }
+
             /** Builds an instance from accumulated values. */
             public @NonNull AndroidLottieResourceByResId build() {
                 return AndroidLottieResourceByResId.fromProto(mImpl.build());
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
index 1a96d0e..d05a53c 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TriggerBuilders.java
@@ -48,6 +48,160 @@
         return new OnConditionMetTrigger.Builder().setCondition(dynamicBool).build();
     }
 
+    /**
+     * Creates a {@link Trigger} that fires *every time* the layout becomes visible.
+     *
+     * <p>As opposed to {@link #createOnLoadTrigger()}, this will wait until layout is fully visible
+     * before firing a trigger.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static @NonNull Trigger createOnVisibleTrigger() {
+        return new OnVisibleTrigger.Builder().build();
+    }
+
+    /**
+     * Creates a {@link Trigger} that fires the first time that layout becomes visible.
+     *
+     * <p>As opposed to {@link #createOnVisibleTrigger()}, this will only be fired the first time
+     * that the layout becomes visible.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static @NonNull Trigger createOnVisibleOnceTrigger() {
+        return new OnVisibleOnceTrigger.Builder().build();
+    }
+
+    /** Triggers when the layout visibility state turns from invisible to fully visible. */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static final class OnVisibleTrigger implements Trigger {
+        private final TriggerProto.OnVisibleTrigger mImpl;
+        private final @Nullable Fingerprint mFingerprint;
+
+        OnVisibleTrigger(TriggerProto.OnVisibleTrigger impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public @Nullable Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static @NonNull OnVisibleTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleTrigger proto, @Nullable Fingerprint fingerprint) {
+            return new OnVisibleTrigger(proto, fingerprint);
+        }
+
+        static @NonNull OnVisibleTrigger fromProto(TriggerProto.@NonNull OnVisibleTrigger proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        TriggerProto.@NonNull OnVisibleTrigger toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public TriggerProto.@NonNull Trigger toTriggerProto() {
+            return TriggerProto.Trigger.newBuilder().setOnVisibleTrigger(mImpl).build();
+        }
+
+        @Override
+        public @NonNull String toString() {
+            return "OnVisibleTrigger";
+        }
+
+        /** Builder for {@link OnVisibleTrigger}. */
+        @SuppressWarnings("HiddenSuperclass")
+        public static final class Builder implements Trigger.Builder {
+            private final TriggerProto.OnVisibleTrigger.Builder mImpl =
+                    TriggerProto.OnVisibleTrigger.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(1416366796);
+
+            /** Creates an instance of {@link Builder}. */
+            @RequiresSchemaVersion(major = 1, minor = 200)
+            public Builder() {}
+
+            /** Builds an instance from accumulated values. */
+            @Override
+            public @NonNull OnVisibleTrigger build() {
+                return new OnVisibleTrigger(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
+    /**
+     * Triggers only once when the layout visibility state turns from invisible to fully visible for
+     * the first time.
+     */
+    @RequiresSchemaVersion(major = 1, minor = 200)
+    public static final class OnVisibleOnceTrigger implements Trigger {
+        private final TriggerProto.OnVisibleOnceTrigger mImpl;
+        private final @Nullable Fingerprint mFingerprint;
+
+        OnVisibleOnceTrigger(
+                TriggerProto.OnVisibleOnceTrigger impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public @Nullable Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static @NonNull OnVisibleOnceTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleOnceTrigger proto,
+                @Nullable Fingerprint fingerprint) {
+            return new OnVisibleOnceTrigger(proto, fingerprint);
+        }
+
+        static @NonNull OnVisibleOnceTrigger fromProto(
+                TriggerProto.@NonNull OnVisibleOnceTrigger proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        TriggerProto.@NonNull OnVisibleOnceTrigger toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public TriggerProto.@NonNull Trigger toTriggerProto() {
+            return TriggerProto.Trigger.newBuilder().setOnVisibleOnceTrigger(mImpl).build();
+        }
+
+        @Override
+        public @NonNull String toString() {
+            return "OnVisibleOnceTrigger";
+        }
+
+        /** Builder for {@link OnVisibleOnceTrigger}. */
+        @SuppressWarnings("HiddenSuperclass")
+        public static final class Builder implements Trigger.Builder {
+            private final TriggerProto.OnVisibleOnceTrigger.Builder mImpl =
+                    TriggerProto.OnVisibleOnceTrigger.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(-1661457257);
+
+            /** Creates an instance of {@link Builder}. */
+            @RequiresSchemaVersion(major = 1, minor = 200)
+            public Builder() {}
+
+            /** Builds an instance from accumulated values. */
+            @Override
+            public @NonNull OnVisibleOnceTrigger build() {
+                return new OnVisibleOnceTrigger(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
     /** Triggers immediately when the layout is loaded / reloaded. */
     @RequiresSchemaVersion(major = 1, minor = 200)
     static final class OnLoadTrigger implements Trigger {
@@ -225,6 +379,12 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static @NonNull Trigger triggerFromProto(
             TriggerProto.@NonNull Trigger proto, @Nullable Fingerprint fingerprint) {
+        if (proto.hasOnVisibleTrigger()) {
+            return OnVisibleTrigger.fromProto(proto.getOnVisibleTrigger(), fingerprint);
+        }
+        if (proto.hasOnVisibleOnceTrigger()) {
+            return OnVisibleOnceTrigger.fromProto(proto.getOnVisibleOnceTrigger(), fingerprint);
+        }
         if (proto.hasOnLoadTrigger()) {
             return OnLoadTrigger.fromProto(proto.getOnLoadTrigger(), fingerprint);
         }
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
new file mode 100644
index 0000000..d3fca75
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Border.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.wear.protolayout.modifiers
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ModifiersBuilders.Border
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.dp
+
+/**
+ * Adds a modifier to apply a border around an element.
+ *
+ * @param width The width of the border, in `DP`.
+ * @param color The color of the border.
+ */
+fun LayoutModifier.border(@Dimension(DP) width: Float, color: LayoutColor): LayoutModifier =
+    this then BaseBorderElement(width, color)
+
+internal class BaseBorderElement(@Dimension(DP) val width: Float, val color: LayoutColor) :
+    LayoutModifier.Element {
+    fun foldIn(initial: Border.Builder?): Border.Builder =
+        (initial ?: Border.Builder()).setWidth(width.dp).setColor(color.prop)
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
index 0b5531a..aee3599 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -16,15 +16,23 @@
 
 package androidx.wear.protolayout.modifiers
 
+import android.annotation.SuppressLint
+import androidx.annotation.OptIn
 import androidx.wear.protolayout.ModifiersBuilders
 import androidx.wear.protolayout.ModifiersBuilders.Background
+import androidx.wear.protolayout.ModifiersBuilders.Border
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
 import androidx.wear.protolayout.ModifiersBuilders.Corner
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Padding
 import androidx.wear.protolayout.ModifiersBuilders.Semantics
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
 
 /** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
+@SuppressLint("ProtoLayoutMinSchema")
+@OptIn(ProtoLayoutExperimental::class)
 fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers {
     var semantics: Semantics.Builder? = null
     var background: Background.Builder? = null
@@ -32,6 +40,9 @@
     var clickable: Clickable.Builder? = null
     var padding: Padding.Builder? = null
     var metadata: ElementMetadata.Builder? = null
+    var border: Border.Builder? = null
+    var visible: BoolProp.Builder? = null
+    var opacity: FloatProp.Builder? = null
 
     this.foldIn(Unit) { _, e ->
         when (e) {
@@ -41,6 +52,9 @@
             is BaseClickableElement -> clickable = e.foldIn(clickable)
             is BasePaddingElement -> padding = e.foldIn(padding)
             is BaseMetadataElement -> metadata = e.foldIn(metadata)
+            is BaseBorderElement -> border = e.foldIn(border)
+            is BaseVisibilityElement -> visible = e.foldIn(visible)
+            is BaseOpacityElement -> opacity = e.foldIn(opacity)
         }
     }
 
@@ -53,6 +67,9 @@
             clickable?.let { setClickable(it.build()) }
             padding?.let { setPadding(it.build()) }
             metadata?.let { setMetadata(it.build()) }
+            border?.let { setBorder(it.build()) }
+            visible?.let { setVisible(it.build()) }
+            opacity?.let { setOpacity(it.build()) }
         }
         .build()
 }
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
new file mode 100644
index 0000000..eeafb8f9
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Opacity.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.annotation.FloatRange
+import androidx.wear.protolayout.TypeBuilders.FloatProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the opacity of the element with a value from 0 to 1, where 0 means the
+ * element is completely transparent and 1 means the element is completely opaque.
+ *
+ * @param staticValue The static value for opacity. This value will be used if [dynamicValue] is
+ *   null, or if can't be resolved.
+ * @param dynamicValue The dynamic value for opacity. This can be used to change the opacity of the
+ *   element dynamically (without changing the layout definition). To create a smooth transition for
+ *   the dynamic change, you can use one of [DynamicFloat.animate] methods.
+ */
+@RequiresSchemaVersion(major = 1, minor = 400)
+fun LayoutModifier.opacity(
+    @FloatRange(from = 0.0, to = 1.0) staticValue: Float,
+    dynamicValue: DynamicFloat? = null
+): LayoutModifier = this then BaseOpacityElement(staticValue, dynamicValue)
+
+@RequiresSchemaVersion(major = 1, minor = 400)
+internal class BaseOpacityElement(val staticValue: Float, val dynamicValue: DynamicFloat? = null) :
+    LayoutModifier.Element {
+    @SuppressLint("ProtoLayoutMinSchema")
+    fun foldIn(initial: FloatProp.Builder?): FloatProp.Builder =
+        (initial ?: FloatProp.Builder(staticValue)).apply {
+            dynamicValue?.let { setDynamicValue(it) }
+        }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
new file mode 100644
index 0000000..b7eabd7
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Visibility.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+
+/**
+ * Adds a modifier to specify the visibility of the element. A hidden element still consume space in
+ * the layout, but will not render any contents, nor will any of its children render any contents.
+ *
+ * Note that hidden elements won't receive input events.
+ *
+ * @param staticVisibility The static value for visibility. This value will be used if
+ *   [dynamicVisibility] is null, or if can't be resolved.
+ * @param dynamicVisibility The dynamic value for visibility. This can be used to change the
+ *   visibility of the element dynamically (without changing the layout definition).
+ */
+@RequiresSchemaVersion(major = 1, minor = 300)
+@ProtoLayoutExperimental
+fun LayoutModifier.visibility(
+    staticVisibility: Boolean,
+    dynamicVisibility: DynamicBool? = null
+): LayoutModifier = this then BaseVisibilityElement(staticVisibility, dynamicVisibility)
+
+@RequiresSchemaVersion(major = 1, minor = 300)
+internal class BaseVisibilityElement(
+    val visibility: Boolean,
+    val dynamicVisibility: DynamicBool? = null
+) : LayoutModifier.Element {
+    @SuppressLint("ProtoLayoutMinSchema")
+    fun foldIn(initial: BoolProp.Builder?): BoolProp.Builder =
+        (initial ?: BoolProp.Builder(visibility)).apply {
+            dynamicVisibility?.let { setDynamicValue(it) }
+        }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
index 3591875..c0256c3 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/ResourceBuildersTest.java
@@ -74,9 +74,25 @@
                         .setProgress(DynamicBuilders.DynamicFloat.from(new AppDataKey<>(stateKey)))
                         .build();
 
-        ResourceProto.AndroidLottieResourceByResId avdProto = lottieResource.toProto();
+        ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
 
-        assertThat(avdProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
-        assertThat(avdProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+        assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+        assertThat(lottieProto.getProgress().getStateSource().getSourceKey()).isEqualTo(stateKey);
+    }
+
+    @Test
+    public void lottieAnimation_hasTrigger() {
+        ResourceBuilders.AndroidLottieResourceByResId lottieResource =
+                new ResourceBuilders.AndroidLottieResourceByResId.Builder(RESOURCE_ID)
+                        .setStartTrigger(TriggerBuilders.createOnVisibleTrigger())
+                        .build();
+
+        ResourceProto.AndroidLottieResourceByResId lottieProto = lottieResource.toProto();
+
+        assertThat(lottieProto.getRawResourceId()).isEqualTo(RESOURCE_ID);
+        assertThat(lottieProto.hasStartTrigger()).isTrue();
+        assertThat(lottieProto.getStartTrigger().hasOnVisibleTrigger()).isTrue();
+        assertThat(lottieProto.getStartTrigger().hasOnVisibleOnceTrigger()).isFalse();
+        assertThat(lottieProto.getStartTrigger().hasOnLoadTrigger()).isFalse();
     }
 }
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
index 80f8b5e..ca402dc 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -24,6 +24,7 @@
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
 import androidx.wear.protolayout.expression.AppDataKey
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue
@@ -241,6 +242,26 @@
         assertThat(modifiers.metadata?.tagData).isEqualTo(METADATA_BYTE_ARRAY)
     }
 
+    @Test
+    fun border_toModifier() {
+        val modifier =
+            LayoutModifier.border(width = WIDTH_DP, color = COLOR).toProtoLayoutModifiers()
+
+        assertThat(modifier.border?.width?.value).isEqualTo(WIDTH_DP)
+        assertThat(modifier.border?.color?.argb).isEqualTo(COLOR.prop.argb)
+    }
+
+    @Test
+    fun visibility_toModifier() {
+        val modifier =
+            LayoutModifier.visibility(staticVisibility = false, dynamicVisibility = DYNAMIC_BOOL)
+                .toProtoLayoutModifiers()
+
+        assertThat(modifier.isVisible.value).isEqualTo(false)
+        assertThat(modifier.isVisible.dynamicValue?.toDynamicBoolProto())
+            .isEqualTo(DYNAMIC_BOOL.toDynamicBoolProto())
+    }
+
     companion object {
         const val STATIC_CONTENT_DESCRIPTION = "content desc"
         val DYNAMIC_CONTENT_DESCRIPTION = DynamicString.constant("dynamic content")
@@ -255,5 +276,7 @@
         const val PADDING_ALL = 5f
         const val METADATA = "metadata"
         val METADATA_BYTE_ARRAY = METADATA.toByteArray()
+        const val WIDTH_DP = 5f
+        val DYNAMIC_BOOL = DynamicBool.constant(true)
     }
 }
diff --git a/wear/wear-phone-interactions/api/1.1.0-beta01.txt b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..22d6f21
--- /dev/null
+++ b/wear/wear-phone-interactions/api/1.1.0-beta01.txt
@@ -0,0 +1,170 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+  public final class PhoneTypeHelper {
+    method public static int getPhoneDeviceType(android.content.Context context);
+    field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+    field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+    field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+    field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+    field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+  }
+
+  public static final class PhoneTypeHelper.Companion {
+    method public int getPhoneDeviceType(android.content.Context context);
+    property public static final int DEVICE_TYPE_ANDROID;
+    property public static final int DEVICE_TYPE_ERROR;
+    property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
+    property public static final int DEVICE_TYPE_UNKNOWN;
+  }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+    ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+    method public String getValue();
+    property public final String value;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+    ctor public CodeVerifier();
+    ctor public CodeVerifier(optional int byteLength);
+    ctor public CodeVerifier(String value);
+    method public String getValue();
+    property public final String value;
+  }
+
+  public final class OAuthRequest {
+    method public String getPackageName();
+    method public String getRedirectUrl();
+    method public android.net.Uri getRequestUrl();
+    property public final String packageName;
+    property public final String redirectUrl;
+    property public final android.net.Uri requestUrl;
+    field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+    field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+    field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+  }
+
+  public static final class OAuthRequest.Builder {
+    ctor public OAuthRequest.Builder(android.content.Context context);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+  }
+
+  public static final class OAuthRequest.Companion {
+    property public static final String WEAR_REDIRECT_URL_PREFIX;
+    property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+  }
+
+  public final class OAuthResponse {
+    method public int getErrorCode();
+    method public android.net.Uri? getResponseUrl();
+    property public final int errorCode;
+    property public final android.net.Uri? responseUrl;
+  }
+
+  public static final class OAuthResponse.Builder {
+    ctor public OAuthResponse.Builder();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(int errorCode);
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+  }
+
+  public final class RemoteAuthClient implements java.lang.AutoCloseable {
+    method @UiThread public void close();
+    method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    method protected void finalize();
+    method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+    method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+    property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
+    field public static final int STATUS_AVAILABLE = 3; // 0x3
+    field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class RemoteAuthClient.Callback {
+    ctor public RemoteAuthClient.Callback();
+    method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, int errorCode);
+    method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+  }
+
+  public static final class RemoteAuthClient.Companion {
+    method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    property public static final int ERROR_PHONE_UNAVAILABLE;
+    property public static final int ERROR_UNSUPPORTED;
+    property public static final int NO_ERROR;
+    property public static final int STATUS_AVAILABLE;
+    property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+    property public static final int STATUS_UNAVAILABLE;
+    property public static final int STATUS_UNKNOWN;
+  }
+
+  public interface RemoteAuthRequestHandler {
+    method public boolean isAuthSupported();
+    method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+  public abstract class RemoteAuthService extends android.app.Service {
+    ctor public RemoteAuthService();
+    method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+    method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+    method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+  }
+
+  public static final class RemoteAuthService.Companion {
+    method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+  public final class BridgingConfig {
+    method public java.util.Set<java.lang.String>? getExcludedTags();
+    method public boolean isBridgingEnabled();
+    property public final java.util.Set<java.lang.String>? excludedTags;
+    property public final boolean isBridgingEnabled;
+  }
+
+  public static final class BridgingConfig.Builder {
+    ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+  }
+
+  public fun interface BridgingConfigurationHandler {
+    method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+  }
+
+  public final class BridgingManager {
+    method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+    method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+    field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+  }
+
+  public static final class BridgingManager.Companion {
+    method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+  }
+
+  public final class BridgingManagerService extends android.app.Service {
+    ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+    method public android.os.IBinder? onBind(android.content.Intent? intent);
+  }
+
+}
+
diff --git a/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/wear-phone-interactions/api/res-1.1.0-beta01.txt
diff --git a/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..0798d72
--- /dev/null
+++ b/wear/wear-phone-interactions/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,173 @@
+// Signature format: 4.0
+package androidx.wear.phone.interactions {
+
+  public final class PhoneTypeHelper {
+    method public static int getPhoneDeviceType(android.content.Context context);
+    field public static final androidx.wear.phone.interactions.PhoneTypeHelper.Companion Companion;
+    field public static final int DEVICE_TYPE_ANDROID = 1; // 0x1
+    field public static final int DEVICE_TYPE_ERROR = 0; // 0x0
+    field public static final int DEVICE_TYPE_IOS = 2; // 0x2
+    field public static final int DEVICE_TYPE_NONE = 4; // 0x4
+    field public static final int DEVICE_TYPE_UNKNOWN = 3; // 0x3
+  }
+
+  public static final class PhoneTypeHelper.Companion {
+    method public int getPhoneDeviceType(android.content.Context context);
+    property public static final int DEVICE_TYPE_ANDROID;
+    property public static final int DEVICE_TYPE_ERROR;
+    property public static final int DEVICE_TYPE_IOS;
+    property public static final int DEVICE_TYPE_NONE;
+    property public static final int DEVICE_TYPE_UNKNOWN;
+  }
+
+}
+
+package androidx.wear.phone.interactions.authentication {
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
+    ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
+    method public String getValue();
+    property public final String value;
+  }
+
+  @RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
+    ctor public CodeVerifier();
+    ctor public CodeVerifier(optional int byteLength);
+    ctor public CodeVerifier(String value);
+    method public String getValue();
+    property public final String value;
+  }
+
+  public final class OAuthRequest {
+    method public String getPackageName();
+    method public String getRedirectUrl();
+    method public android.net.Uri getRequestUrl();
+    property public final String packageName;
+    property public final String redirectUrl;
+    property public final android.net.Uri requestUrl;
+    field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
+    field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
+    field public static final String WEAR_REDIRECT_URL_PREFIX_CN = "https://wear.googleapis-cn.com/3p_auth/";
+  }
+
+  public static final class OAuthRequest.Builder {
+    ctor public OAuthRequest.Builder(android.content.Context context);
+    method @RequiresApi(android.os.Build.VERSION_CODES.O) public androidx.wear.phone.interactions.authentication.OAuthRequest build();
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setAuthProviderUrl(android.net.Uri authProviderUrl);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setClientId(String clientId);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setCodeChallenge(androidx.wear.phone.interactions.authentication.CodeChallenge codeChallenge);
+    method public androidx.wear.phone.interactions.authentication.OAuthRequest.Builder setRedirectUrl(android.net.Uri redirectUrl);
+  }
+
+  public static final class OAuthRequest.Companion {
+    property public static final String WEAR_REDIRECT_URL_PREFIX;
+    property public static final String WEAR_REDIRECT_URL_PREFIX_CN;
+  }
+
+  public final class OAuthResponse {
+    method public int getErrorCode();
+    method public android.net.Uri? getResponseUrl();
+    property @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode public final int errorCode;
+    property public final android.net.Uri? responseUrl;
+  }
+
+  public static final class OAuthResponse.Builder {
+    ctor public OAuthResponse.Builder();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse build();
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setErrorCode(@androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+    method public androidx.wear.phone.interactions.authentication.OAuthResponse.Builder setResponseUrl(android.net.Uri responseUrl);
+  }
+
+  public final class RemoteAuthClient implements java.lang.AutoCloseable {
+    method @UiThread public void close();
+    method public static androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    method protected void finalize();
+    method public kotlinx.coroutines.flow.Flow<java.lang.Integer> getAvailabilityStatus();
+    method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, java.util.concurrent.Executor executor, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
+    property public final kotlinx.coroutines.flow.Flow<java.lang.Integer> availabilityStatus;
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
+    field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+    field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+    field public static final int NO_ERROR = -1; // 0xffffffff
+    field public static final int STATUS_AVAILABLE = 3; // 0x3
+    field public static final int STATUS_TEMPORARILY_UNAVAILABLE = 2; // 0x2
+    field public static final int STATUS_UNAVAILABLE = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+  }
+
+  public abstract static class RemoteAuthClient.Callback {
+    ctor public RemoteAuthClient.Callback();
+    method @UiThread public abstract void onAuthorizationError(androidx.wear.phone.interactions.authentication.OAuthRequest request, @androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode int errorCode);
+    method @UiThread public abstract void onAuthorizationResponse(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.OAuthResponse response);
+  }
+
+  public static final class RemoteAuthClient.Companion {
+    method public androidx.wear.phone.interactions.authentication.RemoteAuthClient create(android.content.Context context);
+    property public static final int ERROR_PHONE_UNAVAILABLE;
+    property public static final int ERROR_UNSUPPORTED;
+    property public static final int NO_ERROR;
+    property public static final int STATUS_AVAILABLE;
+    property public static final int STATUS_TEMPORARILY_UNAVAILABLE;
+    property public static final int STATUS_UNAVAILABLE;
+    property public static final int STATUS_UNKNOWN;
+  }
+
+  @IntDef({androidx.wear.phone.interactions.authentication.RemoteAuthClient.NO_ERROR, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_UNSUPPORTED, androidx.wear.phone.interactions.authentication.RemoteAuthClient.ERROR_PHONE_UNAVAILABLE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) public static @interface RemoteAuthClient.Companion.ErrorCode {
+  }
+
+  public interface RemoteAuthRequestHandler {
+    method public boolean isAuthSupported();
+    method public void sendAuthRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+  public abstract class RemoteAuthService extends android.app.Service {
+    ctor public RemoteAuthService();
+    method protected final android.os.IBinder onBind(android.content.Intent intent, androidx.wear.phone.interactions.authentication.RemoteAuthRequestHandler remoteAuthRequestHandler);
+    method public static final void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+    method protected boolean verifyPackageName(android.content.Context context, String? requestPackageName);
+    field public static final androidx.wear.phone.interactions.authentication.RemoteAuthService.Companion Companion;
+  }
+
+  public static final class RemoteAuthService.Companion {
+    method public void sendResponseToCallback(androidx.wear.phone.interactions.authentication.OAuthResponse response, kotlin.Pair<java.lang.String,java.lang.Integer> packageNameAndRequestId);
+  }
+
+}
+
+package androidx.wear.phone.interactions.notifications {
+
+  public final class BridgingConfig {
+    method public java.util.Set<java.lang.String>? getExcludedTags();
+    method public boolean isBridgingEnabled();
+    property public final java.util.Set<java.lang.String>? excludedTags;
+    property public final boolean isBridgingEnabled;
+  }
+
+  public static final class BridgingConfig.Builder {
+    ctor public BridgingConfig.Builder(android.content.Context context, boolean isBridgingEnabled);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTag(String tag);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig.Builder addExcludedTags(java.util.Collection<java.lang.String> tags);
+    method public androidx.wear.phone.interactions.notifications.BridgingConfig build();
+  }
+
+  public fun interface BridgingConfigurationHandler {
+    method public void applyBridgingConfiguration(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+  }
+
+  public final class BridgingManager {
+    method public static androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+    method public void setConfig(androidx.wear.phone.interactions.notifications.BridgingConfig bridgingConfig);
+    field public static final androidx.wear.phone.interactions.notifications.BridgingManager.Companion Companion;
+  }
+
+  public static final class BridgingManager.Companion {
+    method public androidx.wear.phone.interactions.notifications.BridgingManager fromContext(android.content.Context context);
+  }
+
+  public final class BridgingManagerService extends android.app.Service {
+    ctor public BridgingManagerService(android.content.Context context, androidx.wear.phone.interactions.notifications.BridgingConfigurationHandler bridgingConfigurationHandler);
+    method public android.os.IBinder? onBind(android.content.Intent? intent);
+  }
+
+}
+
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
index e28ae0c..676a0ca 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/PhoneTypeHelper.kt
@@ -34,10 +34,14 @@
                 .path(BLUETOOTH_MODE)
                 .build()
 
+        /**
+         * These values follow the values of platform constants defined in
+         * [Settings.Global.Wearable.PAIRED_DEVICE_OS_TYPE].
+         */
         internal const val UNKNOWN_MODE = 0
         internal const val ANDROID_MODE = 1
         internal const val IOS_MODE = 2
-        internal const val NONE_PAIRED_MODE = 4
+        internal const val NONE_PAIRED_MODE = 3
 
         /** Indicates an error returned retrieving the type of phone we are paired to. */
         public const val DEVICE_TYPE_ERROR: Int = 0
@@ -66,7 +70,7 @@
         @DeviceFamily
         @JvmStatic
         public fun getPhoneDeviceType(context: Context): Int {
-            var bluetoothMode = UNKNOWN_MODE
+            var pairedDeviceType = UNKNOWN_MODE
             if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                 val cursor =
                     context.contentResolver.query(BLUETOOTH_MODE_URI, null, null, null, null)
@@ -74,25 +78,25 @@
                 cursor.use {
                     while (it.moveToNext()) {
                         if (BLUETOOTH_MODE == it.getString(0)) {
-                            bluetoothMode = it.getInt(1)
+                            pairedDeviceType = it.getInt(1)
                             break
                         }
                     }
                 }
+                return when (pairedDeviceType) {
+                    ANDROID_MODE -> DEVICE_TYPE_ANDROID
+                    IOS_MODE -> DEVICE_TYPE_IOS
+                    else -> DEVICE_TYPE_UNKNOWN
+                }
             } else if (
                 Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
                     context.applicationInfo.targetSdkVersion > Build.VERSION_CODES.UPSIDE_DOWN_CAKE
             ) {
                 return DEVICE_TYPE_ANDROID
-            } else {
-                bluetoothMode =
-                    Settings.Global.getInt(
-                        context.contentResolver,
-                        PAIRED_DEVICE_OS_TYPE,
-                        UNKNOWN_MODE
-                    )
             }
-            return when (bluetoothMode) {
+            pairedDeviceType =
+                Settings.Global.getInt(context.contentResolver, PAIRED_DEVICE_OS_TYPE, UNKNOWN_MODE)
+            return when (pairedDeviceType) {
                 ANDROID_MODE -> DEVICE_TYPE_ANDROID
                 IOS_MODE -> DEVICE_TYPE_IOS
                 NONE_PAIRED_MODE -> DEVICE_TYPE_NONE
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -422,8 +422,8 @@
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
     field public static final String MUTE_AUDIO = "MUTE_AUDIO";
     field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
     field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
     field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
     field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
     field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 23ee69f..4a0366f 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -422,8 +422,8 @@
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.MULTI_PROCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isMultiProcessEnabled();
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.VISUAL_STATE_CALLBACK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.POST_WEB_MESSAGE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.PrerenderOperationCallback);
-    method @SuppressCompatibility @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrlAsync(android.webkit.WebView, String, androidx.core.os.CancellationSignal?, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.PrerenderOperationCallback);
+    method @SuppressCompatibility @RequiresFeature(name=androidx.webkit.WebViewFeature.PRERENDER_WITH_URL, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static void prerenderUrl(android.webkit.WebView, String, android.os.CancellationSignal?, java.util.concurrent.Executor, androidx.webkit.SpeculativeLoadingParameters, androidx.webkit.PrerenderOperationCallback);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void removeWebMessageListener(android.webkit.WebView, String);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.MUTE_AUDIO, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") @UiThread public static void setAudioMuted(android.webkit.WebView, boolean);
     method @AnyThread @RequiresFeature(name=androidx.webkit.WebViewFeature.DEFAULT_TRAFFICSTATS_TAGGING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setDefaultTrafficStatsTag(int);
@@ -478,7 +478,7 @@
     field public static final String MUTE_AUDIO = "MUTE_AUDIO";
     field public static final String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
     field public static final String POST_WEB_MESSAGE = "POST_WEB_MESSAGE";
-    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    field @SuppressCompatibility @androidx.webkit.WebViewCompat.ExperimentalUrlPrerender public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
     field @SuppressCompatibility @androidx.webkit.Profile.ExperimentalUrlPrefetch public static final String PROFILE_URL_PREFETCH = "PREFETCH_URL_V3";
     field public static final String PROXY_OVERRIDE = "PROXY_OVERRIDE";
     field public static final String PROXY_OVERRIDE_REVERSE_BYPASS = "PROXY_OVERRIDE_REVERSE_BYPASS";
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 0fde2c0..a461177 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -22,6 +22,7 @@
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Build;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Looper;
 import android.webkit.ValueCallback;
@@ -35,7 +36,6 @@
 import androidx.annotation.RequiresOptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.UiThread;
-import androidx.core.os.CancellationSignal;
 import androidx.webkit.internal.ApiFeature;
 import androidx.webkit.internal.ApiHelperForM;
 import androidx.webkit.internal.ApiHelperForO;
@@ -1341,8 +1341,7 @@
     }
 
     /**
-     * Starts a URL prerender request for this WebView. Can be called from any
-     * thread.
+     * Starts a URL prerender request for this WebView. Must be called from the UI thread.
      * <p>
      * This WebView will use a URL request matching algorithm during execution
      * of all variants of {@link android.webkit.WebView#loadUrl(String)} for
@@ -1363,26 +1362,26 @@
      * <p>
      * The {@link CancellationSignal} will make the best effort to cancel an
      * in-flight prerender request; however cancellation it is not guaranteed.
-     * <p>
-     * All result callbacks will be resolved on the calling thread.
      *
      * @param webView            the WebView for which we trigger the prerender request.
      * @param url                the url associated with the prerender request.
      * @param cancellationSignal used to trigger prerender cancellation.
+     * @param callbackExecutor   the executor to resolve the callback with.
      * @param callback           callbacks for reporting result back to application.
      */
     @RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @AnyThread
+    @UiThread
     @ExperimentalUrlPrerender
-    public static void prerenderUrlAsync(
+    public static void prerenderUrl(
             @NonNull WebView webView,
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull PrerenderOperationCallback callback) {
         ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
         if (feature.isSupportedByWebView()) {
-            getProvider(webView).prerenderUrlAsync(url, cancellationSignal, callback);
+            getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, callback);
         } else {
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
@@ -1390,28 +1389,31 @@
 
     /**
      * The same as
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal, PrerenderOperationCallback)},
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor, PrerenderOperationCallback)},
      * but allows customizing the request by providing {@link SpeculativeLoadingParameters}.
      *
      * @param webView            the WebView for which we trigger the prerender request.
      * @param url                the url associated with the prerender request.
      * @param cancellationSignal used to trigger prerender cancellation.
+     * @param callbackExecutor   the executor to resolve the callback with.
      * @param params             parameters to customize the prerender request.
      * @param callback           callbacks for reporting result back to application.
      */
     @RequiresFeature(name = WebViewFeature.PRERENDER_WITH_URL,
             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
-    @AnyThread
+    @UiThread
     @ExperimentalUrlPrerender
-    public static void prerenderUrlAsync(
+    public static void prerenderUrl(
             @NonNull WebView webView,
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull PrerenderOperationCallback callback) {
         ApiFeature.NoFramework feature = WebViewFeatureInternal.PRERENDER_WITH_URL;
         if (feature.isSupportedByWebView()) {
-            getProvider(webView).prerenderUrlAsync(url, cancellationSignal, params, callback);
+            getProvider(webView).prerenderUrl(url, cancellationSignal, callbackExecutor, params,
+                    callback);
         } else {
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 18747e3..2411346 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -653,11 +653,11 @@
     /**
      * Feature for {@link #isFeatureSupported(String)}.
      * This feature covers
-     * {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
-     * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+     * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal,
+     * Executor, SpeculativeLoadingParameters, PrerenderOperationCallback)}}
      */
     @WebViewCompat.ExperimentalUrlPrerender
-    public static final String PRERENDER_WITH_URL = "PRERENDER_URL";
+    public static final String PRERENDER_WITH_URL = "PRERENDER_URL_V2";
 
     /**
      * Feature for {@link #isFeatureSupported(String)}.
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 fd040be..df7473b 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -31,6 +31,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.webkit.OutcomeReceiverCompat;
+import androidx.webkit.PrerenderOperationCallback;
 import androidx.webkit.Profile;
 import androidx.webkit.ProfileStore;
 import androidx.webkit.ProxyConfig;
@@ -689,8 +690,9 @@
 
     /**
      * Feature for {@link WebViewFeature#isFeatureSupported(String)}.
-     * This feature covers {@link androidx.webkit.WebViewCompat#prerenderUrlAsync(WebView, String,
-     * SpeculativeLoadingParameters, CancellationSignal, PrerenderOperationCallback)}}
+     * This feature covers
+     * {@link androidx.webkit.WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
+     * SpeculativeLoadingParameters, PrerenderOperationCallback)}}
      */
     public static final ApiFeature.NoFramework PRERENDER_WITH_URL =
             new ApiFeature.NoFramework(WebViewFeature.PRERENDER_WITH_URL,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 0bfb95e..44b00b1 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -18,11 +18,13 @@
 
 import android.annotation.SuppressLint;
 import android.net.Uri;
+import android.os.CancellationSignal;
+import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
-import androidx.core.os.CancellationSignal;
+import androidx.webkit.PrerenderException;
 import androidx.webkit.PrerenderOperationCallback;
 import androidx.webkit.Profile;
 import androidx.webkit.SpeculativeLoadingParameters;
@@ -194,24 +196,58 @@
 
     /**
      * Adapter method for
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
      * PrerenderOperationCallback)}.
      */
-    public void prerenderUrlAsync(
+    public void prerenderUrl(
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull PrerenderOperationCallback callback) {
+
+        ValueCallback<Void> activationCallback = (value) -> {
+            // value will always be null.
+            callback.onPrerenderActivated();
+        };
+        ValueCallback<Throwable> errorCallback = (throwable) -> {
+            callback.onError(new PrerenderException("Prerender operation failed", throwable));
+        };
+        mImpl.prerenderUrl(
+                url,
+                cancellationSignal,
+                callbackExecutor,
+                activationCallback,
+                errorCallback);
     }
 
     /**
      * Adapter method for
-     * {@link WebViewCompat#prerenderUrlAsync(WebView, String, CancellationSignal,
+     * {@link WebViewCompat#prerenderUrl(WebView, String, CancellationSignal, Executor,
      * SpeculativeLoadingParameters, PrerenderOperationCallback)}.
      */
-    public void prerenderUrlAsync(
+    public void prerenderUrl(
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull PrerenderOperationCallback callback) {
+
+        InvocationHandler paramsBoundaryInterface =
+                BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+                        new SpeculativeLoadingParametersAdapter(params));
+        ValueCallback<Void> activationCallback = (value) -> {
+            // value will always be null.
+            callback.onPrerenderActivated();
+        };
+        ValueCallback<Throwable> errorCallback = (throwable) -> {
+            callback.onError(new PrerenderException("Prerender operation failed", throwable));
+        };
+        mImpl.prerenderUrl(
+                url,
+                cancellationSignal,
+                callbackExecutor,
+                paramsBoundaryInterface,
+                activationCallback,
+                errorCallback);
     }
 }