Merge "Fix ImageCaptureTest failures in the extensions module" into androidx-main
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index 4deb295..a57afdd 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -40,6 +40,7 @@
 import kotlin.test.assertTrue
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -181,6 +182,7 @@
         validateStartup_fullyDrawn(delayMs = 100)
     }
 
+    @Ignore // b/258335082
     @LargeTest
     @Test
     fun startupInAppNav_immediate() {
@@ -188,6 +190,7 @@
         validateStartup_fullyDrawn(delayMs = 0, useInAppNav = true)
     }
 
+    @Ignore // b/258335082
     @LargeTest
     @Test
     fun startupInAppNav_fullyDrawn() {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 64ab132..a69dfd01 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -47,7 +47,7 @@
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameters
 
-private const val tracingPerfettoVersion = "1.0.0-alpha07" // TODO(224510255): get by 'reflection'
+private const val tracingPerfettoVersion = "1.0.0-alpha08" // TODO(224510255): get by 'reflection'
 private const val minSupportedSdk = Build.VERSION_CODES.R // TODO(234351579): Support API < 30
 
 @RunWith(Parameterized::class)
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
index 9954170..d53c41d 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
@@ -86,34 +86,6 @@
     }
 
     @Test
-    fun customComposeVersions() {
-        val toml = """
-            [versions]
-            COMPOSE_V1 = "1.2.3"
-            [groups]
-            COMPOSE = { group = "androidx.compose.suffix", atomicGroupVersion = "versions.COMPOSE_V1" }
-        """.trimIndent()
-        val noCustomVersion = createLibraryVersionsService(toml)
-        assertThat(
-            noCustomVersion.libraryGroups["COMPOSE"]
-        ).isEqualTo(
-            LibraryGroup(
-                group = "androidx.compose.suffix", atomicGroupVersion = Version("1.2.3")
-            )
-        )
-        val customComposeVersion = createLibraryVersionsService(
-            toml, composeCustomGroup = "not.androidx.compose", composeCustomVersion = "1.1.1"
-        )
-        assertThat(
-            customComposeVersion.libraryGroups["COMPOSE"]
-        ).isEqualTo(
-            LibraryGroup(
-                group = "not.androidx.compose.suffix", atomicGroupVersion = Version("1.1.1")
-            )
-        )
-    }
-
-    @Test
     fun missingVersionReference() {
         val service = createLibraryVersionsService(
             """
@@ -217,8 +189,6 @@
 
     private fun createLibraryVersionsService(
         tomlFile: String,
-        composeCustomVersion: String? = null,
-        composeCustomGroup: String? = null,
         useMultiplatformGroupVersions: Boolean = false
     ): LibraryVersionsService {
         val project = ProjectBuilder.builder().withProjectDir(tempDir.newFolder()).build()
@@ -228,16 +198,10 @@
             spec.parameters.tomlFile = project.provider {
                 tomlFile
             }
-            spec.parameters.composeCustomVersion = project.provider {
-                composeCustomVersion
-            }
-            spec.parameters.composeCustomGroup = project.provider {
-                composeCustomGroup
-            }
             spec.parameters.useMultiplatformGroupVersions = project.provider {
                 useMultiplatformGroupVersions
             }
         }
         return serviceProvider.get()
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 4099584..820c82b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -39,8 +39,6 @@
             File(project.getSupportRootFolder(), "libraryversions.toml")
         )
         val content = project.providers.fileContents(toml)
-        val composeCustomVersion = project.providers.environmentVariable("COMPOSE_CUSTOM_VERSION")
-        val composeCustomGroup = project.providers.environmentVariable("COMPOSE_CUSTOM_GROUP")
         val useMultiplatformVersions = project.provider {
             Multiplatform.isKotlinNativeEnabled(project)
         }
@@ -50,8 +48,6 @@
             LibraryVersionsService::class.java
         ) { spec ->
             spec.parameters.tomlFile = content.asText
-            spec.parameters.composeCustomVersion = composeCustomVersion
-            spec.parameters.composeCustomGroup = composeCustomGroup
             spec.parameters.useMultiplatformGroupVersions = useMultiplatformVersions
         }
         LibraryGroups = serviceProvider.get().libraryGroups
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
index 426bc93..bb8852f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
@@ -30,8 +30,6 @@
 abstract class LibraryVersionsService : BuildService<LibraryVersionsService.Parameters> {
     interface Parameters : BuildServiceParameters {
         var tomlFile: Provider<String>
-        var composeCustomVersion: Provider<String>
-        var composeCustomGroup: Provider<String>
         var useMultiplatformGroupVersions: Provider<Boolean>
     }
 
@@ -43,14 +41,7 @@
         val versions = parsedTomlFile.getTable("versions")
             ?: throw GradleException("Library versions toml file is missing [versions] table")
         versions.keySet().associateWith { versionName ->
-            val versionValue =
-                if (versionName.startsWith("COMPOSE") &&
-                    parameters.composeCustomVersion.isPresent
-                ) {
-                    parameters.composeCustomVersion.get()
-                } else {
-                    versions.getString(versionName)!!
-                }
+            val versionValue = versions.getString(versionName)!!
             Version.parseOrNull(versionValue)
                 ?: throw GradleException(
                     "$versionName does not match expected format - $versionValue"
@@ -83,11 +74,7 @@
         groups.keySet().associateWith { name ->
             val groupDefinition = groups.getTable(name)!!
             val groupName = groupDefinition.getString("group")!!
-            val finalGroupName = if (name.startsWith("COMPOSE") &&
-                parameters.composeCustomGroup.isPresent
-            ) {
-                groupName.replace("androidx.compose", parameters.composeCustomGroup.get())
-            } else groupName
+            val finalGroupName = groupName
 
             val atomicGroupVersion = readGroupVersion(
                 groupDefinition = groupDefinition,
@@ -116,4 +103,4 @@
 
 private const val VersionReferencePrefix = "versions."
 private const val AtomicGroupVersion = "atomicGroupVersion"
-private const val MultiplatformGroupVersion = "multiplatformGroupVersion"
\ No newline at end of file
+private const val MultiplatformGroupVersion = "multiplatformGroupVersion"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 2f49b0ac..6d7dade 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -21,6 +21,7 @@
 import android.annotation.SuppressLint
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraMetadata
+import android.util.Size
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraPipe
@@ -128,6 +129,11 @@
         }
     }
 
+    override fun getSupportedResolutions(format: Int): List<Size> {
+        Log.warn { "TODO: getSupportedResolutions are not yet supported." }
+        return emptyList()
+    }
+
     override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
 
     override fun getCameraQuirks(): Quirks {
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 0aafb22..6348136 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.camera2.CameraCharacteristics
 import android.os.Build
+import android.util.Size
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.integration.adapter.CameraControlStateAdapter
 import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
@@ -188,6 +189,10 @@
             override fun getTimebase(): Timebase {
                 throw NotImplementedError("Not used in testing")
             }
+
+            override fun getSupportedResolutions(format: Int): MutableList<Size> {
+                throw NotImplementedError("Not used in testing")
+            }
         }
         Camera2CameraInfo.from(wrongCameraInfo)
     }
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
index b6949e6..6d48e3e7 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CamcorderProfileProviderTest.kt
@@ -16,12 +16,11 @@
 
 package androidx.camera.camera2.internal
 
-import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
 import android.media.CamcorderProfile
-import android.os.Build
 import android.util.Size
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.impl.ImageFormatConstants
 import androidx.camera.testing.CameraUtil
@@ -155,16 +154,11 @@
     }
 
     private fun getVideoSupportedResolutions(): Array<Size> {
-        val map = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
-
-        // Before Android 23, use {@link SurfaceTexture} will finally mapped to 0x22 in
-        // StreamConfigurationMap to retrieve the output sizes information.
-        return if (Build.VERSION.SDK_INT < 23) {
-            map.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
-        } else {
-            map.getOutputSizes(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
-                ?: emptyArray()
-        }
+        val mapCompat = StreamConfigurationMapCompat.toStreamConfigurationMapCompat(
+            cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]!!
+        )
+        return mapCompat.getOutputSizes(ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE)
+            ?: emptyArray()
     }
 
     private fun CamcorderProfile.size() = Size(videoFrameWidth, videoFrameHeight)
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 6d5f28a..4176afb 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
@@ -22,10 +22,14 @@
 
 import static androidx.camera.camera2.internal.ZslUtil.isCapabilitySupported;
 
+import static java.util.Objects.requireNonNull;
+
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import androidx.annotation.GuardedBy;
@@ -36,6 +40,7 @@
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
@@ -61,6 +66,8 @@
 import androidx.lifecycle.Observer;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -392,6 +399,17 @@
         }
     }
 
+    @NonNull
+    @Override
+    public List<Size> getSupportedResolutions(int format) {
+        StreamConfigurationMap map = requireNonNull(mCameraCharacteristicsCompat.get(
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP));
+        StreamConfigurationMapCompat mapCompat =
+                StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+        Size[] size = mapCompat.getOutputSizes(format);
+        return size != null ? Arrays.asList(size) : Collections.emptyList();
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
index 04688d1..66d79fe3 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/MeteringRepeatingSession.java
@@ -21,7 +21,6 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.params.StreamConfigurationMap;
-import android.os.Build;
 import android.util.Size;
 import android.view.Surface;
 
@@ -29,6 +28,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
 import androidx.camera.camera2.internal.compat.workaround.SupportedRepeatingSurfaceSize;
 import androidx.camera.core.Logger;
 import androidx.camera.core.UseCase;
@@ -166,13 +166,9 @@
             return new Size(0, 0);
         }
 
-        if (Build.VERSION.SDK_INT < 23) {
-            // ImageFormat.PRIVATE is only public after Android level 23. Therefore, using
-            // SurfaceTexture.class to get the supported output sizes before Android level 23.
-            outputSizes = map.getOutputSizes(SurfaceTexture.class);
-        } else {
-            outputSizes = map.getOutputSizes(ImageFormat.PRIVATE);
-        }
+        StreamConfigurationMapCompat mapCompat =
+                StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+        outputSizes = mapCompat.getOutputSizes(ImageFormat.PRIVATE);
         if (outputSizes == null) {
             Logger.e(TAG, "Can not get output size list.");
             return new Size(0, 0);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
index 589530a..f901f3b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
@@ -24,7 +24,6 @@
 
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Build;
@@ -36,13 +35,13 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
 import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
 import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
 import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.Logger;
 import androidx.camera.core.ResolutionSelector;
-import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.SizeCoordinate;
 import androidx.camera.core.impl.SurfaceConfig;
@@ -294,8 +293,6 @@
 
     @NonNull
     private Size[] doGetOutputSizesByFormat(int imageFormat) {
-        Size[] outputSizes;
-
         StreamConfigurationMap map =
                 mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
 
@@ -303,18 +300,9 @@
             throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
         }
 
-        if (Build.VERSION.SDK_INT < 23
-                && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
-            // This is a little tricky that 0x22 that is internal defined in
-            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
-            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
-            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
-            // retrieve the output sizes information.
-            outputSizes = map.getOutputSizes(SurfaceTexture.class);
-        } else {
-            outputSizes = map.getOutputSizes(imageFormat);
-        }
-
+        StreamConfigurationMapCompat mapCompat =
+                StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+        Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
         if (outputSizes == null) {
             throw new IllegalArgumentException(
                     "Can not get supported output size for the format: " + imageFormat);
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 25bf0f8..40e04f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -36,12 +36,10 @@
 import android.content.Context;
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.media.CamcorderProfile;
 import android.media.MediaRecorder;
-import android.os.Build;
 import android.util.Pair;
 import android.util.Rational;
 import android.util.Size;
@@ -54,6 +52,7 @@
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
 import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
 import androidx.camera.camera2.internal.compat.workaround.ExtraSupportedSurfaceCombinationsContainer;
 import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
@@ -63,7 +62,6 @@
 import androidx.camera.core.Logger;
 import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
-import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.SurfaceCombination;
 import androidx.camera.core.impl.SurfaceConfig;
@@ -693,8 +691,6 @@
 
     @NonNull
     private Size[] doGetAllOutputSizesByFormat(int imageFormat) {
-        Size[] outputSizes;
-
         StreamConfigurationMap map =
                 mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
 
@@ -702,18 +698,9 @@
             throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
         }
 
-        if (Build.VERSION.SDK_INT < 23
-                && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
-            // This is a little tricky that 0x22 that is internal defined in
-            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
-            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
-            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
-            // retrieve the output sizes information.
-            outputSizes = map.getOutputSizes(SurfaceTexture.class);
-        } else {
-            outputSizes = map.getOutputSizes(imageFormat);
-        }
-
+        StreamConfigurationMapCompat mapCompat =
+                StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+        Size[] outputSizes = mapCompat.getOutputSizes(imageFormat);
         if (outputSizes == null) {
             throw new IllegalArgumentException(
                     "Can not get supported output size for the format: " + imageFormat);
@@ -815,7 +802,10 @@
             throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
         }
 
-        Size[] videoSizeArr = map.getOutputSizes(MediaRecorder.class);
+        StreamConfigurationMapCompat mapCompat =
+                StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+
+        Size[] videoSizeArr = mapCompat.getOutputSizes(MediaRecorder.class);
 
         if (videoSizeArr == null) {
             return RESOLUTION_480P;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java
new file mode 100644
index 0000000..e8d40e9
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompat.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 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.camera.camera2.internal.compat;
+
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Helper for accessing features in {@link StreamConfigurationMap} in a backwards compatible
+ * fashion.
+ */
+@RequiresApi(21)
+public class StreamConfigurationMapCompat {
+
+    private final StreamConfigurationMapCompatImpl mImpl;
+
+    private StreamConfigurationMapCompat(@NonNull StreamConfigurationMap map) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            mImpl = new StreamConfigurationMapCompatApi23Impl(map);
+        } else {
+            mImpl = new StreamConfigurationMapCompatBaseImpl(map);
+        }
+    }
+
+    /**
+     * Provides a backward-compatible wrapper for {@link StreamConfigurationMap}.
+     *
+     * @param map {@link StreamConfigurationMap} class to wrap
+     * @return wrapped class
+     */
+    @NonNull
+    public static StreamConfigurationMapCompat toStreamConfigurationMapCompat(
+            @NonNull StreamConfigurationMap map) {
+        return new StreamConfigurationMapCompat(map);
+    }
+
+    /**
+     * Get a list of sizes compatible with the requested image {@code format}.
+     *
+     * @param format an image format from {@link ImageFormat} or {@link PixelFormat}
+     * @return an array of supported sizes, or {@code null} if the {@code format} is not a
+     *         supported output
+     *
+     * @see ImageFormat
+     * @see PixelFormat
+     */
+    @Nullable
+    public Size[] getOutputSizes(int format) {
+        return mImpl.getOutputSizes(format);
+    }
+
+    /**
+     * Get a list of sizes compatible with {@code klass} to use as an output.
+     *
+     * @param klass a non-{@code null} {@link Class} object reference
+     * @return an array of supported sizes for {@link ImageFormat#PRIVATE} format,
+     *         or {@code null} iff the {@code klass} is not a supported output.
+     *
+     * @throws NullPointerException if {@code klass} was {@code null}
+     */
+    @Nullable
+    public <T> Size[] getOutputSizes(@NonNull Class<T> klass) {
+        return mImpl.getOutputSizes(klass);
+    }
+
+    interface StreamConfigurationMapCompatImpl {
+
+        @Nullable
+        Size[] getOutputSizes(int format);
+
+        @Nullable
+        <T> Size[] getOutputSizes(@NonNull Class<T> klass);
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
new file mode 100644
index 0000000..23716af
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatApi23Impl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 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.camera.camera2.internal.compat;
+
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class StreamConfigurationMapCompatApi23Impl extends StreamConfigurationMapCompatBaseImpl {
+
+    StreamConfigurationMapCompatApi23Impl(@NonNull StreamConfigurationMap map) {
+        super(map);
+    }
+
+    @Nullable
+    @Override
+    public Size[] getOutputSizes(int format) {
+        return mStreamConfigurationMap.getOutputSizes(format);
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
new file mode 100644
index 0000000..5b797a1
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatBaseImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 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.camera.camera2.internal.compat;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.ImageFormatConstants;
+
+@RequiresApi(21)
+class StreamConfigurationMapCompatBaseImpl
+        implements StreamConfigurationMapCompat.StreamConfigurationMapCompatImpl {
+
+    final StreamConfigurationMap mStreamConfigurationMap;
+
+    StreamConfigurationMapCompatBaseImpl(@NonNull StreamConfigurationMap map) {
+        mStreamConfigurationMap = map;
+    }
+
+    @Nullable
+    @Override
+    public Size[] getOutputSizes(int format) {
+        Size[] sizes;
+        if (format == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+            // This is a little tricky that 0x22 that is internal defined in
+            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+            // retrieve the output sizes information.
+            sizes = mStreamConfigurationMap.getOutputSizes(SurfaceTexture.class);
+        } else {
+            sizes = mStreamConfigurationMap.getOutputSizes(format);
+        }
+        return sizes;
+    }
+
+    @Nullable
+    @Override
+    public <T> Size[] getOutputSizes(@NonNull Class<T> klass) {
+        return mStreamConfigurationMap.getOutputSizes(klass);
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
index d058add..fb2d013 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/CamcorderProfileResolutionQuirk.java
@@ -16,16 +16,15 @@
 
 package androidx.camera.camera2.internal.compat.quirk;
 
-import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.media.CamcorderProfile;
-import android.os.Build;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
 import androidx.camera.camera2.internal.compat.workaround.CamcorderProfileResolutionValidator;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.ImageFormatConstants;
@@ -69,21 +68,17 @@
 
     public CamcorderProfileResolutionQuirk(
             @NonNull CameraCharacteristicsCompat characteristicsCompat) {
+        Size[] sizes = null;
         StreamConfigurationMap map =
                 characteristicsCompat.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-        if (map == null) {
+        if (map != null) {
+            StreamConfigurationMapCompat mapCompat =
+                    StreamConfigurationMapCompat.toStreamConfigurationMapCompat(map);
+            sizes = mapCompat.getOutputSizes(
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+        } else {
             Logger.e(TAG, "StreamConfigurationMap is null");
         }
-        Size[] sizes;
-        // Before Android 23, use {@link SurfaceTexture} will finally mapped to 0x22 in
-        // StreamConfigurationMap to retrieve the output sizes information.
-        if (Build.VERSION.SDK_INT < 23) {
-            sizes = map != null ? map.getOutputSizes(SurfaceTexture.class) : null;
-        } else {
-            sizes = map != null ? map.getOutputSizes(
-                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) : null;
-        }
-
         mSupportedResolutions = sizes != null ? Arrays.asList(sizes.clone())
                 : Collections.emptyList();
 
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 0062a4b..ae0f83b 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
@@ -32,6 +32,7 @@
 import android.hardware.camera2.CameraManager;
 import android.os.Build;
 import android.util.Pair;
+import android.util.Size;
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
@@ -48,6 +49,7 @@
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.internal.ImmutableZoomState;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -61,6 +63,7 @@
 import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowCameraCharacteristics;
 import org.robolectric.shadows.ShadowCameraManager;
+import org.robolectric.shadows.StreamConfigurationMapBuilder;
 import org.robolectric.util.ReflectionHelpers;
 
 import java.util.Arrays;
@@ -543,6 +546,22 @@
         assertThat(cameraInfo.isZslSupported()).isTrue();
     }
 
+    @Test
+    public void canReturnSupportedResolutions() throws CameraAccessExceptionCompat {
+        init(/* hasReprocessingCapabilities = */ true);
+
+        Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
+                mCameraManagerCompat);
+        List<Size> resolutions = cameraInfo.getSupportedResolutions(
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
+
+        assertThat(resolutions).containsExactly(
+                new Size(1920, 1080),
+                new Size(1280, 720),
+                new Size(640, 480)
+        );
+    }
+
     private CameraManagerCompat initCameraManagerWithPhysicalIds(
             List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
         FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
@@ -595,6 +614,18 @@
         shadowCharacteristics0.set(
                 CameraCharacteristics.FLASH_INFO_AVAILABLE, CAMERA0_FLASH_INFO_BOOLEAN);
 
+        // Mock the supported resolutions
+        {
+            int formatPrivate = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+            StreamConfigurationMapBuilder streamMapBuilder =
+                    StreamConfigurationMapBuilder.newBuilder()
+                            .addOutputSize(formatPrivate, new Size(1920, 1080))
+                            .addOutputSize(formatPrivate, new Size(1280, 720))
+                            .addOutputSize(formatPrivate, new Size(640, 480));
+            shadowCharacteristics0.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    streamMapBuilder.build());
+        }
+
         // Mock the request capability
         if (hasReprocessingCapabilities) {
             shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
@@ -628,6 +659,17 @@
         shadowCharacteristics1.set(
                 CameraCharacteristics.FLASH_INFO_AVAILABLE, CAMERA1_FLASH_INFO_BOOLEAN);
 
+        // Mock the supported resolutions
+        {
+            int formatPrivate = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+            StreamConfigurationMapBuilder streamMapBuilder =
+                    StreamConfigurationMapBuilder.newBuilder()
+                            .addOutputSize(formatPrivate, new Size(1280, 720))
+                            .addOutputSize(formatPrivate, new Size(640, 480));
+            shadowCharacteristics1.set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                    streamMapBuilder.build());
+        }
+
         // Add the camera to the camera service
         ((ShadowCameraManager)
                 Shadow.extract(
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt
new file mode 100644
index 0000000..e602ed3
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/StreamConfigurationMapCompatTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 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.camera.camera2.internal.compat
+
+import android.graphics.SurfaceTexture
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.ImageFormatConstants
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+/**
+ * Unit tests for [StreamConfigurationMapCompat].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class StreamConfigurationMapCompatTest {
+
+    companion object {
+        private val SIZE_480P = Size(640, 480)
+        private val SIZE_720P = Size(1080, 720)
+        private val SIZE_1080P = Size(1920, 1080)
+        private const val FORMAT_PRIVATE =
+            ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+    }
+
+    private lateinit var streamConfigurationMapCompat: StreamConfigurationMapCompat
+    private val privateFormatOutputSizes = listOf(SIZE_1080P, SIZE_720P, SIZE_480P)
+
+    @Before
+    fun setUp() {
+        val builder = StreamConfigurationMapBuilder.newBuilder().apply {
+            privateFormatOutputSizes.forEach { size ->
+                addOutputSize(FORMAT_PRIVATE, size)
+            }
+        }
+        streamConfigurationMapCompat =
+            StreamConfigurationMapCompat.toStreamConfigurationMapCompat(builder.build())
+    }
+
+    @Test
+    fun getOutputSizes_withFormat_callGetOutputSizes() {
+        assertThat(
+            streamConfigurationMapCompat.getOutputSizes(FORMAT_PRIVATE)!!.toList()
+        ).containsExactlyElementsIn(privateFormatOutputSizes)
+    }
+
+    @Test
+    fun getOutputSizes_withClass_callGetOutputSizes() {
+        assertThat(
+            streamConfigurationMapCompat.getOutputSizes(SurfaceTexture::class.java)!!.toList()
+        ).containsExactlyElementsIn(privateFormatOutputSizes)
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 36096c8..755c3bf 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.core;
 
+import static androidx.camera.core.CameraEffect.PREVIEW;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
@@ -83,7 +84,6 @@
 import androidx.camera.core.internal.ThreadConfig;
 import androidx.camera.core.processing.Node;
 import androidx.camera.core.processing.SettableSurface;
-import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorInternal;
 import androidx.camera.core.processing.SurfaceProcessorNode;
 import androidx.core.util.Consumer;
@@ -255,7 +255,7 @@
         // Create nodes and edges.
         mNode = new SurfaceProcessorNode(camera, mSurfaceProcessor);
         SettableSurface cameraSurface = new SettableSurface(
-                CameraEffect.PREVIEW,
+                PREVIEW,
                 resolution,
                 ImageFormat.PRIVATE,
                 new Matrix(),
@@ -264,9 +264,12 @@
                 getRelativeRotation(camera),
                 /*mirroring=*/isFrontCamera(camera),
                 this::notifyReset);
-        SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
-        SurfaceEdge outputEdge = mNode.transform(inputEdge);
-        SettableSurface appSurface = outputEdge.getSurfaces().get(0);
+        SurfaceProcessorNode.OutConfig outConfig = SurfaceProcessorNode.OutConfig.of(cameraSurface);
+        SurfaceProcessorNode.In nodeInput = SurfaceProcessorNode.In.of(
+                cameraSurface,
+                singletonList(outConfig));
+        SurfaceProcessorNode.Out nodeOutput = mNode.transform(nodeInput);
+        SettableSurface appSurface = requireNonNull(nodeOutput.get(outConfig));
 
         // Send the app Surface to the app.
         mSessionDeferrableSurface = cameraSurface;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 3bde336..4c87b7e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -82,6 +82,9 @@
     /**
      * Get the rotation degrees.
      *
+     * TODO(b/259308680): hide this from the {@link SurfaceOutput} interface. This is only used
+     * for testing.
+     *
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index 3f580a9..514debc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -16,6 +16,10 @@
 
 package androidx.camera.core.impl;
 
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.util.Size;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -24,6 +28,7 @@
 import androidx.core.util.Preconditions;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -80,6 +85,15 @@
     @NonNull
     Timebase getTimebase();
 
+    /**
+     * Returns the supported resolutions of this camera based on the input image format.
+     *
+     * @param format an image format from {@link ImageFormat} or {@link PixelFormat}.
+     * @return a list of supported resolutions, or an empty list if the format is not supported.
+     */
+    @NonNull
+    List<Size> getSupportedResolutions(int format);
+
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index 081ccc1..87c1c95 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -129,6 +129,17 @@
     }
 
     /**
+     * Gets the size after cropping and rotating.
+     *
+     * @return rotated size
+     * @throws IllegalArgumentException if the rotation degrees is not a multiple of.
+     */
+    @NonNull
+    public static Size getRotatedSize(@NonNull Rect cropRect, int rotationDegrees) {
+        return rotateSize(rectToSize(cropRect), rotationDegrees);
+    }
+
+    /**
      * Converts the degrees to within 360 degrees [0 - 359].
      */
     public static int within360(int degrees) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index 8584670..bdb18cd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -21,6 +21,7 @@
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
 
+import android.graphics.ImageFormat;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.media.ImageReader;
@@ -63,6 +64,11 @@
  *     <--> {@link SurfaceOutput} --> {@link SurfaceProcessor}(surface consumer)
  * </pre>
  *
+ * TODO(b/241910577): rename this class to SurfaceEdge and make it compositing a
+ *  DeferrableSurface instead of inheriting it. This is because in a stream sharing scenario, a
+ *  downstream UseCase may reset and provide a different Surface, and a DeferrableSurface
+ *  cannot be attached to a different Surface.
+ *
  * <p>For the full workflow, please see {@code SettableSurfaceTest
  * #linkBothProviderAndConsumer_surfaceAndResultsArePropagatedE2E}
  */
@@ -253,14 +259,14 @@
      * <p>Do not provide the {@link SurfaceOutput} to external target if the
      * {@link ListenableFuture} fails.
      *
-     * @param resolution         resolution of input image buffer
-     * @param cropRect           crop rect of input image buffer
-     * @param rotationDegrees    expected rotation to the input image buffer
-     * @param mirroring          expected mirroring to the input image buffer
+     * @param inputSize       resolution of input image buffer
+     * @param cropRect        crop rect of input image buffer
+     * @param rotationDegrees expected rotation to the input image buffer
+     * @param mirroring       expected mirroring to the input image buffer
      */
     @MainThread
     @NonNull
-    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size resolution,
+    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size inputSize,
             @NonNull Rect cropRect, int rotationDegrees, boolean mirroring) {
         checkMainThread();
         Preconditions.checkState(!mHasConsumer, "Consumer can only be linked once.");
@@ -274,7 +280,7 @@
                         return Futures.immediateFailedFuture(e);
                     }
                     SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
-                            getTargets(), getFormat(), getSize(), resolution, cropRect,
+                            getTargets(), getFormat(), getSize(), inputSize, cropRect,
                             rotationDegrees, mirroring);
                     surfaceOutputImpl.getCloseFuture().addListener(this::decrementUseCount,
                             directExecutor());
@@ -331,6 +337,9 @@
 
     /**
      * The format of the {@link Surface}.
+     *
+     * TODO(259308680): hide the format. Currently the pipeline only supports
+     *  {@link ImageFormat#PRIVATE}.
      */
     public int getFormat() {
         return getPrescribedStreamFormat();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
deleted file mode 100644
index 20bab64..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceEdge.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2022 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.camera.core.processing;
-
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-
-import com.google.auto.value.AutoValue;
-
-import java.util.List;
-
-/**
- * A data class represents a {@link Node} output that is based on {@link Surface}s.
- */
-@AutoValue
-public abstract class SurfaceEdge {
-
-    /**
-     * Gets output surfaces.
-     *
-     * TODO(b/234180399): consider switching to com.google.common.collect.ImmutableList.
-     */
-    @SuppressWarnings("AutoValueImmutableFields")
-    @NonNull
-    public abstract List<SettableSurface> getSurfaces();
-
-    /**
-     * Creates a {@link SurfaceEdge}.
-     */
-    @NonNull
-    public static SurfaceEdge create(@NonNull List<SettableSurface> surfaces) {
-        return new AutoValue_SurfaceEdge(surfaces);
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 2918de6..5c51b4f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -34,6 +34,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.Logger;
 import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceProcessor;
@@ -185,6 +186,16 @@
         return mFormat;
     }
 
+    @VisibleForTesting
+    public Rect getInputCropRect() {
+        return mInputCropRect;
+    }
+
+    @VisibleForTesting
+    public Size getInputSize() {
+        return mInputSize;
+    }
+
     /**
      * @inheritDoc
      */
@@ -193,6 +204,11 @@
         return mRotationDegrees;
     }
 
+    @VisibleForTesting
+    public boolean getMirroring() {
+        return mMirroring;
+    }
+
     /**
      * This method can be invoked by the processor implementation on any thread.
      *
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index de60c70..d00f151 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -17,35 +17,41 @@
 package androidx.camera.core.processing;
 
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
-import static androidx.camera.core.impl.utils.TransformUtils.is90or270;
-import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
+import static androidx.camera.core.impl.utils.TransformUtils.getRotatedSize;
 import static androidx.camera.core.impl.utils.TransformUtils.sizeToRect;
 import static androidx.camera.core.impl.utils.TransformUtils.sizeToRectF;
 import static androidx.camera.core.impl.utils.TransformUtils.within360;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkArgument;
-
-import static java.util.Collections.singletonList;
 
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.util.Size;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraEffect;
 import androidx.camera.core.Logger;
 import androidx.camera.core.ProcessingException;
 import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.core.util.Preconditions;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  * A {@link Node} implementation that wraps around the public {@link SurfaceProcessor} interface.
  *
@@ -59,7 +65,8 @@
 @RequiresApi(api = 21)
 // TODO(b/233627260): remove once implemented.
 @SuppressWarnings("UnusedVariable")
-public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
+public class SurfaceProcessorNode implements
+        Node<SurfaceProcessorNode.In, SurfaceProcessorNode.Out> {
 
     private static final String TAG = "SurfaceProcessorNode";
 
@@ -69,9 +76,9 @@
     final CameraInternal mCameraInternal;
     // Guarded by main thread.
     @Nullable
-    private SurfaceEdge mOutputEdge;
+    private Out mOutput;
     @Nullable
-    private SurfaceEdge mInputEdge;
+    private In mInput;
 
     /**
      * Constructs the {@link SurfaceProcessorNode}.
@@ -91,51 +98,51 @@
     @Override
     @NonNull
     @MainThread
-    public SurfaceEdge transform(@NonNull SurfaceEdge inputEdge) {
+    public Out transform(@NonNull In input) {
         Threads.checkMainThread();
-        checkArgument(inputEdge.getSurfaces().size() == 1,
-                "Multiple input stream not supported yet.");
-        mInputEdge = inputEdge;
-        SettableSurface inputSurface = inputEdge.getSurfaces().get(0);
-        SettableSurface outputSurface = createOutputSurface(inputSurface);
-        sendSurfacesToProcessorWhenReady(inputSurface, outputSurface);
-        mOutputEdge = SurfaceEdge.create(singletonList(outputSurface));
-        return mOutputEdge;
+        mInput = input;
+        mOutput = new Out();
+
+        SettableSurface inputSurface = input.getSurfaceEdge();
+        for (OutConfig config : input.getOutConfigs()) {
+            mOutput.put(config, transformSingleOutput(inputSurface, config));
+        }
+        sendSurfacesToProcessorWhenReady(inputSurface, mOutput);
+        return mOutput;
     }
 
     @NonNull
-    private SettableSurface createOutputSurface(@NonNull SettableSurface inputSurface) {
+    private SettableSurface transformSingleOutput(@NonNull SettableSurface input,
+            @NonNull OutConfig outConfig) {
         // TODO: Can be improved by only restarting part of the pipeline. e.g. only update the
         //  output Surface (between Effect/App), and still use the same input Surface (between
         //  Camera/Effect). It's just simpler for now.
-        final Runnable onSurfaceInvalidated = inputSurface::invalidate;
+        final Runnable onSurfaceInvalidated = input::invalidate;
 
         SettableSurface outputSurface;
-        Size resolution = inputSurface.getSize();
-        Rect cropRect = inputSurface.getCropRect();
-        int rotationDegrees = inputSurface.getRotationDegrees();
-        boolean mirroring = inputSurface.getMirroring();
-
-        // Calculate rotated resolution and cropRect
-        Size rotatedCroppedSize = is90or270(rotationDegrees)
-                ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
-                : rectToSize(cropRect);
+        Size inputSize = input.getSize();
+        Rect cropRect = outConfig.getCropRect();
+        int rotationDegrees = input.getRotationDegrees();
+        boolean mirroring = input.getMirroring();
 
         // Calculate sensorToBufferTransform
         android.graphics.Matrix sensorToBufferTransform =
-                new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
-        android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
-                new RectF(cropRect), rotationDegrees, mirroring);
+                new android.graphics.Matrix(input.getSensorToBufferTransform());
+        android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(inputSize),
+                sizeToRectF(outConfig.getSize()), rotationDegrees, mirroring);
         sensorToBufferTransform.postConcat(imageTransform);
 
+        // TODO(b/259308680): Checks that the aspect ratio of the rotated crop rect matches the
+        //  output size.
         outputSurface = new SettableSurface(
-                inputSurface.getTargets(),
-                rotatedCroppedSize,
-                inputSurface.getFormat(),
+                outConfig.getTargets(),
+                outConfig.getSize(),
+                input.getFormat(),
                 sensorToBufferTransform,
                 // The Surface transform cannot be carried over during buffer copy.
                 /*hasEmbeddedTransform=*/false,
-                sizeToRect(rotatedCroppedSize),
+                // Crop rect is always the full size.
+                sizeToRect(outConfig.getSize()),
                 /*rotationDegrees=*/0,
                 /*mirroring=*/false,
                 onSurfaceInvalidated);
@@ -144,21 +151,33 @@
     }
 
     private void sendSurfacesToProcessorWhenReady(@NonNull SettableSurface input,
-            @NonNull SettableSurface output) {
+            @NonNull Map<OutConfig, SettableSurface> outputs) {
         SurfaceRequest surfaceRequest = input.createSurfaceRequest(mCameraInternal);
+        List<ListenableFuture<SurfaceOutput>> outputFutures = new ArrayList<>();
+        for (Map.Entry<OutConfig, SettableSurface> output : outputs.entrySet()) {
+            outputFutures.add(output.getValue().createSurfaceOutputFuture(
+                    input.getSize(),
+                    output.getKey().getCropRect(),
+                    input.getRotationDegrees(),
+                    input.getMirroring()));
+        }
         setupRotationUpdates(
                 surfaceRequest,
-                output,
+                outputs.values(),
                 input.getMirroring(),
                 input.getRotationDegrees());
-        Futures.addCallback(output.createSurfaceOutputFuture(input.getSize(), input.getCropRect(),
-                        input.getRotationDegrees(), input.getMirroring()),
-                new FutureCallback<SurfaceOutput>() {
+
+        ListenableFuture<List<SurfaceOutput>> outputListFuture = Futures.allAsList(outputFutures);
+        Futures.addCallback(outputListFuture,
+                new FutureCallback<List<SurfaceOutput>>() {
+
                     @Override
-                    public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
-                        Preconditions.checkNotNull(surfaceOutput);
+                    public void onSuccess(@Nullable List<SurfaceOutput> outputs) {
+                        Preconditions.checkNotNull(outputs);
                         try {
-                            mSurfaceProcessor.onOutputSurface(surfaceOutput);
+                            for (SurfaceOutput output : outputs) {
+                                mSurfaceProcessor.onOutputSurface(output);
+                            }
                             mSurfaceProcessor.onInputSurface(surfaceRequest);
                         } catch (ProcessingException e) {
                             Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
@@ -167,9 +186,9 @@
 
                     @Override
                     public void onFailure(@NonNull Throwable t) {
-                        // Do not send surfaces to the processor if the downstream provider (e.g.
-                        // the app) fails to provide a Surface. Instead, notify the consumer that
-                        // the Surface will not be provided.
+                        // Do not send surfaces to the processor if the downstream provider
+                        // (e.g.the app) fails to provide a Surface. Instead, notify the
+                        // consumer that the Surface will not be provided.
                         surfaceRequest.willNotProvideSurface();
                     }
                 }, mainThreadExecutor());
@@ -187,13 +206,13 @@
      * input edge's rotation changes, we re-calculate the delta and notify the output edge.
      *
      * @param inputSurfaceRequest {@link SurfaceRequest} of the input edge.
-     * @param outputSurface       {@link SettableSurface} of the output edge.
+     * @param outputSurfaces      {@link SettableSurface} of the output edge.
      * @param mirrored            whether the node mirrors the buffer.
      * @param rotatedDegrees      how much the node rotates the buffer.
      */
     void setupRotationUpdates(
             @NonNull SurfaceRequest inputSurfaceRequest,
-            @NonNull SettableSurface outputSurface,
+            @NonNull Collection<SettableSurface> outputSurfaces,
             boolean mirrored,
             int rotatedDegrees) {
         inputSurfaceRequest.setTransformationInfoListener(mainThreadExecutor(), info -> {
@@ -203,7 +222,10 @@
             if (mirrored) {
                 rotationDegrees = -rotationDegrees;
             }
-            outputSurface.setRotationDegrees(within360(rotationDegrees));
+            rotationDegrees = within360(rotationDegrees);
+            for (SettableSurface output : outputSurfaces) {
+                output.setRotationDegrees(rotationDegrees);
+            }
         });
     }
 
@@ -214,12 +236,107 @@
     public void release() {
         mSurfaceProcessor.release();
         mainThreadExecutor().execute(() -> {
-            if (mOutputEdge != null) {
-                for (SettableSurface surface : mOutputEdge.getSurfaces()) {
+            if (mOutput != null) {
+                for (SettableSurface surface : mOutput.values()) {
                     // The output DeferrableSurface will later be terminated by the processor.
                     surface.close();
                 }
             }
         });
     }
+
+
+    /**
+     * The input of a {@link SurfaceProcessorNode}.
+     */
+    @AutoValue
+    public abstract static class In {
+
+        /**
+         * Gets the input stream.
+         *
+         * <p> {@link SurfaceProcessorNode} only supports a single input stream.
+         */
+        @NonNull
+        public abstract SettableSurface getSurfaceEdge();
+
+        /**
+         * Gets the config for generating output streams.
+         *
+         * <p>{@link SurfaceProcessorNode#transform} creates one {@link SettableSurface} per
+         * {@link OutConfig} in this list.
+         */
+        @SuppressWarnings("AutoValueImmutableFields")
+        @NonNull
+        public abstract List<OutConfig> getOutConfigs();
+
+        /**
+         * Creates a {@link In} instance.
+         */
+        @NonNull
+        public static In of(@NonNull SettableSurface edge, @NonNull List<OutConfig> configs) {
+            return new AutoValue_SurfaceProcessorNode_In(edge, configs);
+        }
+    }
+
+    /**
+     * The output of a {@link SurfaceProcessorNode}.
+     *
+     * <p>A map of {@link OutConfig} with their corresponding {@link SettableSurface}.
+     */
+    public static class Out extends HashMap<OutConfig, SettableSurface> {
+    }
+
+    /**
+     * Configuration of how to create an output stream from an input stream.
+     *
+     * <p>The value in this class will override the corresponding value in the
+     * {@link SettableSurface} class. The override is necessary when a single stream is shared
+     * to multiple output streams with different transformations. For example, if a single 4:3
+     * preview stream is shared to a 16:9 video stream, the video stream must override the crop
+     * rect.
+     */
+    @AutoValue
+    public abstract static class OutConfig {
+
+        /**
+         * The target {@link UseCase} of the output stream.
+         */
+        @CameraEffect.Targets
+        abstract int getTargets();
+
+        /**
+         * How the input should be cropped.
+         */
+        @NonNull
+        abstract Rect getCropRect();
+
+        /**
+         * The stream should scale to this size after cropping and rotating.
+         *
+         * <p>The input stream should be scaled to match this size after cropping and rotating
+         */
+        @NonNull
+        abstract Size getSize();
+
+        /**
+         * Creates an {@link OutConfig} instance from the input edge.
+         *
+         * <p>The result is an output edge with the input's transformation applied.
+         */
+        @NonNull
+        public static OutConfig of(@NonNull SettableSurface surface) {
+            return of(surface.getTargets(),
+                    surface.getCropRect(),
+                    getRotatedSize(surface.getCropRect(), surface.getRotationDegrees()));
+        }
+
+        /**
+         * Creates an {@link OutConfig} instance with custom transformations.
+         */
+        @NonNull
+        public static OutConfig of(int targets, @NonNull Rect cropRect, @NonNull Size size) {
+            return new AutoValue_SurfaceProcessorNode_OutConfig(targets, cropRect, size);
+        }
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 8a1c98f..3a2a8c0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -24,6 +24,7 @@
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
+import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.CameraFactory
@@ -324,9 +325,9 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: surfaceOutput received.
-        assertThat(processor.surfaceOutput).isNotNull()
+        assertThat(processor.surfaceOutputs).hasSize(1)
         assertThat(processor.isReleased).isFalse()
-        assertThat(processor.isOutputSurfaceRequestedToClose).isFalse()
+        assertThat(processor.isOutputSurfaceRequestedToClose[PREVIEW]).isNull()
         assertThat(processor.isInputSurfaceReleased).isFalse()
         assertThat(appSurfaceReadyToRelease).isFalse()
         // processor surface is provided to camera.
@@ -339,12 +340,12 @@
 
         // Assert: processor and processor surface is released.
         assertThat(processor.isReleased).isTrue()
-        assertThat(processor.isOutputSurfaceRequestedToClose).isTrue()
+        assertThat(processor.isOutputSurfaceRequestedToClose[PREVIEW]).isTrue()
         assertThat(processor.isInputSurfaceReleased).isTrue()
         assertThat(appSurfaceReadyToRelease).isFalse()
 
         // Act: close SurfaceOutput
-        processor.surfaceOutput!!.close()
+        processor.surfaceOutputs[CameraEffect.PREVIEW]!!.close()
         shadowOf(getMainLooper()).idle()
         assertThat(appSurfaceReadyToRelease).isTrue()
     }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 43fac3e..9139c14 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -16,7 +16,7 @@
 
 package androidx.camera.core.processing
 
-import android.graphics.PixelFormat
+import android.graphics.ImageFormat
 import android.graphics.Rect
 import android.graphics.SurfaceTexture
 import android.os.Build
@@ -24,12 +24,14 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.utils.TransformUtils.is90or270
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
+import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
-import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.core.processing.SurfaceProcessorNode.OutConfig
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
 import com.google.common.truth.Truth.assertThat
@@ -51,76 +53,91 @@
 class SurfaceProcessorNodeTest {
 
     companion object {
-        private const val TARGET = PREVIEW
-        private const val FORMAT = PixelFormat.RGBA_8888
         private const val ROTATION_DEGREES = 90
-        private val SIZE = Size(640, 480)
-        private val CROP_RECT = Rect(0, 0, 600, 400)
+        private const val MIRRORING = false
+        private val INPUT_SIZE = Size(640, 480)
+        private val PREVIEW_CROP_RECT = Rect(0, 0, 600, 400)
+        private val VIDEO_CROP_RECT = Rect(0, 0, 300, 200)
+        private val VIDEO_SIZE = Size(20, 30)
     }
 
     private lateinit var surfaceProcessorInternal: FakeSurfaceProcessorInternal
-    private lateinit var appSurface: Surface
-    private lateinit var appSurfaceTexture: SurfaceTexture
+    private lateinit var previewSurface: Surface
+    private lateinit var previewTexture: SurfaceTexture
+    private lateinit var previewOutConfig: OutConfig
+    private lateinit var videoSurface: Surface
+    private lateinit var videoTexture: SurfaceTexture
+    private lateinit var videoOutConfig: OutConfig
     private lateinit var node: SurfaceProcessorNode
-    private lateinit var inputEdge: SurfaceEdge
-    private lateinit var outputSurfaceRequest: SurfaceRequest
-    private var outputTransformInfo: TransformationInfo? = null
+    private lateinit var nodeInput: SurfaceProcessorNode.In
+    private lateinit var previewSurfaceRequest: SurfaceRequest
+    private lateinit var videoSurfaceRequest: SurfaceRequest
+    private lateinit var previewTransformInfo: TransformationInfo
+    private lateinit var videoTransformInfo: TransformationInfo
 
     @Before
     fun setup() {
-        appSurfaceTexture = SurfaceTexture(0)
-        appSurface = Surface(appSurfaceTexture)
+        previewTexture = SurfaceTexture(0)
+        previewSurface = Surface(previewTexture)
+        videoTexture = SurfaceTexture(0)
+        videoSurface = Surface(videoTexture)
         surfaceProcessorInternal = FakeSurfaceProcessorInternal(mainThreadExecutor())
     }
 
     @After
     fun tearDown() {
-        appSurfaceTexture.release()
-        appSurface.release()
+        previewTexture.release()
+        previewSurface.release()
+        videoTexture.release()
+        videoSurface.release()
         surfaceProcessorInternal.release()
         if (::node.isInitialized) {
             node.release()
         }
-        if (::inputEdge.isInitialized) {
-            inputEdge.surfaces.forEach { it.close() }
+        if (::nodeInput.isInitialized) {
+            nodeInput.surfaceEdge.close()
         }
-        if (::outputSurfaceRequest.isInitialized) {
-            outputSurfaceRequest.deferrableSurface.close()
+        if (::previewSurfaceRequest.isInitialized) {
+            previewSurfaceRequest.deferrableSurface.close()
+        }
+        if (::videoSurfaceRequest.isInitialized) {
+            videoSurfaceRequest.deferrableSurface.close()
         }
         shadowOf(getMainLooper()).idle()
     }
 
     @Test
     fun transformInput_applyCropRotateAndMirroring_outputIsCroppedAndRotated() {
-        val cropRect = Rect(200, 100, 600, 400)
         for (rotationDegrees in arrayOf(0, 90, 180, 270)) {
             // Arrange.
             createSurfaceProcessorNode()
             createInputEdge(
-                size = rectToSize(cropRect),
-                cropRect = cropRect,
-                rotationDegrees = rotationDegrees
+                previewRotationDegrees = rotationDegrees
             )
             // The result cropRect should have zero left and top.
             val expectedCropRect = if (is90or270(rotationDegrees))
-                Rect(0, 0, cropRect.height(), cropRect.width())
+                Rect(0, 0, PREVIEW_CROP_RECT.height(), PREVIEW_CROP_RECT.width())
             else
-                Rect(0, 0, cropRect.width(), cropRect.height())
+                Rect(0, 0, PREVIEW_CROP_RECT.width(), PREVIEW_CROP_RECT.height())
 
             // Act.
-            val outputEdge = node.transform(inputEdge)
+            val nodeOutput = node.transform(nodeInput)
 
             // Assert: with transformation, the output size is cropped/rotated and the rotation
             // degrees is reset.
-            assertThat(outputEdge.surfaces).hasSize(1)
-            val outputSurface = outputEdge.surfaces[0]
-            assertThat(outputSurface.size).isEqualTo(rectToSize(expectedCropRect))
-            assertThat(outputSurface.cropRect).isEqualTo(expectedCropRect)
-            assertThat(outputSurface.rotationDegrees).isEqualTo(0)
+            val previewOutput = nodeOutput[previewOutConfig]!!
+            assertThat(previewOutput.size).isEqualTo(rectToSize(expectedCropRect))
+            assertThat(previewOutput.cropRect).isEqualTo(expectedCropRect)
+            assertThat(previewOutput.rotationDegrees).isEqualTo(0)
+            val videoOutput = nodeOutput[videoOutConfig]!!
+            assertThat(videoOutput.size).isEqualTo(VIDEO_SIZE)
+            assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
+            assertThat(videoOutput.rotationDegrees).isEqualTo(0)
 
             // Clean up.
-            inputEdge.surfaces[0].close()
+            nodeInput.surfaceEdge.close()
             node.release()
+            shadowOf(getMainLooper()).idle()
         }
     }
 
@@ -132,15 +149,14 @@
             createInputEdge(mirroring = mirroring)
 
             // Act.
-            val outputEdge = node.transform(inputEdge)
+            val nodeOutput = node.transform(nodeInput)
 
             // Assert: the mirroring of output is always false.
-            assertThat(outputEdge.surfaces).hasSize(1)
-            val outputSurface = outputEdge.surfaces[0]
-            assertThat(outputSurface.mirroring).isFalse()
+            assertThat(nodeOutput[previewOutConfig]!!.mirroring).isFalse()
+            assertThat(nodeOutput[videoOutConfig]!!.mirroring).isFalse()
 
             // Clean up.
-            inputEdge.surfaces[0].close()
+            nodeInput.surfaceEdge.close()
             node.release()
         }
     }
@@ -149,30 +165,42 @@
     fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
         // Arrange.
         createSurfaceProcessorNode()
-        createInputEdge(rotationDegrees = 90, cropRect = Rect(0, 0, 600, 400))
+        createInputEdge()
 
         // Act.
-        val outputEdge = node.transform(inputEdge)
-        val outputSurface = outputEdge.surfaces[0]
-        createOutputSurfaceRequestAndProvideSurface(outputSurface)
+        val nodeOutput = node.transform(nodeInput)
+        provideSurfaces(nodeOutput)
         shadowOf(getMainLooper()).idle()
 
         // Assert: surfaceOutput of SurfaceProcessor will consume the initial rotation degrees and
         // output surface will receive 0 degrees.
-        assertThat(surfaceProcessorInternal.surfaceOutput!!.rotationDegrees).isEqualTo(90)
-        assertThat(outputTransformInfo!!.rotationDegrees).isEqualTo(0)
-        assertThat(outputTransformInfo!!.cropRect).isEqualTo(Rect(0, 0, 400, 600))
+        val previewSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+        assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+        assertThat(previewSurfaceOutput.size).isEqualTo(Size(400, 600))
+        assertThat(previewSurfaceOutput.inputCropRect).isEqualTo(PREVIEW_CROP_RECT)
+        assertThat(previewTransformInfo.cropRect).isEqualTo(Rect(0, 0, 400, 600))
+        assertThat(previewTransformInfo.rotationDegrees).isEqualTo(0)
+        assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+
+        val videoSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+        assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(ROTATION_DEGREES)
+        assertThat(videoSurfaceOutput.size).isEqualTo(VIDEO_SIZE)
+        assertThat(videoSurfaceOutput.inputCropRect).isEqualTo(VIDEO_CROP_RECT)
+        assertThat(videoTransformInfo.cropRect).isEqualTo(sizeToRect(VIDEO_SIZE))
+        assertThat(videoTransformInfo.rotationDegrees).isEqualTo(0)
+        assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
     }
 
     @Test
     fun setRotationToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
         // Arrange.
         createSurfaceProcessorNode()
-        createInputEdge(rotationDegrees = 90)
-        val inputSurface = inputEdge.surfaces[0]
-        val outputEdge = node.transform(inputEdge)
-        val outputSurface = outputEdge.surfaces[0]
-        createOutputSurfaceRequestAndProvideSurface(outputSurface)
+        createInputEdge(previewRotationDegrees = 90)
+        val inputSurface = nodeInput.surfaceEdge
+        val nodeOutput = node.transform(nodeInput)
+        provideSurfaces(nodeOutput)
         shadowOf(getMainLooper()).idle()
 
         // Act.
@@ -181,8 +209,16 @@
 
         // Assert: surfaceOutput of SurfaceProcessor will consume the initial rotation degrees and
         // output surface will receive the remaining degrees.
-        assertThat(surfaceProcessorInternal.surfaceOutput!!.rotationDegrees).isEqualTo(90)
-        assertThat(outputTransformInfo!!.rotationDegrees).isEqualTo(180)
+        val previewSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[PREVIEW]!! as SurfaceOutputImpl
+        assertThat(previewSurfaceOutput.rotationDegrees).isEqualTo(90)
+        assertThat(previewTransformInfo.rotationDegrees).isEqualTo(180)
+        assertThat(previewSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
+        val videoSurfaceOutput =
+            surfaceProcessorInternal.surfaceOutputs[VIDEO_CAPTURE]!! as SurfaceOutputImpl
+        assertThat(videoSurfaceOutput.rotationDegrees).isEqualTo(90)
+        assertThat(videoTransformInfo.rotationDegrees).isEqualTo(180)
+        assertThat(videoSurfaceOutput.inputSize).isEqualTo(INPUT_SIZE)
     }
 
     @Test
@@ -190,16 +226,16 @@
         // Arrange.
         createSurfaceProcessorNode()
         createInputEdge()
-        val inputSurface = inputEdge.surfaces[0]
-        val outputEdge = node.transform(inputEdge)
-        val outputSurface = outputEdge.surfaces[0]
+        val inputSurface = nodeInput.surfaceEdge
+        val nodeOutput = node.transform(nodeInput)
 
         // Act.
-        outputSurface.setProvider(Futures.immediateFuture(appSurface))
+        provideSurfaces(nodeOutput)
         shadowOf(getMainLooper()).idle()
 
         // Assert: processor receives app Surface. CameraX receives processor Surface.
-        assertThat(surfaceProcessorInternal.outputSurface).isEqualTo(appSurface)
+        assertThat(surfaceProcessorInternal.outputSurfaces[PREVIEW]).isEqualTo(previewSurface)
+        assertThat(surfaceProcessorInternal.outputSurfaces[VIDEO_CAPTURE]).isEqualTo(videoSurface)
         assertThat(inputSurface.surface.get()).isEqualTo(surfaceProcessorInternal.inputSurface)
     }
 
@@ -208,8 +244,8 @@
         // Arrange.
         createSurfaceProcessorNode()
         createInputEdge()
-        val outputSurface = node.transform(inputEdge).surfaces[0]
-        outputSurface.setProvider(Futures.immediateFuture(appSurface))
+        val nodeOutput = node.transform(nodeInput)
+        provideSurfaces(nodeOutput)
         shadowOf(getMainLooper()).idle()
 
         // Act: release the node.
@@ -218,30 +254,40 @@
 
         // Assert: processor is released and has requested processor to close the SurfaceOutput
         assertThat(surfaceProcessorInternal.isReleased).isTrue()
-        assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose).isTrue()
+        assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose[PREVIEW]).isTrue()
+        assertThat(surfaceProcessorInternal.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isTrue()
     }
 
     private fun createInputEdge(
-        target: Int = TARGET,
-        size: Size = SIZE,
-        format: Int = FORMAT,
+        previewTarget: Int = PREVIEW,
+        previewSize: Size = INPUT_SIZE,
+        format: Int = ImageFormat.PRIVATE,
         sensorToBufferTransform: android.graphics.Matrix = android.graphics.Matrix(),
         hasEmbeddedTransform: Boolean = true,
-        cropRect: Rect = CROP_RECT,
-        rotationDegrees: Int = ROTATION_DEGREES,
-        mirroring: Boolean = false
+        previewCropRect: Rect = PREVIEW_CROP_RECT,
+        previewRotationDegrees: Int = ROTATION_DEGREES,
+        mirroring: Boolean = MIRRORING,
     ) {
         val surface = SettableSurface(
-            target,
-            size,
+            previewTarget,
+            previewSize,
             format,
             sensorToBufferTransform,
             hasEmbeddedTransform,
-            cropRect,
-            rotationDegrees,
+            previewCropRect,
+            previewRotationDegrees,
             mirroring
         ) {}
-        inputEdge = SurfaceEdge.create(listOf(surface))
+        videoOutConfig = OutConfig.of(
+            VIDEO_CAPTURE,
+            VIDEO_CROP_RECT,
+            VIDEO_SIZE
+        )
+        previewOutConfig = OutConfig.of(surface)
+        nodeInput = SurfaceProcessorNode.In.of(
+            surface,
+            listOf(previewOutConfig, videoOutConfig)
+        )
     }
 
     private fun createSurfaceProcessorNode() {
@@ -251,15 +297,20 @@
         )
     }
 
-    private fun createOutputSurfaceRequestAndProvideSurface(
-        settableSurface: SettableSurface,
-        surface: Surface = appSurface
-    ) {
-        outputSurfaceRequest = settableSurface.createSurfaceRequest(FakeCamera()).apply {
-            setTransformationInfoListener(mainThreadExecutor()) {
-                outputTransformInfo = it
+    private fun provideSurfaces(nodeOutput: SurfaceProcessorNode.Out) {
+        previewSurfaceRequest =
+            nodeOutput[previewOutConfig]!!.createSurfaceRequest(FakeCamera()).apply {
+                setTransformationInfoListener(mainThreadExecutor()) {
+                    previewTransformInfo = it
+                }
+                provideSurface(previewSurface, mainThreadExecutor()) { previewSurface.release() }
             }
-            provideSurface(surface, mainThreadExecutor()) { surface.release() }
-        }
+        videoSurfaceRequest =
+            nodeOutput[videoOutConfig]!!.createSurfaceRequest(FakeCamera()).apply {
+                setTransformationInfoListener(mainThreadExecutor()) {
+                    videoTransformInfo = it
+                }
+                provideSurface(videoSurface, mainThreadExecutor()) { videoSurface.release() }
+            }
     }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index ecf59053..361de26 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -18,6 +18,7 @@
 
 import android.util.Range;
 import android.util.Rational;
+import android.util.Size;
 import android.view.Surface;
 
 import androidx.annotation.NonNull;
@@ -43,7 +44,10 @@
 import androidx.lifecycle.MutableLiveData;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -59,6 +63,7 @@
     private final int mLensFacing;
     private final MutableLiveData<Integer> mTorchState = new MutableLiveData<>(TorchState.OFF);
     private final MutableLiveData<ZoomState> mZoomLiveData;
+    private final Map<Integer, List<Size>> mSupportedResolutionMap = new HashMap<>();
     private MutableLiveData<CameraState> mCameraStateLiveData;
     private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
 
@@ -180,6 +185,13 @@
         return mTimebase;
     }
 
+    @NonNull
+    @Override
+    public List<Size> getSupportedResolutions(int format) {
+        List<Size> resolutions = mSupportedResolutionMap.get(format);
+        return resolutions != null ? resolutions : Collections.emptyList();
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
@@ -236,6 +248,11 @@
         mTimebase = timebase;
     }
 
+    /** Set the supported resolutions for testing */
+    public void setSupportedResolutions(int format, @NonNull List<Size> resolutions) {
+        mSupportedResolutionMap.put(format, resolutions);
+    }
+
     /** Set the isPrivateReprocessingSupported flag for testing */
     public void setPrivateReprocessingSupported(boolean supported) {
         mIsPrivateReprocessingSupported = supported;
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
index b8d6e160..1ce5970 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceProcessor.java
@@ -28,6 +28,8 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.impl.DeferrableSurface;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 /**
@@ -44,12 +46,12 @@
 
     @Nullable
     private SurfaceRequest mSurfaceRequest;
-    @Nullable
-    private SurfaceOutput mSurfaceOutput;
+    @NonNull
+    private final Map<Integer, SurfaceOutput> mSurfaceOutputs = new HashMap<>();
     boolean mIsInputSurfaceReleased;
-    boolean mIsOutputSurfaceRequestedToClose;
+    private final Map<Integer, Boolean> mIsOutputSurfaceRequestedToClose = new HashMap<>();
 
-    Surface mOutputSurface;
+    private final Map<Integer, Surface> mOutputSurfaces = new HashMap<>();
 
     /**
      * Creates a {@link SurfaceProcessor} that closes the {@link SurfaceOutput} automatically.
@@ -61,7 +63,7 @@
     /**
      * @param autoCloseSurfaceOutput if true, automatically close the {@link SurfaceOutput} once
      *                               the close request is received. Otherwise, the test needs to
-     *                               get {@link #getSurfaceOutput()} and call
+     *                               get {@link #getSurfaceOutputs()} and call
      *                               {@link SurfaceOutput#close()} to avoid the "Completer GCed"
      *                               error in {@link DeferrableSurface}.
      */
@@ -70,7 +72,6 @@
         mInputSurface = new Surface(mSurfaceTexture);
         mExecutor = executor;
         mIsInputSurfaceReleased = false;
-        mIsOutputSurfaceRequestedToClose = false;
         mAutoCloseSurfaceOutput = autoCloseSurfaceOutput;
     }
 
@@ -86,15 +87,15 @@
 
     @Override
     public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
-        mSurfaceOutput = surfaceOutput;
-        mOutputSurface = surfaceOutput.getSurface(mExecutor,
+        mSurfaceOutputs.put(surfaceOutput.getTargets(), surfaceOutput);
+        mOutputSurfaces.put(surfaceOutput.getTargets(), surfaceOutput.getSurface(mExecutor,
                 output -> {
                     if (mAutoCloseSurfaceOutput) {
                         surfaceOutput.close();
                     }
-                    mIsOutputSurfaceRequestedToClose = true;
+                    mIsOutputSurfaceRequestedToClose.put(surfaceOutput.getTargets(), true);
                 }
-        );
+        ));
     }
 
     @Nullable
@@ -102,9 +103,9 @@
         return mSurfaceRequest;
     }
 
-    @Nullable
-    public SurfaceOutput getSurfaceOutput() {
-        return mSurfaceOutput;
+    @NonNull
+    public Map<Integer, SurfaceOutput> getSurfaceOutputs() {
+        return mSurfaceOutputs;
     }
 
     @NonNull
@@ -113,15 +114,16 @@
     }
 
     @NonNull
-    public Surface getOutputSurface() {
-        return mOutputSurface;
+    public Map<Integer, Surface> getOutputSurfaces() {
+        return mOutputSurfaces;
     }
 
     public boolean isInputSurfaceReleased() {
         return mIsInputSurfaceReleased;
     }
 
-    public boolean isOutputSurfaceRequestedToClose() {
+    @NonNull
+    public Map<Integer, Boolean> isOutputSurfaceRequestedToClose() {
         return mIsOutputSurfaceRequestedToClose;
     }
 
@@ -129,8 +131,8 @@
      * Clear up the instance to avoid the "{@link DeferrableSurface} garbage collected" error.
      */
     public void cleanUp() {
-        if (mSurfaceOutput != null) {
-            mSurfaceOutput.close();
+        for (SurfaceOutput surfaceOutput : mSurfaceOutputs.values()) {
+            surfaceOutput.close();
         }
     }
 }
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
index ebf6f60..451594f 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
@@ -20,8 +20,10 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.Build;
+import android.util.Size;
 
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.impl.ImageFormatConstants;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -30,6 +32,9 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -55,4 +60,17 @@
     public void canRetrieveSensorRotation() {
         assertThat(mFakeCameraInfo.getSensorRotationDegrees()).isEqualTo(SENSOR_ROTATION_DEGREES);
     }
+
+    @Test
+    public void canRetrieveSupportedResolutions() {
+        List<Size> resolutions = new ArrayList<>();
+        resolutions.add(new Size(1280, 720));
+        resolutions.add(new Size(640, 480));
+        mFakeCameraInfo.setSupportedResolutions(
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, resolutions);
+
+        assertThat(mFakeCameraInfo.getSupportedResolutions(
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE))
+                .containsExactlyElementsIn(resolutions);
+    }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 3b47dfb..9da1b86 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -99,7 +99,6 @@
 import androidx.camera.core.internal.ThreadConfig;
 import androidx.camera.core.processing.DefaultSurfaceProcessor;
 import androidx.camera.core.processing.SettableSurface;
-import androidx.camera.core.processing.SurfaceEdge;
 import androidx.camera.core.processing.SurfaceProcessorInternal;
 import androidx.camera.core.processing.SurfaceProcessorNode;
 import androidx.camera.video.StreamInfo.StreamState;
@@ -531,9 +530,13 @@
                     getRelativeRotation(camera),
                     /*mirroring=*/false,
                     onSurfaceInvalidated);
-            SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
-            SurfaceEdge outputEdge = mNode.transform(inputEdge);
-            SettableSurface appSurface = outputEdge.getSurfaces().get(0);
+            SurfaceProcessorNode.OutConfig outConfig =
+                    SurfaceProcessorNode.OutConfig.of(cameraSurface);
+            SurfaceProcessorNode.In nodeInput = SurfaceProcessorNode.In.of(
+                    cameraSurface,
+                    singletonList(outConfig));
+            SurfaceProcessorNode.Out nodeOutput = mNode.transform(nodeInput);
+            SettableSurface appSurface = requireNonNull(nodeOutput.get(outConfig));
             mSurfaceRequest = appSurface.createSurfaceRequest(camera, targetFpsRange);
             mDeferrableSurface = cameraSurface;
             cameraSurface.getTerminationFuture().addListener(() -> {
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index 2f15a2a..adfd01e 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -24,6 +24,7 @@
 import android.util.Size
 import android.view.Surface
 import androidx.arch.core.util.Function
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraSelector.LENS_FACING_BACK
 import androidx.camera.core.CameraXConfig
@@ -583,9 +584,9 @@
         addAndAttachUseCases(videoCapture)
 
         // Assert: surfaceOutput received.
-        assertThat(processor.surfaceOutput).isNotNull()
+        assertThat(processor.surfaceOutputs).hasSize(1)
         assertThat(processor.isReleased).isFalse()
-        assertThat(processor.isOutputSurfaceRequestedToClose).isFalse()
+        assertThat(processor.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isNull()
         assertThat(processor.isInputSurfaceReleased).isFalse()
         assertThat(appSurfaceReadyToRelease).isFalse()
         // processor surface is provided to camera.
@@ -597,12 +598,12 @@
 
         // Assert: processor and processor surface is released.
         assertThat(processor.isReleased).isTrue()
-        assertThat(processor.isOutputSurfaceRequestedToClose).isTrue()
+        assertThat(processor.isOutputSurfaceRequestedToClose[VIDEO_CAPTURE]).isTrue()
         assertThat(processor.isInputSurfaceReleased).isTrue()
         assertThat(appSurfaceReadyToRelease).isFalse()
 
         // Act: close SurfaceOutput
-        processor.surfaceOutput!!.close()
+        processor.surfaceOutputs[VIDEO_CAPTURE]!!.close()
         shadowOf(Looper.getMainLooper()).idle()
         assertThat(appSurfaceReadyToRelease).isTrue()
     }
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index cc0c1c7..53e7b06 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -54,6 +54,7 @@
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.junit)
+        androidTestImplementation(libs.truth)
 
         lintPublish project(":compose:animation:animation-lint")
 
@@ -107,6 +108,7 @@
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
+                implementation(libs.truth)
                 implementation(project(":compose:foundation:foundation"))
                 implementation(project(":compose:ui:ui-test-junit4"))
                 implementation(project(":compose:test-utils"))
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
index d6c980c..b23c1f5 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedContentTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.InternalAnimationApi
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
 import androidx.compose.animation.core.keyframes
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.updateTransition
@@ -43,8 +44,10 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -52,6 +55,8 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import kotlinx.coroutines.delay
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -59,7 +64,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.math.roundToInt
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
@@ -242,15 +246,19 @@
                 ) {
                     if (it) {
                         Box(
-                            modifier = Modifier.onGloballyPositioned {
-                                offset1 = it.positionInRoot()
-                            }.size(size1.width.dp, size1.height.dp)
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    offset1 = it.positionInRoot()
+                                }
+                                .size(size1.width.dp, size1.height.dp)
                         )
                     } else {
                         Box(
-                            modifier = Modifier.onGloballyPositioned {
-                                offset2 = it.positionInRoot()
-                            }.size(size2.width.dp, size2.height.dp)
+                            modifier = Modifier
+                                .onGloballyPositioned {
+                                    offset2 = it.positionInRoot()
+                                }
+                                .size(size2.width.dp, size2.height.dp)
                         )
                     }
                 }
@@ -334,16 +342,19 @@
         }
     }
 
-    @OptIn(ExperimentalAnimationApi::class, InternalAnimationApi::class)
+    @OptIn(ExperimentalAnimationApi::class)
     @Test
     fun AnimatedContentSlideInAndOutOfContainerTest() {
-        val transitionState = MutableTransitionState(true).apply { targetState = false }
+        val transitionState = MutableTransitionState(true)
+        // LinearEasing is required to ensure the animation doesn't reach final values before the
+        // duration.
         val animSpec = tween<IntOffset>(200, easing = LinearEasing)
+        lateinit var trueTransition: Transition<EnterExitState>
+        lateinit var falseTransition: Transition<EnterExitState>
+        rule.mainClock.autoAdvance = false
         rule.setContent {
             CompositionLocalProvider(LocalDensity provides Density(1f, 1f)) {
-                if (!transitionState.targetState && !transitionState.currentState) {
-                    transitionState.targetState = true
-                }
+                @Suppress("UpdateTransitionLabel")
                 val rootTransition = updateTransition(transitionState)
                 rootTransition.AnimatedContent(
                     transitionSpec = {
@@ -365,33 +376,65 @@
                         }
                     }
                 ) { target ->
-                    Box(Modifier.requiredSize(200.dp))
-                    LaunchedEffect(transitionState.targetState) {
-                        while (transition.animations.size == 0) {
-                            delay(10)
-                        }
-                        val anim = transition.animations[0]
-                        while (transitionState.currentState != transitionState.targetState) {
-                            val playTime = (transition.playTimeNanos / 1000_000L).toInt()
-                            if (!transitionState.targetState) {
-                                if (target) {
-                                    assertEquals(IntOffset(-playTime, 0), anim.value)
-                                } else {
-                                    assertEquals(IntOffset(200 - playTime, 0), anim.value)
-                                }
-                            } else {
-                                if (target) {
-                                    assertEquals(IntOffset(playTime - 200, 0), anim.value)
-                                } else {
-                                    assertEquals(IntOffset(playTime, 0), anim.value)
-                                }
-                            }
-                            delay(10)
-                        }
+                    if (target) {
+                        trueTransition = transition
+                    } else {
+                        falseTransition = transition
                     }
+                    Box(
+                        Modifier
+                            .requiredSize(200.dp)
+                            .testTag(target.toString())
+                    )
                 }
             }
         }
+
+        // Kick off the first animation.
+        transitionState.targetState = false
+        // The initial composition creates the transition…
+        rule.mainClock.advanceTimeByFrame()
+        rule.onNodeWithTag("true").assertExists()
+        rule.onNodeWithTag("false").assertExists()
+        // …but the animation won't actually start until one frame later.
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(trueTransition.animations).isNotEmpty()
+        assertThat(falseTransition.animations).isNotEmpty()
+
+        // Loop to ensure the content is offset correctly at each frame.
+        var trueAnim = trueTransition.animations[0]
+        var falseAnim = falseTransition.animations[0]
+        assertThat(transitionState.currentState).isTrue()
+        while (transitionState.currentState) {
+            // True is leaving: it should start at 0 and slide out to -200.
+            assertThat(trueAnim.value).isEqualTo(IntOffset(-trueTransition.playTimeMillis, 0))
+            // False is entering: it should start at 200 and slide in to 0.
+            assertThat(falseAnim.value)
+                .isEqualTo(IntOffset(200 - falseTransition.playTimeMillis, 0))
+            rule.mainClock.advanceTimeByFrame()
+        }
+        // The animation should remove the newly-hidden node from the composition.
+        rule.onNodeWithTag("true").assertDoesNotExist()
+
+        // Kick off the second transition.
+        transitionState.targetState = true
+        rule.mainClock.advanceTimeByFrame()
+        rule.onNodeWithTag("true").assertExists()
+        rule.onNodeWithTag("false").assertExists()
+        rule.mainClock.advanceTimeByFrame()
+        assertThat(trueTransition.animations).isNotEmpty()
+
+        trueAnim = trueTransition.animations[0]
+        falseAnim = falseTransition.animations[0]
+        assertThat(transitionState.currentState).isFalse()
+        while (!transitionState.currentState) {
+            // True is entering, it should start at -200 and slide in to 0.
+            assertThat(trueAnim.value).isEqualTo(IntOffset(trueTransition.playTimeMillis - 200, 0))
+            // False is leaving, it should start at 0 and slide out to 200.
+            assertThat(falseAnim.value).isEqualTo(IntOffset(falseTransition.playTimeMillis, 0))
+            rule.mainClock.advanceTimeByFrame()
+        }
+        rule.onNodeWithTag("false").assertDoesNotExist()
     }
 
     @OptIn(ExperimentalAnimationApi::class)
@@ -488,4 +531,7 @@
             flag = false
         }
     }
+
+    @OptIn(InternalAnimationApi::class)
+    private val Transition<*>.playTimeMillis get() = (playTimeNanos / 1_000_000L).toInt()
 }
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
index 94a0da5..dab4a12 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/AnimatedVisibilityTest.kt
@@ -44,8 +44,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -53,6 +55,7 @@
 import androidx.compose.ui.util.lerp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -95,7 +98,8 @@
                     ) { fullSize -> IntSize(fullSize.width / 10, fullSize.height / 5) },
                 ) {
                     Box(
-                        Modifier.requiredSize(100.dp, 100.dp)
+                        Modifier
+                            .requiredSize(100.dp, 100.dp)
                             .onGloballyPositioned {
                                 offset = it.localToRoot(Offset.Zero)
                             }
@@ -204,7 +208,8 @@
                 ) { fullSize -> IntOffset(-fullSize.width / 10, fullSize.height / 5) },
             ) {
                 Box(
-                    Modifier.requiredSize(100.dp, 100.dp)
+                    Modifier
+                        .requiredSize(100.dp, 100.dp)
                         .onGloballyPositioned {
                             offset = it.localToRoot(Offset.Zero)
                         }
@@ -346,7 +351,11 @@
                 enter = fadeIn(animationSpec = tween(500, easing = easing)),
                 exit = fadeOut(animationSpec = tween(300, easing = easingOut)),
             ) {
-                Box(modifier = Modifier.size(size = 20.dp).background(Color.White))
+                Box(
+                    modifier = Modifier
+                        .size(size = 20.dp)
+                        .background(Color.White)
+                )
                 LaunchedEffect(visible) {
                     var exit = false
                     val enterExit = transition
@@ -421,7 +430,11 @@
                 enter = scaleIn(animationSpec = tween(500, easing = easing)),
                 exit = scaleOut(animationSpec = tween(300, easing = easingOut)),
             ) {
-                Box(modifier = Modifier.size(size = 20.dp).background(Color.White))
+                Box(
+                    modifier = Modifier
+                        .size(size = 20.dp)
+                        .background(Color.White)
+                )
                 LaunchedEffect(visible) {
                     var exit = false
                     val enterExit = transition
@@ -544,9 +557,11 @@
                 transition.AnimatedVisibility(
                     // Only visible in State2
                     visible = { it == TestState.State2 },
-                    modifier = testModifier,
-                    enter = expandIn(animationSpec = tween(100)),
-                    exit = shrinkOut(animationSpec = tween(100))
+                    modifier = testModifier.testTag("content"),
+                    // Must use LinearEasing otherwise the target size will be reached before the
+                    // animation finishes running.
+                    enter = expandIn(animationSpec = tween(100, easing = LinearEasing)),
+                    exit = shrinkOut(animationSpec = tween(100, easing = LinearEasing))
                 ) {
                     Box(Modifier.requiredSize(100.dp, 100.dp)) {
                         DisposableEffect(Unit) {
@@ -559,29 +574,30 @@
             }
         }
         rule.runOnIdle {
-            assertEquals(0, testModifier.width)
-            assertEquals(0, testModifier.height)
+            assertThat(testModifier.width).isEqualTo(0)
+            assertThat(testModifier.height).isEqualTo(0)
             testState.value = TestState.State2
         }
         while (currentState != TestState.State2) {
-            assertTrue(testModifier.width < 100)
+            assertThat(testModifier.width).isLessThan(100)
             rule.mainClock.advanceTimeByFrame()
         }
         rule.runOnIdle {
-            assertEquals(100, testModifier.width)
-            assertEquals(100, testModifier.height)
+            assertThat(testModifier.width).isEqualTo(100)
+            assertThat(testModifier.height).isEqualTo(100)
             testState.value = TestState.State3
         }
         while (currentState != TestState.State3) {
-            assertTrue(testModifier.width > 0)
-            assertFalse(disposed)
+            assertThat(testModifier.width).isGreaterThan(0)
+            rule.onNodeWithTag("content").assertExists()
+            assertThat(disposed).isFalse()
             rule.mainClock.advanceTimeByFrame()
         }
-        rule.mainClock.advanceTimeByFrame()
+        // When the hide animation finishes, it will never get measured with size 0 because the
+        // animation will remove it from the composition instead.
+        rule.onNodeWithTag("content").assertDoesNotExist()
         rule.runOnIdle {
-            assertEquals(0, testModifier.width)
-            assertEquals(0, testModifier.height)
-            assertTrue(disposed)
+            assertThat(disposed).isTrue()
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
index a3a9ef1..2293c1a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -47,6 +47,7 @@
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.IterableSubject
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -147,6 +148,7 @@
         )
     }
 
+    @SdkSuppress(minSdkVersion = 29) // b/260010883
     @Test
     fun verifyScrollActionsAtEnd() {
         createScrollableContent_StartAtEnd()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
index 87f9928..a29edf6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollAccessibilityTest.kt
@@ -54,6 +54,7 @@
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.IterableSubject
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -154,6 +155,7 @@
         )
     }
 
+    @SdkSuppress(minSdkVersion = 29) // b/260011449
     @Test
     fun verifyScrollActionsAtEnd() {
         createScrollableContent_StartAtEnd()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
new file mode 100644
index 0000000..992c360
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/BasePagerTest.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+
+@OptIn(ExperimentalFoundationApi::class)
+internal open class BasePagerTest(private val config: ParamConfig) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val isVertical = config.orientation == Orientation.Vertical
+    lateinit var scope: CoroutineScope
+    var pagerSize: Int = 0
+    var placed = mutableSetOf<Int>()
+    var pageSize: Int = 0
+
+    @Stable
+    fun Modifier.crossAxisSize(size: Dp) =
+        if (isVertical) {
+            this.width(size)
+        } else {
+            this.height(size)
+        }
+
+    fun TouchInjectionScope.swipeWithVelocityAcrossMainAxis(velocity: Float, delta: Float? = null) {
+        val end = if (delta == null) {
+            layoutEnd
+        } else {
+            if (isVertical) {
+                layoutStart.copy(y = layoutStart.y + delta)
+            } else {
+                layoutStart.copy(x = layoutStart.x + delta)
+            }
+        }
+        swipeWithVelocity(layoutStart, end, velocity)
+    }
+
+    internal fun createPager(
+        state: PagerState,
+        modifier: Modifier = Modifier,
+        pagerCount: () -> Int = { DefaultPageCount },
+        offscreenPageLimit: Int = 0,
+        pageSize: PageSize = PageSize.Fill,
+        userScrollEnabled: Boolean = true,
+        snappingPage: PagerSnapDistance = PagerSnapDistance.atMost(1),
+        effects: @Composable () -> Unit = {}
+    ) {
+        rule.setContent {
+            val flingBehavior =
+                PagerDefaults.flingBehavior(
+                    state = state,
+                    pagerSnapDistance = snappingPage
+                )
+            CompositionLocalProvider(LocalLayoutDirection provides config.layoutDirection) {
+                scope = rememberCoroutineScope()
+                HorizontalOrVerticalPager(
+                    pageCount = pagerCount(),
+                    state = state,
+                    beyondBoundsPageCount = offscreenPageLimit,
+                    modifier = modifier
+                        .testTag(PagerTestTag)
+                        .onSizeChanged { pagerSize = if (isVertical) it.height else it.width },
+                    pageSize = pageSize,
+                    userScrollEnabled = userScrollEnabled,
+                    reverseLayout = config.reverseLayout,
+                    flingBehavior = flingBehavior,
+                    pageSpacing = config.pageSpacing,
+                    contentPadding = config.mainAxisContentPadding
+                ) {
+                    Page(index = it)
+                }
+                effects()
+            }
+        }
+    }
+
+    @Composable
+    internal fun Page(index: Int) {
+        Box(modifier = Modifier
+            .onPlaced {
+                placed.add(index)
+                pageSize = if (isVertical) it.size.height else it.size.width
+            }
+            .fillMaxSize()
+            .background(Color.Blue)
+            .testTag("$index"),
+            contentAlignment = Alignment.Center) {
+            BasicText(text = index.toString())
+        }
+    }
+
+    internal fun onPager(): SemanticsNodeInteraction {
+        return rule.onNodeWithTag(PagerTestTag)
+    }
+
+    internal val scrollForwardSign: Int
+        get() = if (isVertical) {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                1
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                -1
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                1
+            } else {
+                -1
+            }
+        } else {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                -1
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                1
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                1
+            } else {
+                -1
+            }
+        }
+
+    internal val TouchInjectionScope.layoutStart: Offset
+        get() = if (isVertical) {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                topCenter
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                bottomCenter
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                topCenter
+            } else {
+                bottomCenter
+            }
+        } else {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                centerRight
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                centerLeft
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                centerLeft
+            } else {
+                centerRight
+            }
+        }
+
+    internal val TouchInjectionScope.layoutEnd: Offset
+        get() = if (isVertical) {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                bottomCenter
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                topCenter
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                bottomCenter
+            } else {
+                topCenter
+            }
+        } else {
+            if (config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                centerLeft
+            } else if (!config.reverseLayout && config.layoutDirection == LayoutDirection.Rtl) {
+                centerRight
+            } else if (config.reverseLayout && config.layoutDirection == LayoutDirection.Ltr) {
+                centerRight
+            } else {
+                centerLeft
+            }
+        }
+
+    @OptIn(ExperimentalFoundationApi::class)
+    @Composable
+    internal fun HorizontalOrVerticalPager(
+        pageCount: Int,
+        state: PagerState = rememberPagerState(),
+        modifier: Modifier = Modifier,
+        userScrollEnabled: Boolean = true,
+        reverseLayout: Boolean = false,
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        beyondBoundsPageCount: Int = 0,
+        pageSize: PageSize = PageSize.Fill,
+        flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
+        pageSpacing: Dp = 0.dp,
+        pageContent: @Composable (pager: Int) -> Unit
+    ) {
+        if (isVertical) {
+            VerticalPager(
+                pageCount = pageCount,
+                state = state,
+                modifier = modifier,
+                userScrollEnabled = userScrollEnabled,
+                reverseLayout = reverseLayout,
+                contentPadding = contentPadding,
+                beyondBoundsPageCount = beyondBoundsPageCount,
+                pageSize = pageSize,
+                flingBehavior = flingBehavior,
+                pageSpacing = pageSpacing,
+                pageContent = pageContent
+            )
+        } else {
+            HorizontalPager(
+                pageCount = pageCount,
+                state = state,
+                modifier = modifier,
+                userScrollEnabled = userScrollEnabled,
+                reverseLayout = reverseLayout,
+                contentPadding = contentPadding,
+                beyondBoundsPageCount = beyondBoundsPageCount,
+                pageSize = pageSize,
+                flingBehavior = flingBehavior,
+                pageSpacing = pageSpacing,
+                pageContent = pageContent
+            )
+        }
+    }
+
+    internal fun confirmPageIsInCorrectPosition(
+        currentPageIndex: Int,
+        pageToVerifyPosition: Int = currentPageIndex
+    ) {
+        val leftContentPadding =
+            config.mainAxisContentPadding.calculateLeftPadding(config.layoutDirection)
+        val topContentPadding = config.mainAxisContentPadding.calculateTopPadding()
+
+        val (left, top) = with(rule.density) {
+            val spacings = config.pageSpacing.roundToPx()
+            val initialPageOffset = currentPageIndex * (pageSize + spacings)
+
+            val position = pageToVerifyPosition * (pageSize + spacings) - initialPageOffset
+            if (isVertical) {
+                0.dp to position.toDp()
+            } else {
+                position.toDp() to 0.dp
+            }
+        }
+        rule.onNodeWithTag("$pageToVerifyPosition")
+            .assertPositionInRootIsEqualTo(left + leftContentPadding, top + topContentPadding)
+    }
+}
+
+internal class ParamConfig(
+    val orientation: Orientation,
+    val reverseLayout: Boolean = false,
+    val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+    val pageSpacing: Dp = 0.dp,
+    val mainAxisContentPadding: PaddingValues = PaddingValues(0.dp)
+) {
+    override fun toString(): String {
+        return "orientation=$orientation " +
+            "reverseLayout=$reverseLayout " +
+            "layoutDirection=$layoutDirection " +
+            "pageSpacing=$pageSpacing " +
+            "mainAxisContentPadding=$mainAxisContentPadding"
+    }
+}
+
+internal const val PagerTestTag = "pager"
+internal const val DefaultPageCount = 20
+internal val TestOrientation = listOf(Orientation.Vertical, Orientation.Horizontal)
+internal val TestReverseLayout = listOf(false, true)
+internal val TestLayoutDirection = listOf(LayoutDirection.Rtl, LayoutDirection.Ltr)
+internal val TestPageSpacing = listOf(0.dp, 8.dp)
+internal fun testContentPaddings(orientation: Orientation) = listOf(
+    PaddingValues(0.dp),
+    if (orientation == Orientation.Vertical)
+        PaddingValues(vertical = 16.dp)
+    else PaddingValues(horizontal = 16.dp),
+    PaddingValues(start = 16.dp),
+    PaddingValues(end = 16.dp)
+)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
new file mode 100644
index 0000000..776098c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/EmptyPagerTests.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class EmptyPagerTests(val config: ParamConfig) : BasePagerTest(config) {
+
+    @Test
+    fun checkNoPagesArePlaced() {
+        // Arrange
+        val state = PagerState()
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize(), pagerCount = { 0 })
+
+        // Assert
+        rule.onNodeWithTag("0").assertDoesNotExist()
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                add(ParamConfig(orientation = orientation))
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
new file mode 100644
index 0000000..9f13bea
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerCrossAxisTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.test.filters.LargeTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerCrossAxisTest(val config: ParamConfig) : BasePagerTest(config) {
+
+    @Test
+    fun pagerOnInfiniteCrossAxisLayout_shouldWrapContentSize() {
+        // Arrange
+        rule.setContent {
+            InfiniteAxisRootComposable {
+                HorizontalOrVerticalPager(
+                    pageCount = DefaultPageCount,
+                    state = rememberPagerState(),
+                    modifier = Modifier
+                        .fillMaxHeight()
+                        .fillMaxWidth()
+                        .testTag(PagerTestTag),
+                ) {
+                    val fillModifier = if (isVertical) {
+                        Modifier
+                            .fillMaxHeight()
+                            .width(200.dp)
+                    } else {
+                        Modifier
+                            .fillMaxWidth()
+                            .height(200.dp)
+                    }
+                    Box(fillModifier)
+                }
+            }
+        }
+
+        // Act
+        val rootBounds = rule.onRoot().getUnclippedBoundsInRoot()
+
+        // Assert: Max Cross Axis size is handled well by wrapping content
+        if (isVertical) {
+            rule.onNodeWithTag(PagerTestTag)
+                .assertHeightIsEqualTo(rootBounds.height)
+                .assertWidthIsEqualTo(200.dp)
+        } else {
+            rule.onNodeWithTag(PagerTestTag)
+                .assertWidthIsEqualTo(rootBounds.width)
+                .assertHeightIsEqualTo(200.dp)
+        }
+    }
+
+    @Composable
+    private fun InfiniteAxisRootComposable(content: @Composable () -> Unit) {
+        if (isVertical) {
+            Row(Modifier.horizontalScroll(rememberScrollState())) {
+                content()
+            }
+        } else {
+            Column(Modifier.verticalScroll(rememberScrollState())) {
+                content()
+            }
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                add(ParamConfig(orientation = orientation))
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
new file mode 100644
index 0000000..6e02eab
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerScrollingTest.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.MinFlingVelocityDp
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerScrollingTest(
+    val config: ParamConfig
+) : BasePagerTest(config) {
+
+    @Test
+    fun swipePageTowardsEdge_shouldNotMove() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - backward
+        rule.onNodeWithTag("0").performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                delta * -1.0f
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("0").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(0)
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                delta
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("1").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(1)
+    }
+
+    @Test
+    fun swipeAllTheWay_verifyPagesAreLayedOutCorrectly() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act and Assert - forward
+        repeat(DefaultPageCount) {
+            rule.onNodeWithTag(it.toString()).assertIsDisplayed()
+            confirmPageIsInCorrectPosition(it)
+            rule.onNodeWithTag(it.toString()).performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                    delta
+                )
+            }
+            rule.waitForIdle()
+        }
+
+        // Act - backward
+        repeat(DefaultPageCount) {
+            val countDown = DefaultPageCount - 1 - it
+            rule.onNodeWithTag(countDown.toString()).assertIsDisplayed()
+            confirmPageIsInCorrectPosition(countDown)
+            rule.onNodeWithTag(countDown.toString()).performTouchInput {
+                swipeWithVelocityAcrossMainAxis(
+                    with(rule.density) { 1.5f * MinFlingVelocityDp.toPx() },
+                    delta * -1f
+                )
+            }
+            rule.waitForIdle()
+        }
+    }
+
+    @Test
+    fun swipeWithLowVelocity_shouldBounceBack() {
+        // Arrange
+        val state = PagerState(5)
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                delta
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+
+        // Act - backward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 0.5f * MinFlingVelocityDp.toPx() },
+                delta * -1
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun swipeWithHighVelocity_shouldGoToNextPage() {
+        // Arrange
+        val state = PagerState(5)
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        // make sure the scroll distance is not enough to go to next page
+        val delta = pagerSize * 0.4f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                delta
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("6").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(6)
+
+        // Act - backward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(
+                with(rule.density) { 1.1f * MinFlingVelocityDp.toPx() },
+                delta * -1
+            )
+        }
+        rule.waitForIdle()
+
+        // Assert
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun scrollWithoutVelocity_shouldSettlingInClosestPage() {
+        // Arrange
+        val state = PagerState(5)
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        // This will scroll 1 whole page before flinging
+        val delta = pagerSize * 1.4f * scrollForwardSign
+
+        // Act - forward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(0f, delta)
+        }
+        rule.waitForIdle()
+
+        // Assert
+        assertThat(state.currentPage).isAtMost(7)
+        rule.onNodeWithTag("${state.currentPage}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(state.currentPage)
+
+        // Act - backward
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(0f, delta * -1)
+        }
+        rule.waitForIdle()
+
+        // Assert
+        assertThat(state.currentPage).isAtLeast(5)
+        rule.onNodeWithTag("${state.currentPage}").assertIsDisplayed()
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun offscreenPageLimitIsUsed_shouldPlaceMoreItemsThanVisibleOnesAsWeScroll() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 1)
+        val delta = pagerSize * 1.4f * scrollForwardSign
+
+        repeat(DefaultPageCount) {
+            // Act
+            onPager().performTouchInput {
+                swipeWithVelocityAcrossMainAxis(0f, delta)
+            }
+
+            rule.waitForIdle()
+            // Next page was placed
+            rule.runOnIdle {
+                assertThat(placed).contains(
+                    (state.currentPage + 1)
+                        .coerceAtMost(DefaultPageCount - 1)
+                )
+            }
+        }
+        rule.waitForIdle()
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (reverseLayout in TestReverseLayout) {
+                    for (layoutDirection in TestLayoutDirection) {
+                        for (pageSpacing in TestPageSpacing) {
+                            for (contentPadding in testContentPaddings(orientation)) {
+                                add(
+                                    ParamConfig(
+                                        orientation = orientation,
+                                        reverseLayout = reverseLayout,
+                                        layoutDirection = layoutDirection,
+                                        pageSpacing = pageSpacing,
+                                        mainAxisContentPadding = contentPadding
+                                    )
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
new file mode 100644
index 0000000..e302c0c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerStateTest.kt
@@ -0,0 +1,530 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerStateTest(val config: ParamConfig) : BasePagerTest(config) {
+
+    @Test
+    fun scrollToPage_shouldPlacePagesCorrectly() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act and Assert
+        repeat(DefaultPageCount) {
+            assertThat(state.currentPage).isEqualTo(it)
+            rule.runOnIdle {
+                scope.launch {
+                    state.scrollToPage(state.currentPage + 1)
+                }
+            }
+            confirmPageIsInCorrectPosition(state.currentPage)
+        }
+    }
+
+    @Test
+    fun scrollToPage_longSkipShouldNotPlaceIntermediatePages() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(state.currentPage).isEqualTo(0)
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(DefaultPageCount - 1)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+        }
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun animateScrollToPage_shouldPlacePagesCorrectly() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act and Assert
+        repeat(DefaultPageCount) {
+            assertThat(state.currentPage).isEqualTo(it)
+            rule.runOnIdle {
+                scope.launch {
+                    state.animateScrollToPage(state.currentPage + 1)
+                }
+            }
+            rule.waitForIdle()
+            confirmPageIsInCorrectPosition(state.currentPage)
+        }
+    }
+
+    @Test
+    fun animateScrollToPage_longSkipShouldNotPlaceIntermediatePages() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(state.currentPage).isEqualTo(0)
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 - 1)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2)
+            assertThat(placed).doesNotContain(DefaultPageCount / 2 + 1)
+        }
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun scrollToPage_shouldCoerceWithinRange() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(state.currentPage).isEqualTo(0)
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(DefaultPageCount)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+        // Act
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(-1)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(state.currentPage).isEqualTo(0) }
+    }
+
+    @Test
+    fun animateScrollToPage_shouldCoerceWithinRange() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Act
+        assertThat(state.currentPage).isEqualTo(0)
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(DefaultPageCount)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(state.currentPage).isEqualTo(DefaultPageCount - 1) }
+
+        // Act
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(-1)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle { assertThat(state.currentPage).isEqualTo(0) }
+    }
+
+    @Test
+    fun animateScrollToPage_withPassedAnimation() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        val differentAnimation: AnimationSpec<Float> = tween()
+
+        // Act and Assert
+        repeat(DefaultPageCount) {
+            assertThat(state.currentPage).isEqualTo(it)
+            rule.runOnIdle {
+                scope.launch {
+                    state.animateScrollToPage(state.currentPage + 1, differentAnimation)
+                }
+            }
+            rule.waitForIdle()
+            confirmPageIsInCorrectPosition(state.currentPage)
+        }
+    }
+
+    @Test
+    fun currentPage_shouldChangeWhenClosestPageToSnappedPositionChanges() {
+        // Arrange
+        val state = PagerState()
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+        var previousCurrentPage = state.currentPage
+
+        // Act
+        // Move less than half an item
+        val firstDelta = (pagerSize * 0.4f) * scrollForwardSign
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (isVertical) {
+                moveBy(Offset(0f, firstDelta))
+            } else {
+                moveBy(Offset(firstDelta, 0f))
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(previousCurrentPage)
+        }
+        // Release pointer
+        onPager().performTouchInput { up() }
+
+        rule.runOnIdle {
+            previousCurrentPage = state.currentPage
+        }
+        confirmPageIsInCorrectPosition(state.currentPage)
+
+        // Arrange
+        // Pass closest to snap position threshold (over half an item)
+        val secondDelta = (pagerSize * 0.6f) * scrollForwardSign
+
+        // Act
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (isVertical) {
+                moveBy(Offset(0f, secondDelta))
+            } else {
+                moveBy(Offset(secondDelta, 0f))
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(previousCurrentPage + 1)
+        }
+
+        onPager().performTouchInput { up() }
+        rule.waitForIdle()
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun targetPage_performScroll_shouldShowNextPage() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        val forwardDelta = pagerSize * 0.4f * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            down(layoutStart)
+            moveBy(Offset(forwardDelta, forwardDelta))
+        }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(state.currentPage + 1)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        // Reset
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+        rule.runOnIdle {
+            runBlocking { state.scrollToPage(5) }
+        }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving backward
+        val backwardDelta = -pagerSize * 0.4f * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            down(layoutEnd)
+            moveBy(Offset(backwardDelta, backwardDelta))
+        }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(state.currentPage - 1)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        onPager().performTouchInput { up() }
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+    }
+
+    @Test
+    fun targetPage_performingFling_shouldGoToPredictedPage() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            snappingPage = PagerSnapDistance.atMost(3)
+        )
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        var previousTarget = state.targetPage
+        val forwardDelta = pagerSize * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(20000f, forwardDelta)
+        }
+        rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(state.currentPage + 3)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving backward
+        previousTarget = state.targetPage
+        val backwardDelta = -pagerSize * scrollForwardSign.toFloat()
+        onPager().performTouchInput {
+            swipeWithVelocityAcrossMainAxis(20000f, backwardDelta)
+        }
+        rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(state.currentPage - 3)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+    }
+
+    @Test
+    fun targetPage_shouldReflectTargetWithAnimation() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize()
+        )
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+
+        rule.mainClock.autoAdvance = false
+        // Act
+        // Moving forward
+        var previousTarget = state.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(DefaultPageCount - 1)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(DefaultPageCount - 1)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+        rule.mainClock.autoAdvance = false
+
+        // Act
+        // Moving backward
+        previousTarget = state.targetPage
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(0)
+            }
+        }
+        rule.mainClock.advanceTimeUntil { state.targetPage != previousTarget }
+
+        // Assert
+        assertThat(state.targetPage).isEqualTo(0)
+        assertThat(state.targetPage).isNotEqualTo(state.currentPage)
+
+        rule.mainClock.autoAdvance = true
+        rule.runOnIdle { assertThat(state.targetPage).isEqualTo(state.currentPage) }
+    }
+
+    @Test
+    fun currentPageOffset_shouldReflectScrollingOfCurrentPage() {
+        // Arrange
+        val state = PagerState(DefaultPageCount / 2)
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // No offset initially
+        rule.runOnIdle {
+            assertThat(state.currentPageOffset).isWithin(0.01f).of(0f)
+        }
+
+        // Act
+        // Moving forward
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (isVertical) {
+                moveBy(Offset(0f, scrollForwardSign * pagerSize / 4f))
+            } else {
+                moveBy(Offset(scrollForwardSign * pagerSize / 4f, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.currentPageOffset).isWithin(0.1f).of(0.25f)
+        }
+
+        onPager().performTouchInput { up() }
+        rule.waitForIdle()
+
+        // Reset
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(DefaultPageCount / 2)
+            }
+        }
+
+        // No offset initially (again)
+        rule.runOnIdle {
+            assertThat(state.currentPageOffset).isWithin(0.01f).of(0f)
+        }
+
+        // Act
+        // Moving backward
+        onPager().performTouchInput {
+            down(layoutStart)
+            if (isVertical) {
+                moveBy(Offset(0f, -scrollForwardSign * pagerSize / 4f))
+            } else {
+                moveBy(Offset(-scrollForwardSign * pagerSize / 4f, 0f))
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.currentPageOffset).isWithin(0.1f).of(-0.25f)
+        }
+    }
+
+    @Test
+    fun initialPageOnPagerState_shouldDisplayThatPageFirst() {
+        // Arrange
+        val state = PagerState(5)
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Assert
+        rule.onNodeWithTag("4").assertDoesNotExist()
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        rule.onNodeWithTag("6").assertDoesNotExist()
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun testStateRestoration() {
+        // Arrange
+        val tester = StateRestorationTester(rule)
+        val state = PagerState()
+        tester.setContent {
+            scope = rememberCoroutineScope()
+            HorizontalOrVerticalPager(
+                state = state,
+                pageCount = DefaultPageCount,
+                modifier = Modifier.fillMaxSize()
+            ) {
+                Page(it)
+            }
+        }
+
+        // Act
+        rule.runOnIdle {
+            scope.launch {
+                state.scrollToPage(5)
+            }
+            runBlocking {
+                state.scroll {
+                    scrollBy(50f)
+                }
+            }
+        }
+
+        // TODO(levima) Update to assert offset as well
+        val previousPage = state.currentPage
+        tester.emulateSavedInstanceStateRestore()
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(previousPage)
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (reverseLayout in TestReverseLayout) {
+                    for (layoutDirection in TestLayoutDirection) {
+                        add(
+                            ParamConfig(
+                                orientation = orientation,
+                                reverseLayout = reverseLayout,
+                                layoutDirection = layoutDirection
+                            )
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
new file mode 100644
index 0000000..e88a163
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/pager/PagerTest.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2022 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.compose.foundation.pager
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalFoundationApi::class)
+@LargeTest
+@RunWith(Parameterized::class)
+internal class PagerTest(val config: ParamConfig) : BasePagerTest(config) {
+
+    @Before
+    fun setUp() {
+        placed.clear()
+    }
+
+    @Test
+    fun userScrollEnabledIsOff_shouldNotAllowGestureScroll() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            userScrollEnabled = false,
+            modifier = Modifier.fillMaxSize()
+        )
+
+        // Act
+        onPager().performTouchInput { swipeWithVelocityAcrossMainAxis(1000f) }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(0)
+        }
+
+        confirmPageIsInCorrectPosition(0, 0)
+    }
+
+    @Test
+    fun userScrollEnabledIsOff_shouldAllowAnimationScroll() {
+        // Arrange
+        val state = PagerState()
+        createPager(
+            state = state,
+            userScrollEnabled = false,
+            modifier = Modifier.fillMaxSize()
+        )
+
+        // Act
+        rule.runOnIdle {
+            scope.launch {
+                state.animateScrollToPage(5)
+            }
+        }
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(state.currentPage).isEqualTo(5)
+        }
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun userScrollEnabledIsOn_shouldAllowGestureScroll() {
+        // Arrange
+        val state = PagerState(5)
+        createPager(
+            state = state,
+            userScrollEnabled = true,
+            modifier = Modifier.fillMaxSize()
+        )
+
+        onPager().performTouchInput { swipeWithVelocityAcrossMainAxis(1000f) }
+
+        rule.runOnIdle {
+            assertThat(state.currentPage).isNotEqualTo(5)
+        }
+        confirmPageIsInCorrectPosition(state.currentPage)
+    }
+
+    @Test
+    fun pageSizeFill_onlySnappedItemIsDisplayed() {
+        // Arrange
+        val state = PagerState(5)
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Assert
+        rule.onNodeWithTag("4").assertDoesNotExist()
+        rule.onNodeWithTag("5").assertIsDisplayed()
+        rule.onNodeWithTag("6").assertDoesNotExist()
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun pagerSizeCustom_visibleItemsAreWithinViewport() {
+        // Arrange
+        val state = PagerState(5)
+        val pagerMode = object : PageSize {
+            override fun Density.calculateMainAxisPageSize(
+                availableSpace: Int,
+                pageSpacing: Int
+            ): Int {
+                return 100.dp.roundToPx() + pageSpacing
+            }
+        }
+
+        // Act
+        createPager(
+            state = state,
+            modifier = Modifier.crossAxisSize(200.dp),
+            offscreenPageLimit = 0,
+            pageSize = pagerMode
+        )
+
+        // Assert
+        rule.runOnIdle {
+            val visibleItems = state.lazyListState.layoutInfo.visibleItemsInfo.size
+            val pageCount = with(rule.density) {
+                (pagerSize / (pageSize + config.pageSpacing.roundToPx()))
+            } + 1
+            assertThat(visibleItems).isEqualTo(pageCount)
+        }
+
+        for (pageIndex in 5 until state.lazyListState.layoutInfo.visibleItemsInfo.size + 4) {
+            confirmPageIsInCorrectPosition(5, pageIndex)
+        }
+    }
+
+    @Test
+    fun offscreenPageLimitIsUsed_shouldPlaceMoreItemsThanVisibleOnes() {
+        // Arrange
+        val initialIndex = 5
+        val state = PagerState(initialIndex)
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 2)
+
+        // Assert
+        rule.runOnIdle {
+            assertThat(placed).contains(initialIndex - 2)
+            assertThat(placed).contains(initialIndex - 1)
+            assertThat(placed).contains(initialIndex + 1)
+            assertThat(placed).contains(initialIndex + 2)
+        }
+        confirmPageIsInCorrectPosition(initialIndex, initialIndex - 2)
+        confirmPageIsInCorrectPosition(initialIndex, initialIndex - 1)
+        confirmPageIsInCorrectPosition(initialIndex, initialIndex + 1)
+        confirmPageIsInCorrectPosition(initialIndex, initialIndex + 2)
+    }
+
+    @Test
+    fun offscreenPageLimitIsNotUsed_shouldNotPlaceMoreItemsThanVisibleOnes() {
+        // Arrange
+        val state = PagerState(5)
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize(), offscreenPageLimit = 0)
+
+        // Assert
+        rule.waitForIdle()
+        assertThat(placed).doesNotContain(4)
+        assertThat(placed).contains(5)
+        assertThat(placed).doesNotContain(6)
+        confirmPageIsInCorrectPosition(5)
+    }
+
+    @Test
+    fun pageCount_pagerOnlyContainsGivenPageCountItems() {
+        // Arrange
+        val state = PagerState()
+
+        // Act
+        createPager(state = state, modifier = Modifier.fillMaxSize())
+
+        // Assert
+        repeat(DefaultPageCount) {
+            rule.onNodeWithTag("$it").assertIsDisplayed()
+            rule.runOnIdle {
+                scope.launch {
+                    state.scroll {
+                        scrollBy(pagerSize.toFloat())
+                    }
+                }
+            }
+            rule.waitForIdle()
+        }
+        rule.onNodeWithTag("$DefaultPageCount").assertDoesNotExist()
+    }
+
+    @Test
+    fun mutablePageCount_assertPagesAreChangedIfCountIsChanged() {
+        // Arrange
+        val state = PagerState()
+        val pageCount = mutableStateOf(2)
+        createPager(
+            state = state,
+            modifier = Modifier.fillMaxSize(),
+            pagerCount = { pageCount.value }
+        )
+
+        rule.onNodeWithTag("3").assertDoesNotExist()
+
+        // Act
+        pageCount.value = DefaultPageCount
+        rule.waitForIdle()
+
+        // Assert
+        repeat(DefaultPageCount) {
+            rule.onNodeWithTag("$it").assertIsDisplayed()
+            rule.runOnIdle {
+                scope.launch {
+                    state.scroll {
+                        scrollBy(pagerSize.toFloat())
+                    }
+                }
+            }
+            rule.waitForIdle()
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun params() = mutableListOf<ParamConfig>().apply {
+            for (orientation in TestOrientation) {
+                for (pageSpacing in TestPageSpacing) {
+                    add(
+                        ParamConfig(
+                            orientation = orientation,
+                            pageSpacing = pageSpacing
+                        )
+                    )
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index 42fab8f..b28e202 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
 import androidx.compose.foundation.layout.Arrangement
@@ -205,6 +206,10 @@
         )
     }
 
+    val pagerFlingBehavior = remember(flingBehavior, state) {
+        PagerWrapperFlingBehavior(flingBehavior, state)
+    }
+
     LaunchedEffect(density, state, pageSpacing) {
         with(density) { state.pageSpacing = pageSpacing.roundToPx() }
     }
@@ -246,7 +251,7 @@
             modifier = Modifier,
             state = state.lazyListState,
             contentPadding = contentPadding,
-            flingBehavior = flingBehavior,
+            flingBehavior = pagerFlingBehavior,
             horizontalAlignment = horizontalAlignment,
             horizontalArrangement = Arrangement.spacedBy(
                 pageSpacing,
@@ -497,4 +502,18 @@
             return (finalOffset - initialOffset)
         }
     }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class PagerWrapperFlingBehavior(
+    val originalFlingBehavior: SnapFlingBehavior,
+    val pagerState: PagerState
+) : FlingBehavior {
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        return with(originalFlingBehavior) {
+            performFling(initialVelocity) { remainingScrollOffset ->
+                pagerState.snapRemainingScrollOffset = remainingScrollOffset
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 2e89671..fe9416d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -37,8 +37,11 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastMaxBy
 import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlin.math.sign
 
 /**
  * Creates and remember a [PagerState] to be used with a [Pager]
@@ -65,6 +68,8 @@
     initialPageOffset: Int = 0
 ) : ScrollableState {
 
+    internal var snapRemainingScrollOffset by mutableStateOf(0f)
+
     internal val lazyListState = LazyListState(initialPage, initialPageOffset)
 
     internal var pageSpacing by mutableStateOf(0)
@@ -78,6 +83,16 @@
     private val pageAvailableSpace: Int
         get() = pageSize + pageSpacing
 
+    /**
+     * How far the current page needs to scroll so the target page is considered to be the next
+     * page.
+     */
+    private val positionThresholdFraction: Float
+        get() = with(lazyListState.density) {
+            val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f)
+            minThreshold / pageSize.toFloat()
+        }
+
     internal val pageCount: Int
         get() = lazyListState.layoutInfo.totalItemsCount
 
@@ -113,6 +128,8 @@
      */
     val currentPage: Int by derivedStateOf { closestPageToSnappedPosition?.index ?: 0 }
 
+    private var animationTargetPage by mutableStateOf(-1)
+
     private var settledPageState by mutableStateOf(initialPage)
 
     /**
@@ -125,6 +142,31 @@
     }
 
     /**
+     * The page this [Pager] intends to settle to.
+     * During fling or animated scroll (from [animateScrollToPage] this will represent the page
+     * this pager intends to settle to. When no scroll is ongoing, this will be equal to
+     * [currentPage].
+     */
+    val targetPage: Int by derivedStateOf {
+        if (!isScrollInProgress) {
+            currentPage
+        } else if (animationTargetPage != -1) {
+            animationTargetPage
+        } else {
+            val offsetFromFling = snapRemainingScrollOffset
+            val offsetFromScroll =
+                if (abs(currentPageOffset) >= abs(positionThresholdFraction)) {
+                    (abs(currentPageOffset) + 1) * pageAvailableSpace * currentPageOffset.sign
+                } else {
+                    0f
+                }
+            val pageDisplacement =
+                (offsetFromFling + offsetFromScroll).roundToInt() / pageAvailableSpace
+            (currentPage + pageDisplacement).coerceInPageRange()
+        }
+    }
+
+    /**
      * Indicates how far the current page is to the snapped position, this will vary from
      * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
      * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
@@ -162,6 +204,8 @@
     ) {
         if (page == currentPage) return
         var currentPosition = currentPage
+        val targetPage = page.coerceInPageRange()
+        animationTargetPage = targetPage
         // If our future page is too far off, that is, outside of the current viewport
         val firstVisiblePageIndex = visiblePages.first().index
         val lastVisiblePageIndex = visiblePages.last().index
@@ -179,13 +223,13 @@
             currentPosition = preJumpPosition
         }
 
-        val targetPage = page.coerceInPageRange()
         val targetOffset = targetPage * pageAvailableSpace
         val currentOffset = currentPosition * pageAvailableSpace
         val pageOffsetToSnappedPosition = distanceToSnapPosition
 
         val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
         lazyListState.animateScrollBy(displacement, animationSpec)
+        animationTargetPage = -1
     }
 
     override suspend fun scroll(
@@ -238,4 +282,5 @@
 private const val MaxPageOffset = 0.5f
 internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
     { _, _ -> 0f }
-private const val MaxPagesForAnimateScroll = 3
\ No newline at end of file
+private val DefaultPositionThreshold = 56.dp
+private const val MaxPagesForAnimateScroll = 3
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
index c2e3f2c..706b93a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BadgeTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.compose.material
 
-import android.os.Build
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
@@ -115,7 +114,8 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // captureToImage() requires API level 26
+    // @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // captureToImage() requires API level 26
+    @SdkSuppress(minSdkVersion = 28) // b/260004658
     fun badge_noContent_shape() {
         var errorColor = Color.Unspecified
         rule.setMaterialContent {
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
index 2131748..277eb6b 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.tooling.animation.states.TargetState
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.test.filters.SdkSuppress
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Rule
@@ -212,6 +213,7 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = 29) // b/260004689
     @Test
     fun clockWithAnimatedContent() {
         val clock = createBooleanTransitionClockWithAnimatedContent()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index a7d7eba..03d5552 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -391,6 +391,7 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testCameraDistanceWithRotationY() {
+        if (Build.VERSION.SDK_INT == 28) return // b/260095151
         val testTag = "parent"
         rule.setContent {
             Box(modifier = Modifier.testTag(testTag).wrapContentSize()) {
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 2e29520..f85f282 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -44,7 +44,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 33
+        minSdkVersion 31
         multiDexEnabled = true
     }
 
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 24a6baa..d79149c 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -55,7 +55,7 @@
 android {
     namespace "androidx.core.uwb"
     defaultConfig {
-        minSdkVersion 33
+        minSdkVersion 31
         multiDexEnabled = true
     }
 }
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
index 2094c06..b946a89 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -132,7 +132,6 @@
                 {
                     androidx.core.uwb.backend.UwbDevice _arg0;
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
-                    data.enforceNoDataAvail();
                     this.onRangingInitialized(_arg0);
                     break;
                 }
@@ -142,7 +141,6 @@
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
                     androidx.core.uwb.backend.RangingPosition _arg1;
                     _arg1 = data.readTypedObject(androidx.core.uwb.backend.RangingPosition.CREATOR);
-                    data.enforceNoDataAvail();
                     this.onRangingResult(_arg0, _arg1);
                     break;
                 }
@@ -152,7 +150,6 @@
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
                     int _arg1;
                     _arg1 = data.readInt();
-                    data.enforceNoDataAvail();
                     this.onRangingSuspended(_arg0, _arg1);
                     break;
                 }
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
index e9f53ea..d19d1eb 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -187,7 +187,6 @@
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.RangingParameters.CREATOR);
                     androidx.core.uwb.backend.IRangingSessionCallback _arg1;
                     _arg1 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
-                    data.enforceNoDataAvail();
                     this.startRanging(_arg0, _arg1);
                     reply.writeNoException();
                     break;
@@ -196,7 +195,6 @@
                 {
                     androidx.core.uwb.backend.IRangingSessionCallback _arg0;
                     _arg0 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
-                    data.enforceNoDataAvail();
                     this.stopRanging(_arg0);
                     reply.writeNoException();
                     break;
@@ -205,7 +203,6 @@
                 {
                     androidx.core.uwb.backend.UwbAddress _arg0;
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
-                    data.enforceNoDataAvail();
                     this.addControlee(_arg0);
                     reply.writeNoException();
                     break;
@@ -214,7 +211,6 @@
                 {
                     androidx.core.uwb.backend.UwbAddress _arg0;
                     _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
-                    data.enforceNoDataAvail();
                     this.removeControlee(_arg0);
                     reply.writeNoException();
                     break;
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
index fd59879..3b04bca8 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
@@ -28,12 +28,10 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.credentials.Credential;
 import androidx.credentials.CredentialManagerCallback;
 import androidx.credentials.GetCredentialRequest;
 import androidx.credentials.GetCredentialResponse;
 import androidx.credentials.GetPasswordOption;
-import androidx.credentials.PasswordCredential;
 import androidx.credentials.exceptions.GetCredentialException;
 import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController;
 import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController;
@@ -42,7 +40,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.google.android.gms.auth.api.identity.BeginSignInRequest;
-import com.google.android.gms.auth.api.identity.SignInCredential;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -143,9 +140,9 @@
     public void convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
         ActivityScenario<TestCredentialsActivity> activityScenario =
                 ActivityScenario.launch(TestCredentialsActivity.class);
-        String expectedId = "id";
-        String expectedPassword = "password";
-        String expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL;
+        // TODO add back String expectedId = "id";
+        // TODO add back String expectedPassword = "password";
+        // TODO add back String expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL;
         activityScenario.onActivity(activity -> {
             CredentialProviderBeginSignInController beginSignInController =
                     CredentialProviderBeginSignInController
@@ -163,18 +160,20 @@
             };
             beginSignInController.executor = Runnable::run;
 
+            /** TODO figure out how to test given updated changes
             Credential actualResponse =
                     beginSignInController
                                     .convertResponseToCredentialManager(
                                             new SignInCredential(expectedId, null, null,
                                                     null, null, expectedPassword,
-                                                    null, null)
+                                                    null, null, null)
                                     ).getCredential();
 
             assertThat(actualResponse.getType()).isEqualTo(expectedType);
             assertThat(((PasswordCredential) actualResponse).getPassword())
                     .isEqualTo(expectedPassword);
             assertThat(((PasswordCredential) actualResponse).getId()).isEqualTo(expectedId);
+             */
         });
     }
 
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
index a951af4..4f5a8ce 100644
--- a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
@@ -22,7 +22,6 @@
 import androidx.credentials.GetCredentialRequest
 import androidx.credentials.GetCredentialResponse
 import androidx.credentials.GetPasswordOption
-import androidx.credentials.PasswordCredential
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.playservices.TestUtils.Companion.clearFragmentManager
 import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
@@ -30,7 +29,6 @@
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.google.android.gms.auth.api.identity.SignInCredential
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
 import org.junit.Test
@@ -152,9 +150,9 @@
         val activityScenario = ActivityScenario.launch(
             TestCredentialsActivity::class.java
         )
-        val expectedId = "id"
-        val expectedPassword = "password"
-        val expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
+        // TODO add back val expectedId = "id"
+        // TODO add back val expectedPassword = "password"
+        // TODO add back val expectedType = PasswordCredential.TYPE_PASSWORD_CREDENTIAL
         activityScenario.onActivity { activity: TestCredentialsActivity ->
             val beginSignInController =
                 CredentialProviderBeginSignInController
@@ -168,12 +166,14 @@
             beginSignInController.executor =
                 Executor { obj: Runnable -> obj.run() }
 
+            /**
+             * TODO uncomment once SignInCredential testable solution found outside of Auth 20.3.0
             val actualResponse = beginSignInController
                 .convertResponseToCredentialManager(
                     SignInCredential(
                         expectedId, null, null,
                         null, null, expectedPassword,
-                        null, null
+                        null, null, null
                     )
                 ).credential
 
@@ -182,6 +182,7 @@
                 .isEqualTo(expectedPassword)
             assertThat(actualResponse.id)
                 .isEqualTo(expectedId)
+            */
         }
     }
 
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index a1fecef..cbc3713 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -34,6 +34,7 @@
 import androidx.credentials.exceptions.GetCredentialUnknownException
 import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
 import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
+import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.CredentialProviderCreatePublicKeyCredentialController
 import java.util.concurrent.Executor
 
 /**
@@ -93,7 +94,11 @@
                 callback,
                 executor)
         } else if (request is CreatePublicKeyCredentialRequest) {
-            // TODO("Add in")
+            CredentialProviderCreatePublicKeyCredentialController.getInstance(
+                fragmentManager).invokePlayServices(
+                request,
+                callback,
+                executor)
         } else {
             throw UnsupportedOperationException(
                 "Unsupported request; not password or publickeycredential")
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
index 83f8738..5c242de 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/BeginSignInControllerUtility.kt
@@ -32,8 +32,11 @@
  * limitations under the License.
  */
 
+import android.util.Log
 import androidx.credentials.GetCredentialRequest
 import androidx.credentials.GetPasswordOption
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.playservices.controllers.CreatePublicKeyCredential.PublicKeyCredentialControllerUtility.Companion.convertToPlayAuthPasskeyRequest
 import com.google.android.gms.auth.api.identity.BeginSignInRequest
 
 /**
@@ -44,8 +47,11 @@
 class BeginSignInControllerUtility {
 
     companion object {
+
+        private val TAG = BeginSignInControllerUtility::class.java.name
         internal fun constructBeginSignInRequest(request: GetCredentialRequest):
             BeginSignInRequest {
+            var isPublicKeyCredReqFound = false
             val requestBuilder = BeginSignInRequest.Builder()
             for (option in request.getCredentialOptions) {
                 if (option is GetPasswordOption) {
@@ -54,8 +60,15 @@
                             .setSupported(true)
                             .build()
                     )
+                } else if (option is GetPublicKeyCredentialOption && !isPublicKeyCredReqFound) {
+                    Log.i(TAG, "See request for passkey $option")
+                    requestBuilder.setPasskeysSignInRequestOptions(
+                        convertToPlayAuthPasskeyRequest(option)
+                    )
+                    isPublicKeyCredReqFound = true
+                    // TODO("Confirm logic for single vs multiple options of the same type")
                 }
-                // TODO("Add GoogleIDToken version and passkey version")
+            // TODO("Add GoogleIDToken version")
             }
             return requestBuilder
                 .setAutoSelectEnabled(request.isAutoSelectAllowed)
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
index 21e974f..7307adf 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -28,7 +28,7 @@
 import androidx.credentials.GetCredentialRequest
 import androidx.credentials.GetCredentialResponse
 import androidx.credentials.PasswordCredential
-import androidx.credentials.exceptions.GetCredentialCanceledException
+import androidx.credentials.exceptions.GetCredentialCancellationException
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.exceptions.GetCredentialInterruptedException
 import androidx.credentials.exceptions.GetCredentialUnknownException
@@ -132,7 +132,7 @@
         if (resultCode != Activity.RESULT_OK) {
             var exception: GetCredentialException = GetCredentialUnknownException()
             if (resultCode == Activity.RESULT_CANCELED) {
-                exception = GetCredentialCanceledException()
+                exception = GetCredentialCancellationException()
             }
             this.executor.execute { -> this.callback.onError(exception) }
             return
@@ -149,7 +149,7 @@
             var exception: GetCredentialException = GetCredentialUnknownException()
             if (e.statusCode == CommonStatusCodes.CANCELED) {
                 Log.i(TAG, "User cancelled the prompt!")
-                exception = GetCredentialCanceledException()
+                exception = GetCredentialCancellationException()
             } else if (e.statusCode in this.retryables) {
                 exception = GetCredentialInterruptedException()
             }
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
index 19ff3cf..1232a89 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -27,7 +27,7 @@
 import androidx.credentials.CreatePasswordRequest
 import androidx.credentials.CreatePasswordResponse
 import androidx.credentials.CredentialManagerCallback
-import androidx.credentials.exceptions.CreateCredentialCanceledException
+import androidx.credentials.exceptions.CreateCredentialCancellationException
 import androidx.credentials.exceptions.CreateCredentialException
 import androidx.credentials.exceptions.CreateCredentialInterruptedException
 import androidx.credentials.exceptions.CreateCredentialUnknownException
@@ -126,7 +126,7 @@
         if (resultCode != Activity.RESULT_OK) {
             var exception: CreateCredentialException = CreateCredentialUnknownException()
             if (resultCode == Activity.RESULT_CANCELED) {
-                exception = CreateCredentialCanceledException()
+                exception = CreateCredentialCancellationException()
             }
             this.executor.execute { -> this.callback.onError(exception) }
             return
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
index 204b12f..b8ecc6e 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -16,13 +16,25 @@
 
 package androidx.credentials.playservices.controllers.CreatePublicKeyCredential
 
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.PendingIntent
 import android.content.Intent
+import android.content.IntentSender
+import android.os.Build
 import android.util.Log
 import androidx.credentials.CreateCredentialResponse
 import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePublicKeyCredentialResponse
 import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.exceptions.CreateCredentialCancellationException
 import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
 import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.fido.Fido
 import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
 import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
 import java.util.concurrent.Executor
@@ -52,12 +64,59 @@
      */
     private lateinit var executor: Executor
 
+    @SuppressLint("ClassVerificationFailure")
     override fun invokePlayServices(
         request: CreatePublicKeyCredentialRequest,
         callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
         executor: Executor
     ) {
-        TODO("Not yet implemented")
+        this.callback = callback
+        this.executor = executor
+        val fidoRegistrationRequest: PublicKeyCredentialCreationOptions =
+            this.convertRequestToPlayServices(request)
+        Fido.getFido2ApiClient(getActivity())
+            .getRegisterPendingIntent(fidoRegistrationRequest)
+            .addOnSuccessListener { result: PendingIntent ->
+                try {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                        startIntentSenderForResult(
+                            result.intentSender,
+                            REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL,
+                            null, /* fillInIntent= */
+                            0, /* flagsMask= */
+                            0, /* flagsValue= */
+                            0, /* extraFlags= */
+                            null /* options= */
+                        )
+                    }
+                } catch (e: IntentSender.SendIntentException) {
+                    Log.i(
+                        TAG,
+                        "Failed to send pending intent for fido client " +
+                            " : " + e.message
+                    )
+                    val exception: CreatePublicKeyCredentialException =
+                        CreatePublicKeyCredentialUnknownException()
+                    executor.execute { ->
+                        callback.onError(
+                            exception
+                        )
+                    }
+                }
+            }
+            .addOnFailureListener { e: Exception ->
+                var exception: CreatePublicKeyCredentialException =
+                    CreatePublicKeyCredentialUnknownException()
+                if (e is ApiException && e.statusCode in this.retryables) {
+                    exception = CreatePublicKeyCredentialInterruptedException()
+                }
+                Log.i(TAG, "Fido Registration failed with error: " + e.message)
+                executor.execute { ->
+                    callback.onError(
+                        exception
+                    )
+                }
+            }
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -67,22 +126,49 @@
 
     private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
         Log.i(TAG, "$uniqueRequestCode $resultCode $data")
-        TODO("Not yet implemented")
+        if (uniqueRequestCode != REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL) {
+            return
+        }
+        if (resultCode != Activity.RESULT_OK) {
+            var exception: CreateCredentialException =
+                CreatePublicKeyCredentialUnknownException()
+            if (resultCode == Activity.RESULT_CANCELED) {
+                exception = CreateCredentialCancellationException()
+            }
+            this.executor.execute { -> this.callback.onError(exception) }
+            return
+        }
+        val bytes: ByteArray? = data?.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)
+        if (bytes == null) {
+            this.executor.execute { this.callback.onError(
+                CreatePublicKeyCredentialUnknownException(
+                "Internal error fido module giving null bytes")
+            ) }
+            return
+        }
+        val cred: PublicKeyCredential = PublicKeyCredential.deserializeFromBytes(bytes)
+        if (PublicKeyCredentialControllerUtility.publicKeyCredentialResponseContainsError(
+                this.callback, this.executor, cred)) {
+            return
+        }
+        val response = this.convertResponseToCredentialManager(cred)
+        this.executor.execute { this.callback.onResult(response) }
     }
 
     override fun convertRequestToPlayServices(request: CreatePublicKeyCredentialRequest):
         PublicKeyCredentialCreationOptions {
-        TODO("Not yet implemented")
+        return PublicKeyCredentialControllerUtility.convert(request)
     }
 
     override fun convertResponseToCredentialManager(response: PublicKeyCredential):
         CreateCredentialResponse {
-        TODO("Not yet implemented")
+        return CreatePublicKeyCredentialResponse(PublicKeyCredentialControllerUtility
+            .toCreatePasskeyResponseJson(response))
     }
 
     companion object {
         private val TAG = CredentialProviderCreatePublicKeyCredentialController::class.java.name
-        private const val REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL: Int = 1
+        private const val REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL: Int = 1
         // TODO("Ensure this works with the lifecycle")
 
         /**
@@ -97,12 +183,12 @@
         fun getInstance(fragmentManager: android.app.FragmentManager):
             CredentialProviderCreatePublicKeyCredentialController {
             var controller = findPastController(
-                REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL,
+                REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL,
                 fragmentManager)
             if (controller == null) {
                 controller = CredentialProviderCreatePublicKeyCredentialController()
                 fragmentManager.beginTransaction().add(controller,
-                    REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL.toString())
+                    REQUEST_CODE_GIS_CREATE_PUBLIC_KEY_CREDENTIAL.toString())
                     .commitAllowingStateLoss()
                 fragmentManager.executePendingTransactions()
             }
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
new file mode 100644
index 0000000..dd380f0
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/PublicKeyCredentialControllerUtility.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2022 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.credentials.playservices.controllers.CreatePublicKeyCredential
+
+import android.util.Base64
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.SignInCredential
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse
+import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria
+import com.google.android.gms.fido.fido2.api.common.ErrorCode
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
+import java.util.concurrent.Executor
+import org.json.JSONArray
+import org.json.JSONObject
+
+/**
+ * A utility class to handle logic for the begin sign in controller.
+ *
+ * @hide
+ */
+class PublicKeyCredentialControllerUtility {
+
+    companion object {
+        @JvmStatic
+        fun convert(request: CreatePublicKeyCredentialRequest): PublicKeyCredentialCreationOptions {
+            val requestJson = request.requestJson
+            val json = JSONObject(requestJson)
+            val builder: PublicKeyCredentialCreationOptions.Builder =
+                PublicKeyCredentialCreationOptions.Builder()
+
+            if (json.has("challenge")) {
+                Log.d(TAG, "Set challenge")
+                val challenge: ByteArray =
+                    Base64.decode(json.getString("challenge"), Base64.URL_SAFE)
+                builder.setChallenge(challenge)
+            }
+
+            if (json.has("user")) {
+                Log.d(TAG, "Set user")
+                val user: JSONObject = json.getJSONObject("user")
+                val id: String = user.getString("id")
+                val name: String = user.getString("name")
+                val displayName: String = user.getString("displayName")
+                builder.setUser(
+                    PublicKeyCredentialUserEntity(
+                        id.toByteArray(),
+                        name,
+                        "",
+                        displayName
+                    )
+                )
+            }
+
+            if (json.has("rp")) {
+                Log.d(TAG, "Set rp")
+                val rp: JSONObject = json.getJSONObject("rp")
+                val id: String = rp.getString("id")
+                val name: String = rp.getString("name")
+                builder.setRp(
+                    PublicKeyCredentialRpEntity(
+                        id,
+                        name,
+                        null
+                    )
+                )
+            }
+
+            if (json.has("timeout")) {
+                Log.d(TAG, "Set timeout")
+                val timeout: Double = json.getDouble("timeout") / 1000
+                builder.setTimeoutSeconds(timeout)
+            }
+
+            if (json.has("pubKeyCredParams")) {
+                Log.d(TAG, "Set pubKeyCredParams")
+                val pubKeyCredParams: JSONArray = json.getJSONArray("pubKeyCredParams")
+                val paramsList: MutableList<PublicKeyCredentialParameters> = ArrayList()
+                for (i in 0 until pubKeyCredParams.length()) {
+                    val param = pubKeyCredParams.getJSONObject(i)
+                    paramsList.add(
+                        PublicKeyCredentialParameters(
+                            param.getString("type"), param.getInt("alg"))
+                    )
+                }
+                builder.setParameters(paramsList)
+            }
+
+            if (json.has("authenticatorSelection")) {
+                Log.d("PasskeyRequestConverter", "Set authenticatorSelection")
+                val authenticatorSelection: JSONObject = json.getJSONObject(
+                    "authenticatorSelection")
+                val requireResidentKey = authenticatorSelection.getBoolean(
+                    "requireResidentKey")
+                builder.setAuthenticatorSelection(
+                    AuthenticatorSelectionCriteria.Builder()
+                        .setRequireResidentKey(requireResidentKey).build()
+                )
+            }
+
+            builder.setExcludeList(ArrayList())
+
+            return builder.build()
+        }
+
+        fun toCreatePasskeyResponseJson(cred: PublicKeyCredential): String {
+            val json = JSONObject()
+
+            val authenticatorResponse: AuthenticatorResponse = cred.response
+            if (authenticatorResponse is AuthenticatorAttestationResponse) {
+                val responseJson = JSONObject()
+                responseJson.put(
+                    "clientDataJSON",
+                    Base64.encodeToString(authenticatorResponse.clientDataJSON, Base64.NO_WRAP))
+                responseJson.put(
+                    "attestationObject",
+                    Base64.encodeToString(authenticatorResponse.attestationObject, Base64.NO_WRAP))
+                val transports = JSONArray(listOf(authenticatorResponse.transports))
+                responseJson.put("transports", transports)
+                json.put("response", responseJson)
+            } else {
+                Log.e(
+                    TAG,
+                    "Expected registration response but got: " +
+                        authenticatorResponse.javaClass.name)
+            }
+
+            if (cred.authenticatorAttachment != null) {
+                json.put("authenticatorAttachment", String(cred.rawId))
+            }
+
+            json.put("id", cred.id)
+            json.put("rawId", Base64.encodeToString(cred.rawId, Base64.NO_WRAP))
+            json.put("type", cred.type)
+            // TODO: add ExtensionsClientOUtputsJSON conversion
+            return json.toString()
+        }
+
+        fun toAssertPasskeyResponse(cred: SignInCredential): String {
+            val json = JSONObject()
+            val publicKeyCred = cred.publicKeyCredential
+            val authenticatorResponse: AuthenticatorResponse = publicKeyCred?.response!!
+
+            if (authenticatorResponse is AuthenticatorAssertionResponse) {
+                val responseJson = JSONObject()
+                responseJson.put(
+                    "clientDataJSON",
+                    Base64.encodeToString(authenticatorResponse.clientDataJSON, Base64.NO_WRAP))
+                responseJson.put(
+                    "assertionObject",
+                    Base64.encodeToString(authenticatorResponse.authenticatorData, Base64.NO_WRAP))
+                responseJson.put(
+                    "signature",
+                    Base64.encodeToString(authenticatorResponse.signature, Base64.NO_WRAP))
+                json.put("response", responseJson)
+            } else {
+                Log.e(
+                    TAG,
+                    "Expected assertion response but got: " + authenticatorResponse.javaClass.name)
+            }
+            json.put("id", publicKeyCred.id)
+            json.put("rawId", Base64.encodeToString(publicKeyCred.rawId, Base64.NO_WRAP))
+            json.put("type", publicKeyCred.type)
+            return json.toString()
+        }
+
+        @Suppress("DocumentExceptions")
+        fun convertToPlayAuthPasskeyRequest(request: GetPublicKeyCredentialOption):
+            BeginSignInRequest.PasskeysRequestOptions {
+            // TODO : Make sure this is in compliance with w3
+            val json = JSONObject(request.requestJson)
+            if (json.has("rpId")) {
+                val rpId: String = json.getString("rpId")
+                Log.i(TAG, "Rp Id : $rpId")
+                if (json.has("challenge")) {
+                    val challenge: ByteArray =
+                        Base64.decode(json.getString("challenge"), Base64.URL_SAFE)
+                    return BeginSignInRequest.PasskeysRequestOptions.Builder()
+                        .setSupported(true)
+                        .setRpId(rpId)
+                        .setChallenge(challenge)
+                        .build()
+                } else {
+                    Log.i(TAG, "Challenge not found in request for : $rpId")
+                }
+            } else {
+                Log.i(TAG, "Rp Id not found in request")
+            }
+            throw UnsupportedOperationException("rpId not specified in the request")
+        }
+
+        /**
+         * Indicates if an error was propagated from the underlying Fido API.
+         *
+         * @param callback the callback invoked when the request succeeds or fails
+         * @param executor the callback will take place on this executor
+         * @param cred the public key credential response object from fido
+         *
+         * @return true if there is an error, false otherwise
+         */
+        fun publicKeyCredentialResponseContainsError(
+            callback: CredentialManagerCallback<CreateCredentialResponse,
+                CreateCredentialException>,
+            executor: Executor,
+            cred: PublicKeyCredential
+        ): Boolean {
+            val authenticatorResponse: AuthenticatorResponse = cred.response
+            if (authenticatorResponse is AuthenticatorErrorResponse) {
+                val code = authenticatorResponse.errorCode
+                var exception = orderedErrorCodeToExceptions[code]
+                if (exception == null) {
+                    exception = CreatePublicKeyCredentialUnknownException("unknown fido gms " +
+                        "exception")
+                }
+                executor.execute { callback.onError(
+                    exception
+                ) }
+                return true
+            }
+            return false
+        }
+
+        private val TAG = PublicKeyCredentialControllerUtility::class.java.name
+
+        internal val orderedErrorCodeToExceptions = linkedMapOf(ErrorCode.UNKNOWN_ERR to
+        CreatePublicKeyCredentialUnknownException("fido gms returned unknown transient failure"),
+        ErrorCode.ABORT_ERR to CreatePublicKeyCredentialAbortException("fido gms indicates the " +
+            "operation was aborted"),
+        ErrorCode.CONSTRAINT_ERR to CreatePublicKeyCredentialConstraintException("fido gms " +
+            "indicates a constraint was not satisfied due to some mutation operation"),
+        ErrorCode.ATTESTATION_NOT_PRIVATE_ERR to
+            CreatePublicKeyCredentialNotReadableException("fido gms indicates the " +
+                "authenticator violates privacy requirements")
+        )
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -183,9 +183,9 @@
     ctor public ClearCredentialUnknownException();
   }
 
-  public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public CreateCredentialCanceledException();
+  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public CreateCredentialCancellationException();
   }
 
   public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
     ctor public CreateCredentialUnknownException();
   }
 
-  public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public GetCredentialCanceledException();
+  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public GetCredentialCancellationException();
   }
 
   public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -183,9 +183,9 @@
     ctor public ClearCredentialUnknownException();
   }
 
-  public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public CreateCredentialCanceledException();
+  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public CreateCredentialCancellationException();
   }
 
   public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
     ctor public CreateCredentialUnknownException();
   }
 
-  public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public GetCredentialCanceledException();
+  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public GetCredentialCancellationException();
   }
 
   public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 6d51309..9da76ac 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -183,9 +183,9 @@
     ctor public ClearCredentialUnknownException();
   }
 
-  public final class CreateCredentialCanceledException extends androidx.credentials.exceptions.CreateCredentialException {
-    ctor public CreateCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public CreateCredentialCanceledException();
+  public final class CreateCredentialCancellationException extends androidx.credentials.exceptions.CreateCredentialException {
+    ctor public CreateCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public CreateCredentialCancellationException();
   }
 
   public class CreateCredentialException extends java.lang.Exception {
@@ -205,9 +205,9 @@
     ctor public CreateCredentialUnknownException();
   }
 
-  public final class GetCredentialCanceledException extends androidx.credentials.exceptions.GetCredentialException {
-    ctor public GetCredentialCanceledException(optional CharSequence? errorMessage);
-    ctor public GetCredentialCanceledException();
+  public final class GetCredentialCancellationException extends androidx.credentials.exceptions.GetCredentialException {
+    ctor public GetCredentialCancellationException(optional CharSequence? errorMessage);
+    ctor public GetCredentialCancellationException();
   }
 
   public class GetCredentialException extends java.lang.Exception {
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
deleted file mode 100644
index 903bc23..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2022 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.credentials.exceptions
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class CreateCredentialCanceledExceptionTest {
-    @Test(expected = CreateCredentialCanceledException::class)
-    fun construct_inputNonEmpty_success() {
-        throw CreateCredentialCanceledException("msg")
-    }
-
-    @Test(expected = CreateCredentialCanceledException::class)
-    fun construct_errorMessageNull_success() {
-        throw CreateCredentialCanceledException(null)
-    }
-
-    @Test
-    fun getter_type_success() {
-        val exception = CreateCredentialCanceledException("msg")
-        val expectedType = CreateCredentialCanceledException
-            .TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION
-        Truth.assertThat(exception.type).isEqualTo(expectedType)
-    }
-}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
similarity index 66%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
index 9dc4526..2b11f40 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionJavaTest.java
@@ -26,22 +26,23 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-public class CreateCredentialCanceledExceptionJavaTest {
-    @Test(expected = CreateCredentialCanceledException.class)
-    public void construct_inputNonEmpty_success() throws CreateCredentialCanceledException {
-        throw new CreateCredentialCanceledException("msg");
+public class CreateCredentialCancellationExceptionJavaTest {
+    @Test(expected = CreateCredentialCancellationException.class)
+    public void construct_inputNonEmpty_success() throws CreateCredentialCancellationException {
+        throw new CreateCredentialCancellationException("msg");
     }
 
-    @Test(expected = CreateCredentialCanceledException.class)
-    public void construct_errorMessageNull_success() throws CreateCredentialCanceledException {
-        throw new CreateCredentialCanceledException(null);
+    @Test(expected = CreateCredentialCancellationException.class)
+    public void construct_errorMessageNull_success() throws CreateCredentialCancellationException {
+        throw new CreateCredentialCancellationException(null);
     }
 
     @Test
     public void getter_type_success() {
-        CreateCredentialCanceledException exception = new CreateCredentialCanceledException("msg");
+        CreateCredentialCancellationException exception = new
+                CreateCredentialCancellationException("msg");
         String expectedType =
-                CreateCredentialCanceledException.TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION;
+                CreateCredentialCancellationException.TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION;
         Truth.assertThat(exception.getType()).isEqualTo(expectedType);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
similarity index 69%
copy from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
index 61e7c3c..3ff4aa3 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
@@ -24,21 +24,22 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class GetCredentialCanceledExceptionTest {
-    @Test(expected = GetCredentialCanceledException::class)
+class CreateCredentialCancellationExceptionTest {
+    @Test(expected = CreateCredentialCancellationException::class)
     fun construct_inputNonEmpty_success() {
-        throw GetCredentialCanceledException("msg")
+        throw CreateCredentialCancellationException("msg")
     }
 
-    @Test(expected = GetCredentialCanceledException::class)
+    @Test(expected = CreateCredentialCancellationException::class)
     fun construct_errorMessageNull_success() {
-        throw GetCredentialCanceledException(null)
+        throw CreateCredentialCancellationException(null)
     }
 
     @Test
     fun getter_type_success() {
-        val exception = GetCredentialCanceledException("msg")
-        val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+        val exception = CreateCredentialCancellationException("msg")
+        val expectedType = CreateCredentialCancellationException
+            .TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION
         Truth.assertThat(exception.type).isEqualTo(expectedType)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
similarity index 65%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
index e5cf2f3..64653dc 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionJavaTest.java
@@ -26,21 +26,23 @@
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-public class GetCredentialCanceledExceptionJavaTest {
-    @Test(expected = GetCredentialCanceledException.class)
-    public void construct_inputNonEmpty_success() throws GetCredentialCanceledException {
-        throw new GetCredentialCanceledException("msg");
+public class GetCredentialCancellationExceptionJavaTest {
+    @Test(expected = GetCredentialCancellationException.class)
+    public void construct_inputNonEmpty_success() throws GetCredentialCancellationException {
+        throw new GetCredentialCancellationException("msg");
     }
 
-    @Test(expected = GetCredentialCanceledException.class)
-    public void construct_errorMessageNull_success() throws GetCredentialCanceledException {
-        throw new GetCredentialCanceledException(null);
+    @Test(expected = GetCredentialCancellationException.class)
+    public void construct_errorMessageNull_success() throws GetCredentialCancellationException {
+        throw new GetCredentialCancellationException(null);
     }
 
     @Test
     public void getter_type_success() {
-        GetCredentialCanceledException exception = new GetCredentialCanceledException("msg");
-        String expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION;
+        GetCredentialCancellationException exception = new
+                GetCredentialCancellationException("msg");
+        String expectedType = GetCredentialCancellationException
+                .TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION;
         Truth.assertThat(exception.getType()).isEqualTo(expectedType);
     }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
similarity index 70%
rename from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
index 61e7c3c..bbc2d3e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
@@ -24,21 +24,22 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class GetCredentialCanceledExceptionTest {
-    @Test(expected = GetCredentialCanceledException::class)
+class GetCredentialCancellationExceptionTest {
+    @Test(expected = GetCredentialCancellationException::class)
     fun construct_inputNonEmpty_success() {
-        throw GetCredentialCanceledException("msg")
+        throw GetCredentialCancellationException("msg")
     }
 
-    @Test(expected = GetCredentialCanceledException::class)
+    @Test(expected = GetCredentialCancellationException::class)
     fun construct_errorMessageNull_success() {
-        throw GetCredentialCanceledException(null)
+        throw GetCredentialCancellationException(null)
     }
 
     @Test
     fun getter_type_success() {
-        val exception = GetCredentialCanceledException("msg")
-        val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+        val exception = GetCredentialCancellationException("msg")
+        val expectedType = GetCredentialCancellationException
+            .TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION
         Truth.assertThat(exception.type).isEqualTo(expectedType)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java
new file mode 100644
index 0000000..5978e95
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionJavaTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialAbortExceptionJavaTest {
+
+    @Test(expected = CreatePublicKeyCredentialAbortException.class)
+    public void construct_inputNonEmpty_success() throws
+            CreatePublicKeyCredentialAbortException {
+        throw new CreatePublicKeyCredentialAbortException(
+                "msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialAbortException.class)
+    public void construct_errorMessageNull_success() throws
+            CreatePublicKeyCredentialAbortException {
+        throw new CreatePublicKeyCredentialAbortException(null);
+    }
+
+    @Test
+    public void getter_type_success() {
+        CreatePublicKeyCredentialAbortException exception = new
+                CreatePublicKeyCredentialAbortException("msg");
+        String expectedType =
+                CreatePublicKeyCredentialAbortException
+                        .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION;
+        Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
similarity index 60%
copy from credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
index 61e7c3c..123dc94 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.credentials.exceptions
+package androidx.credentials.exceptions.createpublickeycredential
 
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialAbortException
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth
@@ -24,21 +25,24 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-class GetCredentialCanceledExceptionTest {
-    @Test(expected = GetCredentialCanceledException::class)
+class CreatePublicKeyCredentialAbortExceptionTest {
+
+    @Test(expected = CreatePublicKeyCredentialAbortException::class)
     fun construct_inputNonEmpty_success() {
-        throw GetCredentialCanceledException("msg")
+        throw CreatePublicKeyCredentialAbortException("msg")
     }
 
-    @Test(expected = GetCredentialCanceledException::class)
+    @Test(expected = CreatePublicKeyCredentialAbortException::class)
     fun construct_errorMessageNull_success() {
-        throw GetCredentialCanceledException(null)
+        throw CreatePublicKeyCredentialAbortException(null)
     }
 
     @Test
     fun getter_type_success() {
-        val exception = GetCredentialCanceledException("msg")
-        val expectedType = GetCredentialCanceledException.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION
+        val exception = CreatePublicKeyCredentialAbortException("msg")
+        val expectedType =
+            CreatePublicKeyCredentialAbortException
+                .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION
         Truth.assertThat(exception.type).isEqualTo(expectedType)
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java
new file mode 100644
index 0000000..cde3616
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialConstraintExceptionJavaTest {
+    @Test(expected = CreatePublicKeyCredentialConstraintException.class)
+    public void construct_inputNonEmpty_success() throws
+            CreatePublicKeyCredentialConstraintException {
+        throw new CreatePublicKeyCredentialConstraintException("msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialConstraintException.class)
+    public void construct_errorMessageNull_success() throws
+            CreatePublicKeyCredentialConstraintException {
+        throw new CreatePublicKeyCredentialConstraintException(null);
+    }
+
+    @Test
+    public void getter_type_success() {
+        CreatePublicKeyCredentialConstraintException exception = new
+                CreatePublicKeyCredentialConstraintException("msg");
+        String expectedType =
+                CreatePublicKeyCredentialConstraintException
+                        .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION;
+        Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt
new file mode 100644
index 0000000..1dcfbeb
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialConstraintExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialConstraintException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialConstraintExceptionTest {
+    @Test(expected = CreatePublicKeyCredentialConstraintException::class)
+    fun construct_inputNonEmpty_success() {
+        throw CreatePublicKeyCredentialConstraintException("msg")
+    }
+
+    @Test(expected = CreatePublicKeyCredentialConstraintException::class)
+    fun construct_errorMessageNull_success() {
+        throw CreatePublicKeyCredentialConstraintException(null)
+    }
+
+    @Test
+    fun getter_type_success() {
+        val exception = CreatePublicKeyCredentialConstraintException("msg")
+        val expectedType =
+            CreatePublicKeyCredentialConstraintException
+                .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION
+        Truth.assertThat(exception.type).isEqualTo(expectedType)
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java
new file mode 100644
index 0000000..508f137
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionJavaTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialExceptionJavaTest {
+
+    @Test(expected = CreatePublicKeyCredentialException.class)
+    public void construct_inputsNonEmpty_success() throws CreatePublicKeyCredentialException {
+        throw new CreatePublicKeyCredentialException("type", "msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialException.class)
+    public void construct_errorMessageNull_success() throws CreatePublicKeyCredentialException {
+        throw new CreatePublicKeyCredentialException("type", null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void construct_typeEmpty_throws() throws CreatePublicKeyCredentialException {
+        throw new CreatePublicKeyCredentialException("", "msg");
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void construct_typeNull_throws() throws CreatePublicKeyCredentialException {
+        throw new CreatePublicKeyCredentialException(null, "msg");
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt
new file mode 100644
index 0000000..eeeb47a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialExceptionTest.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialExceptionTest {
+
+    @Test(expected = CreatePublicKeyCredentialException::class)
+    fun construct_inputsNonEmpty_success() {
+        throw CreatePublicKeyCredentialException("type", "msg")
+    }
+
+    @Test(expected = CreatePublicKeyCredentialException::class)
+    fun construct_errorMessageNull_success() {
+        throw CreatePublicKeyCredentialException("type", null)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun construct_typeEmpty_throws() {
+        throw CreatePublicKeyCredentialException("", "msg")
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java
new file mode 100644
index 0000000..89464e7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialInterruptedExceptionJavaTest {
+    @Test(expected = CreatePublicKeyCredentialInterruptedException.class)
+    public void construct_inputNonEmpty_success() throws
+            CreatePublicKeyCredentialInterruptedException {
+        throw new CreatePublicKeyCredentialInterruptedException("msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialInterruptedException.class)
+    public void construct_errorMessageNull_success() throws
+            CreatePublicKeyCredentialInterruptedException {
+        throw new CreatePublicKeyCredentialInterruptedException(null);
+    }
+
+    @Test
+    public void getter_type_success() {
+        CreatePublicKeyCredentialInterruptedException exception = new
+                CreatePublicKeyCredentialInterruptedException("msg");
+        String expectedType =
+                CreatePublicKeyCredentialInterruptedException
+                        .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION;
+        Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt
new file mode 100644
index 0000000..0d73064
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialInterruptedExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialInterruptedException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialInterruptedExceptionTest {
+    @Test(expected = CreatePublicKeyCredentialInterruptedException::class)
+    fun construct_inputNonEmpty_success() {
+        throw CreatePublicKeyCredentialInterruptedException("msg")
+    }
+
+    @Test(expected = CreatePublicKeyCredentialInterruptedException::class)
+    fun construct_errorMessageNull_success() {
+        throw CreatePublicKeyCredentialInterruptedException(null)
+    }
+
+    @Test
+    fun getter_type_success() {
+        val exception = CreatePublicKeyCredentialInterruptedException("msg")
+        val expectedType =
+            CreatePublicKeyCredentialInterruptedException
+                .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION
+        Truth.assertThat(exception.type).isEqualTo(expectedType)
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java
new file mode 100644
index 0000000..c28a1c6
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialNotReadableExceptionJavaTest {
+    @Test(expected = CreatePublicKeyCredentialNotReadableException.class)
+    public void construct_inputNonEmpty_success() throws
+            CreatePublicKeyCredentialNotReadableException {
+        throw new CreatePublicKeyCredentialNotReadableException("msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialNotReadableException.class)
+    public void construct_errorMessageNull_success() throws
+            CreatePublicKeyCredentialNotReadableException {
+        throw new CreatePublicKeyCredentialNotReadableException(null);
+    }
+
+    @Test
+    public void getter_type_success() {
+        CreatePublicKeyCredentialNotReadableException exception = new
+                CreatePublicKeyCredentialNotReadableException("msg");
+        String expectedType =
+                CreatePublicKeyCredentialNotReadableException
+                        .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION;
+        Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt
new file mode 100644
index 0000000..e84984a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialNotReadableExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialNotReadableException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialNotReadableExceptionTest {
+    @Test(expected = CreatePublicKeyCredentialNotReadableException::class)
+    fun construct_inputNonEmpty_success() {
+        throw CreatePublicKeyCredentialNotReadableException("msg")
+    }
+
+    @Test(expected = CreatePublicKeyCredentialNotReadableException::class)
+    fun construct_errorMessageNull_success() {
+        throw CreatePublicKeyCredentialNotReadableException(null)
+    }
+
+    @Test
+    fun getter_type_success() {
+        val exception = CreatePublicKeyCredentialNotReadableException("msg")
+        val expectedType =
+            CreatePublicKeyCredentialNotReadableException
+                .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION
+        Truth.assertThat(exception.type).isEqualTo(expectedType)
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java
new file mode 100644
index 0000000..bf3c6bb
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential;
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreatePublicKeyCredentialUnknownExceptionJavaTest {
+    @Test(expected = CreatePublicKeyCredentialUnknownException.class)
+    public void construct_inputNonEmpty_success() throws
+            CreatePublicKeyCredentialUnknownException {
+        throw new CreatePublicKeyCredentialUnknownException("msg");
+    }
+
+    @Test(expected = CreatePublicKeyCredentialUnknownException.class)
+    public void construct_errorMessageNull_success() throws
+            CreatePublicKeyCredentialUnknownException {
+        throw new CreatePublicKeyCredentialUnknownException(null);
+    }
+
+    @Test
+    public void getter_type_success() {
+        CreatePublicKeyCredentialUnknownException exception = new
+                CreatePublicKeyCredentialUnknownException("msg");
+        String expectedType =
+                CreatePublicKeyCredentialUnknownException
+                        .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION;
+        Truth.assertThat(exception.getType()).isEqualTo(expectedType);
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt
new file mode 100644
index 0000000..c1bf411
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialUnknownExceptionTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.createpublickeycredential
+
+import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialUnknownException
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePublicKeyCredentialUnknownExceptionTest {
+    @Test(expected = CreatePublicKeyCredentialUnknownException::class)
+    fun construct_inputNonEmpty_success() {
+        throw CreatePublicKeyCredentialUnknownException("msg")
+    }
+
+    @Test(expected = CreatePublicKeyCredentialUnknownException::class)
+    fun construct_errorMessageNull_success() {
+        throw CreatePublicKeyCredentialUnknownException(null)
+    }
+
+    @Test
+    fun getter_type_success() {
+        val exception = CreatePublicKeyCredentialUnknownException("msg")
+        val expectedType =
+            CreatePublicKeyCredentialUnknownException
+                .TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION
+        Truth.assertThat(exception.type).isEqualTo(expectedType)
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
index a9793d2..efc15ac 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.VisibleForTesting
 
 /**
- * During the clear credential flow, this is called when some interruption occurs that may warrant
+ * During the clear credential flow, this is returned when some interruption occurs that may warrant
  * retrying or at least does not indicate a purposeful desire to close or tap away from credential
  * manager.
  *
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
similarity index 71%
rename from credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt
rename to credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
index 2c205c4..0be4594 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCanceledException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialCancellationException.kt
@@ -19,20 +19,20 @@
 import androidx.annotation.VisibleForTesting
 
 /**
- * During the create credential flow, this is called when a user intentionally cancels an operation.
+ * During the create credential flow, this is returned when a user intentionally cancels an operation.
  * When this happens, the application should handle logic accordingly, typically under indication
  * the user does not want to see Credential Manager anymore.
  *
  * @see CreateCredentialException
  */
-class CreateCredentialCanceledException @JvmOverloads constructor(
+class CreateCredentialCancellationException @JvmOverloads constructor(
     errorMessage: CharSequence? = null
-) : CreateCredentialException(TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION, errorMessage) {
+) : CreateCredentialException(TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION, errorMessage) {
 
     /** @hide */
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION: String =
-            "androidx.credentials.TYPE_CREATE_CREDENTIAL_CANCELED_EXCEPTION"
+        const val TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_CREDENTIAL_CANCELLATION_EXCEPTION"
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
index ec8c14b..b2ba63c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialException.kt
@@ -25,7 +25,7 @@
  *
  * @see CredentialManager
  * @see CreateCredentialInterruptedException
- * @see CreateCredentialCanceledException
+ * @see CreateCredentialCancellationException
  * @see CreateCredentialUnknownException
  *
  * @property errorMessage a human-readable string that describes the error
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
index 9dbaacb..ec2c746 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.VisibleForTesting
 
 /**
- * During the create credential flow, this is called when some interruption occurs that may warrant
+ * During the create credential flow, this is returned when some interruption occurs that may warrant
  * retrying or at least does not indicate a purposeful desire to close or tap away from credential
  * manager.
  *
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
similarity index 72%
rename from credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt
rename to credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
index 5ed263f..63e5e25 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCanceledException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialCancellationException.kt
@@ -19,20 +19,20 @@
 import androidx.annotation.VisibleForTesting
 
 /**
- * During the get credential flow, this is called when a user intentionally cancels an operation.
+ * During the get credential flow, this is returned when a user intentionally cancels an operation.
  * When this happens, the application should handle logic accordingly, typically under indication
  * the user does not want to see Credential Manager anymore.
  *
  * @see GetCredentialException
  */
-class GetCredentialCanceledException @JvmOverloads constructor(
+class GetCredentialCancellationException @JvmOverloads constructor(
     errorMessage: CharSequence? = null
-) : GetCredentialException(TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION, errorMessage) {
+) : GetCredentialException(TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION, errorMessage) {
 
     /** @hide */
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        const val TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION: String =
-            "androidx.credentials.TYPE_GET_CREDENTIAL_CANCELED_EXCEPTION"
+        const val TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION: String =
+            "androidx.credentials.TYPE_GET_CREDENTIAL_CANCELLATION_EXCEPTION"
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
index 372584c..752252d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialException.kt
@@ -25,7 +25,7 @@
  *
  * @see CredentialManager
  * @see GetCredentialUnknownException
- * @see GetCredentialCanceledException
+ * @see GetCredentialCancellationException
  * @see GetCredentialInterruptedException
  *
  * @property errorMessage a human-readable string that describes the error
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
index f352c65..ae9c993 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialInterruptedException.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.VisibleForTesting
 
 /**
- * During the get credential flow, this is called when some interruption occurs that may warrant
+ * During the get credential flow, this is returned when some interruption occurs that may warrant
  * retrying or at least does not indicate a purposeful desire to close or tap away from credential
  * manager.
  *
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt
new file mode 100644
index 0000000..4681796
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialAbortException.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains and abort-err from the fido spec, indicating the operation was aborted. The
+ * fido spec can be found [here](https://webidl.spec.whatwg.org/#idl-DOMException).
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialAbortException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+    TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION,
+    errorMessage) {
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_ABORT_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+                "_ABORT_EXCEPTION"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt
new file mode 100644
index 0000000..412617f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialConstraintException.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains a constraint_err code, indicating that some mutation operation
+ * occurring during a transaction failed by not satisfying constraints.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialConstraintException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+    TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION,
+    errorMessage) {
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_CONSTRAINT_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+                "_CONSTRAINT_EXCEPTION"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt
new file mode 100644
index 0000000..b5b59c6
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialException.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.credentials.CredentialManager
+import androidx.credentials.exceptions.CreateCredentialException
+
+/**
+ * A subclass of CreateCredentialException for unique errors specific only to PublicKeyCredentials.
+ * See [CredentialManager] for more details on how Credentials work for Credential Manager flows.
+ * See [GMS Error Codes](https://developers.google.com/android/reference/com/google/android/gms/fido/fido2/api/common/ErrorCode)
+ * for more details on some of the subclasses.
+ *
+ * @see CredentialManager
+ * @see CreatePublicKeyCredentialInterruptedException
+ * @see CreatePublicKeyCredentialUnknownException
+ * @see CreatePublicKeyCredentialNotReadableException
+ * @see CreatePublicKeyCredentialAbortException
+ * @see CreatePublicKeyCredentialConstraintException
+ *
+ * @property errorMessage a human-readable string that describes the error
+ * @throws NullPointerException if [type] is null
+ * @throws IllegalArgumentException if [type] is empty
+ *
+ * @hide
+ */
+open class CreatePublicKeyCredentialException @JvmOverloads constructor(
+    type: String,
+    errorMessage: CharSequence? = null
+) : CreateCredentialException(type, errorMessage)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt
new file mode 100644
index 0000000..395183c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialInterruptedException.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential credential flow, this is returned when some interruption
+ * occurs that may warrant retrying or at least does not indicate a purposeful desire to close or
+ * tap away from credential manager.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialInterruptedException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+    TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION,
+    errorMessage) {
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_INTERRUPTED_EXCEPTION"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt
new file mode 100644
index 0000000..afd339f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialNotReadableException.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * During the create public key credential flow, this is returned when an authenticator response
+ * exception contains a NotReadableError from fido, which indicates there was some I/O read
+ * operation that failed.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialNotReadableException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+    TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION,
+    errorMessage) {
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_NOT_READABLE_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL" +
+                "_NOT_READABLE_EXCEPTION"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt
new file mode 100644
index 0000000..ed9538c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/publickeycredential/CreatePublicKeyCredentialUnknownException.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 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.credentials.exceptions.publickeycredential
+
+import androidx.annotation.VisibleForTesting
+
+/**
+ * This create public key credential operation failed with no more detailed information. This could
+ * be something such as out of memory or some other transient reason.
+ *
+ * @see CreatePublicKeyCredentialException
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialUnknownException @JvmOverloads constructor(
+    errorMessage: CharSequence? = null
+) : CreatePublicKeyCredentialException(
+    TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION,
+    errorMessage) {
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION: String =
+            "androidx.credentials.TYPE_CREATE_PUBLIC_KEY_CREDENTIAL_UNKNOWN_EXCEPTION"
+    }
+}
\ No newline at end of file
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 5d5e448..5c5329b 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -307,6 +307,11 @@
     docs(project(":versionedparcelable:versionedparcelable"))
     docs(project(":viewpager2:viewpager2"))
     docs(project(":viewpager:viewpager"))
+    docs(project(":wear:protolayout:protolayout"))
+    docs(project(":wear:protolayout:protolayout-expression"))
+    docs(project(":wear:protolayout:protolayout-expression-pipeline"))
+    docs(project(":wear:protolayout:protolayout-proto"))
+    docs(project(":wear:protolayout:protolayout-renderer"))
     docs(project(":wear:wear"))
     stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
     docs(project(":wear:compose:compose-foundation"))
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
new file mode 100644
index 0000000..e4ee5f4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout-land/main.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.emoji2.emojipicker.EmojiPickerView
+        android:id="@+id/emoji_picker"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:emojiGridRows="6"
+        app:emojiGridColumns="20" />
+</FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
index 3b15586..b31c40e 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
@@ -14,13 +14,15 @@
   limitations under the License.
   -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="match_parent">
 
     <androidx.emoji2.emojipicker.EmojiPickerView
         android:id="@+id/emoji_picker"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-</LinearLayout>
+        android:layout_height="match_parent"
+        app:emojiGridRows="15"
+        app:emojiGridColumns="9" />
+</FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
index b9b80a5..af0e9a2 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -17,14 +17,11 @@
 package androidx.emoji2.emojipicker
 
 import androidx.emoji2.emojipicker.R as EmojiPickerViewR
-import androidx.test.espresso.Espresso.onView
-import org.hamcrest.Description
 import android.app.Activity
 import android.content.Context
 import android.os.Bundle
 import android.view.View
 import android.view.View.GONE
-import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.ImageView
@@ -32,18 +29,12 @@
 import androidx.emoji2.emojipicker.test.R
 import androidx.recyclerview.widget.RecyclerView
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.action.ViewActions.longClick
-import androidx.test.espresso.contrib.RecyclerViewActions
 import androidx.test.espresso.matcher.BoundedMatcher
-import androidx.test.espresso.matcher.RootMatchers.hasWindowLayoutParams
-import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
+import org.hamcrest.Description
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -99,55 +90,6 @@
         }
     }
 
-    @Test
-    @SdkSuppress(minSdkVersion = 24)
-    fun testCustomEmojiPickerView_hasVariant() {
-        // 👃 has variants
-        val noseEmoji = "\uD83D\uDC43"
-        lateinit var bodyView: EmojiPickerBodyView
-        mActivityTestRule.scenario.onActivity {
-            bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
-                .findViewById(EmojiPickerViewR.id.emoji_picker_body)
-        }
-        onView(withId(EmojiPickerViewR.id.emoji_picker_body))
-            .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
-        val targetView = findViewByEmoji(bodyView, noseEmoji)
-        // Variant indicator visible
-        assertEquals(
-            (targetView.parent as FrameLayout).findViewById<ImageView>(
-                EmojiPickerViewR.id.variant_availability_indicator
-            ).visibility, VISIBLE
-        )
-        // Long-clickable
-        assertEquals(targetView.isLongClickable, true)
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 24)
-    fun testStickyVariant_displayAndSaved() {
-        val noseEmoji = "\uD83D\uDC43"
-        val noseEmojiDark = "\uD83D\uDC43\uD83C\uDFFF"
-        lateinit var bodyView: EmojiPickerBodyView
-        mActivityTestRule.scenario.onActivity {
-            bodyView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
-                .findViewById(EmojiPickerViewR.id.emoji_picker_body)
-        }
-        // Scroll to the nose emoji, long click then select nose in dark skin tone
-        onView(withId(EmojiPickerViewR.id.emoji_picker_body))
-            .perform(RecyclerViewActions.scrollToHolder(createEmojiViewHolderMatcher(noseEmoji)))
-        onView(createEmojiViewMatcher(noseEmoji)).perform(longClick())
-        onView(createEmojiViewMatcher(noseEmojiDark))
-            .inRoot(hasWindowLayoutParams())
-            .perform(click())
-        assertNotNull(findViewByEmoji(bodyView, noseEmojiDark))
-        // Switch back to clear saved preference
-        onView(createEmojiViewMatcher(noseEmojiDark)).perform(longClick())
-        onView(createEmojiViewMatcher(noseEmoji))
-            .inRoot(hasWindowLayoutParams())
-            .perform(click())
-        assertNotNull(findViewByEmoji(bodyView, noseEmoji))
-    }
-
     private fun findViewByEmoji(root: View, emoji: String) =
         mutableListOf<View>().apply {
             findViewsById(
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt
new file mode 100644
index 0000000..5a102ac
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DefaultRecentEmojiProvider.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 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.emoji2.emojipicker
+
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+
+/**
+ * Provides recently shared emoji. This is the default recent emoji list provider.
+ * Clients could specify the provider by their own.
+ */
+internal class DefaultRecentEmojiProvider(
+    context: Context
+) : RecentEmojiProvider {
+
+    companion object {
+        private const val PREF_KEY_RECENT_EMOJI = "pref_key_recent_emoji"
+        private const val RECENT_EMOJI_LIST_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+        private const val SPLIT_CHAR = ","
+    }
+
+    private val sharedPreferences =
+        context.getSharedPreferences(RECENT_EMOJI_LIST_FILE_NAME, MODE_PRIVATE)
+    private val recentEmojiList: MutableList<String> =
+        sharedPreferences.getString(PREF_KEY_RECENT_EMOJI, null)
+            ?.split(SPLIT_CHAR)
+            ?.toMutableList()
+            ?: mutableListOf()
+
+    override suspend fun getRecentItemList(): List<String> {
+        return recentEmojiList
+    }
+
+    override fun insert(emoji: String) {
+        recentEmojiList.remove(emoji)
+        recentEmojiList.add(0, emoji)
+        saveToPreferences()
+    }
+
+    private fun saveToPreferences() {
+        sharedPreferences
+            .edit()
+            .putString(PREF_KEY_RECENT_EMOJI, recentEmojiList.joinToString(SPLIT_CHAR))
+            .commit()
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 8356d6c..6820a15 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -25,10 +25,10 @@
 import androidx.annotation.UiThread
 import androidx.appcompat.widget.AppCompatTextView
 import androidx.core.util.Consumer
+import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
 import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import androidx.tracing.Trace
-import androidx.emoji2.emojipicker.EmojiPickerConstants.RECENT_CATEGORY_INDEX
 
 /** RecyclerView adapter for emoji body.  */
 internal class EmojiPickerBodyAdapter(
@@ -36,8 +36,12 @@
     private val emojiGridColumns: Int,
     private val emojiGridRows: Float,
     private val categoryNames: Array<String>,
+    private val variantToBaseEmojiMap: Map<String, String>,
+    private val baseToVariantsEmojiMap: Map<String, List<String>>,
     private val stickyVariantProvider: StickyVariantProvider,
-    private val onEmojiPickedListener: Consumer<EmojiViewItem>?
+    private val onEmojiPickedListener: Consumer<EmojiViewItem>?,
+    private val recentEmojiList: MutableList<String>,
+    private val recentEmojiProvider: RecentEmojiProvider
 ) : Adapter<ViewHolder>() {
     private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
     private var flattenSource: ItemViewDataFlatList
@@ -87,7 +91,24 @@
                         getParentWidth(parent) / emojiGridColumns,
                         (parent.measuredHeight / emojiGridRows).toInt(),
                         stickyVariantProvider,
-                        onEmojiPickedListener,
+                        onEmojiPickedListener = { emojiViewItem ->
+                            recentEmojiProvider.insert(emojiViewItem.emoji)
+                            // update the recentEmojiList in the mean time
+                            recentEmojiList.remove(emojiViewItem.emoji)
+                            recentEmojiList.add(0, emojiViewItem.emoji)
+                            onEmojiPickedListener?.accept(emojiViewItem)
+                            // update the recent category to reload
+                            this@EmojiPickerBodyAdapter.updateRecent(recentEmojiList.map { emoji ->
+                                EmojiViewData(
+                                    RECENT_CATEGORY_INDEX,
+                                    recentEmojiList.indexOf(emoji),
+                                    emoji,
+                                    baseToVariantsEmojiMap[variantToBaseEmojiMap[emoji]]
+                                        ?.toTypedArray()
+                                        ?: arrayOf()
+                                )
+                            })
+                        },
                         onEmojiPickedFromPopupListener = { emoji ->
                             (flattenSource[bindingAdapterPosition] as EmojiViewData).primary = emoji
                             notifyItemChanged(bindingAdapterPosition)
@@ -192,4 +213,12 @@
         )
         notifyDataSetChanged()
     }
+
+    fun updateRecent(recents: List<ItemViewData>) {
+        flattenSource.updateSourcesByIndex(RECENT_CATEGORY_INDEX, recents)
+        notifyItemRangeChanged(
+            RECENT_CATEGORY_INDEX,
+            flattenSource.getCategorySize(RECENT_CATEGORY_INDEX)
+        )
+    }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
deleted file mode 100644
index ffe1390..0000000
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2022 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.emoji2.emojipicker
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-
-/** Body view contains all emojis.  */
-internal class EmojiPickerBodyView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null
-) : RecyclerView(context, attrs) {
-
-    init {
-        val layoutManager = GridLayoutManager(
-            getContext(),
-            EmojiPickerConstants.DEFAULT_BODY_COLUMNS,
-            LinearLayoutManager.VERTICAL,
-            /* reverseLayout = */ false
-        )
-        layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
-            override fun getSpanSize(position: Int): Int {
-                val adapter = adapter ?: return 1
-                val viewType = adapter.getItemViewType(position)
-                // The following viewTypes occupy entire row.
-                return if (
-                    viewType == CategorySeparatorViewData.TYPE ||
-                    viewType == EmptyCategoryViewData.TYPE
-                ) EmojiPickerConstants.DEFAULT_BODY_COLUMNS else 1
-            }
-        }
-        setLayoutManager(layoutManager)
-    }
-}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 2abfc99..8f3f992 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -22,6 +22,7 @@
 import android.util.AttributeSet
 import android.widget.FrameLayout
 import androidx.core.util.Consumer
+import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.CoroutineScope
@@ -62,6 +63,7 @@
         }
 
     private val stickyVariantProvider = StickyVariantProvider(context)
+    private var recentEmojiProvider = DefaultRecentEmojiProvider(context)
 
     private lateinit var headerView: RecyclerView
     private lateinit var bodyView: RecyclerView
@@ -93,13 +95,22 @@
         emojiGridColumns: Int,
         emojiGridRows: Float,
         categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>,
-        onEmojiPickedListener: Consumer<EmojiViewItem>?
+        variantToBaseEmojiMap: Map<String, String>,
+        baseToVariantsEmojiMap: Map<String, List<String>>,
+        onEmojiPickedListener: Consumer<EmojiViewItem>?,
+        recentEmojiList: MutableList<String>,
+        recentEmojiProvider: RecentEmojiProvider
     ): EmojiPickerBodyAdapter {
         val categoryNames = mutableListOf<String>()
         val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
         // add recent category as the first row
         categoryNames.add(resources.getString(R.string.emoji_category_recent))
-        categorizedEmojis.add(mutableListOf())
+        categorizedEmojis.add(recentEmojiList.map { emoji ->
+            EmojiViewItem(
+                emoji,
+                baseToVariantsEmojiMap[variantToBaseEmojiMap[emoji]] ?: listOf()
+            )
+        }.toMutableList())
 
         for (i in categorizedEmojiData.indices) {
             categoryNames.add(categorizedEmojiData[i].categoryName)
@@ -116,8 +127,12 @@
             emojiGridColumns,
             emojiGridRows,
             categoryNames.toTypedArray(),
+            variantToBaseEmojiMap,
+            baseToVariantsEmojiMap,
             stickyVariantProvider,
-            onEmojiPickedListener
+            onEmojiPickedListener,
+            recentEmojiList,
+            recentEmojiProvider
         )
         adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
 
@@ -165,14 +180,39 @@
 
         // set bodyView
         bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
+        bodyView.layoutManager = GridLayoutManager(
+            getContext(),
+            emojiGridColumns,
+            LinearLayoutManager.VERTICAL,
+            /* reverseLayout = */ false
+        ).apply {
+            spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+                override fun getSpanSize(position: Int): Int {
+                    val adapter = bodyView.adapter ?: return 1
+                    val viewType = adapter.getItemViewType(position)
+                    // The following viewTypes occupy entire row.
+                    return if (
+                        viewType == CategorySeparatorViewData.TYPE ||
+                        viewType == EmptyCategoryViewData.TYPE
+                    ) emojiGridColumns else 1
+                }
+            }
+        }
         val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
+        val variantToBaseEmojiMap = BundledEmojiListLoader.getPrimaryEmojiLookup()
+        val baseToVariantsEmojiMap = BundledEmojiListLoader.getEmojiVariantsLookup()
+        val recentEmojiList = recentEmojiProvider.getRecentItemList().toMutableList()
         bodyView.adapter =
             createEmojiPickerBodyAdapter(
                 context,
                 emojiGridColumns,
                 emojiGridRows,
                 categorizedEmojiData,
-                onEmojiPickedListener
+                variantToBaseEmojiMap,
+                baseToVariantsEmojiMap,
+                onEmojiPickedListener,
+                recentEmojiList,
+                recentEmojiProvider
             )
     }
 
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
index a08fe8d..586f828 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -29,7 +29,6 @@
 import android.widget.GridLayout
 import android.widget.ImageView
 import android.widget.PopupWindow
-import androidx.core.util.Consumer
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import kotlin.math.roundToInt
 
@@ -40,15 +39,16 @@
     width: Int,
     height: Int,
     stickyVariantProvider: StickyVariantProvider,
-    onEmojiPickedListener: Consumer<EmojiViewItem>?,
+    onEmojiPickedListener: EmojiViewHolder.(EmojiViewItem) -> Unit,
     onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
 ) : ViewHolder(
     layoutInflater
         .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
 ) {
-    private val onEmojiClickListener: OnClickListener = OnClickListener {
-        // TODO(scduan): Add other on click events (e.g, add to recent)
-        onEmojiPickedListener?.accept(emojiViewItem)
+    private val onEmojiClickListener: OnClickListener = OnClickListener { v ->
+        v.findViewById<EmojiView>(R.id.emoji_view).emoji?.let {
+            onEmojiPickedListener(EmojiViewItem(it.toString(), emojiViewItem.variants))
+        }
     }
 
     private val onEmojiLongClickListener: OnLongClickListener = OnLongClickListener {
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
index 45edc3c..8315a79 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
@@ -127,4 +127,29 @@
         }
         return currentCategoryIndex
     }
+
+    fun updateSourcesByIndex(index: Int, sources: List<ItemViewData>) {
+        if (numberOfCategories == 0) {
+            Log.wtf(LOG_TAG, "Couldn't update due to empty categorizes sources")
+            return
+        }
+        categorizedSources[index] = sources
+        updateIndex()
+    }
+
+    @IntRange(from = 0)
+    fun getCategorySize(categoryIndex: Int): Int {
+        if (categoryIndex >= numberOfCategories) {
+            Log.wtf(
+                LOG_TAG,
+                String.format(
+                    "Too large categoryIndex (%s vs %s)",
+                    categoryIndex,
+                    numberOfCategories
+                )
+            )
+            return 0
+        }
+        return categorySizes[categoryIndex]
+    }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
new file mode 100644
index 0000000..56a756c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 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.emoji2.emojipicker
+
+/** An interface to provide recent emoji list.  */
+internal interface RecentEmojiProvider {
+    /**
+     * Inserts an emoji into recent emoji list. Called by emoji picker when an emoji is shared.
+     */
+    fun insert(emoji: String)
+
+    /** Returns a list of recent items.  */
+    suspend fun getRecentItemList(): List<String>
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
index f1bb622..84192cc 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -26,7 +26,7 @@
         android:paddingEnd="@dimen/emoji_picker_header_padding"
         android:paddingStart="@dimen/emoji_picker_header_padding" />
 
-    <androidx.emoji2.emojipicker.EmojiPickerBodyView
+    <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/emoji_picker_body"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
index 21f04a3..8dc27b4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-af/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOSIEKONE EN EMOSIES"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"MENSE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DIERE EN NATUUR"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
index b3cbd77..4a4c6ac 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-am/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ሳቂታዎች እና ስሜቶች"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ሰዎች"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"እንስሳት እና ተፈጥሮ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ምልክቶች"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ባንዲራዎች"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ምንም ስሜት ገላጭ ምስሎች አይገኙም"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ምንም ስሜት ገላጭ ምስሎችን እስካሁን አልተጠቀሙም"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
index 170353c..4a49b9d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ar/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"الوجوه المبتسمة والرموز التعبيرية"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"الأشخاص"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"الحيوانات والطبيعة"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"الرموز"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"الأعلام"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"لا تتوفر أي رموز تعبيرية."</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"لم تستخدم أي رموز تعبيرية حتى الآن."</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
index f1f6fbef..43191b0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-as/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"স্মাইলী আৰু আৱেগ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"মানুহ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"পশু আৰু প্ৰকৃতি"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"চিহ্ন"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"পতাকা"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনো ইম’জি উপলব্ধ নহয়"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপুনি এতিয়ালৈকে কোনো ইম’জি ব্যৱহাৰ কৰা নাই"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
index 78ed86d..1cc0ebb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-az/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAYLİK VƏ EMOSİYALAR"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ADAMLAR"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"HEYVANLAR VƏ TƏBİƏT"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SİMVOLLAR"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAQLAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Əlçatan emoji yoxdur"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Hələ heç bir emojidən istifadə etməməsiniz"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
index ec5ddd5..f6d0248 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-b+sr+Latn/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIJI I EMOCIJE"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"LJUDI"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
index eda0664..3777b3e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-be/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛІКІ І ЭМОЦЫІ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДЗІ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖЫВЁЛЫ І ПРЫРОДА"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"СІМВАЛЫ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"СЦЯГІ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма даступных эмодзі"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы пакуль не выкарыстоўвалі эмодзі"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
index d22d25b..5ba1de6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bg/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ЕМОТИКОНИ И ЕМОЦИИ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ХОРА"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТНИ И ПРИРОДА"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛИ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЗНАМЕНА"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Няма налични емоджи"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Все още не сте използвали емоджита"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
index 3fd2fae..3a9d0e2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bn/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"স্মাইলি ও আবেগ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ব্যক্তি"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"প্রাণী ও প্রকৃতি"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"প্রতীক"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ফ্ল্যাগ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"কোনও ইমোজি উপলভ্য নেই"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"আপনি এখনও কোনও ইমোজি ব্যবহার করেননি"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
index d189ac9..64e766f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-bs/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIJI I EMOCIJE"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"LJUDI"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
@@ -27,5 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZASTAVE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Emoji sličice nisu dostupne"</string>
-    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste upotrijebili emojije"</string>
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Još niste koristili nijednu emoji sličicu"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
index 4644a8b..2a08c9c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ca/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONES I EMOCIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONES"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS I NATURALESA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLS"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERES"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hi ha cap emoji disponible"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Encara no has fet servir cap emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
index f3932fe..534404e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-cs/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLÍCI A EMOCE"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"LIDÉ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZVÍŘATA A PŘÍRODA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLY"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nejsou k dispozici žádné smajlíky"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatím jste žádná emodži nepoužili"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
index ca542b0..73744d7 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-da/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS OG HUMØRIKONER"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DYR OG NATUR"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
index e836340..a3fdb2a 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-de/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS UND EMOTIONEN"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONEN"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"TIERE UND NATUR"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLE"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGEN"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Keine Emojis verfügbar"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du hast noch keine Emojis verwendet"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
index 64e701f..1c1d893 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-el/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ΕΙΚΟΝΙΔΙΑ SMILEY ΚΑΙ ΣΥΝΑΙΣΘΗΜΑΤΑ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ΑΤΟΜΑ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ΖΩΑ ΚΑΙ ΦΥΣΗ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rAU/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
index 23ba7f6..ad9f1c2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rCA/strings.xml
@@ -17,6 +17,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="emoji_category_recent" msgid="7142376595414250279">"RECENTLY USED"</string>
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rGB/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
index 828f853..3e57307d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rIN/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS AND EMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PEOPLE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALS AND NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
index 7cacbb0..976e527 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-en-rXC/strings.xml
@@ -17,6 +17,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="emoji_category_recent" msgid="7142376595414250279">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‏‎‏‎‏‎‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‏‎‏‏‏‏‎‎‏‏‎‎‏‎‎‏‏‏‎RECENTLY USED‎‏‎‎‏‎"</string>
     <string name="emoji_category_emotions" msgid="1570830970240985537">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‏‏‏‎‎‏‏‎‎‏‎‏‏‎‏‏‎‎‏‏‏‎‎‎‏‏‏‎‏‏‏‏‏‎‏‏‎‎‏‎‏‎‏‏‎‏‎‎‏‏‏‎‎‎‎‎‏‎SMILEYS AND EMOTIONS‎‏‎‎‏‎"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‎‏‎‎‏‎‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‎‏‏‏‏‏‎‎‎‏‏‎‏‎‏‎‏‏‏‏‎‏‎‏‎‏‏‎‎‎‏‎PEOPLE‎‏‎‎‏‎"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‏‏‏‏‏‏‎‎‏‎‎‏‎‎‏‎‎‏‎‏‎‏‎ANIMALS AND NATURE‎‏‎‎‏‎"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
index 790b4e6..8432ac6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es-rUS/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONES Y EMOCIONES"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALES Y NATURALEZA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLOS"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDERAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"No hay ningún emoji disponible"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Todavía no usaste ningún emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
index 3f46a8d..9dbade8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-es/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONOS Y EMOCIONES"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALES Y NATURALEZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
index 2ea6dcd..102f061 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-et/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"NÄOIKOONID JA EMOTSIOONID"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"INIMESED"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"LOOMAD JA LOODUS"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SÜMBOLID"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"LIPUD"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ühtegi emotikoni pole saadaval"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Te pole veel ühtegi emotikoni kasutanud"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
index 71f27ef..8299aa5 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-eu/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"AURPEGIERAK ETA ALDARTEAK"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"JENDEA"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALIAK ETA NATURA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
index 8be716a..94ca207 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fa/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"شکلک‌ها و احساسات"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"افراد"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"حیوانات و طبیعت"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"نمادها"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"پرچم‌ها"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"اموجی دردسترس نیست"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"هنوز از هیچ اموجی‌ای استفاده نکرده‌اید"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
index 31f6de1..e1388e0 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fi/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"HYMIÖT JA TUNNETILAT"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"IHMISET"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ELÄIMET JA LUONTO"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLIT"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"LIPUT"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ei emojeita saatavilla"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Et ole vielä käyttänyt emojeita"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
index 2704128..02583ee 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr-rCA/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ÉMOTICÔNES ET ÉMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONNES"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAUX ET NATURE"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
index 35a8d76..23f7e54 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-fr/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ÉMOTICÔNES ET ÉMOTIONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONNES"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAUX ET NATURE"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLES"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"DRAPEAUX"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Aucun emoji disponible"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Vous n\'avez pas encore utilisé d\'emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
index 857e179..2a45e75 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gl/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ICONAS XESTUAIS E EMOTICONAS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSOAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SÍMBOLOS"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDEIRAS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Non hai ningún emoji dispoñible"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Aínda non utilizaches ningún emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
index 565cd02..1079b02 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-gu/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"સ્માઇલી અને મનોભાવો"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"લોકો"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"પ્રાણીઓ અને પ્રકૃતિ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"પ્રતીકો"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ઝંડા"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"કોઈ ઇમોજી ઉપલબ્ધ નથી"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"તમે હજી સુધી કોઈ ઇમોજીનો ઉપયોગ કર્યો નથી"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
index c61cc03..d7f63c2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hi/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"स्माइली और भावनाएं"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"लोग"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"जानवर और प्रकृति"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"सिंबल"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"झंडे"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोई इमोजी उपलब्ध नहीं है"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"आपने अब तक किसी भी इमोजी का इस्तेमाल नहीं किया है"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
index 54b7e79..2e2076f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hr/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLIĆI I EMOCIJE"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"OSOBE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVOTINJE I PRIRODA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
index 0848ad2..f0888ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hu/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"HANGULATJELEK ÉS HANGULATOK"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"SZEMÉLYEK"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ÁLLATOK ÉS TERMÉSZET"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SZIMBÓLUMOK"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ZÁSZLÓK"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nincsenek rendelkezésre álló emojik"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Még nem használt emojikat"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
index 4ac80b5..691fb63 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-hy/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ԶՄԱՅԼԻԿՆԵՐ ԵՎ ՀՈՒԶԱՊԱՏԿԵՐԱԿՆԵՐ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ՄԱՐԴԻԿ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ԿԵՆԴԱՆԻՆԵՐ ԵՎ ԲՆՈՒԹՅՈՒՆ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ՆՇԱՆՆԵՐ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ԴՐՈՇՆԵՐ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Հասանելի էմոջիներ չկան"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Դուք դեռ չեք օգտագործել էմոջիներ"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
index 7b4f962..d014a1e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-in/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY DAN EMOTIKON"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ORANG"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"HEWAN DAN ALAM"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOL"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Tidak ada emoji yang tersedia"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Anda belum menggunakan emoji apa pun"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
index f743f26..40c34d8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-is/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"BROSKARLAR OG TILFINNINGAR"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"FÓLK"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DÝR OG NÁTTÚRA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"TÁKN"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FÁNAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Engin emoji-tákn í boði"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Þú hefur ekki notað nein emoji enn"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
index 9f9b707..8dd0e79 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-it/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILE ED EMOZIONI"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALI E NATURA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BANDIERE"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nessuna emoji disponibile"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Non hai ancora usato alcuna emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
index db00c09..31051ea 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-iw/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"סמיילי ואמוטיקונים"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"אנשים"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"בעלי חיים וטבע"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"סמלים"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"דגלים"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"אין סמלי אמוג\'י זמינים"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"עדיין לא השתמשת באף אמוג\'י"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
index cbccd1f..570140e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ja/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"顔文字、気分"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物、自然"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
index 3e33c41..b069aae 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ka/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"სიცილაკები და ემოციები"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ადამიანები"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ცხოველები და ბუნება"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
index 22c8157..e6784a8 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kk/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛДАР МЕН ЭМОЦИЯЛАР"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"АДАМДАР"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖАНУАРЛАР ЖӘНЕ ТАБИҒАТ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ТАҢБАЛАР"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЖАЛАУШАЛАР"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Эмоджи жоқ"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Әлі ешқандай эмоджи пайдаланылған жоқ."</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
index b79617d..11c09d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-km/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"រូប​ទឹក​មុខ និងអារម្មណ៍"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"មនុស្ស"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"សត្វ និងធម្មជាតិ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
index 51131c5..e1e11ed 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-kn/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ಸ್ಮೈಲಿಗಳು ಮತ್ತು ಭಾವನೆಗಳು"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ಜನರು"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ಪ್ರಾಣಿಗಳು ಮತ್ತು ಪ್ರಕೃತಿ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ಸಂಕೇತಗಳು"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ಫ್ಲ್ಯಾಗ್‌ಗಳು"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ಯಾವುದೇ ಎಮೊಜಿಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ನೀವು ಇನ್ನೂ ಯಾವುದೇ ಎಮೋಜಿಗಳನ್ನು ಬಳಸಿಲ್ಲ"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
index d482493..0cf33ae 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ko/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"이모티콘 및 감정"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"사람"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"동물 및 자연"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"기호"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"깃발"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"사용 가능한 그림 이모티콘 없음"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"아직 사용한 이모티콘이 없습니다."</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
index cf5d505..185dd34 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ky/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"БЫЙТЫКЧАЛАР ЖАНА ЭМОЦИЯЛАР"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"АДАМДАР"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖАНЫБАРЛАР ЖАНА ЖАРАТЫЛЫШ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛДОР"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ЖЕЛЕКТЕР"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Жеткиликтүү быйтыкчалар жок"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Азырынча быйтыкчаларды колдоно элексиз"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
index 32ac1ec..4472672 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lo/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ໜ້າຍິ້ມ ແລະ ຄວາມຮູ້ສຶກ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ຜູ້ຄົນ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ສັດ ແລະ ທຳມະຊາດ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ສັນຍາລັກ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ທຸງ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ບໍ່ມີອີໂມຈິໃຫ້ນຳໃຊ້"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ທ່ານຍັງບໍ່ໄດ້ໃຊ້ອີໂມຈິໃດເທື່ອ"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
index 8c90dcb..1f11862 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lt/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"JAUSTUKAI IR EMOCIJOS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ŽMONĖS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"GYVŪNAI IR GAMTA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLIAI"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"VĖLIAVOS"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nėra jokių pasiekiamų jaustukų"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Dar nenaudojote jokių jaustukų"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
index c6e30c7..f5f63fb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-lv/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAIDIŅI UN EMOCIJAS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DZĪVNIEKI UN DABA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLI"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"KAROGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nav pieejamu emocijzīmju"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Jūs vēl neesat izmantojis nevienu emocijzīmi"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
index 85ac3d7..79693ce 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mk/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМЕШКОВЦИ И ЕМОТИКОНИ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ЛУЃЕ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТНИ И ПРИРОДА"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
index dd073d9..9dc3db3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ml/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"സ്മൈലികളും ഇമോഷനുകളും"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ആളുകൾ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"മൃഗങ്ങളും പ്രകൃതിയും"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ചിഹ്നങ്ങൾ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"പതാകകൾ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ഇമോജികളൊന്നും ലഭ്യമല്ല"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"നിങ്ങൾ ഇതുവരെ ഇമോജികളൊന്നും ഉപയോഗിച്ചിട്ടില്ല"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
index 53792c9..de3c4dc 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mn/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ИНЭЭМСЭГЛЭЛ БОЛОН СЭТГЭЛ ХӨДЛӨЛ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ХҮМҮҮС"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"АМЬТАД БА БАЙГАЛЬ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
index 82109e0..4af8d70 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-mr/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"स्मायली आणि भावना"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"लोक"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"प्राणी आणि निसर्ग"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"चिन्हे"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ध्वज"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कोणतेही इमोजी उपलब्ध नाहीत"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तुम्ही अद्याप कोणतेही इमोजी वापरलेले नाहीत"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
index 1462102..d59b51e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ms/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY DAN EMOSI"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ORANG"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAIWAN DAN ALAM SEMULA JADI"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
index 536c277..bd58d4c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-my/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"စမိုင်းလီနှင့် ခံစားချက်များ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"လူများ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"တိရစ္ဆာန်များနှင့် သဘာဝ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"သင်္ကေတများ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"အလံများ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"အီမိုဂျီ မရနိုင်ပါ"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"အီမိုဂျီ အသုံးမပြုသေးပါ"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
index f050c34..04a123b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nb/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEFJES OG UTTRYKKSIKONER"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DYR OG NATUR"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLER"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGG"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Ingen emojier er tilgjengelige"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ikke brukt noen emojier ennå"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
index 97739a0..163107c 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ne/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"स्माइली र भावनाहरू"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"मान्छेहरू"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"पशु र प्रकृति"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"चिन्हहरू"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"झन्डाहरू"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"कुनै पनि इमोजी उपलब्ध छैन"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"तपाईंले हालसम्म कुनै पनि इमोजी प्रयोग गर्नुभएको छैन"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
index 6c9d4c3..d00dbdd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-nl/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEYS EN EMOTIES"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"MENSEN"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DIEREN EN NATUUR"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLEN"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAGGEN"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Geen emoji\'s beschikbaar"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Je hebt nog geen emoji\'s gebruikt"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
index ae32bb3..0fea003 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-or/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ସ୍ମାଇଲି ଓ ଆବେଗଗୁଡ଼ିକ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ଲୋକମାନେ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ଜୀବଜନ୍ତୁ ଓ ପ୍ରକୃତି"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
index 72e0b6a..40520be 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pa/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ਸਮਾਈਲੀ ਅਤੇ ਜਜ਼ਬਾਤ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ਲੋਕ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ਜਾਨਵਰ ਅਤੇ ਕੁਦਰਤ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ਚਿੰਨ੍ਹ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ਝੰਡੇ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ਕੋਈ ਇਮੋਜੀ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕਿਸੇ ਵੀ ਇਮੋਜੀ ਦੀ ਵਰਤੋਂ ਨਹੀਂ ਕੀਤੀ ਹੈ"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
index e4d8310..e07c706 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pl/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTIKONY"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"UCZESTNICY"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZWIERZĘTA I PRZYRODA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLE"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Brak dostępnych emotikonów"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Żadne emotikony nie zostały jeszcze użyte"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
index 07507fb..4a7a8d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rBR/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"CARINHAS E EMOTICONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
index e6e29b1..dfe0044 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt-rPT/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICONS E ÍCONES EXPRESSIVOS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
index 07507fb..4a7a8d4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-pt/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"CARINHAS E EMOTICONS"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PESSOAS"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMAIS E NATUREZA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
index 21686b6..b704db4 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ro/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"EMOTICOANE ȘI EMOȚII"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSOANE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ANIMALE ȘI NATURĂ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
index b7a9857..7e5756f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ru/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛИКИ И ЭМОЦИИ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДИ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ПРИРОДА И ЖИВОТНЫЕ"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛЫ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ФЛАГИ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Нет доступных эмодзи"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Вы ещё не использовали эмодзи"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
index e1bfcea..f0a6246 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-si/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"සිනාසීම් සහ චිත්තවේග"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"පුද්ගලයින්"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"සතුන් හා ස්වභාවධර්මය"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"සංකේත"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ධජ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ඉමොජි කිසිවක් නොලැබේ"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"ඔබ තවමත් කිසිදු ඉමෝජියක් භාවිතා කර නැත"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
index 3a557ab..ae90524 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sk/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMAJLÍKY A EMOTIKONY"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ĽUDIA"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ZVIERATÁ A PRÍRODA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLY"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"VLAJKY"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nie sú k dispozícii žiadne emodži"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Zatiaľ ste nepoužili žiadne emodži"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
index 91261f4..3fab8dd 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sl/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ČUSTVENI SIMBOLI IN ČUSTVA"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"OSEBE"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ŽIVALI IN NARAVA"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
index 532209d..9abcb71 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sq/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"BUZËQESHJE DHE EMOCIONE"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"NJERËZ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"KAFSHË DHE NATYRË"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SIMBOLE"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAMUJ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Nuk ofrohen emoji"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Nuk ke përdorur ende asnjë emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
index f899101..94321de 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sr/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЈЛИЈИ И ЕМОЦИЈЕ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ЉУДИ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ЖИВОТИЊЕ И ПРИРОДА"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
index 9ba51c6..9c8815d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sv/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"KÄNSLOIKONER OCH KÄNSLOR"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"PERSONER"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"DJUR OCH NATUR"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SYMBOLER"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"FLAGGOR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Inga emojier tillgängliga"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Du har ännu inte använt emojis"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
index a2b3651..0d6aeb9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-sw/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"VICHESHI NA HISIA"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"WATU"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"WANYAMA NA MAZINGIRA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"ISHARA"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BENDERA"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Hakuna emoji zinazopatikana"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bado hujatumia emoji zozote"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
index 6a24080..1ae568f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ta/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"ஸ்மைலிகளும் எமோடிகான்களும்"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"நபர்"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"விலங்குகளும் இயற்கையும்"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"சின்னங்கள்"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"கொடிகள்"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ஈமோஜிகள் எதுவுமில்லை"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"இதுவரை ஈமோஜி எதையும் நீங்கள் பயன்படுத்தவில்லை"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
index 228b252..115f983 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-te/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"స్మైలీలు, ఎమోషన్‌లు"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"వ్యక్తులు"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"జంతువులు, ప్రకృతి"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"గుర్తులు"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ఫ్లాగ్‌లు"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"ఎమోజీలు ఏవీ అందుబాటులో లేవు"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"మీరు ఇంకా ఎమోజీలు ఏవీ ఉపయోగించలేదు"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
index 4718adb..839d759 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-th/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"หน้ายิ้มและอารมณ์"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ผู้คน"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"สัตว์และธรรมชาติ"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
index d28f9b7..794701d 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tl/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"MGA SMILEY AT MGA EMOSYON"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"MGA TAO"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"MGA HAYOP AT KALIKASAN"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
index f3dd488..a27d6b2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-tr/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"SMILEY\'LER VE İFADELER"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"İNSANLAR"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAYVANLAR VE DOĞA"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"SEMBOLLER"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"BAYRAKLAR"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Kullanılabilir emoji yok"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Henüz emoji kullanmadınız"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
index a1d8ff6..ff9adf2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uk/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"СМАЙЛИКИ Й ЕМОЦІЇ"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ЛЮДИ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ТВАРИНИ ТА ПРИРОДА"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"СИМВОЛИ"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"ПРАПОРИ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Немає смайлів"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Ви ще не використовували смайли"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
index 212835f..110b3cb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-ur/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"اسمائلیز اور جذبات"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"لوگ"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"جانور اور قدرت"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"علامات"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"جھنڈے"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"کوئی بھی ایموجی دستیاب نہیں ہے"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"آپ نے ابھی تک کوئی بھی ایموجی استعمال نہیں کی ہے"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
index 02613bd..0b0feff 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-uz/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"KULGICH VA EMOJILAR"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ODAMLAR"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"HAYVONLAR VA TABIAT"</string>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
index baba3bc..41fef1b 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-vi/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"MẶT CƯỜI VÀ BIỂU TƯỢNG CẢM XÚC"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"MỌI NGƯỜI"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"ĐỘNG VẬT VÀ THIÊN NHIÊN"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"BIỂU TƯỢNG"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"CỜ"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Không có biểu tượng cảm xúc nào"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Bạn chưa sử dụng biểu tượng cảm xúc nào"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
index 385b281..2761523 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rCN/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"表情符号"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"动物和自然"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"符号"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗帜"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"没有可用的表情符号"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用过任何表情符号"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
index 68206d5..b1eabfb 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rHK/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"表情符號"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物和大自然"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"符號"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的 Emoji"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"您尚未使用任何 Emoji"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
index 7892cef..fb39886 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zh-rTW/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"表情符號"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"人物"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"動物與大自然"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"符號"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"旗幟"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"沒有可用的表情符號"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"你尚未使用任何表情符號"</string>
 </resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
index 6c422c9..2fbc5e6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values-zu/strings.xml
@@ -17,6 +17,8 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- no translation found for emoji_category_recent (7142376595414250279) -->
+    <skip />
     <string name="emoji_category_emotions" msgid="1570830970240985537">"AMASMAYILI NEMIZWA"</string>
     <string name="emoji_category_people" msgid="7968173366822927025">"ABANTU"</string>
     <string name="emoji_category_animals_nature" msgid="4640771324837307541">"IZILWANE NENDALO"</string>
@@ -27,6 +29,5 @@
     <string name="emoji_category_symbols" msgid="5626171724310261787">"AMASIMBULI"</string>
     <string name="emoji_category_flags" msgid="6185639503532784871">"AMAFULEGI"</string>
     <string name="emoji_empty_non_recent_category" msgid="288822832574892625">"Awekho ama-emoji atholakalayo"</string>
-    <!-- no translation found for emoji_empty_recent_category (7863877827879290200) -->
-    <skip />
+    <string name="emoji_empty_recent_category" msgid="7863877827879290200">"Awukasebenzisi noma yimaphi ama-emoji okwamanje"</string>
 </resources>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 11e4050..d7c6e4d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -194,10 +194,10 @@
 okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
 playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
 playCore = { module = "com.google.android.play:core", version = "1.10.3" }
-playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.3.0"}
+playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.4.0"}
 playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
 playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
-playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0-beta"}
+playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0"}
 playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
 paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
 paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
index f655aec..f5dc978 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
@@ -25,7 +25,7 @@
 import androidx.annotation.RestrictTo;
 
 /**
- * Simple interface for predicting ink points.
+ * Simple interface for predicting motion points.
  *
  * @hide
  */
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index f5929e0..415e2da 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -117,7 +117,8 @@
         final int pointerCount = mPredictorMap.size();
         // Shortcut for likely case where only zero or one pointer is on the screen
         // this logic exists only to make sure logic when one pointer is on screen then
-        // there is no performance degradation of using MultiPointerPredictor vs KalmanInkPredictor
+        // there is no performance degradation of using MultiPointerPredictor vs
+        // SinglePointerPredictor
         // TODO: verify performance is not degraded by removing this shortcut logic.
         if (pointerCount == 0) {
             if (DEBUG_PREDICTION) {
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
index 0656734..db4ba103 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
@@ -35,7 +35,7 @@
  */
 @RestrictTo(LIBRARY)
 public class SinglePointerPredictor implements KalmanPredictor {
-    private static final String TAG = "KalmanInkPredictor";
+    private static final String TAG = "SinglePointerPredictor";
 
     // Influence of jank during each prediction sample
     private static final float JANK_INFLUENCE = 0.1f;
@@ -86,7 +86,7 @@
     private double mPressure = 0;
 
     /**
-     * Kalman based ink predictor, predicting the location of the pen `predictionTarget`
+     * Kalman based predictor, predicting the location of the pen `predictionTarget`
      * milliseconds into the future.
      *
      * <p>This filter can provide solid prediction up to 25ms into the future. If you are not
diff --git a/libraryversions.toml b/libraryversions.toml
index 23d4145..9215b18 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -9,7 +9,7 @@
 ARCH_CORE = "2.2.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.2.0-beta02"
-BENCHMARK = "1.2.0-alpha07"
+BENCHMARK = "1.2.0-alpha08"
 BIOMETRIC = "1.2.0-alpha06"
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.5.0-alpha02"
@@ -45,7 +45,7 @@
 CURSORADAPTER = "1.1.0-alpha01"
 CUSTOMVIEW = "1.2.0-alpha03"
 CUSTOMVIEW_POOLINGCONTAINER = "1.1.0-alpha01"
-DATASTORE = "1.1.0-alpha01"
+DATASTORE = "1.1.0-alpha02"
 DATASTORE_KMP = "1.1.0-dev01"
 DOCUMENTFILE = "1.1.0-alpha02"
 DRAGANDDROP = "1.1.0-alpha01"
@@ -85,7 +85,7 @@
 MEDIA2 = "1.3.0-alpha01"
 MEDIAROUTER = "1.4.0-alpha02"
 METRICS = "1.0.0-alpha04"
-NAVIGATION = "2.6.0-alpha04"
+NAVIGATION = "2.6.0-alpha05"
 PAGING = "3.2.0-alpha04"
 PAGING_COMPOSE = "1.0.0-alpha18"
 PALETTE = "1.1.0-alpha01"
@@ -95,7 +95,7 @@
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
 PRIVACYSANDBOX_TOOLS = "1.0.0-alpha02"
 PRIVACYSANDBOX_UI = "1.0.0-alpha01"
-PROFILEINSTALLER = "1.3.0-alpha02"
+PROFILEINSTALLER = "1.3.0-alpha03"
 RECOMMENDATION = "1.1.0-alpha01"
 RECYCLERVIEW = "1.4.0-alpha01"
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
@@ -103,7 +103,7 @@
 RESOURCEINSPECTION = "1.1.0-alpha01"
 ROOM = "2.6.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
-SECURITY = "1.1.0-alpha04"
+SECURITY = "1.1.0-alpha05"
 SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
 SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
 SECURITY_BIOMETRIC = "1.0.0-alpha01"
@@ -123,7 +123,7 @@
 TEST_UIAUTOMATOR = "2.3.0-alpha02"
 TEXT = "1.0.0-alpha01"
 TRACING = "1.2.0-alpha02"
-TRACING_PERFETTO = "1.0.0-alpha07"
+TRACING_PERFETTO = "1.0.0-alpha08"
 TRANSITION = "1.5.0-alpha01"
 TV = "1.0.0-alpha03"
 TVPROVIDER = "1.1.0-alpha02"
@@ -140,6 +140,7 @@
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha01"
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
+WEAR_PROTOLAYOUT = "1.0.0-alpha01"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
 WEAR_TILES = "1.2.0-alpha01"
 WEAR_WATCHFACE = "1.2.0-alpha05"
@@ -254,6 +255,7 @@
 VIEWPAGER2 = { group = "androidx.viewpager2", atomicGroupVersion = "versions.VIEWPAGER2" }
 WEAR = { group = "androidx.wear" }
 WEAR_COMPOSE = { group = "androidx.wear.compose", atomicGroupVersion = "versions.WEAR_COMPOSE" }
+WEAR_PROTOLAYOUT = { group = "androidx.wear.protolayout", atomicGroupVersion = "versions.WEAR_PROTOLAYOUT" }
 WEAR_TILES = { group = "androidx.wear.tiles", atomicGroupVersion = "versions.WEAR_TILES" }
 WEAR_WATCHFACE = { group = "androidx.wear.watchface", atomicGroupVersion = "versions.WEAR_WATCHFACE" }
 WEBKIT = { group = "androidx.webkit", atomicGroupVersion = "versions.WEBKIT" }
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 25dd589..c9e8b12 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -17,7 +17,7 @@
 package androidx.privacysandbox.tools.apicompiler.generator
 
 import androidx.privacysandbox.tools.core.generator.build
-import androidx.privacysandbox.tools.core.generator.poetSpec
+import androidx.privacysandbox.tools.core.generator.poetTypeName
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.getOnlyService
@@ -77,7 +77,7 @@
         return FunSpec.builder(createServiceFunctionName(service))
             .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
             .addParameter("context", contextClass)
-            .returns(service.type.poetSpec())
+            .returns(service.type.poetTypeName())
             .build()
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
index 454d7b8..71b9761 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -52,9 +52,19 @@
             "com/mysdk/IResponseTransactionCallback.java",
             "com/mysdk/IStringTransactionCallback.java",
             "com/mysdk/IUnitTransactionCallback.java",
+            "com/mysdk/IListResponseTransactionCallback.java",
+            "com/mysdk/IListIntTransactionCallback.java",
+            "com/mysdk/IListLongTransactionCallback.java",
+            "com/mysdk/IListDoubleTransactionCallback.java",
+            "com/mysdk/IListStringTransactionCallback.java",
+            "com/mysdk/IListBooleanTransactionCallback.java",
+            "com/mysdk/IListFloatTransactionCallback.java",
+            "com/mysdk/IListCharTransactionCallback.java",
+            "com/mysdk/IListShortTransactionCallback.java",
             "com/mysdk/ParcelableRequest.java",
             "com/mysdk/ParcelableResponse.java",
             "com/mysdk/ParcelableStackFrame.java",
+            "com/mysdk/ParcelableInnerValue.java",
             "com/mysdk/PrivacySandboxThrowableParcel.java",
         )
         assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
index 94ec579..1ba71b6 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
@@ -35,11 +35,33 @@
 
 @PrivacySandboxInterface
 interface MySecondInterface {
-    fun doMoreStuff(x: Int)
+    suspend fun doIntStuff(x: List<Int>): List<Int>
+
+    suspend fun doCharStuff(x: List<Char>): List<Char>
+
+    suspend fun doFloatStuff(x: List<Float>): List<Float>
+
+    suspend fun doLongStuff(x: List<Long>): List<Long>
+
+    suspend fun doDoubleStuff(x: List<Double>): List<Double>
+
+    suspend fun doBooleanStuff(x: List<Boolean>): List<Boolean>
+
+    suspend fun doShortStuff(x: List<Short>): List<Short>
+
+    suspend fun doStringStuff(x: List<String>): List<String>
+
+    suspend fun doValueStuff(x: List<Request>): List<Response>
 }
 
 @PrivacySandboxValue
-data class Request(val query: String, val myInterface: MyInterface)
+data class Request(
+    val query: String,
+    val extraValues: List<InnerValue>,
+    val myInterface: MyInterface)
+
+@PrivacySandboxValue
+data class InnerValue(val numbers: List<Int>)
 
 @PrivacySandboxValue
 data class Response(val response: String, val mySecondInterface: MySecondInterface)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt
new file mode 100644
index 0000000..3717090
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/InnerValueConverter.kt
@@ -0,0 +1,15 @@
+package com.mysdk
+
+public object InnerValueConverter {
+    public fun fromParcelable(parcelable: ParcelableInnerValue): InnerValue {
+        val annotatedValue = InnerValue(
+                numbers = parcelable.numbers.toList())
+        return annotatedValue
+    }
+
+    public fun toParcelable(annotatedValue: InnerValue): ParcelableInnerValue {
+        val parcelable = ParcelableInnerValue()
+        parcelable.numbers = annotatedValue.numbers.toIntArray()
+        return parcelable
+    }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
index a871395..494c80c 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
@@ -1,12 +1,167 @@
 package com.mysdk
 
-import kotlin.Int
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
+import com.mysdk.RequestConverter.fromParcelable
+import com.mysdk.ResponseConverter.toParcelable
+import kotlin.Array
+import kotlin.BooleanArray
+import kotlin.CharArray
+import kotlin.DoubleArray
+import kotlin.FloatArray
+import kotlin.IntArray
+import kotlin.LongArray
+import kotlin.String
 import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
 
 public class MySecondInterfaceStubDelegate internal constructor(
   public val `delegate`: MySecondInterface,
 ) : IMySecondInterface.Stub() {
-  public override fun doMoreStuff(x: Int): Unit {
-    delegate.doMoreStuff(x)
+  public override fun doIntStuff(x: IntArray, transactionCallback: IListIntTransactionCallback):
+      Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doIntStuff(x.toList())
+        transactionCallback.onSuccess(result.toIntArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doCharStuff(x: CharArray, transactionCallback: IListCharTransactionCallback):
+      Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doCharStuff(x.toList())
+        transactionCallback.onSuccess(result.toCharArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doFloatStuff(x: FloatArray,
+      transactionCallback: IListFloatTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doFloatStuff(x.toList())
+        transactionCallback.onSuccess(result.toFloatArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doLongStuff(x: LongArray, transactionCallback: IListLongTransactionCallback):
+      Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doLongStuff(x.toList())
+        transactionCallback.onSuccess(result.toLongArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doDoubleStuff(x: DoubleArray,
+      transactionCallback: IListDoubleTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doDoubleStuff(x.toList())
+        transactionCallback.onSuccess(result.toDoubleArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doBooleanStuff(x: BooleanArray,
+      transactionCallback: IListBooleanTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doBooleanStuff(x.toList())
+        transactionCallback.onSuccess(result.toBooleanArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doShortStuff(x: IntArray, transactionCallback: IListShortTransactionCallback):
+      Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doShortStuff(x.map { it.toShort() }.toList())
+        transactionCallback.onSuccess(result.map { it.toInt() }.toIntArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doStringStuff(x: Array<String>,
+      transactionCallback: IListStringTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doStringStuff(x.toList())
+        transactionCallback.onSuccess(result.toTypedArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
+  }
+
+  public override fun doValueStuff(x: Array<ParcelableRequest>,
+      transactionCallback: IListResponseTransactionCallback): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doValueStuff(x.map { fromParcelable(it) }.toList())
+        transactionCallback.onSuccess(result.map { toParcelable(it) }.toTypedArray())
+      }
+      catch (t: Throwable) {
+        transactionCallback.onFailure(toThrowableParcel(t))
+      }
+    }
+    val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+    transactionCallback.onCancellable(cancellationSignal)
   }
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
index 538f1cd..da8b7cf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
@@ -4,6 +4,8 @@
     public fun fromParcelable(parcelable: ParcelableRequest): Request {
         val annotatedValue = Request(
                 query = parcelable.query,
+                extraValues = parcelable.extraValues.map {
+                        com.mysdk.InnerValueConverter.fromParcelable(it) }.toList(),
                 myInterface = (parcelable.myInterface as MyInterfaceStubDelegate).delegate)
         return annotatedValue
     }
@@ -11,6 +13,8 @@
     public fun toParcelable(annotatedValue: Request): ParcelableRequest {
         val parcelable = ParcelableRequest()
         parcelable.query = annotatedValue.query
+        parcelable.extraValues = annotatedValue.extraValues.map {
+                com.mysdk.InnerValueConverter.toParcelable(it) }.toTypedArray()
         parcelable.myInterface = MyInterfaceStubDelegate(annotatedValue.myInterface)
         return parcelable
     }
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
index 5394501..02ecc7d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/InterfaceFileGenerator.kt
@@ -20,17 +20,19 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.poetClassName
 import androidx.privacysandbox.tools.core.generator.poetSpec
+import androidx.privacysandbox.tools.core.generator.poetTypeName
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.TypeSpec
 
-internal class InterfaceFileGenerator() {
+internal class InterfaceFileGenerator {
 
     fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
         val annotatedInterfaceType =
-            TypeSpec.interfaceBuilder(annotatedInterface.type.poetSpec()).build {
+            TypeSpec.interfaceBuilder(annotatedInterface.type.poetClassName()).build {
                 addFunctions(annotatedInterface.methods.map(::generateInterfaceMethod))
             }
 
@@ -47,6 +49,6 @@
                 addModifiers(KModifier.SUSPEND)
             }
             addParameters(method.parameters.map { it.poetSpec() })
-            returns(method.returnType.poetSpec())
+            returns(method.returnType.poetTypeName())
         }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
index 955a26b..7437b18 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
@@ -29,6 +29,14 @@
         "com/mysdk/IBooleanTransactionCallback.java",
         "com/mysdk/ICancellationSignal.java",
         "com/mysdk/IUnitTransactionCallback.java",
+        "com/mysdk/IListLongTransactionCallback.java",
+        "com/mysdk/IListDoubleTransactionCallback.java",
+        "com/mysdk/IListShortTransactionCallback.java",
+        "com/mysdk/IListStringTransactionCallback.java",
+        "com/mysdk/IListBooleanTransactionCallback.java",
+        "com/mysdk/IListFloatTransactionCallback.java",
+        "com/mysdk/IListCharTransactionCallback.java",
+        "com/mysdk/IListIntTransactionCallback.java",
         "com/mysdk/ParcelableStackFrame.java",
         "com/mysdk/PrivacySandboxThrowableParcel.java",
     )
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
index 6eb5997..c729c44 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
@@ -28,6 +28,7 @@
         "com/sdkwithvalues/IMyInterface.java",
         "com/sdkwithvalues/ISdkInterface.java",
         "com/sdkwithvalues/ISdkResponseTransactionCallback.java",
+        "com/sdkwithvalues/IListSdkResponseTransactionCallback.java",
         "com/sdkwithvalues/ParcelableInnerSdkValue.java",
         "com/sdkwithvalues/ParcelableSdkRequest.java",
         "com/sdkwithvalues/ParcelableSdkResponse.java",
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
index 666c0c3..4ba4d45 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/input/com/mysdk/TestSandboxSdk.kt
@@ -25,4 +25,20 @@
     suspend fun doSomethingAsync(first: Int, second: String, third: Long): Boolean
 
     suspend fun receiveAndReturnNothingAsync()
+
+    suspend fun processIntList(x: List<Int>): List<Int>
+
+    suspend fun processCharList(x: List<Char>): List<Char>
+
+    suspend fun processFloatList(x: List<Float>): List<Float>
+
+    suspend fun processLongList(x: List<Long>): List<Long>
+
+    suspend fun processDoubleList(x: List<Double>): List<Double>
+
+    suspend fun processBooleanList(x: List<Boolean>): List<Boolean>
+
+    suspend fun processShortList(x: List<Short>): List<Short>
+
+    suspend fun processStringList(x: List<String>): List<String>
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
index c6132b1..e63fc50 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdk.kt
@@ -21,6 +21,22 @@
 
     public fun echoString(input: String): Unit
 
+    public suspend fun processBooleanList(x: List<Boolean>): List<Boolean>
+
+    public suspend fun processCharList(x: List<Char>): List<Char>
+
+    public suspend fun processDoubleList(x: List<Double>): List<Double>
+
+    public suspend fun processFloatList(x: List<Float>): List<Float>
+
+    public suspend fun processIntList(x: List<Int>): List<Int>
+
+    public suspend fun processLongList(x: List<Long>): List<Long>
+
+    public suspend fun processShortList(x: List<Short>): List<Short>
+
+    public suspend fun processStringList(x: List<String>): List<String>
+
     public fun receiveAndReturnNothing(): Unit
 
     public suspend fun receiveAndReturnNothingAsync(): Unit
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
index 2d5b21d..6a52938 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
@@ -62,6 +62,190 @@
         remote.echoString(input)
     }
 
+    public override suspend fun processBooleanList(x: List<Boolean>): List<Boolean> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListBooleanTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: BooleanArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processBooleanList(x.toBooleanArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processCharList(x: List<Char>): List<Char> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListCharTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: CharArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processCharList(x.toCharArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processDoubleList(x: List<Double>): List<Double> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListDoubleTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: DoubleArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processDoubleList(x.toDoubleArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processFloatList(x: List<Float>): List<Float> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListFloatTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: FloatArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processFloatList(x.toFloatArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processIntList(x: List<Int>): List<Int> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListIntTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: IntArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processIntList(x.toIntArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processLongList(x: List<Long>): List<Long> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListLongTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: LongArray) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processLongList(x.toLongArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processShortList(x: List<Short>): List<Short> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListShortTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: IntArray) {
+                it.resumeWith(Result.success(result.map { it.toShort() }.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processShortList(x.map { it.toInt() }.toIntArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
+    public override suspend fun processStringList(x: List<String>): List<String> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListStringTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: Array<String>) {
+                it.resumeWith(Result.success(result.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processStringList(x.toTypedArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
+
     public override fun receiveAndReturnNothing(): Unit {
         remote.receiveAndReturnNothing()
     }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
index 6265564..4781b61 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
@@ -7,6 +7,8 @@
 @PrivacySandboxService
 interface SdkInterface {
     suspend fun exampleMethod(request: SdkRequest): SdkResponse
+
+    suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse>
 }
 
 @PrivacySandboxValue
@@ -19,10 +21,15 @@
     val floatingPoint: Float,
     val hugeNumber: Double,
     val myInterface: MyInterface,
+    val numbers: List<Int>,
 )
 
 @PrivacySandboxValue
-data class SdkRequest(val id: Long, val innerValue: InnerSdkValue)
+data class SdkRequest(
+    val id: Long,
+    val innerValue: InnerSdkValue,
+    val moreValues: List<InnerSdkValue>
+)
 
 @PrivacySandboxValue
 data class SdkResponse(val success: Boolean, val originalRequest: SdkRequest)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
index dc6d855..4b99a0b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
@@ -7,6 +7,7 @@
     public val id: Int,
     public val message: String,
     public val myInterface: MyInterface,
+    public val numbers: List<Int>,
     public val separator: Char,
     public val shouldBeAwesome: Boolean,
 )
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
index ffa0b12..71ffd8a 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
@@ -9,6 +9,7 @@
                 id = parcelable.id,
                 message = parcelable.message,
                 myInterface = MyInterfaceClientProxy(parcelable.myInterface),
+                numbers = parcelable.numbers.toList(),
                 separator = parcelable.separator,
                 shouldBeAwesome = parcelable.shouldBeAwesome)
         return annotatedValue
@@ -22,6 +23,7 @@
         parcelable.id = annotatedValue.id
         parcelable.message = annotatedValue.message
         parcelable.myInterface = (annotatedValue.myInterface as MyInterfaceClientProxy).remote
+        parcelable.numbers = annotatedValue.numbers.toIntArray()
         parcelable.separator = annotatedValue.separator
         parcelable.shouldBeAwesome = annotatedValue.shouldBeAwesome
         return parcelable
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
index 2912b22..89d0d9b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterface.kt
@@ -2,4 +2,6 @@
 
 public interface SdkInterface {
     public suspend fun exampleMethod(request: SdkRequest): SdkResponse
+
+    public suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse>
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
index 1d6fada..ae8358f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
@@ -1,7 +1,10 @@
 package com.sdkwithvalues
 
+import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter
 import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import com.sdkwithvalues.SdkRequestConverter
 import com.sdkwithvalues.SdkRequestConverter.toParcelable
+import com.sdkwithvalues.SdkResponseConverter
 import com.sdkwithvalues.SdkResponseConverter.fromParcelable
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -31,4 +34,27 @@
             mCancellationSignal?.cancel()
         }
     }
+
+    public override suspend fun processValueList(x: List<SdkRequest>): List<SdkResponse> =
+            suspendCancellableCoroutine {
+        var mCancellationSignal: ICancellationSignal? = null
+        val transactionCallback = object: IListSdkResponseTransactionCallback.Stub() {
+            override fun onCancellable(cancellationSignal: ICancellationSignal) {
+                if (it.isCancelled) {
+                    cancellationSignal.cancel()
+                }
+                mCancellationSignal = cancellationSignal
+            }
+            override fun onSuccess(result: Array<ParcelableSdkResponse>) {
+                it.resumeWith(Result.success(result.map { fromParcelable(it) }.toList()))
+            }
+            override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+                it.resumeWithException(fromThrowableParcel(throwableParcel))
+            }
+        }
+        remote.processValueList(x.map { toParcelable(it) }.toTypedArray(), transactionCallback)
+        it.invokeOnCancellation {
+            mCancellationSignal?.cancel()
+        }
+    }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
index 8909acc..3d82a7b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequest.kt
@@ -3,4 +3,5 @@
 public data class SdkRequest(
     public val id: Long,
     public val innerValue: InnerSdkValue,
+    public val moreValues: List<InnerSdkValue>,
 )
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
index 4ada52a..a652ad2 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkRequestConverter.kt
@@ -5,7 +5,9 @@
         val annotatedValue = SdkRequest(
                 id = parcelable.id,
                 innerValue =
-                        com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(parcelable.innerValue))
+                        com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(parcelable.innerValue),
+                moreValues = parcelable.moreValues.map {
+                        com.sdkwithvalues.InnerSdkValueConverter.fromParcelable(it) }.toList())
         return annotatedValue
     }
 
@@ -14,6 +16,8 @@
         parcelable.id = annotatedValue.id
         parcelable.innerValue =
                 com.sdkwithvalues.InnerSdkValueConverter.toParcelable(annotatedValue.innerValue)
+        parcelable.moreValues = annotatedValue.moreValues.map {
+                com.sdkwithvalues.InnerSdkValueConverter.toParcelable(it) }.toTypedArray()
         return parcelable
     }
 }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
index 1df8157..650c3ca 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
@@ -123,10 +123,11 @@
         check(parameter.type != Types.unit) {
             "Void cannot be a parameter type."
         }
+        val aidlType = getAidlTypeDeclaration(parameter.type)
         addParameter(
             parameter.name,
-            getAidlTypeDeclaration(parameter.type),
-            isIn = api.valueMap.containsKey(parameter.type)
+            aidlType,
+            isIn = api.valueMap.containsKey(parameter.type) || aidlType.isList
         )
     }
 
@@ -229,6 +230,7 @@
             // TODO: AIDL doesn't support short, make sure it's handled correctly.
             Short::class.qualifiedName -> primitive("int")
             Unit::class.qualifiedName -> primitive("void")
+            List::class.qualifiedName -> getAidlTypeDeclaration(type.typeParameters[0]).listSpec()
             else -> throw IllegalArgumentException(
                 "Unsupported type conversion ${type.qualifiedName}"
             )
@@ -249,7 +251,8 @@
 
 fun AnnotatedInterface.aidlName() = "I${type.simpleName}"
 
-fun Type.transactionCallbackName() = "I${simpleName}TransactionCallback"
+fun Type.transactionCallbackName() =
+    "I${simpleName}${typeParameters.joinToString("") { it.simpleName }}TransactionCallback"
 
 internal fun AnnotatedValue.aidlType() =
     AidlTypeSpec(Type(type.packageName, "Parcelable${type.simpleName}"))
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
index c791c9c..4d287b3 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -22,6 +22,8 @@
 import androidx.privacysandbox.tools.core.model.Types
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.TypeName
 
 /** Utility to generate [CodeBlock]s that convert values to/from their binder equivalent. */
 abstract class BinderCodeConverter(private val api: ParsedApi) {
@@ -48,6 +50,21 @@
         if (sandboxInterface != null) {
             return convertToInterfaceModelCode(sandboxInterface, expression)
         }
+        if (type.qualifiedName == List::class.qualifiedName) {
+            val convertToModelCodeBlock = convertToModelCode(type.typeParameters[0], "it")
+            return CodeBlock.of(
+                "%L%L.toList()",
+                expression,
+                // Only convert the list elements if necessary.
+                if (convertToModelCodeBlock == CodeBlock.of("it"))
+                    CodeBlock.of("")
+                else
+                    CodeBlock.of(".map { %L }", convertToModelCodeBlock)
+            )
+        }
+        if (type == Types.short) {
+            return CodeBlock.of("%L.toShort()", expression)
+        }
         return CodeBlock.of(expression)
     }
 
@@ -79,28 +96,70 @@
         if (sandboxInterface != null) {
             return convertToInterfaceBinderCode(sandboxInterface, expression)
         }
+        if (type.qualifiedName == List::class.qualifiedName) {
+            val convertToBinderCodeBlock = convertToBinderCode(type.typeParameters[0], "it")
+            return CodeBlock.of(
+                "%L%L.%L()",
+                expression,
+                // Only convert the list elements if necessary.
+                if (convertToBinderCodeBlock == CodeBlock.of("it"))
+                    CodeBlock.of("")
+                else
+                    CodeBlock.of(".map { %L }", convertToBinderCodeBlock),
+                toBinderList(type.typeParameters[0])
+            )
+        }
+        if (type == Types.short) {
+            return CodeBlock.of("%L.toInt()", expression)
+        }
         return CodeBlock.of(expression)
     }
 
+    private fun toBinderList(type: Type) = when (type) {
+        Types.boolean -> "toBooleanArray"
+        Types.int -> "toIntArray"
+        Types.long -> "toLongArray"
+        Types.short -> "toIntArray"
+        Types.float -> "toFloatArray"
+        Types.double -> "toDoubleArray"
+        Types.char -> "toCharArray"
+        else -> "toTypedArray"
+    }
+
     protected abstract fun convertToInterfaceBinderCode(
         annotatedInterface: AnnotatedInterface,
         expression: String
     ): CodeBlock
 
     /** Convert the given model type declaration to its binder equivalent. */
-    fun convertToBinderType(type: Type): ClassName {
+    fun convertToBinderType(type: Type): TypeName {
         val value = api.valueMap[type]
         if (value != null) {
             return value.parcelableNameSpec()
         }
         val callback = api.callbackMap[type]
         if (callback != null) {
-            return callback.aidlType().innerType.poetSpec()
+            return callback.aidlType().innerType.poetTypeName()
         }
         val sandboxInterface = api.interfaceMap[type]
         if (sandboxInterface != null) {
-            return sandboxInterface.aidlType().innerType.poetSpec()
+            return sandboxInterface.aidlType().innerType.poetTypeName()
         }
-        return type.poetSpec()
+        if (type.qualifiedName == List::class.qualifiedName)
+            return convertToBinderListType(type)
+        return type.poetTypeName()
     }
+
+    private fun convertToBinderListType(type: Type): TypeName =
+        when (type.typeParameters[0]) {
+            Types.boolean -> ClassName("kotlin", "BooleanArray")
+            Types.int -> ClassName("kotlin", "IntArray")
+            Types.long -> ClassName("kotlin", "LongArray")
+            Types.short -> ClassName("kotlin", "IntArray")
+            Types.float -> ClassName("kotlin", "FloatArray")
+            Types.double -> ClassName("kotlin", "DoubleArray")
+            Types.char -> ClassName("kotlin", "CharArray")
+            else -> ClassName("kotlin", "Array")
+                .parameterizedBy(convertToBinderType(type.typeParameters[0]))
+        }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 97472c9..d224852 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -39,10 +39,10 @@
 
     fun generate(annotatedInterface: AnnotatedInterface): FileSpec {
         val className = annotatedInterface.clientProxyNameSpec().simpleName
-        val remoteBinderClassName = annotatedInterface.aidlType().innerType.poetSpec()
+        val remoteBinderClassName = annotatedInterface.aidlType().innerType.poetTypeName()
 
         val classSpec = TypeSpec.classBuilder(className).build {
-            addSuperinterface(annotatedInterface.type.poetSpec())
+            addSuperinterface(annotatedInterface.type.poetTypeName())
 
             primaryConstructor(
                 listOf(
@@ -70,7 +70,7 @@
             addModifiers(KModifier.OVERRIDE)
             addModifiers(KModifier.SUSPEND)
             addParameters(method.parameters.map { it.poetSpec() })
-            returns(method.returnType.poetSpec())
+            returns(method.returnType.poetTypeName())
 
             addCode {
                 addControlFlow("return %M", suspendCancellableCoroutineMethod) {
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index bb26bbd..8eb7838 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -27,17 +27,27 @@
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
 
 /** [ParameterSpec] equivalent to this parameter. */
 fun Parameter.poetSpec(): ParameterSpec {
-    return ParameterSpec.builder(name, type.poetSpec()).build()
+    return ParameterSpec.builder(name, type.poetTypeName()).build()
 }
 
-/** [TypeName] equivalent to this parameter. */
-fun Type.poetSpec() = ClassName(packageName, simpleName)
+/** [TypeName] equivalent to this type. */
+fun Type.poetTypeName(): TypeName {
+    val className = ClassName(packageName, simpleName)
+    if (typeParameters.isEmpty()) {
+        return className
+    }
+    return className.parameterizedBy(typeParameters.map { it.poetTypeName() })
+}
+
+/** [ClassName] equivalent to this type. */
+fun Type.poetClassName() = ClassName(packageName, simpleName)
 
 fun AnnotatedValue.converterNameSpec() =
     ClassName(type.packageName, "${type.simpleName}Converter")
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
index e94ab33..ec416a1 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
@@ -48,7 +48,7 @@
                 listOf(
                     PropertySpec.builder(
                         "delegate",
-                        annotatedInterface.type.poetSpec(),
+                        annotatedInterface.type.poetTypeName(),
                     ).addModifiers(KModifier.PUBLIC).build()
                 ), KModifier.INTERNAL
             )
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
index 0b5ced9..ad6f5f4 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
@@ -47,7 +47,7 @@
 
     private fun generateToParcelable(value: AnnotatedValue) =
         FunSpec.builder(value.toParcelableNameSpec().simpleName).build {
-            addParameter("annotatedValue", value.type.poetSpec())
+            addParameter("annotatedValue", value.type.poetTypeName())
             returns(value.parcelableNameSpec())
             addStatement("val parcelable = %T()", value.parcelableNameSpec())
             value.properties.map(::generateToParcelablePropertyConversion).forEach(::addCode)
@@ -68,10 +68,10 @@
     private fun generateFromParcelable(value: AnnotatedValue) =
         FunSpec.builder(value.fromParcelableNameSpec().simpleName).build {
             addParameter("parcelable", value.parcelableNameSpec())
-            returns(value.type.poetSpec())
+            returns(value.type.poetTypeName())
             val parameters = value.properties.map(::generateFromParcelablePropertyConversion)
             addStatement {
-                add("val annotatedValue = %T(\n", value.type.poetSpec())
+                add("val annotatedValue = %T(\n", value.type.poetTypeName())
                 add(parameters.joinToCode(separator = ",\n"))
                 add(")")
             }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
index 9536140..79055e7 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueFileGenerator.kt
@@ -25,7 +25,7 @@
 /**
  * Generates a file that defines a previously declared SDK value.
  */
-class ValueFileGenerator() {
+class ValueFileGenerator {
     fun generate(value: AnnotatedValue) =
         FileSpec.builder(value.type.packageName, value.type.simpleName).build {
             addCommonSettings()
@@ -33,10 +33,10 @@
         }
 
     private fun generateValue(value: AnnotatedValue) =
-        TypeSpec.classBuilder(value.type.poetSpec()).build {
+        TypeSpec.classBuilder(value.type.poetClassName()).build {
             addModifiers(KModifier.DATA)
             primaryConstructor(value.properties.map {
-                PropertySpec.builder(it.name, it.type.poetSpec())
+                PropertySpec.builder(it.name, it.type.poetTypeName())
                     .mutable(false)
                     .build()
             })
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
index aeee60f..adf1862 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
@@ -24,4 +24,10 @@
     val isList: Boolean = false
 ) {
     override fun toString() = innerType.simpleName + if (isList) "[]" else ""
+
+    /** Returns a new type spec representing a list of this type. */
+    fun listSpec(): AidlTypeSpec {
+        require(!isList) { "Nested lists are not supported." }
+        return AidlTypeSpec(innerType, requiresImport, isList = true)
+    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
index aeb2f9a..b41628c 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
@@ -59,11 +59,17 @@
                             isSuspend = true,
                         ),
                         Method(
+                            name = "suspendMethodWithLists",
+                            parameters = listOf(Parameter("l", Types.list(Types.int))),
+                            returnType = Types.list(Types.string),
+                            isSuspend = true,
+                        ),
+                        Method(
                             name = "methodWithoutReturnValue",
                             parameters = listOf(),
                             returnType = Types.unit,
                             isSuspend = false,
-                        )
+                        ),
                     )
                 )
             )
@@ -73,6 +79,7 @@
         assertThat(javaGeneratedSources.map { it.packageName to it.interfaceName })
             .containsExactly(
                 "com.mysdk" to "IMySdk",
+                "com.mysdk" to "IListStringTransactionCallback",
                 "com.mysdk" to "IStringTransactionCallback",
                 "com.mysdk" to "IUnitTransactionCallback",
                 "com.mysdk" to "ICancellationSignal",
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
index 2b5ee15..48c98e3 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
@@ -47,7 +47,7 @@
             Type(packageName = "com.mysdk", simpleName = "OuterValue"),
             listOf(
                 ValueProperty("innerValue", innerValue.type),
-                ValueProperty("anotherInnerValue", innerValue.type),
+                ValueProperty("innerValueList", Types.list(innerValue.type)),
             )
         )
 
@@ -71,6 +71,14 @@
                             isSuspend = true,
                         ),
                         Method(
+                            name = "suspendMethodWithListsOfValues",
+                            parameters = listOf(
+                                Parameter("inputValues", Types.list(outerValue.type))
+                            ),
+                            returnType = Types.list(outerValue.type),
+                            isSuspend = true,
+                        ),
+                        Method(
                             name = "methodReceivingValue",
                             parameters = listOf(
                                 Parameter(
@@ -95,6 +103,7 @@
                 "com.mysdk" to "ParcelableInnerValue",
                 "com.mysdk" to "IUnitTransactionCallback",
                 "com.mysdk" to "IOuterValueTransactionCallback",
+                "com.mysdk" to "IListOuterValueTransactionCallback",
                 "com.mysdk" to "ICancellationSignal",
                 "com.mysdk" to "PrivacySandboxThrowableParcel",
                 "com.mysdk" to "ParcelableStackFrame",
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl
new file mode 100644
index 0000000..c42fc32
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IListStringTransactionCallback.aidl
@@ -0,0 +1,10 @@
+package com.mysdk;
+
+import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
+
+oneway interface IListStringTransactionCallback {
+    void onCancellable(ICancellationSignal cancellationSignal);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
+    void onSuccess(in String[] result);
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
index 61d5f7f..1893e97 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IMySdk.aidl
@@ -1,10 +1,12 @@
 package com.mysdk;
 
+import com.mysdk.IListStringTransactionCallback;
 import com.mysdk.IStringTransactionCallback;
 import com.mysdk.IUnitTransactionCallback;
 
 oneway interface IMySdk {
     void methodWithoutReturnValue();
+    void suspendMethodWithLists(in int[] l, IListStringTransactionCallback transactionCallback);
     void suspendMethodWithReturnValue(boolean a, int b, long c, float d, double e, char f, int g, IStringTransactionCallback transactionCallback);
     void suspendMethodWithoutReturnValue(IUnitTransactionCallback transactionCallback);
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl
new file mode 100644
index 0000000..3ba8dc8
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IListOuterValueTransactionCallback.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ICancellationSignal;
+import com.mysdk.ParcelableOuterValue;
+import com.mysdk.PrivacySandboxThrowableParcel;
+
+oneway interface IListOuterValueTransactionCallback {
+    void onCancellable(ICancellationSignal cancellationSignal);
+    void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
+    void onSuccess(in ParcelableOuterValue[] result);
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
index 15b63d6..b4adfb2d 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IMySdk.aidl
@@ -1,5 +1,6 @@
 package com.mysdk;
 
+import com.mysdk.IListOuterValueTransactionCallback;
 import com.mysdk.IOuterValueTransactionCallback;
 import com.mysdk.IUnitTransactionCallback;
 import com.mysdk.ParcelableOuterValue;
@@ -8,4 +9,5 @@
     void methodReceivingValue(in ParcelableOuterValue value);
     void suspendMethodReceivingValue(in ParcelableOuterValue inputValue, IUnitTransactionCallback transactionCallback);
     void suspendMethodThatReturnsValue(IOuterValueTransactionCallback transactionCallback);
+    void suspendMethodWithListsOfValues(in ParcelableOuterValue[] inputValues, IListOuterValueTransactionCallback transactionCallback);
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
index 9627006..bd1851a 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableOuterValue.aidl
@@ -3,6 +3,6 @@
 import com.mysdk.ParcelableInnerValue;
 
 parcelable ParcelableOuterValue {
-    ParcelableInnerValue anotherInnerValue;
     ParcelableInnerValue innerValue;
+    ParcelableInnerValue[] innerValueList;
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index ffb490f..2969e3d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -155,11 +155,32 @@
     val JAVA_CLASS = XClassName.get("java.lang", "Class")
 }
 
-object GuavaBaseTypeNames {
+object GuavaTypeNames {
     val OPTIONAL = XClassName.get("com.google.common.base", "Optional")
-}
-
-object GuavaCollectionTypeNames {
+    val IMMUTABLE_MULTIMAP_BUILDER = XClassName.get(
+        "com.google.common.collect",
+        "ImmutableMultimap",
+        "Builder"
+    )
+    val IMMUTABLE_SET_MULTIMAP = XClassName.get(
+        "com.google.common.collect",
+        "ImmutableSetMultimap"
+    )
+    val IMMUTABLE_SET_MULTIMAP_BUILDER = XClassName.get(
+        "com.google.common.collect",
+        "ImmutableSetMultimap",
+        "Builder"
+    )
+    val IMMUTABLE_LIST_MULTIMAP = XClassName.get(
+        "com.google.common.collect",
+        "ImmutableListMultimap"
+    )
+    val IMMUTABLE_LIST_MULTIMAP_BUILDER = XClassName.get(
+        "com.google.common.collect",
+        "ImmutableListMultimap",
+        "Builder"
+    )
+    val IMMUTABLE_MAP = XClassName.get("com.google.common.collect", "ImmutableMap")
     val IMMUTABLE_LIST = XClassName.get("com.google.common.collect", "ImmutableList")
     val IMMUTABLE_LIST_BUILDER = XClassName.get(
         "com.google.common.collect",
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 69d5f4a..7846773 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -24,7 +24,7 @@
 import androidx.room.ext.CollectionTypeNames.INT_SPARSE_ARRAY
 import androidx.room.ext.CollectionTypeNames.LONG_SPARSE_ARRAY
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.ext.isByteBuffer
 import androidx.room.ext.isEntityElement
 import androidx.room.ext.isNotByte
@@ -121,7 +121,6 @@
 import com.google.common.collect.ImmutableMap
 import com.google.common.collect.ImmutableMultimap
 import com.google.common.collect.ImmutableSetMultimap
-import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.TypeName
 
 @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -487,7 +486,7 @@
         } else if (typeMirror.typeArguments.isEmpty()) {
             val rowAdapter = findRowAdapter(typeMirror, query) ?: return null
             return SingleItemQueryResultAdapter(rowAdapter)
-        } else if (typeMirror.rawType.asTypeName() == GuavaBaseTypeNames.OPTIONAL) {
+        } else if (typeMirror.rawType.asTypeName() == GuavaTypeNames.OPTIONAL) {
             // Handle Guava Optional by unpacking its generic type argument and adapting that.
             // The Optional adapter will reappend the Optional type.
             val typeArg = typeMirror.typeArguments.first()
@@ -559,9 +558,9 @@
             }
 
             val immutableClassName = if (typeMirror.isTypeOf(ImmutableListMultimap::class)) {
-                ClassName.get(ImmutableListMultimap::class.java)
+                GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP
             } else if (typeMirror.isTypeOf(ImmutableSetMultimap::class)) {
-                ClassName.get(ImmutableSetMultimap::class.java)
+                GuavaTypeNames.IMMUTABLE_SET_MULTIMAP
             } else {
                 // Return type is base class ImmutableMultimap which is not recommended.
                 context.logger.e(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
index 5b991a1..d57eb7d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -16,14 +16,13 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
 
 class GuavaImmutableMultimapQueryResultAdapter(
     context: Context,
@@ -32,12 +31,11 @@
     override val valueTypeArg: XType,
     private val keyRowAdapter: QueryMappedRowAdapter,
     private val valueRowAdapter: QueryMappedRowAdapter,
-    private val immutableClassName: ClassName,
+    private val immutableClassName: XClassName,
 ) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
-    private val mapType = ParameterizedTypeName.get(
-        immutableClassName,
-        keyTypeArg.typeName,
-        valueTypeArg.typeName
+    private val mapType = immutableClassName.parametrizedBy(
+        keyTypeArg.asTypeName(),
+        valueTypeArg.asTypeName()
     )
 
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
@@ -66,18 +64,41 @@
                     it.onCursorReady(cursorVarName = cursorVarName, scope = scope)
                 }
             }
-            addStatement(
-                "final $T.Builder<$T, $T> $L = $T.builder()",
-                immutableClassName,
-                keyTypeArg.typeName,
-                valueTypeArg.typeName,
-                mapVarName,
-                immutableClassName
+
+            val builderClassName = when (immutableClassName) {
+                GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP ->
+                    GuavaTypeNames.IMMUTABLE_LIST_MULTIMAP_BUILDER
+
+                GuavaTypeNames.IMMUTABLE_SET_MULTIMAP ->
+                    GuavaTypeNames.IMMUTABLE_SET_MULTIMAP_BUILDER
+
+                else ->
+                    // Return type is base class ImmutableMultimap, need the case handled here,
+                    // but won't actually get here in the code if this is the case as we will
+                    // do an early return in TypeAdapterStore.kt.
+                    GuavaTypeNames.IMMUTABLE_MULTIMAP_BUILDER
+            }
+
+            addLocalVariable(
+                name = mapVarName,
+                typeName = builderClassName.parametrizedBy(
+                    keyTypeArg.asTypeName(),
+                    valueTypeArg.asTypeName()
+                ),
+                assignExpr = XCodeBlock.of(
+                    language = language,
+                    format = "%T.builder()",
+                    immutableClassName
+                )
             )
+
             val tmpKeyVarName = scope.getTmpVar("_key")
             val tmpValueVarName = scope.getTmpVar("_value")
-            beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+            beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+                addLocalVariable(
+                    name = tmpKeyVarName,
+                    typeName = keyTypeArg.asTypeName()
+                )
                 keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
 
                 // Iterate over all matched fields to check if all are null. If so, we continue in
@@ -91,16 +112,27 @@
                     indexVars = valueIndexVars
                 )
                 // Perform column null check
-                beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
+                beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
                     addStatement("continue")
                 }.endControlFlow()
 
-                addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+                addLocalVariable(
+                    name = tmpValueVarName,
+                    typeName = valueTypeArg.asTypeName()
+                )
                 valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
-                addStatement("$L.put($L, $L)", mapVarName, tmpKeyVarName, tmpValueVarName)
+                addStatement("%L.put(%L, %L)", mapVarName, tmpKeyVarName, tmpValueVarName)
             }
             endControlFlow()
-            addStatement("final $T $L = $L.build()", mapType, outVarName, mapVarName)
+            addLocalVariable(
+                name = outVarName,
+                typeName = mapType,
+                assignExpr = XCodeBlock.of(
+                    language = language,
+                    format = "%L.build()",
+                    mapVarName
+                )
+            )
         }
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
index fe94c86..26fba57 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaOptionalQueryResultAdapter.kt
@@ -18,7 +18,7 @@
 
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -39,13 +39,13 @@
             resultAdapter.convert(valueVarName, cursorVarName, scope)
             addLocalVariable(
                 name = outVarName,
-                typeName = GuavaBaseTypeNames.OPTIONAL.parametrizedBy(
+                typeName = GuavaTypeNames.OPTIONAL.parametrizedBy(
                     typeArg.asTypeName()
                 ),
                 assignExpr = XCodeBlock.of(
                     language = language,
                     format = "%T.fromNullable(%L)",
-                    GuavaBaseTypeNames.OPTIONAL,
+                    GuavaTypeNames.OPTIONAL,
                     valueVarName
                 )
             )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
index ef1068e..5f5e27b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableListQueryResultAdapter.kt
@@ -19,7 +19,7 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaCollectionTypeNames
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.solver.CodeGenScope
 
 class ImmutableListQueryResultAdapter(
@@ -29,10 +29,10 @@
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
         scope.builder.apply {
             rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
-            val collectionType = GuavaCollectionTypeNames.IMMUTABLE_LIST.parametrizedBy(
+            val collectionType = GuavaTypeNames.IMMUTABLE_LIST.parametrizedBy(
                 typeArg.asTypeName()
             )
-            val immutableListBuilderType = GuavaCollectionTypeNames
+            val immutableListBuilderType = GuavaTypeNames
                 .IMMUTABLE_LIST_BUILDER.parametrizedBy(typeArg.asTypeName())
             val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
             addLocalVariable(
@@ -40,7 +40,7 @@
                 typeName = immutableListBuilderType,
                 assignExpr = XCodeBlock.ofNewInstance(
                     language = language,
-                    GuavaCollectionTypeNames.IMMUTABLE_LIST_BUILDER
+                    GuavaTypeNames.IMMUTABLE_LIST_BUILDER
                 )
             )
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
index 9bddcae..3f99950 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
@@ -16,15 +16,12 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.solver.CodeGenScope
-import com.google.common.collect.ImmutableMap
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
 
 class ImmutableMapQueryResultAdapter(
     context: Context,
@@ -34,19 +31,21 @@
     private val resultAdapter: QueryResultAdapter
 ) : MultimapQueryResultAdapter(context, parsedQuery, resultAdapter.rowAdapters) {
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             val mapVarName = scope.getTmpVar("_mapResult")
             resultAdapter.convert(mapVarName, cursorVarName, scope)
-            addStatement(
-                "final $T $L = $T.copyOf($L)",
-                ParameterizedTypeName.get(
-                    ClassName.get(ImmutableMap::class.java),
-                    keyTypeArg.typeName,
-                    valueTypeArg.typeName
+            addLocalVariable(
+                name = outVarName,
+                typeName = GuavaTypeNames.IMMUTABLE_MAP.parametrizedBy(
+                    keyTypeArg.asTypeName(),
+                    valueTypeArg.asTypeName()
                 ),
-                outVarName,
-                ClassName.get(ImmutableMap::class.java),
-                mapVarName
+                assignExpr = XCodeBlock.of(
+                    language = language,
+                    format = "%T.copyOf(%L)",
+                    GuavaTypeNames.IMMUTABLE_MAP,
+                    mapVarName
+                ),
             )
         }
     }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index b921ed6..247f882 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -1368,6 +1368,87 @@
     }
 
     @Test
+    fun queryResultAdapter_guavaImmutableMultimap() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            interface MyDao {
+                @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+                fun getArtistWithSongs(): com.google.common.collect.ImmutableSetMultimap<Artist, Song>
+
+                @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+                fun getArtistWithSongIds(): com.google.common.collect.ImmutableListMultimap<Artist, Song>
+            }
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: String
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: String,
+                val artistKey: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun queryResultAdapter_guavaImmutableMap() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            interface MyDao {
+                @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+                fun getSongsWithArtist(): com.google.common.collect.ImmutableMap<Song, Artist>
+            }
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: String
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: String,
+                val artistKey: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
     fun queryResultAdapter_map_ambiguousIndexAdapter() {
         val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt
new file mode 100644
index 0000000..2e4041b
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMap.kt
@@ -0,0 +1,68 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import com.google.common.collect.ImmutableMap
+import java.lang.Class
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): ImmutableMap<Song, Artist> {
+        val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _mapResult: MutableMap<Song, Artist> = LinkedHashMap<Song, Artist>()
+            while (_cursor.moveToNext()) {
+                val _key: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _key = Song(_tmpSongId,_tmpArtistKey)
+                if (_cursor.isNull(_cursorIndexOfArtistId)) {
+                    error("Missing value for a key.")
+                }
+                val _value: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _value = Artist(_tmpArtistId)
+                if (!_mapResult.containsKey(_key)) {
+                    _mapResult.put(_key, _value)
+                }
+            }
+            val _result: ImmutableMap<Song, Artist> = ImmutableMap.copyOf(_mapResult)
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt
new file mode 100644
index 0000000..7da4276
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaImmutableMultimap.kt
@@ -0,0 +1,99 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import com.google.common.collect.ImmutableListMultimap
+import com.google.common.collect.ImmutableSetMultimap
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["UNCHECKED_CAST", "DEPRECATION"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getArtistWithSongs(): ImmutableSetMultimap<Artist, Song> {
+        val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _mapBuilder: ImmutableSetMultimap.Builder<Artist, Song> = ImmutableSetMultimap.builder()
+            while (_cursor.moveToNext()) {
+                val _key: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _key = Artist(_tmpArtistId)
+                if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+                    continue
+                }
+                val _value: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _value = Song(_tmpSongId,_tmpArtistKey)
+                _mapBuilder.put(_key, _value)
+            }
+            val _result: ImmutableSetMultimap<Artist, Song> = _mapBuilder.build()
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistWithSongIds(): ImmutableListMultimap<Artist, Song> {
+        val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _mapBuilder: ImmutableListMultimap.Builder<Artist, Song> = ImmutableListMultimap.builder()
+            while (_cursor.moveToNext()) {
+                val _key: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _key = Artist(_tmpArtistId)
+                if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+                    continue
+                }
+                val _value: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _value = Song(_tmpSongId,_tmpArtistKey)
+                _mapBuilder.put(_key, _value)
+            }
+            val _result: ImmutableListMultimap<Artist, Song> = _mapBuilder.build()
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index afcd43e..8b90e5d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -895,6 +895,11 @@
 includeProject(":viewpager2:integration-tests:targetsdk-tests", [BuildType.MAIN])
 includeProject(":viewpager2:viewpager2", [BuildType.MAIN])
 includeProject(":viewpager:viewpager", [BuildType.MAIN])
+includeProject(":wear:protolayout:protolayout", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-expression", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-expression-pipeline", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-proto", [BuildType.WEAR])
+includeProject(":wear:protolayout:protolayout-renderer", [BuildType.WEAR])
 includeProject(":wear:wear", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:benchmark:integration-tests:macrobenchmark-target", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":wear:benchmark:integration-tests:macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index c88a125..15d7d5f 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -35,6 +35,7 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.HashSet;
@@ -702,6 +703,7 @@
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
 
+    @Ignore // b/260235822
     @Test
     public void testSetGestureMargins() {
         launchTestActivity(PinchTestActivity.class);
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
index a0397ce..0687e7c 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
@@ -25,7 +25,7 @@
 // Concept of version useful e.g. for human-readable error messages, and stable once released.
 // Does not replace the need for a binary verification mechanism (e.g. checksum check).
 // TODO: populate using CMake
-#define VERSION "1.0.0-alpha07"
+#define VERSION "1.0.0-alpha08"
 
 namespace tracing_perfetto {
     void RegisterWithPerfetto() {
diff --git a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
index fb25e2e..d789c54 100644
--- a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
+++ b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
@@ -30,7 +30,7 @@
         init {
             PerfettoNative.loadLib()
         }
-        const val libraryVersion = "1.0.0-alpha07" // TODO: get using reflection
+        const val libraryVersion = "1.0.0-alpha08" // TODO: get using reflection
     }
 
     @Test
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index 00e6c5a..307966f 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -25,12 +25,12 @@
 
     // TODO(224510255): load from a file produced at build time
     object Metadata {
-        const val version = "1.0.0-alpha07"
+        const val version = "1.0.0-alpha08"
         val checksums = mapOf(
-            "arm64-v8a" to "3d9fa02a04459e3480f28ae7f3a8ced30f181f02a0e6bc6caf85704faea84857",
-            "armeabi-v7a" to "9eb7bf76a94fce79e682204c9e702c872d15c85f2ebd48de61aa541e7e6de034",
-            "x86" to "6eb72683bf58eeb0175f094d88bc0e00744b1a593dc7eaac9ee15168b1ab9234",
-            "x86_64" to "eabf725464c24c9709d8e2ea073233e5712949bb65aac5c4972528d813b55c74",
+            "arm64-v8a" to "3accfdd32b900833af761bb5cb4576b9d0bf75c49a5ed98b84ea1bc1447d82e5",
+            "armeabi-v7a" to "1fbe02f3cc570677ec55fb1e74e2719e73d6e5914e4f13886f915c9d588b939c",
+            "x86" to "0b6a550f0f95700ab4a8bbbbd04119aa3c4a6dcce3ace6599bb3d4b908b42258",
+            "x86_64" to "deb5b286f109c4bb95a33f168a642c290284aa805bef1f8cac4734b9adb922ab",
         )
     }
 
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index a0f499c..c5dfada 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -18,11 +18,17 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 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.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
 import androidx.compose.material3.Text
@@ -40,9 +46,41 @@
 import androidx.tv.material.carousel.Carousel
 import androidx.tv.material.carousel.CarouselItem
 
+@Composable
+fun FeaturedCarouselContent() {
+    LazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+        item { SampleLazyRow() }
+        item {
+            Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
+                Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+                    repeat(3) {
+                        Box(modifier = Modifier
+                            .background(Color.Magenta.copy(alpha = 0.3f))
+                            .width(50.dp)
+                            .height(50.dp)
+                            .drawBorderOnFocus()
+                        )
+                    }
+                }
+                FeaturedCarousel()
+            }
+        }
+        items(2) { SampleLazyRow() }
+    }
+}
+
+@Composable
+fun Modifier.drawBorderOnFocus(borderColor: Color = Color.White): Modifier {
+    var isFocused by remember { mutableStateOf(false) }
+    return this
+        .border(5.dp, borderColor.copy(alpha = if (isFocused) 1f else 0.2f))
+        .onFocusChanged { isFocused = it.isFocused }
+        .focusable()
+}
+
 @OptIn(ExperimentalTvMaterialApi::class)
 @Composable
-fun FeaturedCarousel() {
+internal fun FeaturedCarousel() {
     val backgrounds = listOf(
         Color.Red.copy(alpha = 0.3f),
         Color.Yellow.copy(alpha = 0.3f),
@@ -77,7 +115,7 @@
         modifier = Modifier
             .fillMaxSize()
             .padding(40.dp),
-        contentAlignment = Alignment.CenterStart
+        contentAlignment = Alignment.BottomStart
     ) {
         var isFocused by remember { mutableStateOf(false) }
 
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
index bbf0111..f28379e 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
@@ -17,19 +17,12 @@
 package androidx.tv.tvmaterial.samples
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyColumn
@@ -42,29 +35,25 @@
 fun LazyRowsAndColumns() {
     TvLazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
         repeat((0 until rowsCount).count()) {
-            item { LazyRow() }
+            item { SampleLazyRow() }
         }
     }
 }
 
 @Composable
-private fun LazyRow() {
+fun SampleLazyRow() {
     val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
     val backgroundColors = (0 until columnsCount).map { colors.random() }
 
     TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
         backgroundColors.forEach { backgroundColor ->
             item {
-                var isFocused by remember { mutableStateOf(false) }
-
                 Box(
                     modifier = Modifier
                         .background(backgroundColor.copy(alpha = 0.3f))
                         .width(200.dp)
                         .height(150.dp)
-                        .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.2f))
-                        .onFocusChanged { isFocused = it.isFocused }
-                        .focusable()
+                        .drawBorderOnFocus()
                 )
             }
         }
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
index 6ed68d5..6648a0a 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -38,7 +38,7 @@
 
 enum class Navigation(val displayName: String, val action: @Composable () -> Unit) {
   LazyRowsAndColumns("Lazy Rows and Columns", { LazyRowsAndColumns() }),
-  FeaturedCarousel("Featured Carousel", { FeaturedCarousel() }),
+  FeaturedCarousel("Featured Carousel", { FeaturedCarouselContent() }),
   ImmersiveList("Immersive List", { SampleImmersiveList() }),
 }
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index c4933a3..158ea43 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -437,6 +438,64 @@
     }
 
     @Test
+    fun carousel_manualScrolling_withFocusableItemsOnTop() {
+        rule.setContent {
+            Column {
+                Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+                    repeat(3) {
+                        SampleButton("Row-button-${it + 1}")
+                    }
+                }
+                SampleCarousel { index ->
+                    SampleButton("Button-${index + 1}")
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // trigger recomposition on requesting focus
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+
+        // Check that slide 1 is in view and button 1 has focus
+        rule.onNodeWithText("Button-1").assertIsDisplayed()
+        rule.onNodeWithText("Button-1").assertIsFocused()
+
+        // press dpad right to scroll to next slide
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.waitForIdle()
+
+        // Check that slide 2 is in view and button 2 has focus
+        rule.onNodeWithText("Button-2").assertIsDisplayed()
+        // TODO: Fix button 2 isn't gaining focus
+        // rule.onNodeWithText("Button-2").assertIsFocused()
+
+        // Check if the first focusable element in parent has focus
+        rule.onNodeWithText("Row-button-1").assertIsNotFocused()
+
+        // press dpad left to scroll to previous slide
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle()
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.waitForIdle()
+
+        // Check that slide 1 is in view and button 1 has focus
+        rule.onNodeWithText("Button-1").assertIsDisplayed()
+        rule.onNodeWithText("Button-1").assertIsFocused()
+    }
+
+    @Test
     fun carousel_manualScrolling_ltr() {
         rule.setContent {
             SampleCarousel { index ->
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index 2b37161..6200545 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -17,6 +17,7 @@
 package androidx.tv.material.carousel
 
 import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibilityScope
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
 import androidx.compose.animation.ExperimentalAnimationApi
@@ -52,7 +53,6 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
@@ -102,7 +102,7 @@
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
     val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
-    val focusRequester = remember { FocusRequester() }
+    val carouselOuterBoxFocusRequester = remember { FocusRequester() }
     var isAutoScrollActive by remember { mutableStateOf(false) }
 
     AutoScrollSideEffect(
@@ -112,76 +112,40 @@
         focusState,
         onAutoScrollChange = { isAutoScrollActive = it })
     Box(modifier = modifier
-        .focusRequester(focusRequester)
+        .focusRequester(carouselOuterBoxFocusRequester)
         .onFocusChanged {
             focusState = it
             if (it.isFocused && isAutoScrollActive) {
                 focusManager.moveFocus(FocusDirection.Enter)
             }
         }
-        .focusProperties {
-            exit = {
-                val showPreviousSlideAndGetFocusRequester = {
-                    if (carouselState
-                            .isFirstSlide()
-                            .not()
-                    ) {
-                        carouselState.moveToPreviousSlide(slideCount)
-                        focusRequester
-                    } else {
-                        FocusRequester.Default
-                    }
-                }
-                val showNextSlideAndGetFocusRequester = {
-                    if (carouselState
-                            .isLastSlide(slideCount)
-                            .not()
-                    ) {
-                        carouselState.moveToNextSlide(slideCount)
-                        focusRequester
-                    } else {
-                        FocusRequester.Default
-                    }
-                }
-                when (it) {
-                    FocusDirection.Left -> {
-                        if (isLtr) {
-                            showPreviousSlideAndGetFocusRequester()
-                        } else {
-                            showNextSlideAndGetFocusRequester()
-                        }
-                    }
-                    FocusDirection.Right -> {
-                        if (isLtr) {
-                            showNextSlideAndGetFocusRequester()
-                        } else {
-                            showPreviousSlideAndGetFocusRequester()
-                        }
-                    }
-                    else -> FocusRequester.Default
-                }
-            }
-        }
+        .manualScrolling(carouselState, slideCount, isLtr)
         .focusable()) {
         AnimatedContent(
             targetState = carouselState.slideIndex,
             transitionSpec = { enterTransition.with(exitTransition) }
         ) {
-            Box(
-                modifier = Modifier
-                    .onPlaced {
-                        if (isAutoScrollActive.not()) {
-                            focusManager.moveFocus(FocusDirection.Enter)
-                        }
+            LaunchedEffect(Unit) {
+                this@AnimatedContent.onAnimationCompletion {
+                    if (isAutoScrollActive.not()) {
+                        carouselOuterBoxFocusRequester.requestFocus()
+                        focusManager.moveFocus(FocusDirection.Enter)
                     }
-            ) {
-                content.invoke(it)
+                }
             }
+            content.invoke(it)
         }
         this.carouselIndicator()
     }
 }
 
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalAnimationApi::class)
+private suspend fun AnimatedVisibilityScope.onAnimationCompletion(action: suspend () -> Unit) {
+    snapshotFlow { transition.currentState == transition.targetState }.first { it }
+    action.invoke()
+}
+
 @OptIn(ExperimentalTvMaterialApi::class)
 @Composable
 private fun AutoScrollSideEffect(
@@ -213,6 +177,53 @@
     onAutoScrollChange(doAutoScroll)
 }
 
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalTvMaterialApi::class, ExperimentalComposeUiApi::class)
+private fun Modifier.manualScrolling(
+    carouselState: CarouselState,
+    slideCount: Int,
+    isLtr: Boolean
+): Modifier =
+    this.focusProperties {
+        exit = {
+            val showPreviousSlideAndGetFocusRequester = {
+                if (carouselState.isFirstSlide().not()) {
+                    carouselState.moveToPreviousSlide(slideCount)
+                    FocusRequester.Cancel
+                } else {
+                    FocusRequester.Default
+                }
+            }
+            val showNextSlideAndGetFocusRequester = {
+                if (carouselState.isLastSlide(slideCount).not()) {
+                    carouselState.moveToNextSlide(slideCount)
+                    FocusRequester.Cancel
+                } else {
+                    FocusRequester.Default
+                }
+            }
+            when (it) {
+                FocusDirection.Left -> {
+                    if (isLtr) {
+                        showPreviousSlideAndGetFocusRequester()
+                    } else {
+                        showNextSlideAndGetFocusRequester()
+                    }
+                }
+
+                FocusDirection.Right -> {
+                    if (isLtr) {
+                        showNextSlideAndGetFocusRequester()
+                    } else {
+                        showPreviousSlideAndGetFocusRequester()
+                    }
+                }
+
+                else -> FocusRequester.Default
+            }
+        }
+    }
+
 @OptIn(ExperimentalTvMaterialApi::class)
 @Composable
 private fun CarouselStateUpdater(carouselState: CarouselState, slideCount: Int) {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 9fa9d6e..6f34d1e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -81,16 +81,17 @@
     val coroutineScope = rememberCoroutineScope()
 
     LaunchedEffect(overlayVisible) {
-        snapshotFlow { overlayVisible.isIdle && overlayVisible.currentState }.first { it }
-        // slide has loaded completely.
-        if (focusState?.isFocused == true) {
-            // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
-            // item is within an animation, bringIntoView scrolls excessively and loses focus.
-            // b/241591211
-            // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
-            // completed animating into position.
-            bringIntoViewRequester.bringIntoView()
-            focusManager.moveFocus(FocusDirection.Enter)
+        overlayVisible.onAnimationCompletion {
+            // slide has loaded completely.
+            if (focusState?.isFocused == true) {
+                // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
+                // item is within an animation, bringIntoView scrolls excessively and loses focus.
+                // b/241591211
+                // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
+                // completed animating into position.
+                bringIntoViewRequester.bringIntoView()
+                focusManager.moveFocus(FocusDirection.Enter)
+            }
         }
     }
 
@@ -119,7 +120,9 @@
             modifier = Modifier
                 .align(Alignment.BottomStart)
                 .onFocusChanged {
-                    if (it.isFocused) { focusManager.moveFocus(FocusDirection.Enter) }
+                    if (it.isFocused) {
+                        focusManager.moveFocus(FocusDirection.Enter)
+                    }
                 }
                 .focusable(),
             visibleState = overlayVisible,
@@ -131,6 +134,13 @@
     }
 }
 
+private suspend fun MutableTransitionState<Boolean>.onAnimationCompletion(
+    action: suspend () -> Unit
+) {
+    snapshotFlow { isIdle && currentState }.first { it }
+    action.invoke()
+}
+
 @ExperimentalTvMaterialApi
 object CarouselItemDefaults {
     /**
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 870a8dc..ac201f8 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -43,6 +43,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -179,6 +180,7 @@
         assertThat(state.selectedOption).isEqualTo(numberOfOptions - 1)
     }
 
+    @SdkSuppress(minSdkVersion = 29) // b/260234023
     @Test
     fun uses_positive_separation_correctly() =
         uses_separation_correctly(1)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index e681cd5..529cdac 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -42,6 +42,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
@@ -203,6 +204,7 @@
         )
     }
 
+    @SdkSuppress(minSdkVersion = 29) // b/260235088
     @Test
     fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPadding() {
         scrollableScalingLazyColumnPositionAndSize(
diff --git a/wear/protolayout/OWNERS b/wear/protolayout/OWNERS
new file mode 100644
index 0000000..bc950d4
--- /dev/null
+++ b/wear/protolayout/OWNERS
@@ -0,0 +1,2 @@
+msab@google.com
+
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt b/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/res-current.txt
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression-pipeline/build.gradle b/wear/protolayout/protolayout-expression-pipeline/build.gradle
new file mode 100644
index 0000000..6bb2d3b
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+}
+
+android {
+    namespace "androidx.wear.protolayout.expression.pipeline"
+}
+
+androidx {
+    name = "ProtoLayout Dynamic Expression Evaluation Pipeline"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+    inceptionYear = "2022"
+    description = "Evaluate dynamic expressions."
+}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/api/res-current.txt b/wear/protolayout/protolayout-expression/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/res-current.txt
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-expression/build.gradle b/wear/protolayout/protolayout-expression/build.gradle
new file mode 100644
index 0000000..6e5826a
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+}
+
+android {
+    namespace "androidx.wear.protolayout.expression"
+}
+
+androidx {
+    name = "ProtoLayout Dynamic Expression"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+    inceptionYear = "2022"
+    description = "Create dynamic expressions (for late evaluation by a remote evaluator)."
+}
diff --git a/wear/protolayout/protolayout-proto/api/current.txt b/wear/protolayout/protolayout-proto/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/api/restricted_current.txt b/wear/protolayout/protolayout-proto/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-proto/build.gradle b/wear/protolayout/protolayout-proto/build.gradle
new file mode 100644
index 0000000..42da9e5
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("java-library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+    implementation("androidx.annotation:annotation:1.1.0")
+}
+
+androidx {
+    name = "Wear ProtoLayout Proto"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+    inceptionYear = "2022"
+    description = "Jarjar the generated proto and proto-lite dependency for use by wear-protolayout"
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java b/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java
new file mode 100644
index 0000000..64a3b00
--- /dev/null
+++ b/wear/protolayout/protolayout-proto/src/main/java/androidx/wear/protolayout/proto/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.wear.protolayout.proto;
+
+import androidx.annotation.RestrictTo;
diff --git a/wear/protolayout/protolayout-renderer/api/current.txt b/wear/protolayout/protolayout-renderer/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/api/res-current.txt b/wear/protolayout/protolayout-renderer/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/res-current.txt
diff --git a/wear/protolayout/protolayout-renderer/api/restricted_current.txt b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout-renderer/build.gradle b/wear/protolayout/protolayout-renderer/build.gradle
new file mode 100644
index 0000000..a9bc2f2
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+}
+
+android {
+    namespace "androidx.wear.protolayout.renderer"
+}
+
+androidx {
+    name = "ProtoLayout Renderer"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+    inceptionYear = "2022"
+    description = "Render ProtoLayouts to an Android surface"
+}
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/api/res-current.txt b/wear/protolayout/protolayout/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wear/protolayout/protolayout/api/res-current.txt
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
new file mode 100644
index 0000000..74f7467
--- /dev/null
+++ b/wear/protolayout/protolayout/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+}
+
+android {
+    namespace "androidx.wear.protolayout"
+}
+
+androidx {
+    name = "ProtoLayout"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.WEAR_PROTOLAYOUT
+    inceptionYear = "2022"
+    description = "Create layouts that can be rendered by a remote host."
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java
new file mode 100644
index 0000000..4496dbc
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+/**
+ * Allows creating layouts and expressions that can be rendered or evaluated at a remote host.
+ */
+package androidx.wear.protolayout;
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
index 1f81fc6..d7c9869 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsUserStyleSettingWireFormat.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.util.List;
@@ -35,20 +36,11 @@
 @VersionedParcelize
 public class ComplicationsUserStyleSettingWireFormat extends UserStyleSettingWireFormat {
 
-    ComplicationsUserStyleSettingWireFormat() {
-    }
+    @Nullable
+    @ParcelField(104)
+    public List<CharSequence> mPerOptionScreenReaderNames;
 
-    /** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
-    @Deprecated
-    public ComplicationsUserStyleSettingWireFormat(
-            @NonNull String id,
-            @NonNull CharSequence displayName,
-            @NonNull CharSequence description,
-            @Nullable Icon icon,
-            @NonNull List<OptionWireFormat> options,
-            int defaultOptionIndex,
-            @NonNull List<Integer> affectsLayers) {
-        super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers);
+    ComplicationsUserStyleSettingWireFormat() {
     }
 
     public ComplicationsUserStyleSettingWireFormat(
@@ -60,8 +52,10 @@
             int defaultOptionIndex,
             @NonNull List<Integer> affectsLayers,
             @Nullable Bundle onWatchFaceEditorBundle,
-            @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles) {
+            @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles,
+            @Nullable List<CharSequence> perOptionScreenReaderNames) {
         super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers,
                 onWatchFaceEditorBundle, perOptionOnWatchFaceEditorBundles);
+        mPerOptionScreenReaderNames = perOptionScreenReaderNames;
     }
 }
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
index 0c50c6c..7669556 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/ListUserStyleSettingWireFormat.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.versionedparcelable.ParcelField;
 import androidx.versionedparcelable.VersionedParcelize;
 
 import java.util.List;
@@ -35,20 +36,11 @@
 @VersionedParcelize
 public class ListUserStyleSettingWireFormat extends UserStyleSettingWireFormat {
 
-    ListUserStyleSettingWireFormat() {}
+    @Nullable
+    @ParcelField(104)
+    public List<CharSequence> mPerOptionScreenReaderNames;
 
-    /** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
-    @Deprecated
-    public ListUserStyleSettingWireFormat(
-            @NonNull String id,
-            @NonNull CharSequence displayName,
-            @NonNull CharSequence description,
-            @Nullable Icon icon,
-            @NonNull List<OptionWireFormat> options,
-            int defaultOptionIndex,
-            @NonNull List<Integer> affectsLayers) {
-        super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers);
-    }
+    ListUserStyleSettingWireFormat() {}
 
     public ListUserStyleSettingWireFormat(
             @NonNull String id,
@@ -59,8 +51,10 @@
             int defaultOptionIndex,
             @NonNull List<Integer> affectsLayers,
             @Nullable Bundle onWatchFaceEditorBundle,
-            @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles) {
+            @Nullable List<Bundle> perOptionOnWatchFaceEditorBundles,
+            @Nullable List<CharSequence> perOptionScreenReaderNames) {
         super(id, displayName, description, icon, options, defaultOptionIndex, affectsLayers,
                 onWatchFaceEditorBundle, perOptionOnWatchFaceEditorBundles);
+        mPerOptionScreenReaderNames = perOptionScreenReaderNames;
     }
 }
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
index b101976..13609b0 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleSettingWireFormat.java
@@ -111,6 +111,8 @@
     @ParcelField(103)
     public List<Bundle> mPerOptionOnWatchFaceEditorBundles = new ArrayList<>();
 
+    // Field 104 is reserved.
+
     UserStyleSettingWireFormat() {}
 
     /** @deprecated use a constructor with List<Bundle> perOptionOnWatchFaceEditorBundles. */
diff --git a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index ae4c81f..f8818fe 100644
--- a/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/watchface/watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -148,9 +148,10 @@
 private typealias WireComplicationProviderInfo =
     android.support.wearable.complications.ComplicationProviderInfo
 
-internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
-internal val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
-internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", icon = null)
+internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
+internal val greenStyleOption =
+    ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
+internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", "Blue", icon = null)
 internal val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 internal val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
     UserStyleSetting.Id("color_style_setting"),
@@ -161,9 +162,12 @@
     listOf(WatchFaceLayer.BASE)
 )
 
-internal val classicStyleOption = ListOption(Option.Id("classic_style"), "Classic", icon = null)
-internal val modernStyleOption = ListOption(Option.Id("modern_style"), "Modern", icon = null)
-internal val gothicStyleOption = ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+internal val classicStyleOption =
+    ListOption(Option.Id("classic_style"), "Classic", "Classic", icon = null)
+internal val modernStyleOption =
+    ListOption(Option.Id("modern_style"), "Modern", "Modern", icon = null)
+internal val gothicStyleOption =
+    ListOption(Option.Id("gothic_style"), "Gothic", "Gothic", icon = null)
 internal val watchHandStyleList =
     listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
 internal val watchHandStyleSetting = UserStyleSetting.ListUserStyleSetting(
@@ -206,6 +210,7 @@
     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
         Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
         "Left And Right",
+        "Show left and right complications",
         null,
         // An empty list means use the initial config.
         emptyList()
@@ -214,6 +219,7 @@
     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
         Option.Id("LEFT_COMPLICATION"),
         "Left",
+        "Show left complication only",
         null,
         listOf(
             UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
@@ -225,6 +231,7 @@
     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
         Option.Id("RIGHT_COMPLICATION"),
         "Right",
+        "Show right complication only",
         null,
         listOf(
             UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
@@ -2413,8 +2420,8 @@
 
     @Test
     public fun cantAssignUnrelatedUserStyle() {
-        val redOption = ListOption(Option.Id("red"), "Red", icon = null)
-        val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
+        val redOption = ListOption(Option.Id("red"), "Red", "Red", icon = null)
+        val greenOption = ListOption(Option.Id("green"), "Green", "Green", icon = null)
         val colorStyleList = listOf(redOption, greenOption)
         val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
             UserStyleSetting.Id("color_id"),
@@ -2446,8 +2453,8 @@
 
     @Test
     public fun cantAssignUnrelatedUserStyle_compareAndSet() {
-        val redOption = ListOption(Option.Id("red"), "Red", icon = null)
-        val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
+        val redOption = ListOption(Option.Id("red"), "Red", "Red", icon = null)
+        val greenOption = ListOption(Option.Id("green"), "Green", "Green", icon = null)
         val colorStyleList = listOf(redOption, greenOption)
         val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
             UserStyleSetting.Id("color_id"),
diff --git a/wear/watchface/watchface-style/api/current.txt b/wear/watchface/watchface-style/api/current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/current.txt
+++ b/wear/watchface/watchface-style/api/current.txt
@@ -157,15 +157,19 @@
   }
 
   public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
@@ -214,14 +218,19 @@
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
diff --git a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
@@ -157,15 +157,19 @@
   }
 
   public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
@@ -214,14 +218,19 @@
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index 274acb9..34ce054 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -157,15 +157,19 @@
   }
 
   public static final class UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays);
     method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> getComplicationSlotOverlays();
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay> complicationSlotOverlays;
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
@@ -214,14 +218,19 @@
   }
 
   public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
-    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon);
+    ctor @Deprecated public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings, optional androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon, optional java.util.Collection<? extends androidx.wear.watchface.style.UserStyleSetting> childSettings);
+    ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, android.content.res.Resources resources, @StringRes int displayNameResourceId, @StringRes int screenReaderNameResourceId, android.graphics.drawable.Icon? icon);
     method public CharSequence getDisplayName();
     method public android.graphics.drawable.Icon? getIcon();
+    method public CharSequence? getScreenReaderName();
     method public androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? getWatchFaceEditorData();
     property public final CharSequence displayName;
     property public final android.graphics.drawable.Icon? icon;
+    property public final CharSequence? screenReaderName;
     property public final androidx.wear.watchface.style.UserStyleSetting.WatchFaceEditorData? watchFaceEditorData;
   }
 
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
index 07829db..73aff62 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/IconWireSizeAndDimensionsTest.kt
@@ -120,18 +120,21 @@
         val classicStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
             UserStyleSetting.Option.Id("classic_style"),
             "Classic",
+            "Classic screen reader name",
             testIcon
         )
 
         val modernStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
             UserStyleSetting.Option.Id("modern_style"),
             "Modern",
+            "Modern screen reader name",
             testIcon
         )
 
         val gothicStyleOption = UserStyleSetting.ListUserStyleSetting.ListOption(
             UserStyleSetting.Option.Id("gothic_style"),
             "Gothic",
+            "Gothic screen reader name",
             testIcon
         )
 
@@ -201,12 +204,14 @@
                 UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     UserStyleSetting.Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
                     "Both",
+                     "Both screen reader name",
                     testIcon,
                     listOf()
                 ),
                 UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     UserStyleSetting.Option.Id("NO_COMPLICATIONS"),
                     "None",
+                    "None screen reader name",
                     testIcon,
                     listOf(
                         UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -222,6 +227,7 @@
                 UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     UserStyleSetting.Option.Id("LEFT_COMPLICATION"),
                     "Left",
+                    "Left screen reader name",
                     testIcon,
                     listOf(
                         UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -233,6 +239,7 @@
                 UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     UserStyleSetting.Option.Id("RIGHT_COMPLICATION"),
                     "Right",
+                    "Right screen reader name",
                     testIcon,
                     listOf(
                         UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
index 024640e..e8bff3b 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSchemaInflateTest.kt
@@ -68,12 +68,14 @@
         val option00 = (setting0.options[0] as ListOption)
         assertThat(option00.id).isEqualTo(UserStyleSetting.Option.Id("red"))
         assertThat(option00.displayName).isEqualTo("Red Style")
+        assertThat(option00.screenReaderName).isEqualTo("Red watch face style")
         assertThat(option00.icon!!.resId).isEqualTo(R.drawable.red_icon)
         assertThat(option00.childSettings).isEmpty()
         assertThat(option00.watchFaceEditorData!!.icon!!.resId).isEqualTo(R.drawable.red_icon_wf)
         val option01 = (setting0.options[1] as ListOption)
         assertThat(option01.id).isEqualTo(UserStyleSetting.Option.Id("green"))
         assertThat(option01.displayName).isEqualTo("Green Style")
+        assertThat(option01.screenReaderName).isEqualTo("Green Style")
         assertThat(option01.icon!!.resId).isEqualTo(R.drawable.green_icon)
         assertThat(option01.childSettings).isEmpty()
         assertThat(option01.watchFaceEditorData!!.icon!!.resId).isEqualTo(R.drawable.green_icon_wf)
@@ -92,12 +94,14 @@
         val option10 = (setting1.options[0] as ListOption)
         assertThat(option10.id).isEqualTo(UserStyleSetting.Option.Id("foo"))
         assertThat(option10.displayName).isEqualTo("Foo")
+        assertThat(option10.screenReaderName).isEqualTo("Foo thing")
         assertThat(option10.icon).isNull()
         assertThat(option10.childSettings).isEmpty()
         assertThat(option10.watchFaceEditorData).isNull()
         val option11 = (setting1.options[1] as ListOption)
         assertThat(option11.id).isEqualTo(UserStyleSetting.Option.Id("bar"))
         assertThat(option11.displayName).isEqualTo("Bar")
+        assertThat(option11.screenReaderName).isEqualTo("Bar thing")
         assertThat(option11.icon).isNull()
         assertThat(option11.childSettings).isEmpty()
         assertThat(option11.watchFaceEditorData).isNull()
@@ -117,11 +121,13 @@
         val option20 = (setting2.options[0] as ListOption)
         assertThat(option20.id).isEqualTo(UserStyleSetting.Option.Id("a"))
         assertThat(option20.displayName).isEqualTo("A")
+        assertThat(option20.screenReaderName).isEqualTo("Option A")
         assertThat(option20.icon).isNull()
         assertThat(option20.childSettings).containsExactly(setting0)
         val option21 = (setting2.options[1] as ListOption)
         assertThat(option21.id).isEqualTo(UserStyleSetting.Option.Id("b"))
         assertThat(option21.displayName).isEqualTo("B")
+        assertThat(option21.screenReaderName).isEqualTo("Option B")
         assertThat(option21.icon).isNull()
         assertThat(option21.childSettings).containsExactly(setting1)
         parser.close()
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
index 1c89c53..81c2872 100644
--- a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/UserStyleSettingWithStringResourcesTest.kt
@@ -21,6 +21,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.wear.watchface.style.test.R
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
 import com.google.common.truth.Truth
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -36,6 +40,7 @@
                 setLocale(Locale.ENGLISH)
             }
         )
+
     private val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
         UserStyleSetting.Id("color_style_setting"),
         context.resources,
@@ -47,12 +52,14 @@
                 UserStyleSetting.Option.Id("red_style"),
                 context.resources,
                 R.string.red_style_name,
+                R.string.red_style_name,
                 null
             ),
             UserStyleSetting.ListUserStyleSetting.ListOption(
                 UserStyleSetting.Option.Id("green_style"),
                 context.resources,
                 R.string.green_style_name,
+                R.string.green_style_name,
                 null
             )
         ),
@@ -106,4 +113,100 @@
                 ).displayName
         ).isEqualTo("Stile verde")
     }
+
+    @Test
+    public fun listOptionsWithIndices() {
+        val listUserStyleSetting = ListUserStyleSetting(
+            UserStyleSetting.Id("list"),
+            context.resources,
+            R.string.colors_style_setting,
+            R.string.colors_style_setting_description,
+            icon = null,
+            options = listOf(
+                ListOption(
+                    UserStyleSetting.Option.Id("one"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null
+                ),
+                ListOption(
+                    UserStyleSetting.Option.Id("two"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null
+                ),
+                ListOption(
+                    UserStyleSetting.Option.Id("three"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null
+                )
+            ),
+            listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+        )
+
+        val option0 = listUserStyleSetting.options[0] as ListOption
+        Truth.assertThat(option0.displayName).isEqualTo("Option 1")
+        Truth.assertThat(option0.screenReaderName).isEqualTo("List option 1")
+
+        val option1 = listUserStyleSetting.options[1] as ListOption
+        Truth.assertThat(option1.displayName).isEqualTo("Option 2")
+        Truth.assertThat(option1.screenReaderName).isEqualTo("List option 2")
+
+        val option2 = listUserStyleSetting.options[2] as ListOption
+        Truth.assertThat(option2.displayName).isEqualTo("Option 3")
+        Truth.assertThat(option2.screenReaderName).isEqualTo("List option 3")
+    }
+
+    @Test
+    public fun complicationSlotsOptionsWithIndices() {
+        val complicationSetting = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("one"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null,
+                    emptyList()
+                ),
+                ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("two"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null,
+                    emptyList()
+                ),
+                ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("three"),
+                    context.resources,
+                    R.string.ith_option,
+                    R.string.ith_option_screen_reader_name,
+                    icon = null,
+                    emptyList()
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val option0 = complicationSetting.options[0] as ComplicationSlotsOption
+        Truth.assertThat(option0.displayName).isEqualTo("Option 1")
+        Truth.assertThat(option0.screenReaderName).isEqualTo("List option 1")
+
+        val option1 = complicationSetting.options[1] as ComplicationSlotsOption
+        Truth.assertThat(option1.displayName).isEqualTo("Option 2")
+        Truth.assertThat(option1.screenReaderName).isEqualTo("List option 2")
+
+        val option2 = complicationSetting.options[2] as ComplicationSlotsOption
+        Truth.assertThat(option2.displayName).isEqualTo("Option 3")
+        Truth.assertThat(option2.screenReaderName).isEqualTo("List option 3")
+    }
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
index 3da6de0..4b8665e 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -17,7 +17,9 @@
 
 <resources>
     <string name="red_style_name">Red Style</string>
+    <string name="red_style_name_screen_reader">Red watch face style</string>
     <string name="green_style_name">Green Style</string>
+    <string name="green_style_name_screen_reader">Green watch face style</string>
     <string name="colors_style_setting">Colors</string>
     <string name="colors_style_setting_description">Watchface colorization</string>
 
@@ -30,4 +32,7 @@
 
     <string name="complication_name" translatable="false">Name</string>
     <string name="complication_screen_reader_name" translatable="false">This is a name</string>
+
+    <string name="ith_option" translatable="false">Option %1$d</string>
+    <string name="ith_option_screen_reader_name" translatable="false">List option %1$d</string>
 </resources>
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
index 7eed53d..0931672 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/list_schema.xml
@@ -28,6 +28,7 @@
         <ListOption
             android:icon="@drawable/red_icon"
             app:displayName="@string/red_style_name"
+            app:nameForScreenReaders="@string/red_style_name_screen_reader"
             app:id="red">
             <OnWatchEditorData android:icon="@drawable/red_icon_wf"/>
         </ListOption>
@@ -45,9 +46,11 @@
         app:id="Thing2">
         <ListOption
             app:displayName="Foo"
+            app:nameForScreenReaders="Foo thing"
             app:id="foo" />
         <ListOption
             app:displayName="Bar"
+            app:nameForScreenReaders="Bar thing"
             app:id="bar" />
     </ListUserStyleSetting>
     <ListUserStyleSetting
@@ -57,11 +60,13 @@
         app:id="TopLevel">
         <ListOption
             app:displayName="A"
+            app:nameForScreenReaders="Option A"
             app:id="a">
             <ChildSetting app:id="ColorStyle" />
         </ListOption>
         <ListOption
             app:displayName="B"
+            app:nameForScreenReaders="Option B"
             app:id="b">
             <ChildSetting app:id="Thing2" />
         </ListOption>
diff --git a/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
index d596af1e..81191e1 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/xml/list_setting_common.xml
@@ -28,12 +28,14 @@
     <ListOption
         android:icon="@drawable/red_icon"
         app:displayName="@string/red_style_name"
+        app:nameForScreenReaders="@string/red_style_name_screen_reader"
         app:id="@string/list_setting_common_option_red_id">
         <OnWatchEditorData android:icon="@drawable/red_icon_wf"/>
     </ListOption>
     <ListOption
         android:icon="@drawable/green_icon"
         app:displayName="@string/green_style_name"
+        app:nameForScreenReaders="@string/green_style_name_screen_reader"
         app:id="@string/list_setting_common_option_green_id">
         <OnWatchEditorData android:icon="@drawable/green_icon_wf"/>
     </ListOption>
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 4691492..158391f 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -80,20 +80,26 @@
     class CharSequenceDisplayText(private val charSequence: CharSequence) : DisplayText() {
         override fun toCharSequence() = charSequence
 
+        // This is used purely to estimate the wireformat size.
         override fun write(dos: DataOutputStream) {
-            dos.writeUTF(charSequence.toString())
+            dos.writeUTF(toCharSequence().toString())
         }
     }
 
-    class ResourceDisplayText(
-        private val resources: Resources,
-        @StringRes private val id: Int
+    open class ResourceDisplayText(
+        protected val resources: Resources,
+        @StringRes protected val id: Int
     ) : DisplayText() {
         override fun toCharSequence() = resources.getString(id)
+    }
 
-        override fun write(dos: DataOutputStream) {
-            dos.writeInt(id)
-        }
+    class ResourceDisplayTextWithIndex(
+        resources: Resources,
+        @StringRes id: Int,
+    ) : ResourceDisplayText(resources, id) {
+        var index: Int? = null
+
+        override fun toCharSequence() = resources.getString(id, index!!)
     }
 }
 
@@ -133,6 +139,30 @@
     public val defaultOptionIndex: Int,
     public val affectedWatchFaceLayers: Collection<WatchFaceLayer>
 ) {
+    init {
+        require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
+            "defaultOptionIndex must be within the range of the options list"
+        }
+
+        requireUniqueOptionIds(id, options)
+
+        // Assign 1 based indices to display names to allow names such as Option 1, Option 2,
+        // etc...
+        for ((index, option) in options.withIndex()) {
+            option.displayNameInternal?.let {
+                if (it is DisplayText.ResourceDisplayTextWithIndex) {
+                    it.index = index + 1
+                }
+            }
+
+            option.screenReaderNameInternal?.let {
+                if (it is DisplayText.ResourceDisplayTextWithIndex) {
+                    it.index = index + 1
+                }
+            }
+        }
+    }
+
     /**
      * Optional data for an on watch face editor (not the companion editor).
      *
@@ -294,11 +324,16 @@
             resources: Resources,
             parser: XmlResourceParser,
             attributeId: String,
-            defaultValue: DisplayText? = null
+            defaultValue: DisplayText? = null,
+            indexedResourceNamesSupported: Boolean = false
         ): DisplayText {
             val displayNameId = parser.getAttributeResourceValue(NAMESPACE_APP, attributeId, -1)
             return if (displayNameId != -1) {
-                DisplayText.ResourceDisplayText(resources, displayNameId)
+                if (indexedResourceNamesSupported) {
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameId)
+                } else {
+                    DisplayText.ResourceDisplayText(resources, displayNameId)
+                }
             } else if (parser.hasValue(attributeId) || defaultValue == null) {
                 DisplayText.CharSequenceDisplayText(
                     parser.getAttributeValue(NAMESPACE_APP, attributeId) ?: "")
@@ -430,12 +465,6 @@
         }
     }
 
-    init {
-        require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
-            "defaultOptionIndex must be in the range [0 .. options.size)"
-        }
-    }
-
     internal fun getSettingOptionForId(id: ByteArray?) =
         if (id == null) {
             options[defaultOptionIndex]
@@ -541,6 +570,12 @@
         @SuppressWarnings("HiddenAbstractMethod")
         internal abstract fun getUserStyleSettingClass(): Class<out UserStyleSetting>
 
+        internal open val displayNameInternal: DisplayText?
+            get() = null
+
+        internal open val screenReaderNameInternal: DisplayText?
+            get() = null
+
         /**
          * Machine readable identifier for [Option]s. The length of this identifier may not exceed
          * [MAX_LENGTH].
@@ -887,6 +922,29 @@
      * Not to be confused with complication data source selection.
      */
     public class ComplicationSlotsUserStyleSetting : UserStyleSetting {
+        private constructor(
+            id: Id,
+            displayNameInternal: DisplayText,
+            descriptionInternal: DisplayText,
+            icon: Icon?,
+            watchFaceEditorData: WatchFaceEditorData?,
+            options: List<ComplicationSlotsOption>,
+            defaultOptionIndex: Int,
+            affectedWatchFaceLayers: Collection<WatchFaceLayer>
+        ) : super(
+            id,
+            displayNameInternal,
+            descriptionInternal,
+            icon,
+            watchFaceEditorData,
+            options,
+            defaultOptionIndex,
+            affectedWatchFaceLayers
+        ) {
+            require(affectedWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+                "ComplicationSlotsUserStyleSetting must affect the complications layer"
+            }
+        }
 
         /**
          * Overrides to be applied to the corresponding [androidx.wear.watchface.ComplicationSlot]'s
@@ -1212,8 +1270,8 @@
          * [WatchFaceLayer.COMPLICATIONS].
          * @param defaultOption The default option, used when data isn't persisted. Optional
          * parameter which defaults to the first element of [complicationConfig].
-         * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
-         * to the companion and its contents may be used in preference to other fields by an on
+         * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+         * sent to the companion and its contents may be used in preference to other fields by an on
          * watch face editor.
          * @hide
          */
@@ -1228,7 +1286,7 @@
             affectsWatchFaceLayers: Collection<WatchFaceLayer>,
             defaultOption: ComplicationSlotsOption = complicationConfig.first(),
             watchFaceEditorData: WatchFaceEditorData? = null
-        ) : super(
+        ) : this(
             id,
             DisplayText.CharSequenceDisplayText(displayName),
             DisplayText.CharSequenceDisplayText(description),
@@ -1237,12 +1295,7 @@
             complicationConfig,
             complicationConfig.indexOf(defaultOption),
             affectsWatchFaceLayers
-        ) {
-            require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
-                "ComplicationSlotsUserStyleSetting must affect the complications layer"
-            }
-            requireUniqueOptionIds(id, complicationConfig)
-        }
+        )
 
         /**
          * Constructs a ComplicationSlotsUserStyleSetting where
@@ -1264,8 +1317,8 @@
          * [WatchFaceLayer.COMPLICATIONS].
          * @param defaultOption The default option, used when data isn't persisted. Optional
          * parameter which defaults to the first element of [complicationConfig].
-         * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
-         * to the companion and its contents may be used in preference to other fields by an on
+         * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
+         * sent to the companion and its contents may be used in preference to other fields by an on
          * watch face editor.
          */
         @JvmOverloads
@@ -1279,7 +1332,7 @@
             affectsWatchFaceLayers: Collection<WatchFaceLayer>,
             defaultOption: ComplicationSlotsOption = complicationConfig.first(),
             watchFaceEditorData: WatchFaceEditorData? = null
-        ) : super(
+        ) : this(
             id,
             DisplayText.ResourceDisplayText(resources, displayNameResourceId),
             DisplayText.ResourceDisplayText(resources, descriptionResourceId),
@@ -1288,12 +1341,7 @@
             complicationConfig,
             complicationConfig.indexOf(defaultOption),
             affectsWatchFaceLayers
-        ) {
-            require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
-                "ComplicationSlotsUserStyleSetting must affect the complications layer"
-            }
-            requireUniqueOptionIds(id, complicationConfig)
-        }
+        )
 
         internal constructor (
             id: Id,
@@ -1304,7 +1352,7 @@
             options: List<ComplicationSlotsOption>,
             affectsWatchFaceLayers: Collection<WatchFaceLayer>,
             defaultOptionIndex: Int
-        ) : super(
+        ) : this(
             id,
             displayName,
             description,
@@ -1313,15 +1361,7 @@
             options,
             defaultOptionIndex,
             affectsWatchFaceLayers
-        ) {
-            require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
-                "defaultOptionIndex must be within the range of the options list"
-            }
-            require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
-                "ComplicationSlotsUserStyleSetting must affect the complications layer"
-            }
-            requireUniqueOptionIds(id, options)
-        }
+        )
 
         internal constructor(
             wireFormat: ComplicationsUserStyleSettingWireFormat
@@ -1335,6 +1375,16 @@
                     }
                 }
             }
+            wireFormat.mPerOptionScreenReaderNames?.let { perOptionScreenReaderNames ->
+                val optionsIterator = options.iterator()
+                for (screenReaderName in perOptionScreenReaderNames) {
+                    val option = optionsIterator.next() as ComplicationSlotsOption
+                    screenReaderName?.let {
+                        option.screenReaderNameInternal =
+                            DisplayText.CharSequenceDisplayText(screenReaderName)
+                    }
+                }
+            }
         }
 
         /** @hide */
@@ -1351,7 +1401,8 @@
                 watchFaceEditorData?.toWireFormat(),
                 options.map {
                     (it as ComplicationSlotsOption).watchFaceEditorData?.toWireFormat() ?: Bundle()
-                }
+                },
+                options.map { (it as ComplicationSlotsOption).screenReaderName }
             )
 
         internal companion object {
@@ -1413,18 +1464,32 @@
          */
         public class ComplicationSlotsOption : Option {
             /**
-             * Overlays to be applied when this ComplicationSlotsOption is selected. If this is empty
-             * then the net result is the initial complication configuration.
+             * Overlays to be applied when this ComplicationSlotsOption is selected. If this is
+             * empty then the net result is the initial complication configuration.
              */
             public val complicationSlotOverlays: Collection<ComplicationSlotOverlay>
 
             /** Backing field for [displayName]. */
-            private val displayNameInternal: DisplayText
+            override val displayNameInternal: DisplayText
 
-            /** Localized human readable name for the setting, used in the style selection UI. */
+            /**
+             * Localized human readable name for the setting, used in the editor style selection UI.
+             * This should be short (ideally < 20 characters).
+             */
             public val displayName: CharSequence
                 get() = displayNameInternal.toCharSequence()
 
+            /** Backing field for [screenReaderName]. */
+            override var screenReaderNameInternal: DisplayText?
+
+            /**
+             * Optional localized human readable name for the setting, used by screen readers. This
+             * should be more descriptive than [displayName]. Note prior to android T this is
+             * ignored by companion editors.
+             */
+            public val screenReaderName: CharSequence?
+                get() = screenReaderNameInternal?.toCharSequence()
+
             /** Icon for use in the companion style selection UI. */
             public val icon: Icon?
 
@@ -1441,15 +1506,17 @@
              *
              * @param id [Id] for the element, must be unique.
              * @param displayName Localized human readable name for the element, used in the
-             * userStyle selection UI.
+             * userStyle selection UI. This should be short, ideally < 20 characters.
+             * @param screenReaderName Localized human readable name for the element, used by
+             * screen readers. This should be more descriptive than [displayName].
              * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
              * companion over bluetooth and should be small (ideally a few kb in size).
              * @param complicationSlotOverlays Overlays to be applied when this
              * ComplicationSlotsOption is selected. If this is empty then the net result is the
              * initial complication configuration.
-             * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
-             * sent to the companion and its contents may be used in preference to other fields by
-             * an on watch face editor.
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
              * @hide
              */
             @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -1457,12 +1524,15 @@
             public constructor(
                 id: Id,
                 displayName: CharSequence,
+                screenReaderName: CharSequence,
                 icon: Icon?,
                 complicationSlotOverlays: Collection<ComplicationSlotOverlay>,
                 watchFaceEditorData: WatchFaceEditorData? = null
             ) : super(id, emptyList()) {
                 this.complicationSlotOverlays = complicationSlotOverlays
-                this.displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+                displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+                screenReaderNameInternal =
+                    DisplayText.CharSequenceDisplayText(screenReaderName)
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -1474,16 +1544,20 @@
              * @param id [Id] for the element, must be unique.
              * @param resources The [Resources] from which [displayNameResourceId] is load.
              * @param displayNameResourceId String resource id for a human readable name for the
-             * element, used in the userStyle selection UI.
+             * element, used in the userStyle selection UI. This should be short, ideally < 20
+             * characters. Note if the resource string contains `%1$d` that will get replaced with
+             * the 1-based index of the ComplicationSlotsOption in the list of
+             * ComplicationSlotsOptions.
              * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
              * companion over bluetooth and should be small (ideally a few kb in size).
              * @param complicationSlotOverlays Overlays to be applied when this
              * ComplicationSlotsOption is selected. If this is empty then the net result is the
              * initial complication configuration.
-             * @param watchFaceEditorData Optional data for an on watch face editor, this will not be sent
-             * to the companion and its contents may be used in preference to other fields by an on
-             * watch face editor.
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
              */
+            @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
             @JvmOverloads
             public constructor(
                 id: Id,
@@ -1494,8 +1568,54 @@
                 watchFaceEditorData: WatchFaceEditorData? = null
             ) : super(id, emptyList()) {
                 this.complicationSlotOverlays = complicationSlotOverlays
+                displayNameInternal =
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+                screenReaderNameInternal = null
+                this.icon = icon
+                this.watchFaceEditorData = watchFaceEditorData
+            }
+
+            /**
+             * Constructs a ComplicationSlotsUserStyleSetting with [displayName] constructed from
+             * Resources.
+             *
+             * @param id [Id] for the element, must be unique.
+             * @param resources The [Resources] from which [displayNameResourceId] is load.
+             * @param displayNameResourceId String resource id for a human readable name for the
+             * element, used in the userStyle selection UI. This should be short, ideally < 20
+             * characters. Note if the resource string contains `%1$d` that will get replaced with
+             * the 1-based index of the ComplicationSlotsOption in the list of
+             * ComplicationSlotsOptions.
+             * @param screenReaderNameResourceId String resource id for a human readable name for
+             * the element, used by screen readers. This should be more descriptive than
+             * [displayNameResourceId]. Note if the resource string contains `%1$d` that will get
+             * replaced with the 1-based index of the ComplicationSlotsOption in the list of
+             * ComplicationSlotsOptions. Note prior to android T this is ignored by companion
+             * editors.
+             * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
+             * companion over bluetooth and should be small (ideally a few kb in size).
+             * @param complicationSlotOverlays Overlays to be applied when this
+             * ComplicationSlotsOption is selected. If this is empty then the net result is the
+             * initial complication configuration.
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
+             */
+            @JvmOverloads
+            public constructor(
+                id: Id,
+                resources: Resources,
+                @StringRes displayNameResourceId: Int,
+                @StringRes screenReaderNameResourceId: Int,
+                icon: Icon?,
+                complicationSlotOverlays: Collection<ComplicationSlotOverlay>,
+                watchFaceEditorData: WatchFaceEditorData? = null
+            ) : super(id, emptyList()) {
+                this.complicationSlotOverlays = complicationSlotOverlays
                 this.displayNameInternal =
-                    DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+                this.screenReaderNameInternal =
+                    DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -1503,12 +1623,14 @@
             internal constructor(
                 id: Id,
                 displayName: DisplayText,
+                screenReaderName: DisplayText,
                 icon: Icon?,
                 watchFaceEditorData: WatchFaceEditorData?,
                 complicationSlotOverlays: Collection<ComplicationSlotOverlay>
             ) : super(id, emptyList()) {
                 this.complicationSlotOverlays = complicationSlotOverlays
                 this.displayNameInternal = displayName
+                this.screenReaderNameInternal = screenReaderName
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -1528,6 +1650,7 @@
                         )
                     }
                 displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
+                screenReaderNameInternal = null // This will get overwritten.
                 icon = wireFormat.mIcon
                 watchFaceEditorData = null // This will get overwritten.
             }
@@ -1541,6 +1664,9 @@
                 @Px maxHeight: Int
             ): Int {
                 var sizeEstimate = id.value.size + displayName.length
+                screenReaderName?.let {
+                    sizeEstimate + it.length
+                }
                 for (overlay in complicationSlotOverlays) {
                     sizeEstimate += overlay.estimateWireSizeInBytes()
                 }
@@ -1587,6 +1713,7 @@
                     overlay.write(dos)
                 }
                 displayNameInternal.write(dos)
+                screenReaderNameInternal?.write(dos)
                 icon?.write(dos)
                 watchFaceEditorData?.write(dos)
             }
@@ -1604,7 +1731,15 @@
                     val displayName = createDisplayText(
                         resources,
                         parser,
-                        "displayName"
+                        "displayName",
+                        indexedResourceNamesSupported = true
+                    )
+                    val screenReaderName = createDisplayText(
+                        resources,
+                        parser,
+                        "nameForScreenReaders",
+                        defaultValue = displayName,
+                        indexedResourceNamesSupported = true
                     )
                     val icon = createIcon(
                         resources,
@@ -1640,6 +1775,7 @@
                     return ComplicationSlotsOption(
                         Id(id),
                         displayName,
+                        screenReaderName,
                         icon,
                         watchFaceEditorData,
                         complicationSlotOverlays
@@ -1963,9 +2099,7 @@
             options,
             options.indexOf(defaultOption),
             affectsWatchFaceLayers
-        ) {
-            requireUniqueOptionIds(id, options)
-        }
+        )
 
         /**
          * Constructs a ListUserStyleSetting where [ListUserStyleSetting.displayName] and
@@ -2008,9 +2142,7 @@
             options,
             options.indexOf(defaultOption),
             affectsWatchFaceLayers
-        ) {
-            requireUniqueOptionIds(id, options)
-        }
+        )
 
         internal constructor (
             id: Id,
@@ -2030,11 +2162,7 @@
             options,
             defaultOptionIndex,
             affectsWatchFaceLayers
-        ) {
-            require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
-                "defaultOptionIndex must be within the range of the options list"
-            }
-        }
+        )
 
         internal constructor(wireFormat: ListUserStyleSettingWireFormat) : super(wireFormat) {
             wireFormat.mPerOptionOnWatchFaceEditorBundles?.let { optionsOnWatchFaceEditorIcons ->
@@ -2046,6 +2174,16 @@
                     }
                 }
             }
+            wireFormat.mPerOptionScreenReaderNames?.let { perOptionScreenReaderNames ->
+                val optionsIterator = options.iterator()
+                for (screenReaderName in perOptionScreenReaderNames) {
+                    val option = optionsIterator.next() as ListOption
+                    screenReaderName?.let {
+                        option.screenReaderNameInternal =
+                            DisplayText.CharSequenceDisplayText(screenReaderName)
+                    }
+                }
+            }
         }
 
         /** @hide */
@@ -2060,7 +2198,8 @@
                 defaultOptionIndex,
                 affectedWatchFaceLayers.map { it.ordinal },
                 watchFaceEditorData?.toWireFormat(),
-                options.map { (it as ListOption).watchFaceEditorData?.toWireFormat() ?: Bundle() }
+                options.map { (it as ListOption).watchFaceEditorData?.toWireFormat() ?: Bundle() },
+                options.map { (it as ListOption).screenReaderName }
             )
 
         internal companion object {
@@ -2117,12 +2256,26 @@
          */
         public class ListOption : Option {
             /** Backing field for [displayName]. */
-            private val displayNameInternal: DisplayText
+            override val displayNameInternal: DisplayText
 
-            /** Localized human readable name for the setting, used in the style selection UI. */
+            /**
+             * Localized human readable name for the setting, used in the editor style selection UI.
+             * This should be short (ideally < 20 characters).
+             */
             public val displayName: CharSequence
                 get() = displayNameInternal.toCharSequence()
 
+            /** Backing field for [screenReaderName]. */
+            override var screenReaderNameInternal: DisplayText?
+
+            /**
+             * Optional localized human readable name for the setting, used by screen readers. This
+             * should be more descriptive than [displayName].  Note prior to android T this is
+             * ignored by companion editors.
+             */
+            public val screenReaderName: CharSequence?
+                get() = screenReaderNameInternal?.toCharSequence()
+
             /** Icon for use in the companion style selection UI. */
             public val icon: Icon?
 
@@ -2139,10 +2292,14 @@
              *
              * @param id The [Id] of this ListOption, must be unique within the
              * [ListUserStyleSetting].
-             * @param displayName Localized human readable name for the setting, used in the style
-             * selection UI.
+             ** @param displayName Localized human readable name for the element, used in the
+             * userStyle selection UI. This should be short, ideally < 20 characters.
+             * @param screenReaderName Localized human readable name for the element, used by
+             * screen readers. This should be more descriptive than [displayName].
              * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
              * companion over bluetooth and should be small (ideally a few kb in size).
+             * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
+             * child settings must be listed in [UserStyleSchema.userStyleSettings].
              * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
              * sent to the companion and its contents may be used in preference to other fields by
              * an on watch face editor.
@@ -2152,11 +2309,14 @@
             constructor(
                 id: Id,
                 displayName: CharSequence,
+                screenReaderName: CharSequence,
                 icon: Icon?,
                 childSettings: Collection<UserStyleSetting> = emptyList(),
                 watchFaceEditorData: WatchFaceEditorData? = null
             ) : super(id, childSettings) {
                 displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
+                screenReaderNameInternal =
+                    DisplayText.CharSequenceDisplayText(screenReaderName)
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -2168,14 +2328,17 @@
              * [ListUserStyleSetting].
              * @param resources The [Resources] used to load [displayNameResourceId].
              * @param displayNameResourceId String resource id for a human readable name for the
-             * setting, used in the style selection UI.
+             * element, used in the userStyle selection UI. This should be short, ideally < 20
+             * characters.  Note if the resource string contains `%1$d` that will get replaced with
+             * the 1-based index of the ListOption in the list of ListOptions.
              * @param icon [Icon] for use in the companion style selection UI. This gets sent to the
              * companion over bluetooth and should be small (ideally a few kb in size)
-             * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
-             * sent to the companion and its contents may be used in preference to other fields by
-             * an on watch face editor.
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
              */
             @JvmOverloads
+            @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
             constructor(
                 id: Id,
                 resources: Resources,
@@ -2184,7 +2347,8 @@
                 watchFaceEditorData: WatchFaceEditorData? = null
             ) : super(id, emptyList()) {
                 displayNameInternal =
-                    DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+                screenReaderNameInternal = null
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -2196,16 +2360,17 @@
              * [ListUserStyleSetting].
              * @param resources The [Resources] used to load [displayNameResourceId].
              * @param displayNameResourceId String resource id for a human readable name for the
-             * setting, used in the style selection UI.
+             * element, used in the userStyle selection UI. This should be short, ideally < 20
+             * characters.
              * @param icon [Icon] for use in the style selection UI. This gets sent to the
              * companion over bluetooth and should be small (ideally a few kb in size).
-             * These must be in
              * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
              * child settings must be listed in [UserStyleSchema.userStyleSettings].
-             * @param watchFaceEditorData Optional data for an on watch face editor, this will not be
-             * sent to the companion and its contents may be used in preference to other fields by
-             * an on watch face editor.
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
              */
+            @Deprecated("Use a constructor that sets the screenReaderNameResourceId")
             constructor(
                 id: Id,
                 resources: Resources,
@@ -2215,7 +2380,49 @@
                 watchFaceEditorData: WatchFaceEditorData? = null
             ) : super(id, childSettings) {
                 displayNameInternal =
-                    DisplayText.ResourceDisplayText(resources, displayNameResourceId)
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+                screenReaderNameInternal = null
+                this.icon = icon
+                this.watchFaceEditorData = watchFaceEditorData
+            }
+
+            /**
+             * Constructs a ListOption.
+             *
+             * @param id The [Id] of this ListOption, must be unique within the
+             * [ListUserStyleSetting].
+             * @param resources The [Resources] used to load [displayNameResourceId].
+             * @param displayNameResourceId String resource id for a human readable name for the
+             * element, used in the userStyle selection UI. This should be short, ideally < 20
+             * characters. Note if the resource string contains `%1$d` that will get replaced with
+             * the 1-based index of the ListOption in the list of ListOptions.
+             * @param screenReaderNameResourceId String resource id for a human readable name for
+             * the element, used by screen readers. This should be more descriptive than
+             * [displayNameResourceId]. Note if the resource string contains `%1$d` that will get
+             * replaced with the 1-based index of the ListOption in the list of ListOptions. Note
+             * prior to android T this is ignored by companion editors.
+             * @param icon [Icon] for use in the style selection UI. This gets sent to the
+             * companion over bluetooth and should be small (ideally a few kb in size).
+             * @param childSettings The list of child [UserStyleSetting]s, which may be empty. Any
+             * child settings must be listed in [UserStyleSchema.userStyleSettings].
+             * @param watchFaceEditorData Optional data for an on watch face editor, this will not
+             * be sent to the companion and its contents may be used in preference to other fields
+             * by an on watch face editor.
+             */
+            @JvmOverloads
+            constructor(
+                id: Id,
+                resources: Resources,
+                @StringRes displayNameResourceId: Int,
+                @StringRes screenReaderNameResourceId: Int,
+                icon: Icon?,
+                childSettings: Collection<UserStyleSetting> = emptyList(),
+                watchFaceEditorData: WatchFaceEditorData? = null
+            ) : super(id, childSettings) {
+                displayNameInternal =
+                    DisplayText.ResourceDisplayTextWithIndex(resources, displayNameResourceId)
+                screenReaderNameInternal =
+                    DisplayText.ResourceDisplayTextWithIndex(resources, screenReaderNameResourceId)
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -2223,11 +2430,13 @@
             internal constructor(
                 id: Id,
                 displayName: DisplayText,
+                screenReaderName: DisplayText,
                 icon: Icon?,
                 watchFaceEditorData: WatchFaceEditorData?,
                 childSettings: Collection<UserStyleSetting> = emptyList()
             ) : super(id, childSettings) {
                 displayNameInternal = displayName
+                screenReaderNameInternal = screenReaderName
                 this.icon = icon
                 this.watchFaceEditorData = watchFaceEditorData
             }
@@ -2236,6 +2445,7 @@
                 wireFormat: ListOptionWireFormat
             ) : super(Id(wireFormat.mId), ArrayList()) {
                 displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
+                screenReaderNameInternal = null // This will get overwritten.
                 icon = wireFormat.mIcon
                 watchFaceEditorData = null // This gets overwritten.
             }
@@ -2249,6 +2459,9 @@
                 @Px maxHeight: Int
             ): Int {
                 var sizeEstimate = id.value.size + displayName.length
+                screenReaderName?.let {
+                    sizeEstimate + it.length
+                }
                 icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
                     wireSizeAndDimensions.wireSizeBytes?.let {
                         sizeEstimate += it
@@ -2277,6 +2490,7 @@
             override fun write(dos: DataOutputStream) {
                 dos.write(id.value)
                 displayNameInternal.write(dos)
+                screenReaderNameInternal?.write(dos)
                 icon?.write(dos)
                 watchFaceEditorData?.write(dos)
             }
@@ -2293,7 +2507,15 @@
                     val displayName = createDisplayText(
                         resources,
                         parser,
-                        "displayName"
+                        "displayName",
+                        indexedResourceNamesSupported = true
+                    )
+                    val screenReaderName = createDisplayText(
+                        resources,
+                        parser,
+                        "nameForScreenReaders",
+                        defaultValue = displayName,
+                        indexedResourceNamesSupported = true
                     )
                     val icon = createIcon(
                         resources,
@@ -2333,6 +2555,7 @@
                     return ListOption(
                         Id(id),
                         displayName,
+                        screenReaderName,
                         icon,
                         watchFaceEditorData,
                         childSettings
diff --git a/wear/watchface/watchface-style/src/main/res/values/attrs.xml b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
index 996af90..915948b 100644
--- a/wear/watchface/watchface-style/src/main/res/values/attrs.xml
+++ b/wear/watchface/watchface-style/src/main/res/values/attrs.xml
@@ -14,9 +14,10 @@
   limitations under the License.
   -->
 
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
     <attr name="defaultOptionIndex" format="integer" />
     <attr name="displayName" format="string" />
+    <attr name="nameForScreenReaders" format="string|reference" tools:ignore="MissingDefaultResource" />
     <attr name="id" format="reference|string" />
     <attr name="description" format="string" />
     <attr name="accessibilityTraversalIndex" format="integer" />
@@ -178,6 +179,9 @@
         <attr name="id" />
         <!-- A displayName is required. Consider referencing a string resource for localization. -->
         <attr name="displayName" />
+        <!-- A nameForScreenReaders is optional. Consider referencing a string resource for
+        localization. -->
+        <attr name="nameForScreenReaders" />
         <!-- A link to a companion editor icon is optional but encouraged. -->
         <attr name="android:icon" />
     </declare-styleable>
@@ -189,6 +193,9 @@
         <attr name="id" />
         <!-- A displayName is required. Consider referencing a string resource for localization. -->
         <attr name="displayName" />
+        <!-- A nameForScreenReaders is optional. Consider referencing a string resource for
+        localization. -->
+        <attr name="nameForScreenReaders" />
         <!-- A link to a companion editor icon is optional but encouraged. -->
         <attr name="android:icon" />
     </declare-styleable>
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 8afe47f..cdd035f 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -37,14 +37,26 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-private val redStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
+private val redStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("red_style"),
+    "Red",
+    "Red",
+    icon = null
+)
 
-private val greenStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
+private val greenStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("green_style"),
+    "Green",
+    "Green",
+    icon = null
+)
 
-private val blueStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
+private val blueStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("blue_style"),
+    "Blue",
+    "Blue",
+    icon = null
+)
 
 private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 
@@ -57,14 +69,26 @@
     listOf(WatchFaceLayer.BASE)
 )
 
-private val classicStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
+private val classicStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("classic_style"),
+    "Classic",
+    "Classic",
+    icon = null
+)
 
-private val modernStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
+private val modernStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("modern_style"),
+    "Modern",
+    "Modern",
+    icon = null
+)
 
-private val gothicStyleOption =
-    ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+private val gothicStyleOption = ListUserStyleSetting.ListOption(
+    Option.Id("gothic_style"),
+    "Gothic",
+    "Gothic",
+    icon = null
+)
 
 private val watchHandStyleList =
     listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
@@ -352,21 +376,25 @@
         val option0 = ListUserStyleSetting.ListOption(
             Option.Id("0"),
             "option 0",
+            "option 0",
             icon = null
         )
         val option1 = ListUserStyleSetting.ListOption(
             Option.Id("1"),
             "option 1",
+            "option 1",
             icon = null
         )
         val option0Copy = ListUserStyleSetting.ListOption(
             Option.Id("0"),
             "option #0",
+            "option #0",
             icon = null
         )
         val option1Copy = ListUserStyleSetting.ListOption(
             Option.Id("1"),
             "option #1",
+            "option #1",
             icon = null
         )
         val setting = ListUserStyleSetting(
@@ -686,10 +714,10 @@
     @Test
     fun hierarchicalStyle() {
         val twelveHourClockOption =
-            ListUserStyleSetting.ListOption(Option.Id("12_style"), "12", icon = null)
+            ListUserStyleSetting.ListOption(Option.Id("12_style"), "12", "12", icon = null)
 
         val twentyFourHourClockOption =
-            ListUserStyleSetting.ListOption(Option.Id("24_style"), "24", icon = null)
+            ListUserStyleSetting.ListOption(Option.Id("24_style"), "24", "24", icon = null)
 
         val digitalClockStyleSetting = ListUserStyleSetting(
             UserStyleSetting.Id("digital_clock_style"),
@@ -703,6 +731,7 @@
         val digitalWatchFaceType = ListUserStyleSetting.ListOption(
             Option.Id("digital"),
            "Digital",
+            "Digital",
             icon = null,
             childSettings = listOf(digitalClockStyleSetting, colorStyleSetting)
         )
@@ -710,6 +739,7 @@
         val analogWatchFaceType = ListUserStyleSetting.ListOption(
             Option.Id("analog"),
             "Analog",
+            "Analog",
             icon = null,
             childSettings = listOf(watchHandLengthStyleSetting, watchHandStyleSetting)
         )
@@ -747,6 +777,7 @@
         val leftAndRightComplications = ComplicationSlotsOption(
             Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
             displayName = "Both",
+            screenReaderName = "Both complications",
             icon = null,
             emptyList()
         )
@@ -769,12 +800,14 @@
         val optionA1 = ListUserStyleSetting.ListOption(
             Option.Id("a1_style"),
             displayName = "A1",
+            screenReaderName = "A1 style",
             icon = null,
             childSettings = listOf(complicationSetting1, complicationSetting2)
         )
         val optionA2 = ListUserStyleSetting.ListOption(
             Option.Id("a2_style"),
             displayName = "A2",
+            screenReaderName = "A2 style",
             icon = null,
             childSettings = listOf(complicationSetting2)
         )
@@ -802,6 +835,7 @@
         val leftAndRightComplications = ComplicationSlotsOption(
             Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
             displayName = "Both",
+            screenReaderName = "Both complications",
             icon = null,
             emptyList()
         )
@@ -824,12 +858,14 @@
         val optionA1 = ListUserStyleSetting.ListOption(
             Option.Id("a1_style"),
             displayName = "A1",
+            screenReaderName = "A1 style",
             icon = null,
             childSettings = listOf(complicationSetting1)
         )
         val optionA2 = ListUserStyleSetting.ListOption(
             Option.Id("a2_style"),
             displayName = "A2",
+            screenReaderName = "A2 style",
             icon = null
         )
 
@@ -871,12 +907,14 @@
         val leftAndRightComplications = ComplicationSlotsOption(
             Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
             displayName = "Both",
+            screenReaderName = "Both complications",
             icon = null,
             emptyList()
         )
         val noComplications = ComplicationSlotsOption(
             Option.Id("NO_COMPLICATIONS"),
             displayName = "None",
+            screenReaderName = "No complications",
             icon = null,
             listOf(
                 ComplicationSlotOverlay(leftComplicationID, enabled = false),
@@ -895,12 +933,14 @@
         val leftComplication = ComplicationSlotsOption(
             Option.Id("LEFT_COMPLICATION"),
             displayName = "Left",
+            screenReaderName = "Left complication",
             icon = null,
             listOf(ComplicationSlotOverlay(rightComplicationID, enabled = false))
         )
         val rightComplication = ComplicationSlotsOption(
             Option.Id("RIGHT_COMPLICATION"),
             displayName = "Right",
+            screenReaderName = "Right complication",
             icon = null,
             listOf(ComplicationSlotOverlay(leftComplicationID, enabled = false))
         )
@@ -916,12 +956,14 @@
         val normal = ComplicationSlotsOption(
             Option.Id("Normal"),
             displayName = "Normal",
+            screenReaderName = "Normal",
             icon = null,
             emptyList()
         )
         val traversal = ComplicationSlotsOption(
             Option.Id("Traversal"),
             displayName = "Traversal",
+            screenReaderName = "Traversal",
             icon = null,
             listOf(
                 ComplicationSlotOverlay(leftComplicationID, accessibilityTraversalIndex = 3),
@@ -940,17 +982,23 @@
         val optionA1 = ListUserStyleSetting.ListOption(
             Option.Id("a1_style"),
             displayName = "A1",
+            screenReaderName = "A1 style",
             icon = null,
             childSettings = listOf(complicationSetting1)
         )
         val optionA2 = ListUserStyleSetting.ListOption(
             Option.Id("a2_style"),
             displayName = "A2",
+            screenReaderName = "A2 style",
             icon = null,
             childSettings = listOf(complicationSetting2)
         )
-        val optionA3 =
-            ListUserStyleSetting.ListOption(Option.Id("a3_style"), "A3", icon = null)
+        val optionA3 = ListUserStyleSetting.ListOption(
+            Option.Id("a3_style"),
+            "A3",
+            screenReaderName = "A3 style",
+            icon = null
+        )
 
         val a123Choice = ListUserStyleSetting(
             UserStyleSetting.Id("a123"),
@@ -964,11 +1012,16 @@
         val optionB1 = ListUserStyleSetting.ListOption(
             Option.Id("b1_style"),
             displayName = "B1",
+            screenReaderName = "B1 style",
             icon = null,
             childSettings = listOf(complicationSetting3)
         )
-        val optionB2 =
-            ListUserStyleSetting.ListOption(Option.Id("b2_style"), "B2", icon = null)
+        val optionB2 = ListUserStyleSetting.ListOption(
+            Option.Id("b2_style"),
+            "B2",
+            screenReaderName = "B2 style",
+            icon = null
+        )
 
         val b12Choice = ListUserStyleSetting(
             UserStyleSetting.Id("b12"),
@@ -982,12 +1035,14 @@
         val rootOptionA = ListUserStyleSetting.ListOption(
             Option.Id("a_style"),
             displayName = "A",
+            screenReaderName = "A style",
             icon = null,
             childSettings = listOf(a123Choice)
         )
         val rootOptionB = ListUserStyleSetting.ListOption(
             Option.Id("b_style"),
             displayName = "B",
+            screenReaderName = "B style",
             icon = null,
             childSettings = listOf(b12Choice)
         )
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index cd39c27..4157013 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -60,24 +60,28 @@
     private val option1 = ListOption(
         Option.Id("1"),
         "one",
+        "one screen reader",
         icon1,
         watchFaceEditorData = WatchFaceEditorData(wfIcon1)
     )
     private val option2 = ListOption(
         Option.Id("2"),
         "two",
+        "two screen reader",
         icon2,
         watchFaceEditorData = WatchFaceEditorData(wfIcon2)
     )
     private val option3 = ListOption(
         Option.Id("3"),
         "three",
+        "three screen reader",
         icon3,
         watchFaceEditorData = WatchFaceEditorData(wfIcon3)
     )
     private val option4 = ListOption(
         Option.Id("4"),
         "four",
+        "four screen reader",
         icon4,
         watchFaceEditorData = WatchFaceEditorData(wfIcon4)
     )
@@ -277,10 +281,10 @@
     @Suppress("Deprecation") // userStyleSettings
     public fun parcelAndUnparcelHierarchicalSchema() {
         val twelveHourClockOption =
-            ListOption(Option.Id("12_style"), "12", icon = null)
+            ListOption(Option.Id("12_style"), "12", "12", icon = null)
 
         val twentyFourHourClockOption =
-            ListOption(Option.Id("24_style"), "24", icon = null)
+            ListOption(Option.Id("24_style"), "24", "24", icon = null)
 
         val digitalClockStyleSetting = ListUserStyleSetting(
             UserStyleSetting.Id("digital_clock_style"),
@@ -294,6 +298,7 @@
         val digitalWatchFaceType = ListOption(
             Option.Id("digital"),
             "Digital",
+            "Digital setting",
             icon = null,
             childSettings = listOf(digitalClockStyleSetting)
         )
@@ -322,6 +327,7 @@
         val analogWatchFaceType = ListOption(
             Option.Id("analog"),
             "Analog",
+            "Analog setting",
             icon = null,
             childSettings = listOf(styleSetting1, styleSetting2)
         )
@@ -556,12 +562,14 @@
                 ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
                     "Both",
+                    "Both complications visible",
                     null,
                     listOf()
                 ),
                 ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     Option.Id("NO_COMPLICATIONS"),
                     "None",
+                    "No complications visible",
                     null,
                     listOf(
                         ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -577,6 +585,7 @@
                 ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     Option.Id("LEFT_COMPLICATION"),
                     "Left",
+                    "Left complication visible",
                     null,
                     listOf(
                         ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -594,6 +603,7 @@
                 ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     Option.Id("RIGHT_COMPLICATION"),
                     "Right",
+                    "Right complication visible",
                     null,
                     listOf(
                         ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
@@ -605,6 +615,7 @@
                 ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
                     Option.Id("RIGHT_COMPLICATION_MOVED"),
                     "MoveRight",
+                    "Right complication moved",
                     null,
                     listOf(
                         ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
index 5528d83..0101694 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
@@ -269,25 +269,29 @@
                 icon = null,
                 listOf(
                     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                        UserStyleSetting.Option.Id("both"),
+                        Option.Id("both"),
+                        "left and right complications",
                         "left and right complications",
                         icon = null,
                         listOf(leftComplicationSlot, rightComplicationSlot),
                     ),
                     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                        UserStyleSetting.Option.Id("left"),
+                        Option.Id("left"),
+                        "left complication",
                         "left complication",
                         icon = null,
                         listOf(leftComplicationSlot),
                     ),
                     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                        UserStyleSetting.Option.Id("right"),
+                        Option.Id("right"),
+                        "right complication",
                         "right complication",
                         icon = null,
                         listOf(rightComplicationSlot),
                     ),
                     UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                        UserStyleSetting.Option.Id("both"),
+                        Option.Id("both"),
+                        "right and left complications",
                         "right and left complications",
                         icon = null,
                         listOf(rightComplicationSlot, leftComplicationSlot),
@@ -310,21 +314,25 @@
                     UserStyleSetting.ListUserStyleSetting.ListOption(
                         UserStyleSetting.Option.Id("plain"),
                         "plain hands",
+                        "plain hands",
                         icon = null
                     ),
                     UserStyleSetting.ListUserStyleSetting.ListOption(
                         UserStyleSetting.Option.Id("florescent"),
                         "florescent hands",
+                        "florescent hands",
                         icon = null
                     ),
                     UserStyleSetting.ListUserStyleSetting.ListOption(
                         UserStyleSetting.Option.Id("thick"),
                         "thick hands",
+                        "thick hands",
                         icon = null
                     ),
                     UserStyleSetting.ListUserStyleSetting.ListOption(
                         UserStyleSetting.Option.Id("plain"),
                         "simple hands",
+                        "simple hands",
                         icon = null
                     )
                 ),
@@ -392,6 +400,7 @@
         val option = UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
             Option.Id("both"),
             "right and left complications",
+            "right and left complications",
             icon = null,
             listOf(rightComplicationSlot, leftComplicationSlot),
         )
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index d2186c6..c70d866 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -90,18 +90,21 @@
                     Option.Id(RED_STYLE),
                     resources,
                     R.string.colors_style_red,
+                    R.string.colors_style_red_screen_reader,
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 ListUserStyleSetting.ListOption(
                     Option.Id(GREEN_STYLE),
                     resources,
                     R.string.colors_style_green,
+                    R.string.colors_style_green_screen_reader,
                     Icon.createWithResource(this, R.drawable.green_style)
                 ),
                 ListUserStyleSetting.ListOption(
                     Option.Id(BLUE_STYLE),
                     resources,
                     R.string.colors_style_blue,
+                    R.string.colors_style_blue_screen_reader,
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index 3fd4f52..5f497f3 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -87,18 +87,21 @@
                     Option.Id(RED_STYLE),
                     resources,
                     R.string.colors_style_red,
+                    R.string.colors_style_red_screen_reader,
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
                     Option.Id(GREEN_STYLE),
                     resources,
                     R.string.colors_style_green,
+                    R.string.colors_style_green_screen_reader,
                     Icon.createWithResource(this, R.drawable.green_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
                     Option.Id(BLUE_STYLE),
                     resources,
                     R.string.colors_style_blue,
+                    R.string.colors_style_blue_screen_reader,
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 1ae8d16..766c9e5 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -62,6 +62,7 @@
             UserStyleSetting.Option.Id("12_style"),
             resources,
             R.string.digital_clock_style_12,
+            R.string.digital_clock_style_12_screen_reader,
             Icon.createWithResource(this, R.drawable.red_style)
         )
     }
@@ -71,6 +72,7 @@
             UserStyleSetting.Option.Id("24_style"),
             resources,
             R.string.digital_clock_style_24,
+            R.string.digital_clock_style_24_screen_reader,
             Icon.createWithResource(this, R.drawable.red_style)
         )
     }
@@ -133,6 +135,7 @@
             UserStyleSetting.Option.Id(RED_STYLE),
             resources,
             R.string.colors_style_red,
+            R.string.colors_style_red_screen_reader,
             Icon.createWithResource(this, R.drawable.red_style)
         )
     }
@@ -142,6 +145,7 @@
             UserStyleSetting.Option.Id(GREEN_STYLE),
             resources,
             R.string.colors_style_green,
+            R.string.colors_style_green_screen_reader,
             Icon.createWithResource(this, R.drawable.green_style)
         )
     }
@@ -151,6 +155,7 @@
             UserStyleSetting.Option.Id(BLUE_STYLE),
             resources,
             R.string.colors_style_blue,
+            R.string.colors_style_blue_screen_reader,
             Icon.createWithResource(this, R.drawable.blue_style)
         )
     }
@@ -236,6 +241,7 @@
             UserStyleSetting.Option.Id("digital"),
             resources,
             R.string.style_digital_watch,
+            R.string.style_digital_watch_screen_reader,
             icon = Icon.createWithResource(this, R.drawable.d),
             childSettings = listOf(
                 digitalClockStyleSetting,
@@ -250,6 +256,7 @@
             UserStyleSetting.Option.Id("analog"),
             resources,
             R.string.style_analog_watch,
+            R.string.style_analog_watch_screen_reader,
             icon = Icon.createWithResource(this, R.drawable.a),
             childSettings = listOf(
                 colorStyleSetting,
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index 7e40c2a..cb5ad23 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -56,12 +56,14 @@
                     UserStyleSetting.Option.Id("yellow_style"),
                     resources,
                     R.string.colors_style_yellow,
+                    R.string.colors_style_yellow_screen_reader,
                     Icon.createWithResource(this, R.drawable.yellow_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
                     UserStyleSetting.Option.Id("blue_style"),
                     resources,
                     R.string.colors_style_blue,
+                    R.string.colors_style_blue_screen_reader,
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 72ec126..5d59314 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -81,12 +81,14 @@
                     Option.Id("red_style"),
                     resources,
                     R.string.colors_style_red,
+                    R.string.colors_style_red_screen_reader,
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 ListUserStyleSetting.ListOption(
                     Option.Id("green_style"),
                     resources,
                     R.string.colors_style_green,
+                    R.string.colors_style_green_screen_reader,
                     Icon.createWithResource(this, R.drawable.green_style)
                 )
             ),
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index c2404ff..622c224 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -76,18 +76,21 @@
                                 Option.Id(RED_STYLE),
                                 resources,
                                 R.string.colors_style_red,
+                                R.string.colors_style_red_screen_reader,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
                                 Option.Id(GREEN_STYLE),
                                 resources,
                                 R.string.colors_style_green,
+                                R.string.colors_style_green_screen_reader,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
                                 Option.Id(BLUE_STYLE),
                                 resources,
                                 R.string.colors_style_blue,
+                                R.string.colors_style_blue_screen_reader,
                                 icon = null
                             )
                         ),
@@ -108,18 +111,21 @@
                                 Option.Id(CLASSIC_STYLE),
                                 resources,
                                 R.string.hand_style_classic,
+                                R.string.hand_style_classic_screen_reader,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
                                 Option.Id(MODERN_STYLE),
                                 resources,
                                 R.string.hand_style_modern,
+                                R.string.hand_style_modern_screen_reader,
                                 icon = null
                             ),
                             ListUserStyleSetting.ListOption(
                                 Option.Id(GOTHIC_STYLE),
                                 resources,
                                 R.string.hand_style_gothic,
+                                R.string.hand_style_gothic_screen_reader,
                                 icon = null
                             )
                         ),
diff --git a/wear/watchface/watchface/samples/src/main/res/values/strings.xml b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
index de9835e..8dce3e9 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -54,6 +54,18 @@
     <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
     <string name="colors_style_yellow">Yellow</string>
 
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_red_screen_reader">Red color theme</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_green_screen_reader">Green color theme</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_blue_screen_reader">Blue color theme</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_yellow_screen_reader">Yellow color theme</string>
+
     <!-- Name of watchface style category for selecting the hand style [CHAR LIMIT=20] -->
     <string name="hand_style_setting" translatable="false">Hand Style</string>
 
@@ -63,12 +75,24 @@
     <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
     <string name="hand_style_classic" translatable="false">Classic</string>
 
+    <!-- An option with the watch face hand style settings  for use by screen readers -->
+    <string name="hand_style_classic_screen_reader" translatable="false">Classic watch hand
+    style</string>
+
     <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
     <string name="hand_style_modern" translatable="false">Modern</string>
 
+    <!-- An option with the watch face hand style settings  for use by screen readers -->
+    <string name="hand_style_modern_screen_reader" translatable="false">Modern watch hand
+    style</string>
+
     <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
     <string name="hand_style_gothic" translatable="false">Gothic</string>
 
+    <!-- An option with the watch face hand style settings for use by screen readers -->
+    <string name="hand_style_gothic_screen_reader" translatable="false">Gothic watch hand
+    style</string>
+
     <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
     <string name="watchface_pips_setting">Hour Pips</string>
 
@@ -171,15 +195,27 @@
     <!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
     <string name="digital_clock_style_12">12</string>
 
-    <!-- Menu option for a 24 hour digital clock display. [CHAR LIMIT=20] -->
+    <!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
+    <string name="digital_clock_style_12_screen_reader">12 hour clock</string>
+
+    <!-- Menu option for a 24 hour digital clock display used by screen reader. -->
     <string name="digital_clock_style_24">24</string>
 
+    <!-- Menu option for a 24 hour digital clock display used by screen reader.-->
+    <string name="digital_clock_style_24_screen_reader">24 hour clock</string>
+
     <!-- Menu option for selecting a digital clock [CHAR LIMIT=20] -->
     <string name="style_digital_watch">Digital</string>
 
+    <!-- Menu option for selecting a digital clock from a screen reader. -->
+    <string name="style_digital_watch_screen_reader">Digital watch style</string>
+
     <!-- Menu option for selecting an analog clock [CHAR LIMIT=20] -->
     <string name="style_analog_watch">Analog</string>
 
+    <!-- Menu option for selecting an analog clock  from a screen reader.-->
+    <string name="style_analog_watch_screen_reader">Analog watch style </string>
+
     <!-- Title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
     <string name="clock_type">Clock type</string>
 
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
index 2f2f817..7f3f0ad 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/WatchFaceServiceAndroidTest.kt
@@ -38,28 +38,31 @@
         val engine = serviceSpy.onCreateEngine() as WatchFaceService.EngineWrapper
 
         try {
-            val schema = UserStyleSchema(listOf(
-                UserStyleSetting.ListUserStyleSetting(
-                    UserStyleSetting.Id("someId"),
-                    "displayName",
-                    "description",
-                    Icon.createWithResource(
-                        context,
-                        androidx.wear.watchface.test.R.drawable.example_icon_24
-                    ),
-                    listOf(
-                        UserStyleSetting.ListUserStyleSetting.ListOption(
-                            UserStyleSetting.Option.Id("red_style"),
-                            displayName = "Red",
-                            icon = Icon.createWithResource(
-                                context,
-                                androidx.wear.watchface.test.R.drawable.example_icon_24
-                            ),
-                        )
-                    ),
-                    listOf(WatchFaceLayer.BASE)
+            val schema = UserStyleSchema(
+                listOf(
+                    UserStyleSetting.ListUserStyleSetting(
+                        UserStyleSetting.Id("someId"),
+                        "displayName",
+                        "description",
+                        Icon.createWithResource(
+                            context,
+                            androidx.wear.watchface.test.R.drawable.example_icon_24
+                        ),
+                        listOf(
+                            UserStyleSetting.ListUserStyleSetting.ListOption(
+                                UserStyleSetting.Option.Id("red_style"),
+                                displayName = "Red",
+                                screenReaderName = "Red watchface style",
+                                icon = Icon.createWithResource(
+                                    context,
+                                    androidx.wear.watchface.test.R.drawable.example_icon_24
+                                ),
+                            )
+                        ),
+                        listOf(WatchFaceLayer.BASE)
+                    )
                 )
-            ))
+            )
 
             // expect no exception
             engine.validateSchemaWireSize(schema)
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 520e72f..7bcd27e 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -195,13 +195,13 @@
     private val complicationDrawableBackground = ComplicationDrawable(context)
 
     private val redStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", "Red", icon = null)
 
     private val greenStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", "Green", icon = null)
 
     private val blueStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", "Blue", icon = null)
 
     private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
 
@@ -214,14 +214,26 @@
         listOf(WatchFaceLayer.BASE)
     )
 
-    private val classicStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
+    private val classicStyleOption = ListUserStyleSetting.ListOption(
+        Option.Id("classic_style"),
+        "Classic",
+        "Classic",
+        icon = null
+    )
 
-    private val modernStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
+    private val modernStyleOption = ListUserStyleSetting.ListOption(
+        Option.Id("modern_style"),
+        "Modern",
+        "Modern",
+        icon = null
+    )
 
-    private val gothicStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
+    private val gothicStyleOption = ListUserStyleSetting.ListOption(
+        Option.Id("gothic_style"),
+        "Gothic",
+        "Gothic",
+        icon = null
+    )
 
     private val watchHandStyleList =
         listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
@@ -236,7 +248,7 @@
     )
 
     private val badStyleOption =
-        ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", icon = null)
+        ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", "Bad", icon = null)
 
     @Suppress("DEPRECATION") // setDefaultDataSourceType
     private val leftComplication =
@@ -356,6 +368,7 @@
     private val leftAndRightComplicationsOption = ComplicationSlotsOption(
         Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
         "Left and Right",
+        "Left and Right complications",
         null,
         // An empty list means use the initial config.
         emptyList()
@@ -363,6 +376,7 @@
     private val noComplicationsOption = ComplicationSlotsOption(
         Option.Id(NO_COMPLICATIONS),
         "None",
+        "No complications",
         null,
         listOf(
             ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -374,6 +388,7 @@
     private val leftOnlyComplicationsOption = ComplicationSlotsOption(
         Option.Id(LEFT_COMPLICATION),
         "Left",
+        "Left complication",
         null,
         listOf(
             ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -385,6 +400,7 @@
     private val rightOnlyComplicationsOption = ComplicationSlotsOption(
         Option.Id(RIGHT_COMPLICATION),
         "Right",
+        "Right complication",
         null,
         listOf(
             ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -2027,6 +2043,7 @@
         val rightAndSelectComplicationsOption = ComplicationSlotsOption(
             Option.Id(RIGHT_AND_LEFT_COMPLICATIONS),
             "Right and Left",
+            "Right and Left complications",
             null,
             listOf(
                 ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
@@ -2648,6 +2665,7 @@
                 ComplicationSlotsOption(
                     Option.Id("one"),
                     "one",
+                    "one",
                     null,
                     listOf(
                         ComplicationSlotOverlay(
@@ -2658,7 +2676,8 @@
                 ),
                 ComplicationSlotsOption(
                     Option.Id("two"),
-                    "teo",
+                    "two",
+                    "two",
                     null,
                     listOf(
                         ComplicationSlotOverlay(
@@ -2720,16 +2739,23 @@
         val option1 = ListUserStyleSetting.ListOption(
             Option.Id("1"),
             displayName = "1",
+            screenReaderName = "1",
             icon = null,
             childSettings = listOf(complicationsStyleSetting)
         )
         val option2 = ListUserStyleSetting.ListOption(
             Option.Id("2"),
             displayName = "2",
+            screenReaderName = "2",
             icon = null,
             childSettings = listOf(complicationsStyleSetting2)
         )
-        val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "3", icon = null)
+        val option3 = ListUserStyleSetting.ListOption(
+            Option.Id("3"),
+            displayName = "3",
+            screenReaderName = "3",
+            icon = null
+        )
         val choice = ListUserStyleSetting(
             UserStyleSetting.Id("123"),
             displayName = "123",
@@ -3919,6 +3945,7 @@
         val rightComplicationBoundsOption = ComplicationSlotsOption(
             Option.Id(RIGHT_COMPLICATION),
             "Right",
+            "Right",
             null,
             listOf(
                 ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
@@ -3936,6 +3963,7 @@
                 ComplicationSlotsOption(
                     Option.Id("Default"),
                     "Default",
+                    "Default",
                     null,
                     emptyList()
                 ),
@@ -4347,7 +4375,7 @@
         val longOptionsList = ArrayList<ListUserStyleSetting.ListOption>()
         for (i in 0..10000) {
             longOptionsList.add(
-                ListUserStyleSetting.ListOption(Option.Id("id$i"), "Name", icon = null)
+                ListUserStyleSetting.ListOption(Option.Id("id$i"), "Name", "Name", icon = null)
             )
         }
         val tooLargeList = ListUserStyleSetting(
@@ -5413,6 +5441,7 @@
             ComplicationSlotsOption(
                 Option.Id("123"),
                 "testOption",
+                "testOption",
                 icon = null,
                 complicationSlotOverlays = listOf(
                     ComplicationSlotOverlay(
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index 91ae917..acad3fa 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -43,9 +43,10 @@
             WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
             return;
         }
-        ProcessGlobalConfig.createInstance()
-                .setDataDirectorySuffix(this, "per_process_webview_data_0")
-                .apply();
+        ProcessGlobalConfig config = new ProcessGlobalConfig();
+        config.setDataDirectorySuffix(this,
+                "per_process_webview_data_0");
+        ProcessGlobalConfig.apply(config);
         setContentView(R.layout.activity_process_global_config);
         WebView wv = findViewById(R.id.process_global_config_webview);
         wv.setWebViewClient(new WebViewClient());
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -10,8 +10,8 @@
   }
 
   public class ProcessGlobalConfig {
-    method public void apply();
-    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    ctor public ProcessGlobalConfig();
+    method public static void apply(androidx.webkit.ProcessGlobalConfig);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
   }
 
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -10,8 +10,8 @@
   }
 
   public class ProcessGlobalConfig {
-    method public void apply();
-    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    ctor public ProcessGlobalConfig();
+    method public static void apply(androidx.webkit.ProcessGlobalConfig);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
   }
 
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index f354f29..faf13cb 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -10,8 +10,8 @@
   }
 
   public class ProcessGlobalConfig {
-    method public void apply();
-    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    ctor public ProcessGlobalConfig();
+    method public static void apply(androidx.webkit.ProcessGlobalConfig);
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
   }
 
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
index bdcfa0f..b193b2a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.ApiHelperForP;
 import androidx.webkit.internal.StartupApiFeature;
 import androidx.webkit.internal.WebViewFeatureInternal;
 
@@ -28,7 +30,6 @@
 import java.io.File;
 import java.lang.reflect.Field;
 import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 /**
@@ -37,7 +38,8 @@
  * WebView has some process-global configuration parameters that cannot be changed once WebView has
  * been loaded. This class allows apps to set these parameters.
  * <p>
- * If it is used, the configuration should be set and {@link #apply()} should be called prior to
+ * If it is used, the configuration should be set and
+ * {@link #apply(androidx.webkit.ProcessGlobalConfig)} should be called prior to
  * loading WebView into the calling process. Most of the methods in
  * {@link android.webkit} and {@link androidx.webkit} packages load WebView, so the
  * configuration should be applied before calling any of these methods.
@@ -45,13 +47,12 @@
  * The following code configures the data directory suffix that WebView
  * uses and then applies the configuration. WebView uses this configuration when it is loaded.
  * <pre class="prettyprint">
- * ProcessGlobalConfig.createInstance()
- *                    .setDataDirectorySuffix("random_suffix")
- *                    .apply();
+ * ProcessGlobalConfig config = new ProcessGlobalConfig();
+ * config.setDataDirectorySuffix("random_suffix")
+ * ProcessGlobalConfig.apply(config);
  * </pre>
  * <p>
- * Restrictions are in place to ensure that {@link #createInstance()} can only be called once.
- * The setters and {@link #apply()} can also only be called once.
+ * {@link ProcessGlobalConfig#apply(androidx.webkit.ProcessGlobalConfig)} can only be called once.
  * <p>
  * Only a single thread should access this class at a given time.
  * <p>
@@ -61,87 +62,16 @@
 public class ProcessGlobalConfig {
     private static final AtomicReference<HashMap<String, Object>> sProcessGlobalConfig =
             new AtomicReference<HashMap<String, Object>>();
-    private static AtomicBoolean sInstanceCreated = new AtomicBoolean(false);
-    private boolean mApplyCalled = false;
-    private String mDataDirectorySuffix;
+    private static final Object sLock = new Object();
+    @GuardedBy("sLock")
+    private static boolean sApplyCalled = false;
+    String mDataDirectorySuffix;
 
-    private ProcessGlobalConfig() {
-    }
 
     /**
-     * Creates instance of {@link ProcessGlobalConfig}.
-     *
-     * This method can only be called once.
-     *
-     * @return {@link ProcessGlobalConfig} object where configuration can be set and applied
-     *
-     * @throws IllegalStateException if this method was called before
+     * Creates a {@link ProcessGlobalConfig} object.
      */
-    @NonNull
-    public static ProcessGlobalConfig createInstance() {
-        if (!sInstanceCreated.compareAndSet(false, true)) {
-            throw new IllegalStateException("ProcessGlobalConfig#createInstance was "
-                    + "called more than once, which is an illegal operation. The configuration "
-                    + "settings provided by ProcessGlobalConfig take effect only once, when "
-                    + "WebView is first loaded into the current process. Every process should "
-                    + "only ever create a single instance of ProcessGlobalConfig and apply it "
-                    + "once, before any calls to android.webkit APIs, such as during early app "
-                    + "startup."
-            );
-        }
-        return new ProcessGlobalConfig();
-    }
-
-    /**
-     * Applies the configuration to be used by WebView on loading.
-     *
-     * If this method is not called, the configuration that is set will not be applied.
-     * This method can only be called once.
-     * <p>
-     * Calling this method will not cause WebView to be loaded and will not block the calling
-     * thread.
-     *
-     * @throws IllegalStateException if WebView has already been initialized
-     *                               in the current process or if this method was called before
-     */
-    public void apply() {
-        // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
-        //  WebView is going to look for it, and throw if they are not the same.
-        //  For this, we would need to reflect into Android Framework internals to get
-        //  ActivityThread.currentApplication().getClassLoader() and see if it is the same as
-        //  this.getClass().getClassLoader(). This would add reflection that we might not add a
-        //  framework API for. Once we know what framework path we will take for
-        //  ProcessGlobalConfig, revisit this.
-        HashMap<String, Object> configMap = new HashMap<String, Object>();
-        if (mApplyCalled) {
-            throw new IllegalStateException("ProcessGlobalConfig#apply was "
-                    + "called more than once, which is an illegal operation. The configuration "
-                    + "settings provided by ProcessGlobalConfig take effect only once, when "
-                    + "WebView is first loaded into the current process. Every process should "
-                    + "only ever create a single instance of ProcessGlobalConfig and apply it "
-                    + "once, before any calls to android.webkit APIs, such as during early app "
-                    + "startup."
-            );
-        }
-        mApplyCalled = true;
-        if (webViewCurrentlyLoaded()) {
-            throw new IllegalStateException("WebView has already been loaded in the current "
-                    + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
-                    + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
-                    + "calls to android.webkit APIs, such as during early app startup.");
-        }
-
-        final StartupApiFeature.P feature =
-                WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
-        if (feature.isSupportedByFramework()) {
-            androidx.webkit.internal.ApiHelperForP.setDataDirectorySuffix(mDataDirectorySuffix);
-        } else {
-            configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX, mDataDirectorySuffix);
-        }
-        if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
-            throw new RuntimeException("Attempting to set ProcessGlobalConfig"
-                    + "#sProcessGlobalConfig when it was already set");
-        }
+    public ProcessGlobalConfig() {
     }
 
     /**
@@ -172,6 +102,7 @@
      * @param context a Context to access application assets This value cannot be null.
      * @param suffix The directory name suffix to be used for the current
      *               process. Must not contain a path separator and should not be empty.
+     * @return the ProcessGlobalConfig that has the value set to allow chaining of setters
      * @throws IllegalStateException if WebView has already been initialized
      *                               in the current process or if this method was called before
      * @throws IllegalArgumentException if the suffix contains a path separator or is empty.
@@ -182,11 +113,6 @@
     @NonNull
     public ProcessGlobalConfig setDataDirectorySuffix(@NonNull Context context,
             @NonNull String suffix) {
-        if (mDataDirectorySuffix != null) {
-            throw new IllegalStateException(
-                    "ProcessGlobalConfig#setDataDirectorySuffix(String) was already "
-                            + "called");
-        }
         final StartupApiFeature.P feature =
                 WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
         if (!feature.isSupported(context)) {
@@ -203,7 +129,62 @@
         return this;
     }
 
-    private boolean webViewCurrentlyLoaded() {
+    /**
+     * Applies the configuration to be used by WebView on loading.
+     *
+     * This method can only be called once.
+     * <p>
+     * Calling this method will not cause WebView to be loaded and will not block the calling
+     * thread.
+     *
+     * @param config the config to be applied
+     * @throws IllegalStateException if WebView has already been initialized
+     *                               in the current process or if this method was called before
+     */
+    public static void apply(@NonNull ProcessGlobalConfig config) {
+        // TODO(crbug.com/1355297): We can check if we are storing the config in the place that
+        //  WebView is going to look for it, and throw if they are not the same.
+        //  For this, we would need to reflect into Android Framework internals to get
+        //  ActivityThread.currentApplication().getClassLoader() and see if it is the same as
+        //  this.getClass().getClassLoader(). This would add reflection that we might not add a
+        //  framework API for. Once we know what framework path we will take for
+        //  ProcessGlobalConfig, revisit this.
+        synchronized (sLock) {
+            if (sApplyCalled) {
+                throw new IllegalStateException("ProcessGlobalConfig#apply was "
+                        + "called more than once, which is an illegal operation. The configuration "
+                        + "settings provided by ProcessGlobalConfig take effect only once, when "
+                        + "WebView is first loaded into the current process. Every process should "
+                        + "only ever create a single instance of ProcessGlobalConfig and apply it "
+                        + "once, before any calls to android.webkit APIs, such as during early app "
+                        + "startup."
+                );
+            }
+            sApplyCalled = true;
+        }
+        HashMap<String, Object> configMap = new HashMap<String, Object>();
+        if (webViewCurrentlyLoaded()) {
+            throw new IllegalStateException("WebView has already been loaded in the current "
+                    + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
+                    + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
+                    + "calls to android.webkit APIs, such as during early app startup.");
+        }
+
+        final StartupApiFeature.P feature =
+                WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
+        if (feature.isSupportedByFramework()) {
+            ApiHelperForP.setDataDirectorySuffix(config.mDataDirectorySuffix);
+        } else {
+            configMap.put(ProcessGlobalConfigConstants.DATA_DIRECTORY_SUFFIX,
+                    config.mDataDirectorySuffix);
+        }
+        if (!sProcessGlobalConfig.compareAndSet(null, configMap)) {
+            throw new RuntimeException("Attempting to set ProcessGlobalConfig"
+                    + "#sProcessGlobalConfig when it was already set");
+        }
+    }
+
+    private static boolean webViewCurrentlyLoaded() {
         // TODO(crbug.com/1355297): This is racy but it is the best we can do for now since we can't
         //  access the lock for sProviderInstance in WebView. Evaluate a framework path for
         //  ProcessGlobalConfig.