Merge "Fix: Crash IllegalArgumentException due to difference in mMaxPages and numPages" into androidx-main
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
index 11684ed..441c6bb 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionAppExceptions.kt
@@ -17,31 +17,21 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_APP
-import androidx.core.util.Preconditions
 
 /**
  * Thrown when an error is caused by the app providing the function.
  *
  * <p>For example, the app crashed when the system is executing the request.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_APP].
  */
 public abstract class AppFunctionAppException
 internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(errorCode, errorMessage, extras) {
-    init {
-        Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_APP)
-    }
-}
+    AppFunctionException(errorCode, errorMessage, extras)
 
 /**
  * Thrown when an unknown error occurred while processing the call in the AppFunctionService.
  *
  * <p>This error is thrown when the service is connected in the remote application but an unexpected
  * error is thrown from the bound application.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionAppUnknownException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
@@ -59,8 +49,6 @@
  *
  * <p> This is different from [AppFunctionDeniedException] in that the required permission is
  * missing from the target app, as opposed to the caller.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionPermissionRequiredException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
@@ -74,8 +62,6 @@
  *
  * <p>For example, a clock app might support updating timer properties such as label but may not
  * allow updating the timer's duration once the timer has already started.
- *
- * <p>This error is in the [ERROR_CATEGORY_APP] category.
  */
 public class AppFunctionNotSupportedException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
index 1813e6c..fc16714 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionRequestExceptions.kt
@@ -17,32 +17,22 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_REQUEST_ERROR
-import androidx.core.util.Preconditions
 
 /**
  * Thrown when the error is caused by the app requesting a function execution.
  *
  * <p>For example, the caller provided invalid parameters in the execution request e.g. an invalid
  * function ID.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_REQUEST_ERROR].
  */
 public abstract class AppFunctionRequestException
 internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(errorCode, errorMessage, extras) {
-    init {
-        Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_REQUEST_ERROR)
-    }
-}
+    AppFunctionException(errorCode, errorMessage, extras)
 
 /**
  * Thrown when the caller does not have the permission to execute an app function.
  *
  * <p> This is different from [AppFunctionPermissionRequiredException] in that the caller is missing
  * this specific permission, as opposed to the target app missing a permission.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 public class AppFunctionDeniedException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
@@ -55,8 +45,6 @@
  * Thrown when the caller supplied invalid arguments to ExecuteAppFunctionRequest's parameters.
  *
  * <p>This error may be considered similar to [IllegalArgumentException].
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 // TODO(b/389738031): add reference to ExecuteAppFunctionRequest's builder when it is added.
 public class AppFunctionInvalidArgumentException
@@ -70,8 +58,6 @@
  * Thrown when the caller tried to execute a disabled app function. An app function can be enabled
  * at runtime through the AppFunctionManager or by setting enabledByDefault=true in the AppFunction
  * annotation.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 // TODO(b/389738031): add reference to setAppFunctionEnabled and @AppFunction when they are added.
 public class AppFunctionDisabledException
@@ -81,11 +67,7 @@
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller tries to execute a function that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tries to execute a function that does not exist. */
 public class AppFunctionFunctionNotFoundException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
     AppFunctionRequestException(ERROR_FUNCTION_NOT_FOUND, errorMessage, extras) {
@@ -93,11 +75,7 @@
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller tried to request a resource/entity that does not exist.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller tried to request a resource/entity that does not exist. */
 public class AppFunctionElementNotFoundException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
     AppFunctionRequestException(ERROR_RESOURCE_NOT_FOUND, errorMessage, extras) {
@@ -105,11 +83,7 @@
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when the caller exceeded the allowed request rate.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
- */
+/** Thrown when the caller exceeded the allowed request rate. */
 public class AppFunctionLimitExceededException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
     AppFunctionRequestException(ERROR_LIMIT_EXCEEDED, errorMessage, extras) {
@@ -120,8 +94,6 @@
 /**
  * Thrown when the caller tried to create a resource/entity that already exists or has conflicts
  * with existing resource/entity.
- *
- * <p>This error is in the [ERROR_CATEGORY_REQUEST_ERROR] category.
  */
 public class AppFunctionElementAlreadyExistsException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
diff --git a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
index 5f38a8d..66596b4 100644
--- a/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
+++ b/appfunctions/appfunctions-common/src/main/java/androidx/appfunctions/AppFunctionSystemExceptions.kt
@@ -17,30 +17,20 @@
 package androidx.appfunctions
 
 import android.os.Bundle
-import androidx.appfunctions.AppFunctionException.Companion.ERROR_CATEGORY_SYSTEM
-import androidx.core.util.Preconditions
 
 /**
  * Thrown when an internal unexpected error comes from the system.
  *
  * <p>For example, the AppFunctionService implementation is not found by the system.
- *
- * <p>Reports errors of the category [ERROR_CATEGORY_SYSTEM].
  */
 public abstract class AppFunctionSystemException
 internal constructor(errorCode: Int, errorMessage: String? = null, extras: Bundle) :
-    AppFunctionException(errorCode, errorMessage, extras) {
-    init {
-        Preconditions.checkArgument(errorCategory == ERROR_CATEGORY_SYSTEM)
-    }
-}
+    AppFunctionException(errorCode, errorMessage, extras)
 
 /**
  * Thrown when an internal unexpected error comes from the system.
  *
  * <p>For example, the AppFunctionService implementation is not found by the system.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
  */
 public class AppFunctionSystemUnknownException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
@@ -49,11 +39,7 @@
     public constructor(errorMessage: String? = null) : this(errorMessage, Bundle.EMPTY)
 }
 
-/**
- * Thrown when an operation was cancelled.
- *
- * <p>This error is in the [ERROR_CATEGORY_SYSTEM] category.
- */
+/** Thrown when an operation was cancelled. */
 public class AppFunctionCancelledException
 internal constructor(errorMessage: String? = null, extras: Bundle) :
     AppFunctionSystemException(ERROR_CANCELLED, errorMessage, extras) {
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/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/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
index 8fd4b86..616baa6 100644
--- a/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
+++ b/savedstate/savedstate-samples/src/main/java/androidx/savedstate/SavedStateCodecSamples.kt
@@ -18,6 +18,8 @@
 
 package androidx.savedstate
 
+import android.os.Parcel
+import android.os.Parcelable
 import androidx.annotation.Sampled
 import androidx.savedstate.serialization.decodeFromSavedState
 import androidx.savedstate.serialization.encodeToSavedState
@@ -96,7 +98,6 @@
     val uuid = decodeFromSavedState(UUIDSerializer(), uuidSavedState)
 }
 
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") // The lint warning does not show up for external users.
 @Sampled
 fun savedStateSerializer() {
     @Serializable
@@ -125,20 +126,36 @@
     )
 }
 
+private class MyJavaSerializable : java.io.Serializable
+
+private class MyJavaSerializableSerializer : JavaSerializableSerializer<MyJavaSerializable>()
+
 @Sampled
 fun serializableSerializer() {
     @Serializable
     data class MyModel(
-        @Serializable(with = JavaSerializableSerializer::class)
-        val serializable: java.io.Serializable
+        @Serializable(with = MyJavaSerializableSerializer::class)
+        val serializable: MyJavaSerializable
     )
 }
 
+private class MyParcelable : Parcelable {
+    override fun describeContents(): Int {
+        TODO("Not yet implemented")
+    }
+
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        TODO("Not yet implemented")
+    }
+}
+
+private class MyParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
 @Sampled
 fun parcelableSerializer() {
     @Serializable
     data class MyModel(
-        @Serializable(with = ParcelableSerializer::class) val parcelable: android.os.Parcelable
+        @Serializable(with = MyParcelableSerializer::class) val parcelable: MyParcelable
     )
 }
 
@@ -172,13 +189,11 @@
 fun charSequenceListSerializer() {
     @Serializable
     class MyModel(
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable(with = CharSequenceListSerializer::class)
         val charSequenceList: List<CharSequence>
     )
 }
 
-@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
 @Sampled
 fun parcelableListSerializer() {
     @Serializable
diff --git a/savedstate/savedstate/api/current.txt b/savedstate/savedstate/api/current.txt
index a3e7083..8a63bfe 100644
--- a/savedstate/savedstate/api/current.txt
+++ b/savedstate/savedstate/api/current.txt
@@ -195,12 +195,12 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+  public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
     ctor public CharSequenceSerializer();
-    method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
-    method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
-    method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
-    property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+    method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+    method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+    method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+    property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
   public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -211,7 +211,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
     ctor public JavaSerializableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -235,7 +235,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
     ctor public ParcelableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/api/restricted_current.txt b/savedstate/savedstate/api/restricted_current.txt
index eb7a636..0f65840 100644
--- a/savedstate/savedstate/api/restricted_current.txt
+++ b/savedstate/savedstate/api/restricted_current.txt
@@ -220,12 +220,12 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class CharSequenceSerializer<T extends java.lang.CharSequence> implements kotlinx.serialization.KSerializer<T> {
+  public final class CharSequenceSerializer implements kotlinx.serialization.KSerializer<java.lang.CharSequence> {
     ctor public CharSequenceSerializer();
-    method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
-    method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
-    method public final void serialize(kotlinx.serialization.encoding.Encoder encoder, T value);
-    property public final kotlinx.serialization.descriptors.SerialDescriptor descriptor;
+    method public CharSequence deserialize(kotlinx.serialization.encoding.Decoder decoder);
+    method public kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
+    method public void serialize(kotlinx.serialization.encoding.Encoder encoder, CharSequence value);
+    property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
   public final class IBinderSerializer implements kotlinx.serialization.KSerializer<android.os.IBinder> {
@@ -236,7 +236,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class JavaSerializableSerializer<T extends java.io.Serializable> implements kotlinx.serialization.KSerializer<T> {
     ctor public JavaSerializableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
@@ -260,7 +260,7 @@
     property public kotlinx.serialization.descriptors.SerialDescriptor descriptor;
   }
 
-  public class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
+  public abstract class ParcelableSerializer<T extends android.os.Parcelable> implements kotlinx.serialization.KSerializer<T> {
     ctor public ParcelableSerializer();
     method public final T deserialize(kotlinx.serialization.encoding.Decoder decoder);
     method public final kotlinx.serialization.descriptors.SerialDescriptor getDescriptor();
diff --git a/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidInstrumentedTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+    val parcel =
+        Parcel.obtain().apply {
+            savedState.writeToParcel(this, 0)
+            setDataPosition(0)
+        }
+    return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
index f0dacc5..071f4c1 100644
--- a/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
+++ b/savedstate/savedstate/src/androidMain/kotlin/androidx/savedstate/serialization/serializers/BuiltInSerializer.android.kt
@@ -107,10 +107,10 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class CharSequenceSerializer<T : CharSequence> : KSerializer<T> {
-    final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
+public class CharSequenceSerializer : KSerializer<CharSequence> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CharSequence")
 
-    final override fun serialize(encoder: Encoder, value: T) {
+    override fun serialize(encoder: Encoder, value: CharSequence) {
         require(encoder is SavedStateEncoder) {
             encoderErrorMessage(descriptor.serialName, encoder)
         }
@@ -118,17 +118,18 @@
     }
 
     @Suppress("UNCHECKED_CAST")
-    final override fun deserialize(decoder: Decoder): T {
+    override fun deserialize(decoder: Decoder): CharSequence {
         require(decoder is SavedStateDecoder) {
             decoderErrorMessage(descriptor.serialName, decoder)
         }
-        return decoder.run { savedState.read { getCharSequence(key) as T } }
+        return decoder.run { savedState.read { getCharSequence(key) } }
     }
 }
 
 /**
  * A serializer for [java.io.Serializable]. This serializer uses [SavedState]'s API directly to
- * save/load a [java.io.Serializable].
+ * save/load a [java.io.Serializable]. You must extend this serializer for each of your
+ * [java.io.Serializable] subclasses.
  *
  * Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
  * Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -138,7 +139,7 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
+public abstract class JavaSerializableSerializer<T : JavaSerializable> : KSerializer<T> {
     final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("JavaSerializable")
 
     final override fun serialize(encoder: Encoder, value: T) {
@@ -159,7 +160,7 @@
 
 /**
  * A serializer for [Parcelable]. This serializer uses [SavedState]'s API directly to save/load a
- * [Parcelable].
+ * [Parcelable]. You must extend this serializer for each of your [Parcelable] subclasses.
  *
  * Note that this serializer should be used with [SavedStateEncoder] or [SavedStateDecoder] only.
  * Using it with other Encoders/Decoders may throw [IllegalArgumentException].
@@ -169,7 +170,7 @@
  * @see androidx.savedstate.serialization.decodeFromSavedState
  */
 @OptIn(ExperimentalSerializationApi::class)
-public open class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
+public abstract class ParcelableSerializer<T : Parcelable> : KSerializer<T> {
     final override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Parcelable")
 
     final override fun serialize(encoder: Encoder, value: T) {
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
index 2c34ddb..743c3e4 100644
--- a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecAndroidTest.android.kt
@@ -26,7 +26,6 @@
 import android.util.SizeF
 import android.util.SparseArray
 import androidx.core.os.bundleOf
-import androidx.core.util.forEach
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
 import androidx.savedstate.SavedStateCodecTestUtils.encodeDecode
@@ -59,7 +58,6 @@
 import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.encoding.decodeStructure
 import kotlinx.serialization.encoding.encodeStructure
-import kotlinx.serialization.serializer
 
 @ExperimentalSerializationApi
 internal class SavedStateCodecAndroidTest : RobolectricTest() {
@@ -100,18 +98,7 @@
             "SERIALIZER_TYPE_INCOMPATIBLE"
         ) // The lint warning does not show up for external users.
         @Serializable
-        class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as MyClass
-                return s.read { contentDeepEquals(other.s) }
-            }
-
-            override fun hashCode(): Int {
-                return s.read { contentDeepHashCode() }
-            }
-        }
+        class MyClass(@Serializable(with = SavedStateSerializer::class) val s: Bundle)
         MyClass(
                 bundleOf(
                     "i" to 1,
@@ -120,19 +107,24 @@
                     "ss" to bundleOf("s" to "bar")
                 )
             )
-            .encodeDecode {
-                assertThat(size()).isEqualTo(1)
-                getSavedState("s").read {
-                    assertThat(size()).isEqualTo(4)
-                    assertThat(getInt("i")).isEqualTo(1)
-                    assertThat(getString("s")).isEqualTo("foo")
-                    assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
-                    getSavedState("ss").read {
-                        assertThat(size()).isEqualTo(1)
-                        assertThat(getString("s")).isEqualTo("bar")
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(decoded.s.read { contentDeepEquals(original.s) }).isTrue()
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    getSavedState("s").read {
+                        assertThat(size()).isEqualTo(4)
+                        assertThat(getInt("i")).isEqualTo(1)
+                        assertThat(getString("s")).isEqualTo("foo")
+                        assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+                        getSavedState("ss").read {
+                            assertThat(size()).isEqualTo(1)
+                            assertThat(getString("s")).isEqualTo("bar")
+                        }
                     }
                 }
-            }
+            )
 
         // Bundle at root.
         val origin = bundleOf("i" to 3, "s" to "foo", "d" to 3.14)
@@ -180,7 +172,8 @@
 
         @Serializable
         data class SerializableContainer(
-            @Serializable(with = JavaSerializableSerializer::class) val value: java.io.Serializable
+            @Serializable(with = CustomJavaSerializableSerializer::class)
+            val value: java.io.Serializable
         )
         val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
         SerializableContainer(myJavaSerializable).encodeDecode {
@@ -191,7 +184,7 @@
 
         @Serializable
         data class ParcelableContainer(
-            @Serializable(with = ParcelableSerializer::class) val value: Parcelable
+            @Serializable(with = CustomParcelableSerializer::class) val value: Parcelable
         )
         val myParcelable = MyParcelable(3, "foo", 3.14)
         ParcelableContainer(myParcelable).encodeDecode {
@@ -257,19 +250,9 @@
             error("VERSION.SDK_INT < Q")
         }
 
+        @Suppress("ArrayInDataClass")
         @Serializable
-        data class CharSequenceArrayContainer(val value: Array<out CharSequence>) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+        data class CharSequenceArrayContainer(val value: Array<out CharSequence>)
         assertThrows<SerializationException> {
                 CharSequenceArrayContainer(arrayOf("foo", "bar")).encodeDecode {}
             }
@@ -281,20 +264,10 @@
 
     @Test
     fun concreteTypesInsteadOfInterfaceTypes() {
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
-        @Serializable
-        data class CharSequenceContainer(
-            @Serializable(with = CharSequenceSerializer::class) val value: String
-        )
-        CharSequenceContainer("foo").encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequence("value")).isEqualTo("foo")
-        }
-
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class SerializableContainer(
-            @Serializable(with = JavaSerializableSerializer::class) val value: MyJavaSerializable
+            @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
+            val value: MyJavaSerializable
         )
         val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
         SerializableContainer(myJavaSerializable).encodeDecode {
@@ -303,10 +276,9 @@
                 .isEqualTo(myJavaSerializable)
         }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class ParcelableContainer(
-            @Serializable(with = ParcelableSerializer::class) val value: MyParcelable
+            @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
         )
         val myParcelable = MyParcelable(3, "foo", 3.14)
         ParcelableContainer(myParcelable).encodeDecode {
@@ -315,10 +287,11 @@
         }
 
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
             @Serializable
             data class IBinderContainer(
-                @Serializable(with = IBinderSerializer::class) val value: Binder
+                @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+                @Serializable(with = IBinderSerializer::class)
+                val value: Binder
             )
             val binder = Binder("foo")
             IBinderContainer(binder).encodeDecode {
@@ -333,47 +306,36 @@
     @Test
     fun collectionTypes() {
         @Serializable
+        @Suppress("ArrayInDataClass")
         data class CharSequenceArrayContainer(
             @Serializable(with = CharSequenceArraySerializer::class)
             val value: Array<out CharSequence>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
-        val myCharSequenceArray = arrayOf("foo", "bar")
-        CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
-        }
+        )
+        val myCharSequenceArray = arrayOf(StringBuilder("foo"), StringBuilder("bar"))
+        CharSequenceArrayContainer(myCharSequenceArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
+                }
+            )
 
         @Serializable
+        @Suppress("ArrayInDataClass")
         data class ParcelableArrayContainer(
             @Serializable(with = ParcelableArraySerializer::class) val value: Array<out Parcelable>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as ParcelableArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+        )
         val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
-        ParcelableArrayContainer(myParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
-        }
+        ParcelableArrayContainer(myParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original -> decoded.value.contentEquals(original.value) },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getParcelableArray<MyParcelable>("value"))
+                        .isEqualTo(myParcelableArray)
+                }
+            )
 
         @Serializable
         data class CharSequenceListContainer(
@@ -406,76 +368,96 @@
                 append(1, MyParcelable(3, "foo", 3.14))
                 append(3, MyParcelable(4, "bar", 1.73))
             }
-        SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getSparseParcelableArray<Parcelable>("value"))
-                .isEqualTo(mySparseParcelableArray)
-        }
+        SparseParcelableArrayContainer(mySparseParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        decoded.value.contentEquals(original.value)
+                    } else {
+                        error("VERSION.SDK_INT < S")
+                    }
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getSparseParcelableArray<Parcelable>("value"))
+                        .isEqualTo(mySparseParcelableArray)
+                }
+            )
     }
 
     @Test
     fun collectionTypesWithConcreteElement() {
+        @Suppress("ArrayInDataClass")
         @Serializable
         data class CharSequenceArrayContainer(
-            @Serializable(with = CharSequenceArraySerializer::class) val value: Array<String>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as CharSequenceArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
+            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+            @Serializable(with = CharSequenceArraySerializer::class)
+            val value: Array<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
+        )
+        val myCharSequenceArray = arrayOf<StringBuilder>(StringBuilder("foo"), StringBuilder("bar"))
+        // `Bundle.getCharSequenceArray()` returns a `CharSequence[]` and the actual element type
+        // is not being retained after parcel/unparcel so the plugin-generated serializer will
+        // get `ClassCastException` when trying to cast it back to `Array<StringBuilder>`.
+        assertThrows(ClassCastException::class) {
+            CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
             }
         }
-        val myCharSequenceArray = arrayOf("foo", "bar")
-        CharSequenceArrayContainer(myCharSequenceArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceArray("value")).isEqualTo(myCharSequenceArray)
-        }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+        @Suppress("ArrayInDataClass")
         @Serializable
         data class ParcelableArrayContainer(
             @Serializable(with = ParcelableArraySerializer::class)
             // Here the serializer for the element is actually not used, but leaving it out leads
             // to SERIALIZER_NOT_FOUND compile error.
-            val value: Array<@Serializable(with = ParcelableSerializer::class) MyParcelable>
-        ) {
-            override fun equals(other: Any?): Boolean {
-                if (this === other) return true
-                if (javaClass != other?.javaClass) return false
-                other as ParcelableArrayContainer
-                return value.contentEquals(other.value)
-            }
-
-            override fun hashCode(): Int {
-                return value.contentHashCode()
-            }
-        }
+            val value:
+                Array<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
+        )
         val myParcelableArray = arrayOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
-        ParcelableArrayContainer(myParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+        // Even though `Bundle` does retain the actual `Parcelable` type there's no way for us to
+        // specify this `Parcelable` element type for the array, so the restored array is still of
+        // type `Array<Parcelable>` and the plugin-generated serializer will get
+        // `ClassCastException` when trying to cast it back to `Array<MyParcelable>`.
+        assertThrows(ClassCastException::class) {
+            ParcelableArrayContainer(myParcelableArray).encodeDecode {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getParcelableArray<MyParcelable>("value")).isEqualTo(myParcelableArray)
+            }
         }
 
         @Serializable
         data class CharSequenceListContainer(
-            @Serializable(with = CharSequenceListSerializer::class) val value: List<String>
+            @Serializable(with = CharSequenceListSerializer::class)
+            @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+            val value: List<@Serializable(with = CharSequenceSerializer::class) StringBuilder>
         )
-        val myCharSequenceList = arrayListOf("foo", "bar")
-        CharSequenceListContainer(myCharSequenceList).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
-        }
+        val myCharSequenceList = arrayListOf(StringBuilder("foo"), StringBuilder("bar"))
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
+        CharSequenceListContainer(myCharSequenceList)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(original.value[0]::class).isEqualTo(StringBuilder::class)
+                    // This is similar to the `CharSequenceArray` case where the element type of the
+                    // restored List after parcel/unparcel is of `String` instead of
+                    // `StringBuilder`. However, since the element type of Lists is erased no
+                    // `CastCastException` is thrown when the plugin-generated serializer tried to
+                    // assign the restored list back to `List<StringBuilder>`.
+                    assertThat(decoded.value[0]::class).isEqualTo(String::class)
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getCharSequenceList("value")).isEqualTo(myCharSequenceList)
+                }
+            )
+
         @Serializable
         data class ParcelableListContainer(
+            // Unlike arrays this works as `List`s can be down-casted, e.g.
+            // a `List<Parcelable>` can be casted to `List<MyParcelable>`.
             @Serializable(with = ParcelableListSerializer::class)
-            val value: List<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+            val value:
+                List<@Serializable(with = MyParcelableAsParcelableSerializer::class) MyParcelable>
         )
         val myParcelableList =
             arrayListOf(MyParcelable(3, "foo", 3.14), MyParcelable(4, "bar", 1.73))
@@ -484,57 +466,37 @@
             assertThat(getParcelableList<MyParcelable>("value")).isEqualTo(myParcelableList)
         }
 
-        @Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
         @Serializable
         data class SparseParcelableArrayContainer(
+            // Unlike arrays this works as `SparseArray`s can be down-casted, e.g.
+            // a `SparseArray<Parcelable>` can be casted to `SparseArray<MyParcelable>`.
             @Serializable(with = SparseParcelableArraySerializer::class)
-            val value: SparseArray<@Serializable(with = ParcelableSerializer::class) MyParcelable>
+            val value:
+                SparseArray<
+                    @Serializable(with = MyParcelableAsParcelableSerializer::class)
+                    MyParcelable
+                >
         )
         val mySparseParcelableArray =
             SparseArray<MyParcelable>().apply {
                 append(1, MyParcelable(3, "foo", 3.14))
                 append(3, MyParcelable(4, "bar", 1.73))
             }
-        SparseParcelableArrayContainer(mySparseParcelableArray).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getSparseParcelableArray<Parcelable>("value"))
-                .isEqualTo(mySparseParcelableArray)
-        }
-    }
-
-    @Test
-    fun concreteTypeSerializers() {
-        // No need to suppress SERIALIZER_TYPE_INCOMPATIBLE with these serializers.
-        @Serializable
-        data class CharSequenceContainer(
-            @Serializable(with = StringAsCharSequenceSerializer::class) val value: String
-        )
-        CharSequenceContainer("foo").encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getCharSequence("value")).isEqualTo("foo")
-        }
-
-        @Serializable
-        data class SerializableContainer(
-            @Serializable(with = MyJavaSerializableAsJavaSerializableSerializer::class)
-            val value: MyJavaSerializable
-        )
-        val myJavaSerializable = MyJavaSerializable(3, "foo", 3.14)
-        SerializableContainer(myJavaSerializable).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getJavaSerializable<MyJavaSerializable>("value"))
-                .isEqualTo(myJavaSerializable)
-        }
-
-        @Serializable
-        data class ParcelableContainer(
-            @Serializable(with = MyParcelableAsParcelableSerializer::class) val value: MyParcelable
-        )
-        val myParcelable = MyParcelable(3, "foo", 3.14)
-        ParcelableContainer(myParcelable).encodeDecode {
-            assertThat(size()).isEqualTo(1)
-            assertThat(getParcelable<MyParcelable>("value")).isEqualTo(myParcelable)
-        }
+        SparseParcelableArrayContainer(mySparseParcelableArray)
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        assertThat(decoded.value.contentEquals(original.value))
+                    } else {
+                        error("VERSION.SDK_INT < S")
+                    }
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getSparseParcelableArray<Parcelable>("value"))
+                        .isEqualTo(mySparseParcelableArray)
+                }
+            )
     }
 }
 
@@ -610,45 +572,11 @@
     }
 }
 
-private object CharArrayAsStringSerializer : KSerializer<Array<Char>> {
-    private val delegateSerializer = serializer<String>()
-    override val descriptor: SerialDescriptor =
-        PrimitiveSerialDescriptor("Array<Char>", PrimitiveKind.STRING)
-
-    override fun deserialize(decoder: Decoder): Array<Char> {
-        val s = decoder.decodeSerializableValue(delegateSerializer)
-        val result = Array(s.length) { s[it] }
-        return result
-    }
-
-    override fun serialize(encoder: Encoder, value: Array<Char>) {
-        val charArray = CharArray(value.size)
-        value.forEachIndexed { index, c -> charArray[index] = c }
-        encoder.encodeSerializableValue(delegateSerializer, String(charArray))
-    }
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-private object SparseStringArrayAsMapSerializer : KSerializer<SparseArray<String>> {
-    private val delegateSerializer = serializer<Map<Int, String>>()
-    override val descriptor = SerialDescriptor("SparseArray<String>", delegateSerializer.descriptor)
-
-    override fun deserialize(decoder: Decoder): SparseArray<String> {
-        val m = decoder.decodeSerializableValue(delegateSerializer)
-        val result = SparseArray<String>()
-        m.forEach { (k, v) -> result.append(k, v) }
-        return result
-    }
-
-    override fun serialize(encoder: Encoder, value: SparseArray<String>) {
-        val map = buildMap { value.forEach { k, v -> put(k, v) } }
-        encoder.encodeSerializableValue(delegateSerializer, map)
-    }
-}
-
-private class StringAsCharSequenceSerializer : CharSequenceSerializer<String>()
-
 private class MyJavaSerializableAsJavaSerializableSerializer :
     JavaSerializableSerializer<MyJavaSerializable>()
 
 private class MyParcelableAsParcelableSerializer : ParcelableSerializer<MyParcelable>()
+
+private class CustomJavaSerializableSerializer : JavaSerializableSerializer<java.io.Serializable>()
+
+private class CustomParcelableSerializer : ParcelableSerializer<Parcelable>()
diff --git a/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
new file mode 100644
index 0000000..edf79cc
--- /dev/null
+++ b/savedstate/savedstate/src/androidUnitTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.android.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.savedstate
+
+import android.os.Parcel
+
+actual fun platformEncodeDecode(savedState: SavedState): SavedState {
+    val parcel =
+        Parcel.obtain().apply {
+            savedState.writeToParcel(this, 0)
+            setDataPosition(0)
+        }
+    return SavedState.CREATOR.createFromParcel(parcel)
+}
diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
index 65ccd1f..3f06dff 100644
--- a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
+++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/serialization/SavedStateDecoder.kt
@@ -22,6 +22,7 @@
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.SerializationException
 import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.StructureKind
 import kotlinx.serialization.encoding.AbstractDecoder
 import kotlinx.serialization.encoding.CompositeDecoder
 import kotlinx.serialization.modules.EmptySerializersModule
@@ -73,9 +74,31 @@
     private var index = 0
 
     override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
-        if (index == savedState.read { size() }) return CompositeDecoder.DECODE_DONE
-        key = descriptor.getElementName(index)
-        return index++
+        val size =
+            if (descriptor.kind == StructureKind.LIST || descriptor.kind == StructureKind.MAP) {
+                // Use the number of elements encoded for collections.
+                savedState.read { size() }
+            } else {
+                // We may skip elements when encoding so if we used `size()`
+                // here we may miss some fields.
+                descriptor.elementsCount
+            }
+        fun hasDefaultValueDefined(index: Int) = descriptor.isElementOptional(index)
+        fun presentInEncoding(index: Int) =
+            savedState.read {
+                val key = descriptor.getElementName(index)
+                contains(key)
+            }
+        // Skip elements omitted from encoding (those assigned with its default values).
+        while (index < size && hasDefaultValueDefined(index) && !presentInEncoding(index)) {
+            index++
+        }
+        if (index < size) {
+            key = descriptor.getElementName(index)
+            return index++
+        } else {
+            return CompositeDecoder.DECODE_DONE
+        }
     }
 
     override fun decodeBoolean(): Boolean = savedState.read { getBoolean(key) }
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
index 86adf0b..88c33e4 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTest.kt
@@ -401,15 +401,19 @@
 
     @Test
     fun sealedClasses() {
-        Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode {
+        // Should use base type for encoding/decoding.
+        Node.Add(Node.Operand(3), Node.Operand(5)).encodeDecode<Node> {
             assertThat(size()).isEqualTo(2)
-            getSavedState("lhs").read {
-                assertThat(size()).isEqualTo(1)
-                assertThat(getInt("value")).isEqualTo(3)
-            }
-            getSavedState("rhs").read {
-                assertThat(size()).isEqualTo(1)
-                assertThat(getInt("value")).isEqualTo(5)
+            assertThat(getString("type")).isEqualTo("androidx.savedstate.Node.Add")
+            getSavedState("value").read {
+                getSavedState("lhs").read {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getInt("value")).isEqualTo(3)
+                }
+                getSavedState("rhs").read {
+                    assertThat(size()).isEqualTo(1)
+                    assertThat(getInt("value")).isEqualTo(5)
+                }
             }
         }
     }
@@ -425,12 +429,24 @@
         }
 
         // Nullable with default value.
-        @Serializable data class B(val s: String? = "foo")
-        B().encodeDecode()
-        B(s = "bar").encodeDecode {
+        @Serializable data class B(val s: String? = "foo", val i: Int)
+        B(i = 3).encodeDecode {
             assertThat(size()).isEqualTo(1)
+            assertThat(getInt("i")).isEqualTo(3)
+        }
+        B(s = null, i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(2)
+            assertThat(isNull("s")).isTrue()
+        }
+        B(s = "bar", i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(2)
             assertThat(getString("s")).isEqualTo("bar")
         }
+        // The value of `s` is the same as its default value so it's omitted from encoding.
+        B(s = "foo", i = 3).encodeDecode {
+            assertThat(size()).isEqualTo(1)
+            assertThat(getInt("i")).isEqualTo(3)
+        }
 
         // Nullable without default value
         @Serializable data class C(val s: String?)
@@ -450,6 +466,22 @@
             assertThat(getInt("i")).isEqualTo(5)
             assertThat(getString("s")).isEqualTo("foo")
         }
+
+        // Nullable with null as default value.
+        @Serializable data class E(val s: String? = null)
+        // Even though we encode `null`s in general as we don't encode default values
+        // nothing is encoded.
+        E().encodeDecode()
+
+        // Nullable in parent
+        G(i = 3).encodeDecode<F> {
+            assertThat(size()).isEqualTo(2)
+            assertThat(getString("type")).isEqualTo("androidx.savedstate.G")
+            getSavedState("value").read {
+                assertThat(size()).isEqualTo(1)
+                assertThat(getInt("i")).isEqualTo(3)
+            }
+        }
     }
 
     @Test
@@ -464,19 +496,24 @@
                     putSavedState("ss", savedState { putString("s", "bar") })
                 }
             )
-            .encodeDecode {
-                assertThat(size()).isEqualTo(1)
-                getSavedState("s").read {
-                    assertThat(size()).isEqualTo(4)
-                    assertThat(getInt("i")).isEqualTo(1)
-                    assertThat(getString("s")).isEqualTo("foo")
-                    assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
-                    getSavedState("ss").read {
-                        assertThat(size()).isEqualTo(1)
-                        assertThat(getString("s")).isEqualTo("bar")
+            .encodeDecode(
+                checkDecoded = { decoded, original ->
+                    assertThat(decoded.s.read { contentDeepEquals(original.s) })
+                },
+                checkEncoded = {
+                    assertThat(size()).isEqualTo(1)
+                    getSavedState("s").read {
+                        assertThat(size()).isEqualTo(4)
+                        assertThat(getInt("i")).isEqualTo(1)
+                        assertThat(getString("s")).isEqualTo("foo")
+                        assertThat(getIntArray("a")).isEqualTo(intArrayOf(1, 3, 5))
+                        getSavedState("ss").read {
+                            assertThat(size()).isEqualTo(1)
+                            assertThat(getString("s")).isEqualTo("bar")
+                        }
                     }
                 }
-            }
+            )
 
         val origin = savedState {
             putInt("i", 1)
@@ -563,6 +600,7 @@
 
 private typealias MyNestedTypeAlias = MyTypeAliasToInt
 
+@Serializable
 private sealed class Node {
     @Serializable data class Add(val lhs: Operand, val rhs: Operand) : Node()
 
@@ -605,3 +643,10 @@
         return MyColor(array[0], array[1], array[2])
     }
 }
+
+@Serializable
+private sealed class F {
+    val s: String? = null
+}
+
+@Serializable private data class G(val i: Int) : F()
diff --git a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
index e9ac161..67930cd 100644
--- a/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
+++ b/savedstate/savedstate/src/commonTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.kt
@@ -23,16 +23,30 @@
 import kotlinx.serialization.serializer
 
 internal object SavedStateCodecTestUtils {
+    /* Test the following steps: 1. encode `T` to a `SavedState`, 2. parcelize it to a `Parcel`,
+     * 3. un-parcelize it back to a `SavedState`, and 4. decode it back to a `T`. Step 2 and 3
+     * are only performed on Android. Here's the whole process:
+     *
+     * (A)Serializable -1-> (B)SavedState -2-> (C)Parcel -3-> (D)SavedState -4-> (E)Serializable
+     *
+     * `checkEncoded` can be used to check the content of "B", and `checkDecoded` can be
+     *  used to compare the instances of "E" and "A".
+     */
     inline fun <reified T : Any> T.encodeDecode(
         serializer: KSerializer<T> = serializer<T>(),
-        checkContent: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
+        checkDecoded: (T, T) -> Unit = { decoded, original ->
+            assertThat(decoded).isEqualTo(original)
+        },
+        checkEncoded: SavedStateReader.() -> Unit = { assertThat(size()).isEqualTo(0) }
     ) {
-        assertThat(
-                decodeFromSavedState(
-                    serializer,
-                    encodeToSavedState(serializer, this).apply { read { checkContent() } }
-                )
-            )
-            .isEqualTo(this)
+        val encoded = encodeToSavedState(serializer, this)
+        encoded.read { checkEncoded() }
+
+        val restored = platformEncodeDecode(encoded)
+
+        val decoded = decodeFromSavedState(serializer, restored)
+        checkDecoded(decoded, this)
     }
 }
+
+expect fun platformEncodeDecode(savedState: SavedState): SavedState
diff --git a/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
new file mode 100644
index 0000000..023701a8
--- /dev/null
+++ b/savedstate/savedstate/src/nonAndroidTest/kotlin/androidx/savedstate/SavedStateCodecTestUtils.nonAndroid.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.savedstate
+
+// No parceling in non-Android platforms.
+actual fun platformEncodeDecode(savedState: SavedState): SavedState = savedState
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 8426c60..efb8ff6 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
     defaultConfig {
         applicationId = "androidx.wear.compose.integration.demos"
         minSdk = 25
-        versionCode = 63
-        versionName = "1.63"
+        versionCode = 64
+        versionName = "1.64"
     }
 
     buildTypes {
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