Merge "Use new image capture pipeline for software JPEG path / remove unused classes and methods." into androidx-main
diff --git a/activity/settings.gradle b/activity/settings.gradle
index 34492d9..91aede1 100644
--- a/activity/settings.gradle
+++ b/activity/settings.gradle
@@ -28,7 +28,7 @@
     setupPlayground("..")
     selectProjectsFromAndroidX({ name ->
         if (name.startsWith(":activity")) return true
-        if (name.startsWith(":lifecycle")) return true
+        if (name.startsWith(":lifecycle") && !name.contains("integration-tests")) return true
         if (name.startsWith(":annotation")) return true
         if (name == ":internal-testutils-runtime") return true
         if (name == ":internal-testutils-truth") return true
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/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 8784f69..94141ef 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -34,6 +34,8 @@
 import com.google.gson.GsonBuilder
 import java.io.File
 import java.io.FileNotFoundException
+import java.time.Duration
+import java.time.LocalDateTime
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
 import org.gradle.api.Plugin
@@ -435,7 +437,12 @@
             task.destinationFile.set(getMetadataRegularFile(project))
         }
 
+        val metricsDirectory = project.buildDir
+        val metricsFile = File(metricsDirectory, "build-metrics.json")
+        val projectName = project.name
+
         val dackkaTask = project.tasks.register("docs", DackkaTask::class.java) { task ->
+            var taskStartTime: LocalDateTime? = null
             task.apply {
                 dependsOn(unzipJvmSourcesTask)
                 dependsOn(unzipSamplesTask)
@@ -462,6 +469,17 @@
                 // See go/dackka-source-link for details on this link.
                 baseSourceLink = "https://cs.android.com/search?" +
                     "q=file:%s+class:%s&ss=androidx/platform/frameworks/support"
+                task.doFirst {
+                    taskStartTime = LocalDateTime.now()
+                }
+                task.doLast {
+                    val taskEndTime = LocalDateTime.now()
+                    val duration = Duration.between(taskStartTime, taskEndTime).toMillis()
+                    metricsDirectory.mkdirs()
+                    metricsFile.writeText(
+                        "{ \"${projectName}_docs_execution_duration\": $duration }"
+                    )
+                }
             }
         }
 
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 52fb12e..2e0f9e9 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -28,7 +28,7 @@
 
   # Parse performance profile reports (generated with the --profile option above) and re-export
   # the metrics in an easily machine-readable format for tracking
-  impl/parse_profile_htmls.sh
+  impl/parse_profile_data.sh
 fi
 
 echo "Completing $0 at $(date) with exit value $EXIT_VALUE"
diff --git a/busytown/androidx_incremental.sh b/busytown/androidx_incremental.sh
index 4b17487..a9f5b1a 100755
--- a/busytown/androidx_incremental.sh
+++ b/busytown/androidx_incremental.sh
@@ -66,7 +66,7 @@
     fi
 
     # Parse performance profile reports (generated with the --profile option above) and re-export the metrics in an easily machine-readable format for tracking
-    impl/parse_profile_htmls.sh
+    impl/parse_profile_data.sh
 fi
 
 echo "Completing $0 at $(date) with exit value $EXIT_VALUE"
diff --git a/busytown/impl/build-studio-and-androidx.sh b/busytown/impl/build-studio-and-androidx.sh
index 2d87b18..0359b84 100755
--- a/busytown/impl/build-studio-and-androidx.sh
+++ b/busytown/impl/build-studio-and-androidx.sh
@@ -108,7 +108,7 @@
   else
     RETURN_CODE=1
   fi
-  $SCRIPTS_DIR/impl/parse_profile_htmls.sh
+  $SCRIPTS_DIR/impl/parse_profile_data.sh
 
   # zip build scan
   scanZip="$DIST_DIR/scan.zip"
diff --git a/busytown/impl/parse_profile_data.sh b/busytown/impl/parse_profile_data.sh
new file mode 100755
index 0000000..84f6844
--- /dev/null
+++ b/busytown/impl/parse_profile_data.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -e
+
+# This is a helper script to be called by androidx.sh
+# This script locates, parses, and merges build profiling information from various report files
+
+cd "$(dirname $0)"
+
+if [ "$OUT_DIR" == "" ]; then
+  OUT_DIR=../../../../out
+fi
+if [ "$DIST_DIR" == "" ]; then
+  DIST_DIR="$OUT_DIR/dist"
+fi
+
+METRICS_DIR="$DIST_DIR/librarymetrics/build"
+INTERMEDIATES_DIR=$OUT_DIR
+
+# Find the metrics files that exist
+METRICS_FILES="$(echo $OUT_DIR/androidx/*/build/build-metrics.json | grep -v '*' || true)"
+
+# Look for a profile file and attempt to make a metrics json out of it
+PROFILE_FILES="$OUT_DIR/androidx/build/reports/profile/*.html"
+if ls $PROFILE_FILES >/dev/null 2>&1 ; then
+  # parse the profile file and generate a .json file summarizing it
+  PROFILE_JSON=$INTERMEDIATES_DIR/build_androidx.json
+  ./parse_profile_html.py --input-profile "$(ls $PROFILE_FILES | sort | tail -n 2 | head -n 1)" --output-summary $PROFILE_JSON
+  METRICS_FILES="$METRICS_FILES $PROFILE_JSON"
+fi
+
+if [ "$METRICS_FILES" != "" ]; then
+  # merge all profiles
+  mkdir -p "$METRICS_DIR"
+  # concatenate files, and replace "}{" with ", ", ignoring whitespace
+  cat $METRICS_FILES | sed 's/ *} *{ */, /g' > $METRICS_DIR/build_androidx.json
+  # remove metrics files so that next time if Gradle skips emitting them then we don't get old results
+  rm -f $METRICS_FILES
+fi
diff --git a/busytown/impl/parse_profile_htmls.sh b/busytown/impl/parse_profile_htmls.sh
deleted file mode 100755
index 8c5e2b6..0000000
--- a/busytown/impl/parse_profile_htmls.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-set -e
-
-# This is a helper script to be called by androidx.sh
-# This script asks parse_profile_html.py to parse the appropriate report html files
-
-cd "$(dirname $0)"
-
-if [ "$OUT_DIR" == "" ]; then
-  OUT_DIR=../../../../out
-fi
-if [ "$DIST_DIR" == "" ]; then
-  DIST_DIR="$OUT_DIR/dist"
-fi
-
-METRICS_DIR="$DIST_DIR/librarymetrics/build"
-
-# If a profile file exists, parse it. If not, do nothing
-PROFILE_FILES="$OUT_DIR/androidx/build/reports/profile/*.html"
-if ls $PROFILE_FILES >/dev/null 2>&1 ; then
-  ./parse_profile_html.py --input-profile "$(ls $PROFILE_FILES | sort | tail -n 2 | head -n 1)" --output-summary $METRICS_DIR/build_androidx.json
-fi
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/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 9ab3c3c..3c3ebe9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -23,6 +23,7 @@
 import android.hardware.camera2.params.MeteringRectangle
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
@@ -35,8 +36,10 @@
 import dagger.Module
 import javax.inject.Inject
 import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
 
 internal val useCaseCameraIds = atomic(0)
@@ -85,7 +88,7 @@
     override val requestControl: UseCaseCameraRequestControl,
 ) : UseCaseCamera {
     private val debugId = useCaseCameraIds.incrementAndGet()
-    private val graphStateJob: Job
+    private val closed = atomic(false)
 
     override var runningUseCases = setOf<UseCase>()
         set(value) {
@@ -109,21 +112,27 @@
         useCaseGraphConfig.apply {
             cameraStateAdapter.onGraphUpdated(graph)
         }
-        graphStateJob = threads.scope.launch {
+        threads.scope.launch {
             useCaseGraphConfig.apply {
                 graph.graphState.collect {
                     cameraStateAdapter.onGraphStateUpdated(graph, it)
+                    if (closed.value && it is GraphStateStopped) {
+                        cancel()
+                    }
                 }
             }
         }
     }
 
     override fun close(): Job {
-        graphStateJob.cancel()
-        return threads.scope.launch {
-            debug { "Closing $this" }
-            useCaseGraphConfig.graph.close()
-            useCaseSurfaceManager.stopAsync().await()
+        return if (closed.compareAndSet(expect = false, update = true)) {
+            threads.scope.launch {
+                debug { "Closing $this" }
+                useCaseGraphConfig.graph.close()
+                useCaseSurfaceManager.stopAsync().await()
+            }
+        } else {
+            CompletableDeferred(Unit)
         }
     }
 
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 8f0865500..6a89e2b6 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..a1fe196 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
@@ -80,14 +80,6 @@
     int getFormat();
 
     /**
-     * Get the rotation degrees.
-     *
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    int getRotationDegrees();
-
-    /**
      * Call this method to mark the {@link Surface} as no longer in use.
      *
      * <p>Once the {@link SurfaceProcessor} implementation receives a request to close the
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..b0ddbab 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) {
@@ -196,6 +207,17 @@
     /**
      * Checks if aspect ratio matches while tolerating rounding error.
      *
+     * @see #isAspectRatioMatchingWithRoundingError(Size, boolean, Size, boolean)
+     */
+    public static boolean isAspectRatioMatchingWithRoundingError(
+            @NonNull Size size1, @NonNull Size size2) {
+        return isAspectRatioMatchingWithRoundingError(
+                size1, /*isAccurate1=*/ false, size2, /*isAccurate2=*/ false);
+    }
+
+    /**
+     * Checks if aspect ratio matches while tolerating rounding error.
+     *
      * <p> One example of the usage is comparing the viewport-based crop rect from different use
      * cases. The crop rect is rounded because pixels are integers, which may introduce an error
      * when we check if the aspect ratio matches. For example, when
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 ef86985..c0b26c5 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..508b3ff 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,14 +186,26 @@
         return mFormat;
     }
 
-    /**
-     * @inheritDoc
-     */
-    @Override
+    @VisibleForTesting
+    public Rect getInputCropRect() {
+        return mInputCropRect;
+    }
+
+    @VisibleForTesting
+    public Size getInputSize() {
+        return mInputSize;
+    }
+
+    @VisibleForTesting
     public int getRotationDegrees() {
         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..a72db62 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,43 @@
 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.isAspectRatioMatchingWithRoundingError;
 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 +67,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 +78,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 +100,54 @@
     @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);
 
+        // The aspect ratio of the output must match the aspect ratio of the crop rect. Otherwise
+        // the output will be stretched.
+        Size rotatedCropSize = getRotatedSize(outConfig.getCropRect(), rotationDegrees);
+        checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
+
         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 +156,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 +191,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 +211,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 +227,10 @@
             if (mirrored) {
                 rotationDegrees = -rotationDegrees;
             }
-            outputSurface.setRotationDegrees(within360(rotationDegrees));
+            rotationDegrees = within360(rotationDegrees);
+            for (SettableSurface output : outputSurfaces) {
+                output.setRotationDegrees(rotationDegrees);
+            }
         });
     }
 
@@ -214,12 +241,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..a5a6cde 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,15 @@
 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.rotateSize
+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,79 +54,105 @@
 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()
+            val videoOutputSize = rotateSize(VIDEO_SIZE, rotationDegrees - ROTATION_DEGREES)
             createInputEdge(
-                size = rectToSize(cropRect),
-                cropRect = cropRect,
-                rotationDegrees = rotationDegrees
+                previewRotationDegrees = rotationDegrees,
+                videoOutputSize = videoOutputSize
             )
             // 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(videoOutputSize)
+            assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
+            assertThat(videoOutput.rotationDegrees).isEqualTo(0)
 
             // Clean up.
-            inputEdge.surfaces[0].close()
+            nodeInput.surfaceEdge.close()
             node.release()
+            shadowOf(getMainLooper()).idle()
         }
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun cropSizeMismatchesOutputSize_throwsException() {
+        createSurfaceProcessorNode()
+        createInputEdge(
+            videoOutputSize = Size(VIDEO_SIZE.width - 2, VIDEO_SIZE.height + 2)
+        )
+        node.transform(nodeInput)
+    }
+
     @Test
     fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
         for (mirroring in arrayOf(false, true)) {
@@ -132,15 +161,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 +177,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 +221,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 +238,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 +256,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 +266,41 @@
 
         // 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,
+        videoOutputSize: Size = VIDEO_SIZE
     ) {
         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,
+            videoOutputSize
+        )
+        previewOutConfig = OutConfig.of(surface)
+        nodeInput = SurfaceProcessorNode.In.of(
+            surface,
+            listOf(previewOutConfig, videoOutConfig)
+        )
     }
 
     private fun createSurfaceProcessorNode() {
@@ -251,15 +310,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-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index f419244..145105e 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -20,7 +20,6 @@
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
-import android.os.Build;
 import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
@@ -289,11 +288,7 @@
     @Override
     public SessionProcessor createSessionProcessor(@NonNull Context context) {
         Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
-        if (Build.VERSION.SDK_INT >= 26) {
-            return new BasicExtenderSessionProcessor(
-                    mPreviewExtenderImpl, mImageCaptureExtenderImpl, context);
-        } else {
-            throw new IllegalArgumentException("SessionProcessor is not supported");
-        }
+        return new BasicExtenderSessionProcessor(mPreviewExtenderImpl, mImageCaptureExtenderImpl,
+                context);
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 74de26b..5049106 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -62,7 +62,7 @@
  * A {@link SessionProcessor} based on OEMs' basic extender implementation.
  */
 @OptIn(markerClass = ExperimentalCamera2Interop.class)
-@RequiresApi(26) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class BasicExtenderSessionProcessor extends SessionProcessorBase {
     private static final String TAG = "BasicSessionProcessor";
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
index 0f34acb..1e8acf7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
@@ -39,7 +39,7 @@
  *
  * <p>Please note that output preview surface must be closed AFTER this processor is closed.
  */
-@RequiresApi(26)
+@RequiresApi(21)
 class PreviewProcessor {
     private static final String TAG = "PreviewProcessor";
     @NonNull
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
index d3f47fa..b1d702f 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
@@ -61,7 +61,7 @@
  *    Please note that the output JPEG surface should be closed AFTER this processor is closed().
  * </pre>
  */
-@RequiresApi(26)
+@RequiresApi(21)
 class StillCaptureProcessor {
     private static final String TAG = "StillCaptureProcessor";
     private static final int MAX_IMAGES = 2;
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-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index b33eb0f..96e9f73 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Requires API 23 for ImageWriter
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             return false;
         }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
index 4c2e2a1..18c9c9d 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
@@ -53,7 +53,10 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Implement the logic to check whether the extension function is supported or not.
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            return false;
+        }
 
         if (cameraCharacteristics == null) {
             return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 39e2fb8..4257f2c 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -72,8 +72,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Requires API 23 for ImageWriter
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             return false;
         }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
index ba8c013..1665c36 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
@@ -59,7 +59,10 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Implement the logic to check whether the extension function is supported or not.
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            return false;
+        }
 
         if (cameraCharacteristics == null) {
             return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index 0eedc2d..fdb7e01 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Requires API 23 for ImageWriter
-        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             return false;
         }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 60d6246..99fb8d9 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -59,7 +59,10 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Implement the logic to check whether the extension function is supported or not.
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            return false;
+        }
 
         if (cameraCharacteristics == null) {
             return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index a5cdf2a..6a16dff 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -74,8 +74,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Requires API 23 for ImageWriter
-        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             return false;
         }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index 09c2ca37..defe8e9 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -20,6 +20,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.TotalCaptureResult;
 import android.media.Image;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.util.Pair;
@@ -63,8 +64,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Implement the logic to check whether the extension function is supported or not.
-        return true;
+        // Return false to skip tests since old devices do not support extensions.
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
     }
 
     @NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 4febfb4..98c171e 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Requires API 23 for ImageWriter
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             return false;
         }
 
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
index f9b95c0..46512ee 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
@@ -53,7 +53,10 @@
     @Override
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @Nullable CameraCharacteristics cameraCharacteristics) {
-        // Implement the logic to check whether the extension function is supported or not.
+        // Return false to skip tests since old devices do not support extensions.
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            return false;
+        }
 
         if (cameraCharacteristics == null) {
             return false;
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 91b870c..416e857 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/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index b618ac7..ff30013 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -202,6 +202,7 @@
     Long mLastDataStopTimestamp = null;
     @SuppressWarnings("WeakerAccess") // synthetic accessor
     Future<?> mStopTimeoutFuture = null;
+    private MediaCodecCallback mMediaCodecCallback = null;
 
     private boolean mIsFlushedAfterEndOfStream = false;
     private boolean mSourceStoppedSignalled = false;
@@ -282,7 +283,12 @@
             mStopTimeoutFuture.cancel(true);
             mStopTimeoutFuture = null;
         }
-        mMediaCodec.setCallback(new MediaCodecCallback());
+        if (mMediaCodecCallback != null) {
+            mMediaCodecCallback.stop();
+        }
+        mMediaCodecCallback = new MediaCodecCallback();
+        mMediaCodec.setCallback(mMediaCodecCallback);
+
         mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 
         if (mEncoderInput instanceof SurfaceInput) {
@@ -1014,6 +1020,7 @@
         private long mLastSentAdjustedTimeUs = 0L;
         private boolean mIsOutputBufferInPauseState = false;
         private boolean mIsKeyFrameRequired = false;
+        private boolean mStopped = false;
 
         MediaCodecCallback() {
             if (mIsVideoEncoder) {
@@ -1032,6 +1039,10 @@
         @Override
         public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives input frame after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1057,6 +1068,10 @@
         public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
                 @NonNull BufferInfo bufferInfo) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives frame after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1374,6 +1389,10 @@
         public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
                 @NonNull MediaFormat mediaFormat) {
             mEncoderExecutor.execute(() -> {
+                if (mStopped) {
+                    Logger.w(mTag, "Receives onOutputFormatChanged after codec is reset.");
+                    return;
+                }
                 switch (mState) {
                     case STARTED:
                     case PAUSED:
@@ -1404,6 +1423,12 @@
                 }
             });
         }
+
+        /** Stop process further frame output. */
+        @ExecutedBy("mEncoderExecutor")
+        void stop() {
+            mStopped = true;
+        }
     }
 
     @SuppressWarnings("WeakerAccess") // synthetic accessor
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/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
index 630e7f3..fcd7805 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
@@ -112,6 +112,35 @@
         }
     }
 
+    @Test(expected = IllegalArgumentException::class)
+    fun setInvalidEffectsCombination_throwsException() {
+        // Arrange: setup PreviewView and CameraController
+        var previewView: PreviewView? = null
+        activityScenario!!.onActivity {
+            // Arrange.
+            previewView = PreviewView(context)
+            it.setContentView(previewView)
+            previewView!!.controller = controller
+            controller!!.bindToLifecycle(FakeLifecycleOwner())
+            controller!!.initializationFuture.get()
+        }
+        waitUtilPreviewViewIsReady(previewView!!)
+
+        // Act: set the same effect twice, which is invalid.
+        val previewEffect = FakePreviewEffect(
+            mainThreadExecutor(),
+            FakeSurfaceProcessor(mainThreadExecutor())
+        )
+        instrumentation.runOnMainSync {
+            controller!!.setEffects(
+                listOf(
+                    previewEffect,
+                    previewEffect
+                )
+            )
+        }
+    }
+
     @Test
     fun setEffectBundle_effectSetOnUseCase() {
         // Arrange: setup PreviewView and CameraController
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index ce1a726..6a9ec13 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -420,6 +420,9 @@
 
     /**
      * Implemented by children to refresh after {@link UseCase} is changed.
+     *
+     * @throws IllegalStateException for invalid {@link UseCase} combinations.
+     * @throws RuntimeException      for invalid {@link CameraEffect} combinations.
      */
     @Nullable
     abstract Camera startCamera();
@@ -1928,21 +1931,19 @@
     /**
      * @param restoreStateRunnable runnable to restore the controller to the previous good state if
      *                             the binding fails.
-     * @throws IllegalStateException if binding fails.
+     * @throws IllegalStateException for invalid {@link UseCase} combinations.
+     * @throws RuntimeException      for invalid {@link CameraEffect} combinations.
      */
     void startCameraAndTrackStates(@Nullable Runnable restoreStateRunnable) {
         try {
             mCamera = startCamera();
-        } catch (IllegalArgumentException exception) {
+        } catch (RuntimeException exception) {
+            // Restore the previous state before re-throwing the exception.
             if (restoreStateRunnable != null) {
                 restoreStateRunnable.run();
             }
-            // Catches the core exception and throw a more readable one.
-            String errorMessage =
-                    "The selected camera does not support the enabled use cases. Please "
-                            + "disable use case and/or select a different camera. e.g. "
-                            + "#setVideoCaptureEnabled(false)";
-            throw new IllegalStateException(errorMessage, exception);
+            // This exception will handled by the app.
+            throw exception;
         }
         if (!isCameraAttached()) {
             Logger.d(TAG, CAMERA_NOT_ATTACHED);
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
index 2f751af..9f695f9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
@@ -30,6 +30,7 @@
 import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.Camera;
+import androidx.camera.core.CameraEffect;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.UseCaseGroup;
 import androidx.camera.core.impl.utils.Threads;
@@ -109,6 +110,8 @@
      * Unbind and rebind all use cases to {@link LifecycleOwner}.
      *
      * @return null if failed to start camera.
+     * @throws IllegalStateException for invalid {@link UseCase} combinations.
+     * @throws RuntimeException      for invalid {@link CameraEffect} combinations.
      */
     @RequiresPermission(Manifest.permission.CAMERA)
     @Override
@@ -128,7 +131,16 @@
             // Use cases can't be created.
             return null;
         }
-        return mCameraProvider.bindToLifecycle(mLifecycleOwner, mCameraSelector, useCaseGroup);
+        try {
+            return mCameraProvider.bindToLifecycle(mLifecycleOwner, mCameraSelector, useCaseGroup);
+        } catch (IllegalArgumentException e) {
+            // Catches the invalid use case combination exception and throw a more readable one.
+            String errorMessage =
+                    "The selected camera does not support the enabled use cases. Please "
+                            + "disable use case and/or select a different camera. e.g. "
+                            + "#setVideoCaptureEnabled(false)";
+            throw new IllegalStateException(errorMessage, e);
+        }
     }
 
     /**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
index 9c441dc..6994283 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
@@ -83,9 +83,8 @@
         //  the transform from sensor to surface. But it will require the view artifact to
         //  depend on a new internal API in the core artifact, which we can't do at the
         //  moment because of the version mismatch between view and core.
-        if (!isAspectRatioMatchingWithRoundingError(
-                source.getViewPortSize(), /* isAccurate1= */ false,
-                target.getViewPortSize(), /* isAccurate2= */ false)) {
+        if (!isAspectRatioMatchingWithRoundingError(source.getViewPortSize(),
+                target.getViewPortSize())) {
             // Mismatched aspect ratio means the outputs are not associated with the same Viewport.
             Logger.w(TAG, String.format(MISMATCH_MSG, source.getViewPortSize(),
                     target.getViewPortSize()));
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 1781ca6..d13c4c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -61,16 +61,29 @@
 
 /**
  * Waits until an image has been saved and its idling resource has become idle.
+ *
+ * @param captureRequestsCount the capture requests count to issue to continuously take pictures
+ * without waiting for the previous capture requests to be done.
  */
-internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
+internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle(
+    captureRequestsCount: Int = 1
+) {
     val idlingResource = withActivity {
         cleanTakePictureErrorMessage()
         imageSavedIdlingResource
     }
     try {
-        IdlingRegistry.getInstance().register(idlingResource)
         // Perform click to take a picture.
-        Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(click())
+        Espresso.onView(ViewMatchers.withId(R.id.Picture)).apply {
+            repeat(captureRequestsCount) {
+                perform(click())
+            }
+        }
+        // Registers the idling resource and wait for it being idle after performing the click
+        // operations. So that the click operations can be performed continuously without wait for
+        // previous capture results.
+        IdlingRegistry.getInstance().register(idlingResource)
+        Espresso.onIdle()
     } finally { // Always release the idling resource, in case of timeout exceptions.
         IdlingRegistry.getInstance().unregister(idlingResource)
         withActivity {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index a447c7f..578ae88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -273,4 +273,24 @@
         val randomDelayDuration = (Random.nextInt(maxDelaySeconds) + 1).toLong()
         delay(TimeUnit.SECONDS.toMillis(randomDelayDuration))
     }
+
+    @LabTestRule.LabTestOnly
+    @Test
+    @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
+    fun launchActivity_thenTakeMultiplePictures_withoutWaitingPreviousResults() {
+        val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+
+        // Launches CameraXActivity and wait for the preview ready.
+        val activityScenario =
+            launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+        with(activityScenario) {
+            use {
+                // Checks whether multiple images can be captured successfully
+                repeat(STRESS_TEST_OPERATION_REPEAT_COUNT) {
+                    takePictureAndWaitForImageSavedIdle(3)
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
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/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index a01aa33..de01a82 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -366,6 +366,7 @@
   @androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
     ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
     method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onSettlingDistanceUpdated, kotlin.coroutines.Continuation<? super java.lang.Float>);
   }
 
   public final class SnapFlingBehaviorKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
index 2428fea..d43a2b8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
@@ -17,8 +17,11 @@
 package androidx.compose.foundation.gesture.snapping
 
 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.MinFlingVelocityDp
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
 import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
 import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
@@ -46,6 +49,7 @@
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
 import kotlin.math.abs
 import kotlin.math.absoluteValue
 import kotlin.test.assertEquals
@@ -64,6 +68,9 @@
     private val density: Density
         get() = rule.density
 
+    private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
+    private lateinit var snapFlingBehavior: FlingBehavior
+
     @Test
     fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
         var lazyListState: LazyListState? = null
@@ -360,15 +367,73 @@
         }
     }
 
+    @Test
+    fun remainingScrollOffset_shouldFollowAnimationOffsets() {
+        var stepSize = 0f
+        var velocityThreshold = 0f
+        val scrollOffset = mutableListOf<Float>()
+        // arrange
+        rule.setContent {
+            val density = LocalDensity.current
+            val state = rememberLazyListState()
+            stepSize = with(density) { ItemSize.toPx() }
+            velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+            MainLayout(state = state, scrollOffset)
+        }
+
+        rule.mainClock.autoAdvance = false
+        // act
+        val velocity = velocityThreshold * 3
+        onMainList().performTouchInput {
+            swipeMainAxisWithVelocity(
+                1.5f * stepSize,
+                velocity
+            )
+        }
+        rule.mainClock.advanceTimeByFrame()
+
+        // assert
+        val initialTargetOffset =
+            with(snapLayoutInfoProvider) { density.calculateApproachOffset(velocity) }
+        Truth.assertThat(scrollOffset.first { it != 0f }).isWithin(0.5f)
+            .of(initialTargetOffset)
+
+        // act: wait for remaining offset to grow instead of decay, this indicates the last
+        // snap step will start
+        rule.mainClock.advanceTimeUntil {
+            scrollOffset.size > 2 &&
+                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+        }
+
+        // assert: next calculated bound is the first value emitted by remainingScrollOffset
+        val bounds = with(snapLayoutInfoProvider) { density.calculateSnappingOffsetBounds() }
+        val finalRemainingOffset = bounds.endInclusive
+        Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
+            .of(finalRemainingOffset)
+        rule.mainClock.autoAdvance = true
+
+        // assert: value settles back to zero
+        rule.runOnIdle {
+            Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
+        }
+    }
+
     private fun onMainList() = rule.onNodeWithTag(TestTag)
 
     @Composable
-    fun MainLayout(state: LazyListState) {
-        val layoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+    fun MainLayout(state: LazyListState, scrollOffset: MutableList<Float> = mutableListOf()) {
+        snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+        val innerFlingBehavior =
+            rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
+        snapFlingBehavior = remember(innerFlingBehavior) {
+            QuerySnapFlingBehavior(innerFlingBehavior) {
+                scrollOffset.add(it)
+            }
+        }
         LazyColumnOrRow(
             state = state,
             modifier = Modifier.testTag(TestTag),
-            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfoProvider)
+            flingBehavior = snapFlingBehavior
         ) {
             items(200) {
                 Box(modifier = Modifier.size(ItemSize))
@@ -435,4 +500,16 @@
         val CenterToCenter: Density.(Float, Float) -> Float =
             { layoutSize, itemSize -> layoutSize / 2f - itemSize / 2f }
     }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class QuerySnapFlingBehavior(
+    val snapFlingBehavior: SnapFlingBehavior,
+    val onAnimationStep: (Float) -> Unit
+) : FlingBehavior {
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        return with(snapFlingBehavior) {
+            performFling(initialVelocity, onAnimationStep)
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index a09f72b..543f121 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -64,10 +64,10 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth
 import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import kotlin.test.assertNotEquals
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -112,6 +112,86 @@
     }
 
     @Test
+    fun remainingScrollOffset_whenVelocityIsBelowThreshold_shouldRepresentShortSnapOffsets() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() - 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        // Will Snap Back
+        rule.runOnIdle {
+            assertEquals(scrollOffset.first(), testLayoutInfoProvider.minOffset)
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_whenVelocityIsAboveThreshold_shouldRepresentLongSnapOffsets() {
+        val testLayoutInfoProvider = TestLayoutInfoProvider()
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() + 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(scrollOffset.first { it != 0f }, testLayoutInfoProvider.maxOffset)
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
+    fun remainingScrollOffset_longSnap_targetShouldChangeInAccordanceWithAnimation() {
+        // Arrange
+        val initialOffset = 250f
+        val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = initialOffset)
+        lateinit var testFlingBehavior: SnapFlingBehavior
+        val scrollOffset = mutableListOf<Float>()
+        rule.mainClock.autoAdvance = false
+        rule.setContent {
+            testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+            VelocityEffect(
+                testFlingBehavior,
+                calculateVelocityThreshold() + 1
+            ) { remainingScrollOffset ->
+                scrollOffset.add(remainingScrollOffset)
+            }
+        }
+
+        // assert the initial value emitted by remainingScrollOffset was the one provider by the
+        // snap layout info provider
+        assertEquals(scrollOffset.first(), initialOffset)
+
+        // Act: Advance until remainingScrollOffset grows again
+        rule.mainClock.advanceTimeUntil {
+            scrollOffset.size > 2 &&
+                scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+        }
+
+        assertEquals(scrollOffset.last(), testLayoutInfoProvider.maxOffset)
+
+        rule.mainClock.autoAdvance = true
+        // Assert
+        rule.runOnIdle {
+            assertEquals(scrollOffset.last(), 0f)
+        }
+    }
+
+    @Test
     fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
         val testLayoutInfoProvider = TestLayoutInfoProvider()
         var afterFlingVelocity = 0f
@@ -360,13 +440,18 @@
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun VelocityEffect(testFlingBehavior: FlingBehavior, velocity: Float) {
+private fun VelocityEffect(
+    testFlingBehavior: FlingBehavior,
+    velocity: Float,
+    onSettlingDistanceUpdated: (Float) -> Unit = {}
+) {
     val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
     LaunchedEffect(Unit) {
         scrollableState.scroll {
-            with(testFlingBehavior) {
-                performFling(velocity)
+            with(testFlingBehavior as SnapFlingBehavior) {
+                performFling(velocity, onSettlingDistanceUpdated)
             }
         }
     }
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/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index 6e34166..5e7a819 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.textfield
 
 import android.view.KeyEvent
+import android.view.KeyEvent.META_ALT_ON
 import android.view.KeyEvent.META_CTRL_ON
 import android.view.KeyEvent.META_SHIFT_ON
 import androidx.compose.foundation.ExperimentalFoundationApi
@@ -241,6 +242,45 @@
     }
 
     @Test
+    fun textField_deleteToBeginningOfLine() {
+        keysSequenceTest(initText = "hello world\nhi world") {
+            Key.DirectionRight.downAndUp(META_CTRL_ON)
+            Key.Backspace.downAndUp(META_ALT_ON)
+            expectedText(" world\nhi world")
+            Key.Backspace.downAndUp(META_ALT_ON)
+            expectedText(" world\nhi world")
+            repeat(3) { Key.DirectionRight.downAndUp() }
+            Key.Backspace.downAndUp(META_ALT_ON)
+            expectedText("rld\nhi world")
+            Key.DirectionDown.downAndUp()
+            Key.MoveEnd.downAndUp()
+            Key.Backspace.downAndUp(META_ALT_ON)
+            expectedText("rld\n")
+            Key.Backspace.downAndUp(META_ALT_ON)
+            expectedText("rld\n")
+        }
+    }
+
+    @Test
+    fun textField_deleteToEndOfLine() {
+        keysSequenceTest(initText = "hello world\nhi world") {
+            Key.DirectionRight.downAndUp(META_CTRL_ON)
+            Key.Delete.downAndUp(META_ALT_ON)
+            expectedText("hello\nhi world")
+            Key.Delete.downAndUp(META_ALT_ON)
+            expectedText("hello\nhi world")
+            repeat(3) { Key.DirectionRight.downAndUp() }
+            Key.Delete.downAndUp(META_ALT_ON)
+            expectedText("hello\nhi")
+            Key.MoveHome.downAndUp()
+            Key.Delete.downAndUp(META_ALT_ON)
+            expectedText("hello\n")
+            Key.Delete.downAndUp(META_ALT_ON)
+            expectedText("hello\n")
+        }
+    }
+
+    @Test
     fun textField_paragraphNavigation() {
         keysSequenceTest(initText = "hello world\nhi") {
             Key.DirectionDown.downAndUp(META_CTRL_ON)
@@ -352,7 +392,7 @@
         sequence: SequenceScope.() -> Unit,
     ) {
         val inputService = TextInputService(mock())
-        val focusFequester = FocusRequester()
+        val focusRequester = FocusRequester()
         rule.setContent {
             CompositionLocalProvider(
                 LocalTextInputService provides inputService
@@ -363,13 +403,13 @@
                         fontFamily = TEST_FONT_FAMILY,
                         fontSize = 10.sp
                     ),
-                    modifier = modifier.focusRequester(focusFequester),
+                    modifier = modifier.focusRequester(focusRequester),
                     onValueChange = onValueChange
                 )
             }
         }
 
-        rule.runOnIdle { focusFequester.requestFocus() }
+        rule.runOnIdle { focusRequester.requestFocus() }
 
         sequence(SequenceScope(value) { rule.onNode(hasSetTextAction()) })
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 59b76f4..b6e53ad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -94,14 +94,31 @@
     internal var motionScaleDuration = DefaultScrollMotionDurationScale
 
     override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
-        // If snapping from scroll (short snap) or fling (long snap)
+        return performFling(initialVelocity) {}
+    }
+
+    /**
+     * Perform a snapping fling animation with given velocity and suspend until fling has
+     * finished. This will behave the same way as [performFling] except it will report on
+     * each remainingOffsetUpdate using the [onSettlingDistanceUpdated] lambda.
+     *
+     * @param initialVelocity velocity available for fling in the orientation specified in
+     * [androidx.compose.foundation.gestures.scrollable] that invoked this method.
+     *
+     * @param onSettlingDistanceUpdated a lambda that will be called anytime the
+     * distance to the settling offset is updated. The settling offset is the final offset where
+     * this fling will stop and may change depending on the snapping animation progression.
+     *
+     * @return remaining velocity after fling operation has ended
+     */
+    suspend fun ScrollScope.performFling(
+        initialVelocity: Float,
+        onSettlingDistanceUpdated: (Float) -> Unit
+    ): Float {
         val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
-            if (abs(initialVelocity) <= abs(velocityThreshold)) {
-                shortSnap(initialVelocity)
-            } else {
-                longSnap(initialVelocity)
-            }
+            fling(initialVelocity, onSettlingDistanceUpdated)
         }
+
         debugLog { "Post Settling Offset=$remainingOffset" }
         // No remaining offset means we've used everything, no need to propagate velocity. Otherwise
         // we couldn't use everything (probably because we have hit the min/max bounds of the
@@ -109,39 +126,83 @@
         return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
     }
 
+    private suspend fun ScrollScope.fling(
+        initialVelocity: Float,
+        onRemainingScrollOffsetUpdate: (Float) -> Unit
+    ): AnimationResult<Float, AnimationVector1D> {
+        // If snapping from scroll (short snap) or fling (long snap)
+        val result = withContext(motionScaleDuration) {
+            if (abs(initialVelocity) <= abs(velocityThreshold)) {
+                shortSnap(initialVelocity, onRemainingScrollOffsetUpdate)
+            } else {
+                longSnap(initialVelocity, onRemainingScrollOffsetUpdate)
+            }
+        }
+
+        onRemainingScrollOffsetUpdate(0f) // Animation finished or was cancelled
+        return result
+    }
+
     private suspend fun ScrollScope.shortSnap(
-        velocity: Float
+        velocity: Float,
+        onRemainingScrollOffsetUpdate: (Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Short Snapping" }
         val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
+        var remainingScrollOffset = closestOffset
+
         val animationState = AnimationState(NoDistance, velocity)
-        return animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
+        return animateSnap(
+            closestOffset,
+            closestOffset,
+            animationState,
+            snapAnimationSpec
+        ) { delta ->
+            remainingScrollOffset -= delta
+            onRemainingScrollOffsetUpdate(remainingScrollOffset)
+        }
     }
 
     private suspend fun ScrollScope.longSnap(
-        initialVelocity: Float
+        initialVelocity: Float,
+        onAnimationStep: (remainingScrollOffset: Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         debugLog { "Long Snapping" }
         val initialOffset =
             with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
                 abs(it) * sign(initialVelocity) // ensure offset sign is correct
             }
+        var remainingScrollOffset = initialOffset
 
-        val (remainingOffset, animationState) = runApproach(initialOffset, initialVelocity)
+        onAnimationStep(remainingScrollOffset) // First Scroll Offset
+
+        val (remainingOffset, animationState) = runApproach(
+            initialOffset,
+            initialVelocity
+        ) { delta ->
+            remainingScrollOffset -= delta
+            onAnimationStep(remainingScrollOffset)
+        }
 
         debugLog { "Settling Final Bound=$remainingOffset" }
 
+        remainingScrollOffset = remainingOffset
+
         return animateSnap(
             remainingOffset,
             remainingOffset,
             animationState.copy(value = 0f),
             snapAnimationSpec
-        )
+        ) { delta ->
+            remainingScrollOffset -= delta
+            onAnimationStep(remainingScrollOffset)
+        }
     }
 
     private suspend fun ScrollScope.runApproach(
         initialTargetOffset: Float,
-        initialVelocity: Float
+        initialVelocity: Float,
+        onAnimationStep: (delta: Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
 
         val animation =
@@ -162,7 +223,8 @@
             initialVelocity,
             animation,
             snapLayoutInfoProvider,
-            density
+            density,
+            onAnimationStep
         )
     }
 
@@ -244,13 +306,15 @@
     initialVelocity: Float,
     animation: ApproachAnimation<Float, AnimationVector1D>,
     snapLayoutInfoProvider: SnapLayoutInfoProvider,
-    density: Density
+    density: Density,
+    onAnimationStep: (delta: Float) -> Unit
 ): AnimationResult<Float, AnimationVector1D> {
 
     val (_, currentAnimationState) = animation.approachAnimation(
         this,
         initialTargetOffset,
-        initialVelocity
+        initialVelocity,
+        onAnimationStep
     )
 
     val remainingOffset =
@@ -318,16 +382,24 @@
 
 /**
  * Run a [DecayAnimationSpec] animation up to before [targetOffset] using [animationState]
+ *
+ * @param targetOffset The destination of this animation. Since this is a decay animation, we can
+ * use this value to prevent the animation to run until the end.
+ * @param animationState The previous [AnimationState] for continuation purposes.
+ * @param decayAnimationSpec The [DecayAnimationSpec] that will drive this animation
+ * @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
  */
 private suspend fun ScrollScope.animateDecay(
     targetOffset: Float,
     animationState: AnimationState<Float, AnimationVector1D>,
-    decayAnimationSpec: DecayAnimationSpec<Float>
+    decayAnimationSpec: DecayAnimationSpec<Float>,
+    onAnimationStep: (delta: Float) -> Unit
 ): AnimationResult<Float, AnimationVector1D> {
     var previousValue = 0f
 
     fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
         val consumed = scrollBy(delta)
+        onAnimationStep(consumed)
         if (abs(delta - consumed) > 0.5f) cancelAnimation()
     }
 
@@ -355,12 +427,19 @@
 /**
  * Runs a [AnimationSpec] to snap the list into [targetOffset]. Uses [cancelOffset] to stop this
  * animation before it reaches the target.
+ *
+ * @param targetOffset The final target of this animation
+ * @param cancelOffset If we'd like to finish the animation earlier we use this value
+ * @param animationState The current animation state for continuation purposes
+ * @param snapAnimationSpec The [AnimationSpec] that will drive this animation
+ * @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
  */
 private suspend fun ScrollScope.animateSnap(
     targetOffset: Float,
     cancelOffset: Float,
     animationState: AnimationState<Float, AnimationVector1D>,
-    snapAnimationSpec: AnimationSpec<Float>
+    snapAnimationSpec: AnimationSpec<Float>,
+    onAnimationStep: (delta: Float) -> Unit
 ): AnimationResult<Float, AnimationVector1D> {
     var consumedUpToNow = 0f
     val initialVelocity = animationState.velocity
@@ -372,6 +451,7 @@
         val realValue = value.coerceToTarget(cancelOffset)
         val delta = realValue - consumedUpToNow
         val consumed = scrollBy(delta)
+        onAnimationStep(consumed)
         // stop when unconsumed or when we reach the desired value
         if (abs(delta - consumed) > 0.5f || realValue != value) {
             cancelAnimation()
@@ -399,7 +479,8 @@
     suspend fun approachAnimation(
         scope: ScrollScope,
         offset: T,
-        velocity: T
+        velocity: T,
+        onAnimationStep: (delta: T) -> Unit
     ): AnimationResult<T, V>
 }
 
@@ -412,7 +493,8 @@
     override suspend fun approachAnimation(
         scope: ScrollScope,
         offset: Float,
-        velocity: Float
+        velocity: Float,
+        onAnimationStep: (delta: Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
         val targetOffset =
@@ -424,7 +506,8 @@
                 targetOffset = targetOffset,
                 cancelOffset = offset,
                 animationState = animationState,
-                snapAnimationSpec = lowVelocityAnimationSpec
+                snapAnimationSpec = lowVelocityAnimationSpec,
+                onAnimationStep = onAnimationStep
             )
         }
     }
@@ -436,11 +519,12 @@
     override suspend fun approachAnimation(
         scope: ScrollScope,
         offset: Float,
-        velocity: Float
+        velocity: Float,
+        onAnimationStep: (delta: Float) -> Unit
     ): AnimationResult<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
         return with(scope) {
-            animateDecay(offset, animationState, decayAnimationSpec)
+            animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep)
         }
     }
 }
@@ -448,8 +532,8 @@
 internal val MinFlingVelocityDp = 400.dp
 internal const val NoDistance = 0f
 internal const val NoVelocity = 0f
-private const val DEBUG = false
 
+private const val DEBUG = false
 private inline fun debugLog(generateMsg: () -> String) {
     if (DEBUG) {
         println("SnapFlingBehavior: ${generateMsg()}")
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 6169203..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
@@ -89,7 +90,7 @@
     beyondBoundsPageCount: Int = 0,
     pageSpacing: Dp = 0.dp,
     verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
-    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
     userScrollEnabled: Boolean = true,
     reverseLayout: Boolean = false,
     pageContent: @Composable (page: Int) -> Unit
@@ -149,7 +150,7 @@
     beyondBoundsPageCount: Int = 0,
     pageSpacing: Dp = 0.dp,
     horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
-    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
     userScrollEnabled: Boolean = true,
     reverseLayout: Boolean = false,
     pageContent: @Composable (page: Int) -> Unit
@@ -184,7 +185,7 @@
     verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
     horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
     contentPadding: PaddingValues,
-    flingBehavior: FlingBehavior,
+    flingBehavior: SnapFlingBehavior,
     userScrollEnabled: Boolean,
     reverseLayout: Boolean,
     pageContent: @Composable (page: Int) -> Unit
@@ -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,
@@ -365,7 +370,7 @@
         lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
         highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
         snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
-    ): FlingBehavior {
+    ): SnapFlingBehavior {
         val density = LocalDensity.current
 
         return remember(
@@ -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/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
index f5404bb..c5be867 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.isAltPressed
 import androidx.compose.ui.input.key.isCtrlPressed
 import androidx.compose.ui.input.key.isShiftPressed
 import androidx.compose.ui.input.key.key
@@ -148,6 +149,12 @@
                             MappedKeys.MoveEnd -> KeyCommand.SELECT_END
                             else -> null
                         }
+                    event.isAltPressed ->
+                        when (event.key) {
+                            MappedKeys.Backspace -> KeyCommand.DELETE_FROM_LINE_START
+                            MappedKeys.Delete -> KeyCommand.DELETE_TO_LINE_END
+                            else -> null
+                        }
                     else -> null
                 } ?: common.map(event)
             }
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/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index 63c1027..fe4190a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -79,13 +79,14 @@
 
     @Test
     fun pullLessThanOrEqualToThreshold_doesNot_triggerRefresh() {
+        lateinit var state: PullRefreshState
         var refreshCount = 0
         var touchSlop = 0f
         val threshold = 400f
 
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
-            val state = rememberPullRefreshState(
+            state = rememberPullRefreshState(
                 refreshing = false,
                 onRefresh = { refreshCount++ },
                 refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
@@ -110,7 +111,12 @@
         // Equal to threshold
         pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop) }
 
-        rule.runOnIdle { assertThat(refreshCount).isEqualTo(0) }
+        rule.runOnIdle {
+            assertThat(refreshCount).isEqualTo(0)
+            // Since onRefresh was not called, we should reset the position back to 0
+            assertThat(state.progress).isEqualTo(0f)
+            assertThat(state.position).isEqualTo(0f)
+        }
     }
 
     @Test
@@ -309,6 +315,43 @@
         }
     }
 
+    @Test
+    fun pullBeyondThreshold_refreshingNotChangedToTrue_animatePositionBackToZero() {
+        lateinit var state: PullRefreshState
+        var refreshCount = 0
+        var touchSlop = 0f
+        val threshold = 400f
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            state = rememberPullRefreshState(
+                refreshing = false,
+                onRefresh = { refreshCount++ },
+                refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
+            )
+
+            Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+                LazyColumn {
+                    items(100) {
+                        Text("item $it")
+                    }
+                }
+            }
+        }
+
+        // Account for PullModifier - pull down twice the threshold value.
+        pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop + 1f) }
+
+        rule.runOnIdle {
+            // onRefresh should be called
+            assertThat(refreshCount).isEqualTo(1)
+            // Since onRefresh did not change refreshing, we should have reset the position back to
+            // 0
+            assertThat(state.progress).isEqualTo(0f)
+            assertThat(state.position).isEqualTo(0f)
+        }
+    }
+
     /**
      * Taken from the private function of the same name in [PullRefreshState].
      */
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index 9ee4ffb..21d797a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -63,7 +63,7 @@
  *
  * @param state The associated [SwipeableV2State].
  * @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
  * swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
  * @param interactionSource Optional [MutableInteractionSource] that will passed on to
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
index e673400..b425a38 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material.pullrefresh
 
 import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatorMutex
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
@@ -136,9 +137,8 @@
         if (!this._refreshing) {
             if (adjustedDistancePulled > threshold) {
                 onRefreshState.value()
-            } else {
-                animateIndicatorTo(0f)
             }
+            animateIndicatorTo(0f)
         }
         distancePulled = 0f
     }
@@ -151,9 +151,16 @@
         }
     }
 
+    // Make sure to cancel any existing animations when we launch a new one. We use this instead of
+    // Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
+    // overhead of running through the animation pipeline instead of directly mutating the state.
+    private val mutatorMutex = MutatorMutex()
+
     private fun animateIndicatorTo(offset: Float) = animationScope.launch {
-        animate(initialValue = _position, targetValue = offset) { value, _ ->
-            _position = value
+        mutatorMutex.mutate {
+            animate(initialValue = _position, targetValue = offset) { value, _ ->
+                _position = value
+            }
         }
     }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 4849ec9..021de9a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -35,8 +35,15 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
@@ -46,19 +53,19 @@
 import kotlinx.coroutines.launch
 
 /**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
  *
  * When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
  * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
  * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
  * the new anchor.
  *
  * Swiping is constrained between the minimum and maximum anchors.
  *
  * @param state The associated [SwipeableV2State].
  * @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
  * swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
  * @param interactionSource Optional [MutableInteractionSource] that will passed on to
@@ -86,36 +93,46 @@
  * the state with them.
  *
  * @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
  * @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
  * Components with custom reconciliation logic should implement this callback, i.e. to re-target an
  * in-progress animation.
  * @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
  */
 @ExperimentalMaterial3Api
 internal fun <T> Modifier.swipeAnchors(
     state: SwipeableV2State<T>,
-    possibleStates: Set<T>,
+    possibleValues: Set<T>,
     anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
-    calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
-    val previousAnchors = state.anchors
-    val newAnchors = mutableMapOf<T, Float>()
-    possibleStates.forEach {
-        val anchorValue = calculateAnchor(it, layoutSize)
-        if (anchorValue != null) {
-            newAnchors[it] = anchorValue
+    calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
+) = this.then(SwipeAnchorsModifier(
+    onDensityChanged = { state.density = it },
+    onSizeChanged = { layoutSize ->
+        val previousAnchors = state.anchors
+        val newAnchors = mutableMapOf<T, Float>()
+        possibleValues.forEach {
+            val anchorValue = calculateAnchor(it, layoutSize)
+            if (anchorValue != null) {
+                newAnchors[it] = anchorValue
+            }
         }
+        if (previousAnchors != newAnchors) {
+            state.updateAnchors(newAnchors)
+            if (previousAnchors.isNotEmpty()) {
+                anchorsChanged?.invoke(previousAnchors, newAnchors)
+            }
+        }
+    },
+    inspectorInfo = debugInspectorInfo {
+        name = "swipeAnchors"
+        properties["state"] = state
+        properties["possibleValues"] = possibleValues
+        properties["anchorsChanged"] = anchorsChanged
+        properties["calculateAnchor"] = calculateAnchor
     }
-    if (previousAnchors == newAnchors) return@onSizeChanged
-    state.updateAnchors(newAnchors)
-
-    if (previousAnchors.isNotEmpty()) {
-        anchorsChanged?.invoke(previousAnchors, newAnchors)
-    }
-}
+))
 
 /**
  * State of the [swipeableV2] modifier.
@@ -124,10 +141,9 @@
  * to change the state either immediately or by starting an animation. To create and remember a
  * [SwipeableV2State] use [rememberSwipeableV2State].
  *
- * @param initialState The initial value of the state.
- * @param density The density used to convert thresholds from px to dp.
+ * @param initialValue The initial value of the state.
  * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
  * @param positionalThreshold The positional threshold to be used when calculating the target state
  * while a swipe is in progress and when settling after the swipe ends. This is the distance from
  * the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -140,31 +156,30 @@
 @Stable
 @ExperimentalMaterial3Api
 internal class SwipeableV2State<T>(
-    initialState: T,
-    internal val density: Density,
+    initialValue: T,
     internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+    internal val confirmValueChange: (newValue: T) -> Boolean = { true },
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
         SwipeableV2Defaults.PositionalThreshold,
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
 ) {
 
     /**
-     * The current state of the [SwipeableV2State].
+     * The current value of the [SwipeableV2State].
      */
-    var currentState: T by mutableStateOf(initialState)
+    var currentValue: T by mutableStateOf(initialValue)
         private set
 
     /**
-     * The target state. This is the closest state to the current offset (taking into account
+     * The target value. This is the closest value to the current offset (taking into account
      * positional thresholds). If no interactions like animations or drags are in progress, this
-     * will be the current state.
+     * will be the current value.
      */
-    val targetState: T by derivedStateOf {
+    val targetValue: T by derivedStateOf {
         val currentOffset = offset
         if (currentOffset != null) {
-            computeTarget(currentOffset, currentState, velocity = 0f)
-        } else currentState
+            computeTarget(currentOffset, currentValue, velocity = 0f)
+        } else currentValue
     }
 
     /**
@@ -180,6 +195,7 @@
      *
      * To guarantee stricter semantics, consider using [requireOffset].
      */
+    @get:Suppress("AutoBoxing")
     val offset: Float? by derivedStateOf {
         dragPosition?.coerceIn(minBound, maxBound)
     }
@@ -201,12 +217,13 @@
         private set
 
     /**
-     * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+     * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+     * bounds.
      */
     /*@FloatRange(from = 0f, to = 1f)*/
     val progress: Float by derivedStateOf {
-        val a = anchors[currentState] ?: 0f
-        val b = anchors[targetState] ?: 0f
+        val a = anchors[currentValue] ?: 0f
+        val b = anchors[targetValue] ?: 0f
         val distance = abs(b - a)
         if (distance > 1e-6f) {
             val progress = (this.requireOffset() - a) / (b - a)
@@ -229,57 +246,57 @@
     private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
     private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
 
-    private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
     internal val draggableState = DraggableState {
         dragPosition = (dragPosition ?: 0f) + it
     }
 
     internal var anchors by mutableStateOf(emptyMap<T, Float>())
 
+    internal var density: Density? = null
+
     internal fun updateAnchors(newAnchors: Map<T, Float>) {
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
         if (previousAnchorsEmpty) {
-            dragPosition = anchors.requireAnchor(this.currentState)
+            dragPosition = anchors.requireAnchor(this.currentValue)
         }
     }
 
     /**
-     * Whether the [state] has an anchor associated with it.
+     * Whether the [value] has an anchor associated with it.
      */
-    fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+    fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
 
     /**
-     * Snap to a [targetState] without any animation.
+     * Snap to a [targetValue] without any animation.
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      */
-    suspend fun snapTo(targetState: T) {
-        val targetOffset = anchors.requireAnchor(targetState)
+    suspend fun snapTo(targetValue: T) {
+        val targetOffset = anchors.requireAnchor(targetValue)
         draggableState.drag {
             dragBy(targetOffset - requireOffset())
         }
-        this.currentState = targetState
+        this.currentValue = targetValue
     }
 
     /**
-     * Animate to a [targetState].
+     * Animate to a [targetValue].
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
      *
-     * @param targetState The target state of the animation
+     * @param targetValue The target value of the animation
      * @param velocity The velocity the animation should start with, [lastVelocity] by default
      */
     suspend fun animateTo(
-        targetState: T,
+        targetValue: T,
         velocity: Float = lastVelocity,
     ) {
-        val targetOffset = anchors.requireAnchor(targetState)
+        val targetOffset = anchors.requireAnchor(targetValue)
         try {
             draggableState.drag {
                 isAnimationRunning = true
@@ -304,7 +321,7 @@
                 .entries
                 .firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
                 ?.key
-            this.currentState = endState ?: currentState
+            this.currentValue = endState ?: currentValue
         }
     }
 
@@ -312,17 +329,17 @@
      * Find the closest anchor taking into account the velocity and settle at it with an animation.
      */
     suspend fun settle(velocity: Float) {
-        val previousState = this.currentState
-        val targetState = computeTarget(
+        val previousValue = this.currentValue
+        val targetValue = computeTarget(
             offset = requireOffset(),
-            currentState = previousState,
+            currentValue = previousValue,
             velocity = velocity
         )
-        if (confirmStateChange(targetState)) {
-            animateTo(targetState, velocity)
+        if (confirmValueChange(targetValue)) {
+            animateTo(targetValue, velocity)
         } else {
             // If the user vetoed the state change, rollback to the previous state.
-            animateTo(previousState, velocity)
+            animateTo(previousValue, velocity)
         }
     }
 
@@ -344,42 +361,49 @@
 
     private fun computeTarget(
         offset: Float,
-        currentState: T,
+        currentValue: T,
         velocity: Float
     ): T {
         val currentAnchors = anchors
-        val currentAnchor = currentAnchors.requireAnchor(currentState)
+        val currentAnchor = currentAnchors.requireAnchor(currentValue)
+        val currentDensity = requireDensity()
+        val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
         return if (currentAnchor <= offset) {
             // Swiping from lower to upper (positive).
             if (velocity >= velocityThresholdPx) {
-                currentAnchors.closestState(offset, true)
+                currentAnchors.closestAnchor(offset, true)
             } else {
-                val upper = currentAnchors.closestState(offset, true)
+                val upper = currentAnchors.closestAnchor(offset, true)
                 val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor + relativeThreshold)
-                if (offset < absoluteThreshold) currentState else upper
+                if (offset < absoluteThreshold) currentValue else upper
             }
         } else {
             // Swiping from upper to lower (negative).
             if (velocity <= -velocityThresholdPx) {
-                currentAnchors.closestState(offset, false)
+                currentAnchors.closestAnchor(offset, false)
             } else {
-                val lower = currentAnchors.closestState(offset, false)
+                val lower = currentAnchors.closestAnchor(offset, false)
                 val distance = abs(currentAnchor - currentAnchors.getValue(lower))
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor - relativeThreshold)
                 if (offset < 0) {
                     // For negative offsets, larger absolute thresholds are closer to lower anchors
                     // than smaller ones.
-                    if (abs(offset) < absoluteThreshold) currentState else lower
+                    if (abs(offset) < absoluteThreshold) currentValue else lower
                 } else {
-                    if (offset > absoluteThreshold) currentState else lower
+                    if (offset > absoluteThreshold) currentValue else lower
                 }
             }
         }
     }
 
+    private fun requireDensity() = requireNotNull(density) {
+        "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+            "this=$this SwipeableState?"
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [SwipeableV2State].
@@ -387,20 +411,18 @@
         @ExperimentalMaterial3Api
         fun <T : Any> Saver(
             animationSpec: AnimationSpec<Float>,
-            confirmStateChange: (T) -> Boolean,
+            confirmValueChange: (T) -> Boolean,
             positionalThreshold: Density.(distance: Float) -> Float,
-            velocityThreshold: Dp,
-            density: Density
+            velocityThreshold: Dp
         ) = Saver<SwipeableV2State<T>, T>(
-            save = { it.currentState },
+            save = { it.currentValue },
             restore = {
                 SwipeableV2State(
-                    initialState = it,
+                    initialValue = it,
                     animationSpec = animationSpec,
-                    confirmStateChange = confirmStateChange,
+                    confirmValueChange = confirmValueChange,
                     positionalThreshold = positionalThreshold,
-                    velocityThreshold = velocityThreshold,
-                    density = density
+                    velocityThreshold = velocityThreshold
                 )
             }
         )
@@ -410,35 +432,32 @@
 /**
  * Create and remember a [SwipeableV2State].
  *
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
  */
 @Composable
 @ExperimentalMaterial3Api
 internal fun <T : Any> rememberSwipeableV2State(
-    initialState: T,
+    initialValue: T,
     animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
-    confirmStateChange: (newValue: T) -> Boolean = { true }
+    confirmValueChange: (newValue: T) -> Boolean = { true }
 ): SwipeableV2State<T> {
-    val density = LocalDensity.current
     return rememberSaveable(
-        initialState, animationSpec, confirmStateChange, density,
+        initialValue, animationSpec, confirmValueChange,
         saver = SwipeableV2State.Saver(
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         ),
     ) {
         SwipeableV2State(
-            initialState = initialState,
+            initialValue = initialValue,
             animationSpec = animationSpec,
-            confirmStateChange = confirmStateChange,
+            confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         )
     }
 }
@@ -492,11 +511,42 @@
         fixedPositionalThreshold(56.dp)
 }
 
-private fun <T> Map<T, Float>.closestState(
+@Stable
+private class SwipeAnchorsModifier(
+    private val onDensityChanged: (density: Density) -> Unit,
+    private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+    inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+    private var lastDensity: Float = -1f
+    private var lastFontScale: Float = -1f
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        if (density != lastDensity || fontScale != lastFontScale) {
+            onDensityChanged(Density(density, fontScale))
+            lastDensity = density
+            lastFontScale = fontScale
+        }
+        val placeable = measurable.measure(constraints)
+        return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+    }
+
+    override fun onRemeasured(size: IntSize) {
+        onSizeChanged(size)
+    }
+
+    override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+        "onSizeChanged=$onSizeChanged)"
+}
+
+private fun <T> Map<T, Float>.closestAnchor(
     offset: Float = 0f,
     searchUpwards: Boolean = false
 ): T {
-    require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+    require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
     return minBy { (_, anchor) ->
         val delta = if (searchUpwards) anchor - offset else offset - anchor
         if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -505,6 +555,6 @@
 
 private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
 private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
-    "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+    "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
 }
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -48,6 +48,7 @@
     method public abstract boolean showBackground() default false;
     method public abstract boolean showSystemUi() default false;
     method public abstract int uiMode() default 0;
+    method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
     method public abstract int widthDp() default -1;
     property public abstract int apiLevel;
     property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
     property public abstract boolean showBackground;
     property public abstract boolean showSystemUi;
     property public abstract int uiMode;
+    property public abstract int wallpaper;
     property public abstract int widthDp;
   }
 
@@ -81,6 +83,15 @@
     property public abstract kotlin.sequences.Sequence<T> values;
   }
 
+  public final class Wallpapers {
+    field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+    field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+    field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+    field public static final int NONE = -1; // 0xffffffff
+    field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+    field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+  }
+
 }
 
 package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
@@ -48,6 +48,7 @@
     method public abstract boolean showBackground() default false;
     method public abstract boolean showSystemUi() default false;
     method public abstract int uiMode() default 0;
+    method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
     method public abstract int widthDp() default -1;
     property public abstract int apiLevel;
     property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
     property public abstract boolean showBackground;
     property public abstract boolean showSystemUi;
     property public abstract int uiMode;
+    property public abstract int wallpaper;
     property public abstract int widthDp;
   }
 
@@ -81,6 +83,15 @@
     property public abstract kotlin.sequences.Sequence<T> values;
   }
 
+  public final class Wallpapers {
+    field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+    field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+    field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+    field public static final int NONE = -1; // 0xffffffff
+    field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+    field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+  }
+
 }
 
 package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -48,6 +48,7 @@
     method public abstract boolean showBackground() default false;
     method public abstract boolean showSystemUi() default false;
     method public abstract int uiMode() default 0;
+    method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
     method public abstract int widthDp() default -1;
     property public abstract int apiLevel;
     property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
     property public abstract boolean showBackground;
     property public abstract boolean showSystemUi;
     property public abstract int uiMode;
+    property public abstract int wallpaper;
     property public abstract int widthDp;
   }
 
@@ -81,6 +83,15 @@
     property public abstract kotlin.sequences.Sequence<T> values;
   }
 
+  public final class Wallpapers {
+    field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+    field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+    field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+    field public static final int NONE = -1; // 0xffffffff
+    field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+    field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+  }
+
 }
 
 package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
index f581bf0..c064824 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
@@ -51,6 +51,8 @@
  * @param uiMode Bit mask of the ui mode as per [android.content.res.Configuration.uiMode]
  * @param device Device string indicating the device to use in the preview. See the available
  * devices in [Devices].
+ * @param wallpaper Integer defining which wallpaper from those available in Android Studio
+ * to use for dynamic theming.
  */
 @MustBeDocumented
 @Retention(AnnotationRetention.BINARY)
@@ -73,5 +75,6 @@
     val showBackground: Boolean = false,
     val backgroundColor: Long = 0,
     @UiMode val uiMode: Int = 0,
-    @Device val device: String = Devices.DEFAULT
+    @Device val device: String = Devices.DEFAULT,
+    @Wallpaper val wallpaper: Int = Wallpapers.NONE,
 )
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt
new file mode 100644
index 0000000..4dbc0972
--- /dev/null
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.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.compose.ui.tooling.preview
+
+import androidx.annotation.IntDef
+
+/**
+ * Wallpapers available to be used in the [Preview].
+ */
+object Wallpapers {
+    /** Default value, representing dynamic theming not enabled. */
+    const val NONE = -1
+    /** Example wallpaper whose dominant colour is red. */
+    const val RED_DOMINATED_EXAMPLE = 0
+    /** Example wallpaper whose dominant colour is green. */
+    const val GREEN_DOMINATED_EXAMPLE = 1
+    /** Example wallpaper whose dominant colour is blue. */
+    const val BLUE_DOMINATED_EXAMPLE = 2
+    /** Example wallpaper whose dominant colour is yellow. */
+    const val YELLOW_DOMINATED_EXAMPLE = 3
+}
+
+/**
+ * Annotation for defining the wallpaper to use for dynamic theming in the [Preview].
+ * @suppress
+ */
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(Wallpapers.NONE, Wallpapers.RED_DOMINATED_EXAMPLE, Wallpapers.GREEN_DOMINATED_EXAMPLE,
+    Wallpapers.BLUE_DOMINATED_EXAMPLE, Wallpapers.YELLOW_DOMINATED_EXAMPLE)
+annotation class Wallpaper
diff --git a/compose/ui/ui-tooling/OWNERS b/compose/ui/ui-tooling/OWNERS
index 1211f02..32894f6 100644
--- a/compose/ui/ui-tooling/OWNERS
+++ b/compose/ui/ui-tooling/OWNERS
@@ -2,4 +2,5 @@
 diegoperez@google.com
 jlauridsen@google.com
 amaurym@google.com
-kudasov@google.com
\ No newline at end of file
+kudasov@google.com
+kseniia@google.com
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index e99e3fb..8afd4d8 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -420,6 +420,19 @@
         )
     }
 
+    /**
+     * Verifies the use of inline classes as preview default parameters. Methods with inline
+     * classes as parameters will get the name mangled so we need to ensure we invoke correctly
+     * the right method.
+     */
+    @Test
+    fun defaultParametersComposableTest4() {
+        assertRendersCorrectly(
+            "androidx.compose.ui.tooling.SimpleComposablePreviewKt",
+            "DefaultParametersPreview4"
+        )
+    }
+
     @Test
     fun previewInClass() {
         assertRendersCorrectly(
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
index 9c8819b..eabc75a 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
@@ -76,6 +76,14 @@
 
 @Preview
 @Composable
+fun DefaultParametersPreview4(a: String = "Hello", b: Color = Color.White) {
+    if (a != "Hello") throw IllegalArgumentException("Unexpected default value")
+    if (b != Color.White) throw IllegalArgumentException("Unexpected default value")
+    Text("Default parameter  $a $b")
+}
+
+@Preview
+@Composable
 private fun LifecyclePreview() {
     val lifecycleState = LocalLifecycleOwner.current.lifecycle.currentState
     if (lifecycleState != Lifecycle.State.RESUMED) throw IllegalArgumentException(
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..1e0c7d7 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
@@ -152,14 +152,14 @@
                 assertEquals(0, it.startTimeMillis)
                 assertEquals(330, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
-                assertEquals(5, it.values.size)
+                assertGreaterThanOrEqualTo(4, it.values.size)
             }
             transitions[1].let {
                 assertEquals("Built-in shrink/expand", it.label)
                 assertEquals(0, it.startTimeMillis)
                 assertEquals(350, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
-                assertEquals(5, it.values.size)
+                assertGreaterThanOrEqualTo(4, it.values.size)
             }
             transitions[2].let {
                 assertEquals("Built-in InterruptionHandlingOffset", it.label)
@@ -201,7 +201,7 @@
                 assertEquals(0, it.startTimeMillis)
                 assertEquals(300, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
-                assertEquals(4, it.values.size)
+                assertGreaterThanOrEqualTo(3, it.values.size)
             }
             transitions[0].values.let {
                 assertTrue(it.containsKey(0L))
@@ -240,21 +240,21 @@
                 assertEquals(0, it.startTimeMillis)
                 assertEquals(190, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
-                assertEquals(3, it.values.size)
+                assertGreaterThanOrEqualTo(3, it.values.size)
             }
             transitions[1].let {
                 assertEquals("Built-in alpha", it.label)
                 assertEquals(90, it.startTimeMillis, 30)
                 assertEquals(310, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
-                assertEquals(4, it.values.size)
+                assertGreaterThanOrEqualTo(3, it.values.size)
             }
             transitions[2].let {
                 assertEquals("Built-in scale", it.label)
                 assertEquals(90, it.startTimeMillis, 30)
                 assertEquals(310, it.endTimeMillis, 30)
                 assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
-                assertEquals(4, it.values.size)
+                assertGreaterThanOrEqualTo(3, it.values.size)
             }
             transitions[3].let {
                 // This animation probably will be removed.
@@ -498,6 +498,10 @@
         assertEquals(null, expected.toFloat(), actual.toFloat(), delta.toFloat())
     }
 
+    private fun assertGreaterThanOrEqualTo(min: Int, actual: Int) {
+        assertTrue(actual >= min)
+    }
+
     fun assertEquals(expected: Long, actual: Long, delta: Long) {
         assertEquals(null, expected.toFloat(), actual.toFloat(), delta.toFloat())
     }
diff --git a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
index 04b7bfd..24f2f87 100644
--- a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
@@ -52,7 +52,10 @@
     ): Method {
         val actualTypes: Array<Class<*>> = arrayOf(*args)
         return declaredMethods.firstOrNull {
-            methodName == it.name && compatibleTypes(it.parameterTypes, actualTypes)
+            // Methods with inlined classes as parameter will have the name mangled
+            // so we need to check for methodName-xxxx as well
+            (methodName == it.name || it.name.startsWith("$methodName-")) &&
+                compatibleTypes(it.parameterTypes, actualTypes)
         } ?: throw NoSuchMethodException("$methodName not found")
     }
 
@@ -72,11 +75,16 @@
                 methodName,
                 *args.mapNotNull { it?.javaClass }.toTypedArray(),
                 Composer::class.java, // composer param
-                *kotlin.Int::class.java.dup(changedParams) // changed params
+                *kotlin.Int::class.java.dup(changedParams) // changed param
             )
         } catch (e: ReflectiveOperationException) {
             try {
-                declaredMethods.find { it.name == methodName }
+                declaredMethods.find {
+                    it.name == methodName ||
+                        // Methods with inlined classes as parameter will have the name mangled
+                        // so we need to check for methodName-xxxx as well
+                        it.name.startsWith("$methodName-")
+                }
             } catch (e: ReflectiveOperationException) {
                 null
             }
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/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 02c2c59..65fe4f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -558,6 +558,7 @@
         assertNotNull("test did not run", result?.getOrThrow())
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVerticallyDown_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
@@ -583,6 +584,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVerticallyUp_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
@@ -610,6 +612,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollVertically_returnsFalse_afterDownEventOutsideScrollable() {
         lateinit var composeView: View
@@ -633,6 +636,7 @@
         }
     }
 
+    @Ignore // b/260006789
     @Test
     fun canScrollHorizontallyRight_returnsTrue_onlyAfterDownEventInScrollable() {
         lateinit var composeView: View
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index f3ce5e4..164b02e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -449,7 +449,7 @@
     )
 
 /**
- * Determines when to rendering the contents of the buffer into an offscreen layer before
+ * Determines when to render the contents of a layer into an offscreen buffer before
  * being drawn to the destination.
  */
 @Immutable
@@ -467,7 +467,9 @@
          * a compositing layer is created automatically to first render the contents fully opaque,
          * then draw this offscreen buffer to the destination with the corresponding alpha. This is
          * necessary for correctness otherwise alpha applied to individual drawing instructions that
-         * overlap will have a different result than expected
+         * overlap will have a different result than expected. Additionally usage of [RenderEffect]
+         * on the graphicsLayer will also render into an intermediate offscreen buffer before
+         * being drawn into the destination.
          */
         val Auto = CompositingStrategy(0)
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
index 406a1c2..381d62c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
@@ -21,9 +21,27 @@
 import androidx.compose.runtime.ComposeNode
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.PathMeasure
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.drawscope.Stroke
 
+/**
+ * Defines a group of [Path]s and other [Group]s inside a [VectorPainter]. This is not a regular UI
+ * composable, it can only be called inside composables called from the content parameter to
+ * [rememberVectorPainter].
+ *
+ * @param name Optional name of the group used when describing the vector as a string.
+ * @param rotation The rotation of the group around the Z axis, in degrees.
+ * @param pivotX The horizontal pivot point used for rotation, in pixels.
+ * @param pivotY The vertical pivot point used for rotation, in pixels.
+ * @param scaleX Factor to scale the group by horizontally.
+ * @param scaleY Factor to scale the group by vertically.
+ * @param translationX Horizontal offset of the group, in pixels.
+ * @param translationY Vertical offset of the group, in pixels.
+ * @param clipPathData A list of [PathNode]s that define how to clip the group. Empty by default.
+ * @param content A composable that defines the contents of the group.
+ */
 @Composable
 @VectorComposable
 fun Group(
@@ -56,6 +74,27 @@
     }
 }
 
+/**
+ * Defines a path inside a [VectorPainter]. This is not a regular UI composable, it can only be
+ * called inside composables called from the content parameter to [rememberVectorPainter].
+ *
+ * @param pathData List of [PathNode]s that define the path.
+ * @param pathFillType The [PathFillType] that specifies how to fill the path.
+ * @param name Optional name of the path used when describing the vector as a string.
+ * @param fill The [Brush] used to fill the path.
+ * @param fillAlpha The alpha value to use for [fill].
+ * @param stroke The [Brush] used to stroke the path.
+ * @param strokeAlpha The alpha value to use for [stroke].
+ * @param strokeLineWidth The width of the [stroke]. See [Stroke.width] for details.
+ * @param strokeLineCap The [StrokeCap] of [stroke]. See [Stroke.cap] for details.
+ * @param strokeLineJoin The [StrokeJoin] of [stroke]. See [Stroke.join] for details.
+ * @param strokeLineMiter The stroke miter value. See [Stroke.miter] for details.
+ * @param trimPathStart The fraction of the path that specifies the start of the clipped region of
+ * the path. See [PathMeasure.getSegment].
+ * @param trimPathEnd The fraction of the path that specifies the end of the clipped region of the
+ * path. See [PathMeasure.getSegment].
+ * @param trimPathOffset The amount to offset both [trimPathStart] and [trimPathEnd].
+ */
 @Composable
 @VectorComposable
 fun Path(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index 8a0a4c9..3963cc0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -94,8 +94,9 @@
     )
 
 /**
- * Create a [VectorPainter] with the Vector defined by the provided
- * sub-composition
+ * Create a [VectorPainter] with the Vector defined by the provided sub-composition.
+ *
+ * Inside [content] use the [Group] and [Path] composables to define the vector.
  *
  * @param [defaultWidth] Intrinsic width of the Vector in [Dp]
  * @param [defaultHeight] Intrinsic height of the Vector in [Dp]
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index e54edbd..c072b44 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -136,12 +136,12 @@
         rule.runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (displaySize.width / 2),
+                (displaySize.width / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (displaySize.width / 2 / 2),
+                (displaySize.width / 2f / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -197,12 +197,12 @@
         rule.runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (displaySize.width / 2),
+                (displaySize.width / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (displaySize.width / 2 / 2),
+                (displaySize.width / 2f / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
@@ -258,12 +258,12 @@
         rule.runOnIdle {
             // The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
             assertEquals(
-                (displaySize.width / 2),
+                (displaySize.width / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.width
             )
             // Aspect ratio is preserved.
             assertEquals(
-                (displaySize.width / 2 / 2),
+                (displaySize.width / 2f / 2f).roundToInt(),
                 aspectRatioBoxSize.value!!.height
             )
             // Divider has fixed width 1.dp in constraint set.
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/AndroidManifest.xml b/credentials/credentials-play-services-auth/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..d7b0e91
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<!--
+  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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application>
+        <activity
+            android:name="androidx.credentials.playservices.TestCredentialsActivity"
+            android:exported="false"
+            />
+    </application>
+</manifest>
\ No newline at end of file
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
new file mode 100644
index 0000000..3b04bca8
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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;
+
+
+import static androidx.credentials.playservices.TestUtils.EXPECTED_LIFECYCLE_TAG;
+import static androidx.credentials.playservices.TestUtils.clearFragmentManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.credentials.CredentialManagerCallback;
+import androidx.credentials.GetCredentialRequest;
+import androidx.credentials.GetCredentialResponse;
+import androidx.credentials.GetPasswordOption;
+import androidx.credentials.exceptions.GetCredentialException;
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController;
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController;
+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.BeginSignInRequest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@RequiresApi(api = Build.VERSION_CODES.O)
+@SuppressWarnings("deprecation")
+public class CredentialProviderBeginSignInControllerJavaTest {
+
+    @Test
+    public void getInstance_createBrandNewFragment_constructSuccess() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+            android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+            clearFragmentManager(reusedFragmentManager);
+
+            assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+                    EXPECTED_LIFECYCLE_TAG));
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+            CredentialProviderBeginSignInController actualBeginSignInController =
+                    CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+            assertThat(actualBeginSignInController).isNotNull();
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+        });
+
+    }
+
+    @Test
+    public void getInstance_createDifferentFragment_replaceWithNewFragmentSuccess() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+            android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+            clearFragmentManager(reusedFragmentManager);
+
+            assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+                    EXPECTED_LIFECYCLE_TAG));
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+            CredentialProviderCreatePasswordController oldFragment =
+                    CredentialProviderCreatePasswordController.getInstance(reusedFragmentManager);
+
+            assertThat(oldFragment).isNotNull();
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+            CredentialProviderBeginSignInController newFragment =
+                    CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+            assertThat(newFragment).isNotNull();
+            assertThat(newFragment).isNotSameInstanceAs(oldFragment);
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+        });
+    }
+
+    @Test
+    public void getInstance_createFragment_replaceAttemptGivesBackSameFragmentSuccess() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+            android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+            clearFragmentManager(reusedFragmentManager);
+
+            assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+                    EXPECTED_LIFECYCLE_TAG));
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+            CredentialProviderBeginSignInController expectedBeginSignInController =
+                    CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+            assertThat(expectedBeginSignInController).isNotNull();
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+            CredentialProviderBeginSignInController actualBeginSignInController =
+                    CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+            assertThat(actualBeginSignInController).isNotNull();
+            assertThat(actualBeginSignInController)
+                    .isSameInstanceAs(expectedBeginSignInController);
+            assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+        });
+    }
+
+    @Test
+    public void invokePlayServices_success() {
+        // TODO(" Requires mocking inner Identity call. ")
+    }
+
+    @Test
+    public void convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        // 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
+                            .getInstance(activity.getFragmentManager());
+            beginSignInController.callback = new CredentialManagerCallback<GetCredentialResponse,
+                    GetCredentialException>() {
+                @Override
+                public void onResult(@NonNull GetCredentialResponse result) {
+
+                }
+                @Override
+                public void onError(@NonNull GetCredentialException e) {
+
+                }
+            };
+            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)
+                                    ).getCredential();
+
+            assertThat(actualResponse.getType()).isEqualTo(expectedType);
+            assertThat(((PasswordCredential) actualResponse).getPassword())
+                    .isEqualTo(expectedPassword);
+            assertThat(((PasswordCredential) actualResponse).getId()).isEqualTo(expectedId);
+             */
+        });
+    }
+
+    @Test
+    public void
+            convertRequestToPlayServices_setPasswordOptionRequestAndFalseAutoSelect_success() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+
+            BeginSignInRequest actualResponse =
+                    CredentialProviderBeginSignInController
+                            .getInstance(activity.getFragmentManager())
+                            .convertRequestToPlayServices(new GetCredentialRequest(List.of(
+                                    new GetPasswordOption()
+                            )));
+
+            assertThat(actualResponse.getPasswordRequestOptions().isSupported()).isTrue();
+            assertThat(actualResponse.isAutoSelectEnabled()).isFalse();
+        });
+    }
+
+    @Test
+    public void convertRequestToPlayServices_setPasswordOptionRequestAndTrueAutoSelect_success() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+
+            BeginSignInRequest actualResponse =
+                    CredentialProviderBeginSignInController
+                        .getInstance(activity.getFragmentManager())
+                        .convertRequestToPlayServices(new GetCredentialRequest(List.of(
+                                new GetPasswordOption()
+                        ), true));
+
+            assertThat(actualResponse.getPasswordRequestOptions().isSupported()).isTrue();
+            assertThat(actualResponse.isAutoSelectEnabled()).isTrue();
+        });
+    }
+
+    @Test
+    public void convertRequestToPlayServices_nullRequest_throws() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+
+            assertThrows(
+                    "null get credential request must throw exception",
+                    NullPointerException.class,
+                    () -> CredentialProviderBeginSignInController
+                            .getInstance(activity.getFragmentManager())
+                            .convertRequestToPlayServices(null)
+            );
+        });
+    }
+
+    @Test
+    public void convertResponseToCredentialManager_nullRequest_throws() {
+        ActivityScenario<TestCredentialsActivity> activityScenario =
+                ActivityScenario.launch(TestCredentialsActivity.class);
+        activityScenario.onActivity(activity -> {
+
+            assertThrows(
+                    "null sign in credential response must throw exception",
+                    NullPointerException.class,
+                    () -> CredentialProviderBeginSignInController
+                            .getInstance(activity.getFragmentManager())
+                            .convertResponseToCredentialManager(null)
+            );
+        });
+    }
+}
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
new file mode 100644
index 0000000..4f5a8ce
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.playservices.TestUtils.Companion.clearFragmentManager
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@Suppress("deprecation")
+@RequiresApi(api = Build.VERSION_CODES.O)
+class CredentialProviderBeginSignInControllerTest {
+    @Test
+    fun getInstance_createBrandNewFragment_constructSuccess() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity ->
+            val reusedFragmentManager = activity.fragmentManager
+
+            clearFragmentManager(
+                reusedFragmentManager
+            )
+
+            assertThat(
+                reusedFragmentManager.fragments[0].tag ==
+                    TestUtils.EXPECTED_LIFECYCLE_TAG
+            )
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+
+            val actualBeginSignInController =
+                CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+            assertThat(actualBeginSignInController).isNotNull()
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun getInstance_createDifferentFragment_replaceWithNewFragmentSuccess() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity ->
+            val reusedFragmentManager = activity.fragmentManager
+
+            clearFragmentManager(
+                reusedFragmentManager
+            )
+
+            assertThat(
+                reusedFragmentManager.fragments[0].tag ==
+                    TestUtils.EXPECTED_LIFECYCLE_TAG
+            )
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+
+            val oldFragment =
+                CredentialProviderCreatePasswordController.getInstance(reusedFragmentManager)
+
+            assertThat(oldFragment).isNotNull()
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+
+            val newFragment =
+                CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+            assertThat(newFragment).isNotNull()
+            assertThat(newFragment).isNotSameInstanceAs(oldFragment)
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun getInstance_createFragment_replaceAttemptGivesBackSameFragmentSuccess() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity ->
+            val reusedFragmentManager = activity.fragmentManager
+
+            clearFragmentManager(
+                reusedFragmentManager
+            )
+
+            assertThat(
+                reusedFragmentManager.fragments[0].tag ==
+                    TestUtils.EXPECTED_LIFECYCLE_TAG
+            )
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+
+            val expectedBeginSignInController =
+                CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+            assertThat(expectedBeginSignInController).isNotNull()
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+
+            val actualBeginSignInController =
+                CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+            assertThat(actualBeginSignInController).isNotNull()
+            assertThat(actualBeginSignInController)
+                .isSameInstanceAs(expectedBeginSignInController)
+            assertThat(reusedFragmentManager.fragments.size)
+                .isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun invokePlayServices_success() {
+        // TODO(" Requires mocking inner Identity call. ")
+    }
+
+    @Test
+    fun convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        // 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
+                    .getInstance(activity.fragmentManager)
+            beginSignInController.callback =
+                object :
+                    CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
+                    override fun onResult(result: GetCredentialResponse) {}
+                    override fun onError(e: GetCredentialException) {}
+                }
+            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
+                    )
+                ).credential
+
+            assertThat(actualResponse.type).isEqualTo(expectedType)
+            assertThat((actualResponse as PasswordCredential).password)
+                .isEqualTo(expectedPassword)
+            assertThat(actualResponse.id)
+                .isEqualTo(expectedId)
+            */
+        }
+    }
+
+    @Test
+    fun convertRequestToPlayServices_setPasswordOptionRequestAndFalseAutoSelect_success() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity ->
+
+            val actualResponse = CredentialProviderBeginSignInController
+                .getInstance(activity.fragmentManager)
+                .convertRequestToPlayServices(
+                    GetCredentialRequest(
+                        listOf(
+                            GetPasswordOption()
+                        )
+                    )
+                )
+
+            assertThat(
+                actualResponse.passwordRequestOptions.isSupported
+            ).isTrue()
+            assertThat(actualResponse.isAutoSelectEnabled).isFalse()
+        }
+    }
+
+    @Test
+    fun convertRequestToPlayServices_setPasswordOptionRequestAndTrueAutoSelect_success() {
+        val activityScenario = ActivityScenario.launch(
+            TestCredentialsActivity::class.java
+        )
+        activityScenario.onActivity { activity: TestCredentialsActivity ->
+
+            val actualResponse = CredentialProviderBeginSignInController
+                .getInstance(activity.fragmentManager)
+                .convertRequestToPlayServices(
+                    GetCredentialRequest(
+                        listOf(
+                            GetPasswordOption()
+                        ), true
+                    )
+                )
+
+            assertThat(
+                actualResponse.passwordRequestOptions.isSupported
+            ).isTrue()
+            assertThat(actualResponse.isAutoSelectEnabled).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt
new file mode 100644
index 0000000..d57a9b6
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+import androidx.core.app.ComponentActivity
+
+/**
+ * This is a test activity used by the Robolectric Activity Scenario tests. It acts
+ * as a calling activity in our test cases.
+ */
+class TestCredentialsActivity : ComponentActivity()
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
new file mode 100644
index 0000000..b27d22c
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.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.playservices
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+class TestUtils {
+    companion object {
+        @JvmStatic
+        @RequiresApi(api = Build.VERSION_CODES.O)
+        @Suppress("deprecation")
+        fun clearFragmentManager(fragmentManager: android.app.FragmentManager) {
+            fragmentManager.fragments.forEach { f ->
+                fragmentManager.beginTransaction().remove(f)
+                    ?.commitAllowingStateLoss()
+            }
+            Log.i("Test", fragmentManager.fragments.toString())
+        // Within fragmentManager.fragments, even after removal of all, this exists by default:
+        // [ReportFragment{92dad5d #0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}]
+        // It will only be removed after an actual fragment is added.
+        // This may be due to ActivityScenario simulations of Fragments and FragmentManagers.
+        }
+
+        const val EXPECTED_LIFECYCLE_TAG =
+            "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag"
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml b/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..09cd014
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml
@@ -0,0 +1,17 @@
+<!--
+  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.
+  -->
+
+<resources></resources>
\ No newline at end of file
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 e5e59bf..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")
@@ -116,7 +121,7 @@
             cancellationSignal.setOnCancelListener {
                 // When this callback is notified, fragment manager may have fragments
                 fragmentManager.fragments.forEach { f ->
-                    f?.parentFragment?.fragmentManager?.beginTransaction()?.remove(f)
+                    fragmentManager.beginTransaction().remove(f)
                         ?.commitAllowingStateLoss()
                 }
             }
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 1c9f3c1..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
@@ -20,14 +20,15 @@
 import android.app.Activity
 import android.content.Intent
 import android.content.IntentSender
-import android.os.Bundle
+import android.os.Build
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import androidx.credentials.Credential
 import androidx.credentials.CredentialManagerCallback
 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
@@ -57,14 +58,16 @@
     /**
      * The callback object state, used in the protected handleResponse method.
      */
-    private lateinit var callback: CredentialManagerCallback<GetCredentialResponse,
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    lateinit var callback: CredentialManagerCallback<GetCredentialResponse,
         GetCredentialException>
     /**
      * The callback requires an executor to invoke it.
      */
-    private lateinit var executor: Executor
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    lateinit var executor: Executor
 
-    @SuppressLint("ClassVerificationFailure", "NewApi")
+    @SuppressLint("ClassVerificationFailure")
     override fun invokePlayServices(
         request: GetCredentialRequest,
         callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
@@ -77,15 +80,17 @@
             .beginSignIn(convertedRequest)
             .addOnSuccessListener { result: BeginSignInResult ->
                 try {
-                    startIntentSenderForResult(
-                        result.pendingIntent.intentSender,
-                        REQUEST_CODE_BEGIN_SIGN_IN,
-                        null, /* fillInIntent= */
-                        0, /* flagsMask= */
-                        0, /* flagsValue= */
-                        0, /* extraFlags= */
-                        null /* options= */
-                    )
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                        startIntentSenderForResult(
+                            result.pendingIntent.intentSender,
+                            REQUEST_CODE_BEGIN_SIGN_IN,
+                            null, /* fillInIntent= */
+                            0, /* flagsMask= */
+                            0, /* flagsValue= */
+                            0, /* extraFlags= */
+                            null /* options= */
+                        )
+                    }
                 } catch (e: IntentSender.SendIntentException) {
                     Log.e(TAG, "Couldn't start One Tap UI in beginSignIn: " +
                         e.localizedMessage
@@ -127,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
@@ -144,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()
             }
@@ -154,15 +159,23 @@
                 )
             }
             return
+        } catch (e: GetCredentialException) {
+            executor.execute { ->
+                callback.onError(
+                    e
+                )
+            }
         }
     }
 
-    override fun convertRequestToPlayServices(request: GetCredentialRequest):
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public override fun convertRequestToPlayServices(request: GetCredentialRequest):
         BeginSignInRequest {
         return constructBeginSignInRequest(request)
     }
 
-    override fun convertResponseToCredentialManager(response: SignInCredential):
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+    public override fun convertResponseToCredentialManager(response: SignInCredential):
         GetCredentialResponse {
         var cred: Credential? = null
         if (response.password != null) {
@@ -175,11 +188,7 @@
             Log.i(TAG, "Credential returned but no google Id or password or passkey found")
         }
         if (cred == null) {
-            executor.execute { callback.onError(
-                GetCredentialUnknownException(
-                    "cred should not be null")
-            ) }
-            return GetCredentialResponse(Credential("ERROR", Bundle.EMPTY))
+            throw GetCredentialUnknownException("null credential found")
         }
         return GetCredentialResponse(cred)
     }
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 be6ba81..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
@@ -76,7 +76,7 @@
             .savePassword(convertedRequest)
             .addOnSuccessListener { result: SavePasswordResult ->
                 try {
-                    if (Build.VERSION.SDK_INT >= 24) {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                         startIntentSenderForResult(
                             result.pendingIntent.intentSender,
                             REQUEST_CODE_GIS_SAVE_PASSWORD,
@@ -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/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
index 7bf3ba1..7ef9840 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
@@ -23,7 +23,6 @@
 import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
-import java.util.Collection;
 
 /**
  * Combines with {@link CreatePublicKeyCredentialRequestPrivilegedJavaTest} for full tests.
@@ -50,11 +49,11 @@
     }
 
     @Parameterized.Parameters
-    public static Collection failureCases() {
+    public static Iterable<String[]> failureCases() {
         // Allows checking improper formations for builder and normal construction.
         // Handles both null and empty cases.
         // For successful cases, see the non parameterized tests.
-        return Arrays.asList(new Object[][] {
+        return Arrays.asList(new String[][] {
                 { "{\"hi\":21}", "rp", "", null, "rp", "hash"},
                 { "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
                 { "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
@@ -84,6 +83,7 @@
 
     @Test
     public void constructor_nullInput_throwsNullPointerException() {
+        convertAPIIssueToProperNull();
         assertThrows("If at least one arg null, should throw NullPointerException",
                 NullPointerException.class,
                 () -> new CreatePublicKeyCredentialRequestPrivileged(this.mNullRequestJson,
@@ -94,6 +94,7 @@
 
     @Test
     public void builder_build_nullInput_throwsNullPointerException() {
+        convertAPIIssueToProperNull();
         assertThrows(
                 "If at least one arg null to builder, should throw NullPointerException",
                 NullPointerException.class,
@@ -101,4 +102,20 @@
                                 mNullRp, mNullClientDataHash).build()
         );
     }
+
+    // Certain API levels have parameterized tests that automatically convert null to a string
+    // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
+    // workaround. Note this is *not* always the case but only for certain API levels (we have
+    // recorded 21, 22, and 23 as such levels).
+    private void convertAPIIssueToProperNull() {
+        if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
+            mNullRequestJson = null;
+        }
+        if (mNullRp != null && mNullRp.equals("null")) {
+            mNullRp = null;
+        }
+        if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
+            mNullClientDataHash = null;
+        }
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
index 9984393..be01f1c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
@@ -23,7 +23,6 @@
 import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
-import java.util.Collection;
 
 /**
  * Combines with {@link GetPublicKeyCredentialOptionPrivilegedJavaTest} for full tests.
@@ -50,11 +49,11 @@
     }
 
     @Parameterized.Parameters
-    public static Collection failureCases() {
+    public static Iterable<String[]> failureCases() {
         // Allows checking improper formations for builder and normal construction.
         // Handles both null and empty cases.
         // For successful cases, see the non parameterized privileged tests.
-        return Arrays.asList(new Object[][] {
+        return Arrays.asList(new String[][] {
                 { "{\"hi\":21}", "rp", "", null, "rp", "hash"},
                 { "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
                 { "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
@@ -84,6 +83,7 @@
 
     @Test
     public void constructor_nullInput_throwsNullPointerException() {
+        convertAPIIssueToProperNull();
         assertThrows(
                 "If at least one arg null, should throw NullPointerException",
                 NullPointerException.class,
@@ -95,6 +95,7 @@
 
     @Test
     public void builder_build_nullInput_throwsNullPointerException() {
+        convertAPIIssueToProperNull();
         assertThrows(
                 "If at least one arg null to builder, should throw NullPointerException",
                 NullPointerException.class,
@@ -102,4 +103,20 @@
                         mNullRp, mNullClientDataHash).build()
         );
     }
+
+    // Certain API levels have parameterized tests that automatically convert null to a string
+    // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
+    // workaround. Note this is *not* always the case but only for certain API levels (we have
+    // recorded 21, 22, and 23 as such levels).
+    private void convertAPIIssueToProperNull() {
+        if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
+            mNullRequestJson = null;
+        }
+        if (mNullRp != null && mNullRp.equals("null")) {
+            mNullRp = null;
+        }
+        if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
+            mNullClientDataHash = null;
+        }
+    }
 }
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/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index add01af..1fa5322 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -792,3 +792,7 @@
 xcframework successfully .*
 # iOS benchmark invocation
 /usr/bin/xcodebuild
+# > Configure project :internal-testutils-ktx
+WARNING:The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
+# Building XCFrameworks (b/260140834)
+.*xcodebuild.*
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/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index dd40d3a..bcc3091 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -33,6 +33,8 @@
     implementation 'androidx.tracing:tracing-ktx:1.0.0'
     implementation 'androidx.test.ext:junit-ktx:1.1.3'
 
+    androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
+    androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
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/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
index 5f967ca..5ab8cb5 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import androidx.emoji2.emojipicker.utils.FileCache
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertFalse
@@ -69,6 +70,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 24)
     fun testGetEmojiVariantsLookup_loaded() = runBlocking {
         // delete cache and load again
         FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
@@ -80,4 +82,19 @@
         // 😀 has no variant
         assertFalse(result.containsKey("\uD83D\uDE00"))
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 24)
+    fun testGetPrimaryEmojiLookup_loaded() = runBlocking {
+        // delete cache and load again
+        FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
+        BundledEmojiListLoader.load(context)
+        val result = BundledEmojiListLoader.getPrimaryEmojiLookup()
+
+        // 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
+        assertTrue(result["\uD83D\uDC43\uD83C\uDFFD"]!! == "\uD83D\uDC43")
+        assertTrue(result["\uD83D\uDC43\uD83C\uDFFF"]!! == "\uD83D\uDC43")
+        // 😀 has no variant
+        assertFalse(result.containsKey("\uD83D\uDE00"))
+    }
 }
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 a63f725..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
@@ -16,15 +16,24 @@
 
 package androidx.emoji2.emojipicker
 
+import androidx.emoji2.emojipicker.R as EmojiPickerViewR
 import android.app.Activity
 import android.content.Context
 import android.os.Bundle
+import android.view.View
+import android.view.View.GONE
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
 import androidx.core.view.isVisible
 import androidx.emoji2.emojipicker.test.R
+import androidx.recyclerview.widget.RecyclerView
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.matcher.BoundedMatcher
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import org.hamcrest.Description
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -61,4 +70,63 @@
             assertEquals(mEmojiPickerView.emojiGridColumns, 10)
         }
     }
+
+    @Test
+    fun testCustomEmojiPickerView_noVariant() {
+        mActivityTestRule.scenario.onActivity {
+            val targetView = findViewByEmoji(
+                it.findViewById(R.id.emojiPickerTest),
+                "\uD83D\uDE00"
+            )
+            // No variant indicator
+            assertEquals(
+                (targetView.parent as FrameLayout).findViewById<ImageView>(
+                    EmojiPickerViewR.id.variant_availability_indicator
+                ).visibility,
+                GONE
+            )
+            // Not long-clickable
+            assertEquals(targetView.isLongClickable, false)
+        }
+    }
+
+    private fun findViewByEmoji(root: View, emoji: String) =
+        mutableListOf<View>().apply {
+            findViewsById(
+                root,
+                EmojiPickerViewR.id.emoji_view, this
+            )
+        }.first { (it as EmojiView).emoji == emoji }
+
+    private fun findViewsById(root: View, id: Int, output: MutableList<View>) {
+        if (root !is ViewGroup) {
+            return
+        }
+        for (i in 0 until root.childCount) {
+            root.getChildAt(i).apply {
+                if (this.id == id) {
+                    output.add(this)
+                }
+            }.also {
+                findViewsById(it, id, output)
+            }
+        }
+    }
+
+    private fun createEmojiViewHolderMatcher(emoji: String) =
+        object :
+            BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
+            override fun describeTo(description: Description) {}
+            override fun matchesSafely(item: EmojiViewHolder) =
+                (item.itemView as FrameLayout)
+                    .findViewById<EmojiView>(EmojiPickerViewR.id.emoji_view)
+                    .emoji == emoji
+        }
+
+    private fun createEmojiViewMatcher(emoji: String) =
+        object :
+            BoundedMatcher<View, EmojiView>(EmojiView::class.java) {
+            override fun describeTo(description: Description) {}
+            override fun matchesSafely(item: EmojiView) = item.emoji == emoji
+        }
 }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 57136d4..ed65d91 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -39,6 +39,7 @@
 internal object BundledEmojiListLoader {
     private var categorizedEmojiData: List<EmojiDataCategory>? = null
     private var emojiVariantsLookup: Map<String, List<String>>? = null
+    private var primaryEmojiLookup: Map<String, String>? = null
 
     private var deferred: List<Deferred<EmojiDataCategory>>? = null
 
@@ -67,6 +68,14 @@
             .associate { it.emoji to it.variants }
             .also { emojiVariantsLookup = it }
 
+    internal suspend fun getPrimaryEmojiLookup() =
+        primaryEmojiLookup ?: getCategorizedEmojiData()
+            .flatMap { it.emojiDataList }
+            .filter { it.variants.isNotEmpty() }
+            .flatMap { it.variants.associateWith { _ -> it.emoji }.entries }
+            .associate { it.toPair() }
+            .also { primaryEmojiLookup = it }
+
     private suspend fun loadEmojiAsync(
         ta: TypedArray,
         categoryNames: Array<String>,
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 0a0b29c..6b5897a 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,22 +25,25 @@
 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(
-    context: Context,
+    private val context: Context,
     private val emojiGridColumns: Int,
     private val emojiGridRows: Float,
     private val categoryNames: Array<String>,
-    private val onEmojiPickedListener: Consumer<EmojiViewItem>?
+    private val variantToBaseEmojiMap: Map<String, String>,
+    private val baseToVariantsEmojiMap: Map<String, List<String>>,
+    private val stickyVariantProvider: StickyVariantProvider,
+    private val onEmojiPickedListener: Consumer<EmojiViewItem>?,
+    private val recentEmojiList: MutableList<String>,
+    private val recentEmojiProvider: RecentEmojiProvider
 ) : Adapter<ViewHolder>() {
     private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
-    private val context = context
-
     private var flattenSource: ItemViewDataFlatList
 
     init {
@@ -64,7 +67,7 @@
                     view = layoutInflater.inflate(
                         R.layout.category_text_view,
                         parent,
-                        /* attachToRoot= */ false
+                        /* attachToRoot = */ false
                     )
                     view.layoutParams =
                         LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
@@ -74,7 +77,7 @@
                     view = layoutInflater.inflate(
                         R.layout.emoji_picker_empty_category_text_view,
                         parent,
-                        /* attachToRoot= */ false
+                        /* attachToRoot = */ false
                     )
                     view.layoutParams =
                         LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
@@ -83,11 +86,34 @@
 
                 EmojiViewData.TYPE -> {
                     return EmojiViewHolder(
+                        context,
                         parent,
                         layoutInflater,
                         getParentWidth(parent) / emojiGridColumns,
                         (parent.measuredHeight / emojiGridRows).toInt(),
-                        onEmojiPickedListener
+                        stickyVariantProvider,
+                        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)
+                        }
                     )
                 }
 
@@ -188,4 +214,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 0d8b1e5..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
@@ -61,6 +62,9 @@
             field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_COLUMNS
         }
 
+    private val stickyVariantProvider = StickyVariantProvider(context)
+    private var recentEmojiProvider = DefaultRecentEmojiProvider(context)
+
     private lateinit var headerView: RecyclerView
     private lateinit var bodyView: RecyclerView
     private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null
@@ -91,18 +95,31 @@
         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)
             categorizedEmojis.add(
-                categorizedEmojiData[i].emojiDataList.toMutableList()
+                categorizedEmojiData[i].emojiDataList.map {
+                    stickyVariantProvider.stickyVariantMap[it.emoji]?.let { stickyVariant ->
+                        EmojiViewItem(stickyVariant, it.variants)
+                    } ?: it
+                }.toMutableList()
             )
         }
         val adapter = EmojiPickerBodyAdapter(
@@ -110,7 +127,12 @@
             emojiGridColumns,
             emojiGridRows,
             categoryNames.toTypedArray(),
-            onEmojiPickedListener
+            variantToBaseEmojiMap,
+            baseToVariantsEmojiMap,
+            stickyVariantProvider,
+            onEmojiPickedListener,
+            recentEmojiList,
+            recentEmojiProvider
         )
         adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
 
@@ -152,20 +174,45 @@
             LinearLayoutManager(
                 context,
                 LinearLayoutManager.HORIZONTAL,
-                /* reverseLayout= */ false
+                /* reverseLayout = */ false
             )
         headerView.adapter = EmojiPickerHeaderAdapter(context)
 
         // 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/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
index bba8664..1e12a47 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -80,6 +80,7 @@
             if (value != null) {
                 post {
                     drawEmoji(value)
+                    contentDescription = value
                     invalidate()
                 }
             }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
index aace300..3c34b4d 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
@@ -30,8 +30,9 @@
     /** The id of this emoji view in the category, usually is the position of the emoji.  */
     private val idInCategory: Int
 
-    /** Primary key which is used for labeling and for PRESS action.  */
-    val primary: String
+    /** Primary key which is used for labeling and for PRESS action. This value could be updated
+     * to one of its variants. */
+    var primary: String
 
     /** Secondary keys which are used for LONG_PRESS action.  */
     val secondaries: Array<String>
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 723c646..f65bf97 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
@@ -16,41 +16,119 @@
 
 package androidx.emoji2.emojipicker
 
+import android.content.Context
+import android.view.Gravity
 import android.view.LayoutInflater
+import android.view.View.GONE
+import android.view.View.OnClickListener
+import android.view.View.OnLongClickListener
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
-import androidx.core.util.Consumer
+import android.view.accessibility.AccessibilityEvent
+import android.widget.GridLayout
+import android.widget.ImageView
+import android.widget.PopupWindow
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlin.math.roundToInt
 
 /** A [ViewHolder] containing an emoji view and emoji data.  */
 internal class EmojiViewHolder(
+    context: Context,
     parent: ViewGroup,
     layoutInflater: LayoutInflater,
     width: Int,
     height: Int,
-    onEmojiPickedListener: Consumer<EmojiViewItem>?
+    private val stickyVariantProvider: StickyVariantProvider,
+    private val onEmojiPickedListener: EmojiViewHolder.(EmojiViewItem) -> Unit,
+    private val onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
 ) : ViewHolder(
     layoutInflater
-        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
 ) {
+    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 {
+        showPopupWindow(layoutInflater, emojiView) {
+            PopupViewHelper(context).fillPopupView(
+                it,
+                layoutInflater,
+                emojiView.measuredWidth,
+                emojiView.measuredHeight,
+                emojiViewItem.variants,
+                clickListener = { view ->
+                    val emojiPickedInPopup = (view as EmojiView).emoji.toString()
+                    onEmojiPickedFromPopupListener(emojiPickedInPopup)
+                    onEmojiClickListener.onClick(view)
+                    // variants[0] is always the base (i.e., primary) emoji
+                    stickyVariantProvider.update(emojiViewItem.variants[0], emojiPickedInPopup)
+                    this.dismiss()
+                    // Hover on the base emoji after popup dismissed
+                    emojiView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+                }
+            )
+        }
+        true
+    }
+
     private val emojiView: EmojiView
+    private val indicator: ImageView
     private lateinit var emojiViewItem: EmojiViewItem
 
     init {
         itemView.layoutParams = LayoutParams(width, height)
         emojiView = itemView.findViewById(R.id.emoji_view)
         emojiView.isClickable = true
-
-        // set emojiViewListener
-        emojiView.setOnClickListener {
-            onEmojiPickedListener?.accept(emojiViewItem)
-        }
+        emojiView.setOnClickListener(onEmojiClickListener)
+        indicator = itemView.findViewById(R.id.variant_availability_indicator)
     }
 
     fun bindEmoji(
-        emojiViewItem: EmojiViewItem
+        emojiViewItem: EmojiViewItem,
     ) {
         emojiView.emoji = emojiViewItem.emoji
         this.emojiViewItem = emojiViewItem
+
+        if (emojiViewItem.variants.isNotEmpty()) {
+            indicator.visibility = VISIBLE
+            emojiView.setOnLongClickListener(onEmojiLongClickListener)
+            emojiView.isLongClickable = true
+        } else {
+            indicator.visibility = GONE
+            emojiView.setOnLongClickListener(null)
+            emojiView.isLongClickable = false
+        }
+    }
+
+    private fun showPopupWindow(
+        layoutInflater: LayoutInflater,
+        parent: EmojiView,
+        init: PopupWindow.(GridLayout) -> Unit
+    ) {
+        val popupView = layoutInflater
+            .inflate(R.layout.variant_popup, null, false)
+            .findViewById<GridLayout>(R.id.variant_popup)
+        PopupWindow(popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, false).apply {
+            init(popupView)
+            val location = IntArray(2)
+            parent.getLocationInWindow(location)
+            // Make the popup view center align with the target emoji view.
+            val x =
+                location[0] + parent.width / 2f - popupView.columnCount * parent.width / 2f
+            val y = location[1] - popupView.rowCount * parent.height
+            isOutsideTouchable = true
+            isTouchable = true
+            animationStyle = R.style.VariantPopupAnimation
+            showAtLocation(
+                parent,
+                Gravity.NO_GRAVITY,
+                x.roundToInt(),
+                y
+            )
+        }
     }
 }
\ No newline at end of file
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/PopupViewHelper.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
new file mode 100644
index 0000000..af0f9cb
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
@@ -0,0 +1,176 @@
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.GridLayout
+import androidx.core.content.ContextCompat
+
+/*
+ * 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.
+ */
+
+internal class PopupViewHelper(private val context: Context) {
+    companion object {
+        private enum class Layout { FLAT, SQUARE, SQUARE_WITH_SKIN_TONE_CIRCLE }
+
+        private const val SQUARE_LAYOUT_VARIANT_COUNT = 26
+
+        // Set of emojis that use the square layout without skin tone.
+        private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")
+
+        private val SKIN_TONE_COLOR_RES_IDS = listOf(
+            R.color.light_skin_tone,
+            R.color.medium_light_skin_tone,
+            R.color.medium_skin_tone,
+            R.color.medium_dark_skin_tone,
+            R.color.dark_skin_tone
+        )
+
+        /**
+         * Square variant layout template with skin tone.
+         * 0 : a place holder
+         * -5: light skin tone circle
+         * -4: medium-light skin tone circle
+         * -3: medium skin tone circle
+         * -2: medium-dark skin tone circle
+         * -1: dark skin tone circle
+         * Positive number is the index + 1 in the variant array
+         */
+        private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
+            intArrayOf(0, 0, -5, -4, -3, -2, -1),
+            intArrayOf(0, -5, 2, 3, 4, 5, 6),
+            intArrayOf(0, -4, 7, 8, 9, 10, 11),
+            intArrayOf(0, -3, 12, 13, 14, 15, 16),
+            intArrayOf(0, -2, 17, 18, 19, 20, 21),
+            intArrayOf(1, -1, 22, 23, 24, 25, 26)
+        )
+
+        /**
+         * Square variant layout template without skin tone.
+         * 0 : a place holder
+         * Positive number is the index + 1 in the variant array
+         */
+        private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
+            intArrayOf(0, 2, 3, 4, 5, 6),
+            intArrayOf(0, 7, 8, 9, 10, 11),
+            intArrayOf(0, 12, 13, 14, 15, 16),
+            intArrayOf(0, 17, 18, 19, 20, 21),
+            intArrayOf(1, 22, 23, 24, 25, 26)
+        )
+    }
+
+    fun fillPopupView(
+        popupView: GridLayout,
+        layoutInflater: LayoutInflater,
+        gridWidth: Int,
+        gridHeight: Int,
+        variants: List<String>,
+        clickListener: OnClickListener
+    ): GridLayout {
+        val gridTemplate = getGridTemplate(variants)
+        popupView
+            .apply {
+                columnCount = gridTemplate.column
+                rowCount = gridTemplate.row
+                orientation = GridLayout.HORIZONTAL
+            }
+        gridTemplate.template.flatMap { it.asIterable() }.forEach {
+            val gridCell = when (it) {
+                in 1..variants.size -> (layoutInflater.inflate(
+                    R.layout.emoji_view_holder,
+                    null,
+                    false
+                ) as FrameLayout).apply {
+                    (this.getChildAt(0) as EmojiView).apply {
+                        emoji = variants[it - 1]
+                        setOnClickListener(clickListener)
+                        if (it == 1) {
+                            // Hover on the first emoji in the popup
+                            popupView.post {
+                                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+                            }
+                        }
+                    }
+                }
+
+                0 -> layoutInflater
+                    .inflate(R.layout.emoji_view_holder, null, false)
+
+                else -> SkinToneCircleViewView(context).apply {
+                    paint = Paint().apply {
+                        color = ContextCompat.getColor(context, SKIN_TONE_COLOR_RES_IDS[it + 5])
+                        style = Paint.Style.FILL
+                    }
+                }
+            }
+            popupView.addView(gridCell)
+            gridCell.layoutParams.width = gridWidth
+            gridCell.layoutParams.height = gridHeight
+        }
+        return popupView
+    }
+
+    private fun getGridTemplate(variants: List<String>): GridTemplate {
+        val layout =
+            if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
+                if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
+                    Layout.SQUARE
+                else Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
+            else Layout.FLAT
+        val template = when (layout) {
+            Layout.SQUARE -> SQUARE_LAYOUT_TEMPLATE
+            Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE
+            Layout.FLAT -> arrayOf(variants.indices.map { it + 1 }.toIntArray())
+        }
+        val column = when (layout) {
+            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template[0].size
+            Layout.FLAT -> minOf(6, template[0].size)
+        }
+        val row = when (layout) {
+            Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template.size
+            Layout.FLAT -> variants.size / column + if (variants.size % column == 0) 0 else 1
+        }
+
+        return GridTemplate(template, row, column)
+    }
+
+    private data class GridTemplate(
+        val template: Array<IntArray>,
+        val row: Int,
+        val column: Int
+    )
+}
+
+internal class SkinToneCircleViewView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : View(context, attrs) {
+    private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
+    var paint: Paint? = null
+
+    override fun draw(canvas: Canvas?) {
+        super.draw(canvas)
+        canvas?.apply {
+            paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
+        }
+    }
+}
\ 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/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
new file mode 100644
index 0000000..7c45cf6
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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
+
+/**
+ * A class that handles user's emoji variant selection using SharedPreferences.
+ */
+internal class StickyVariantProvider(context: Context) {
+    companion object {
+        const val PREFERENCES_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+        const val STICKY_VARIANT_PROVIDER_KEY = "pref_key_sticky_variant"
+        const val KEY_VALUE_DELIMITER = "="
+        const val ENTRY_DELIMITER = "|"
+    }
+
+    private val sharedPreferences =
+        context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE)
+
+    internal val stickyVariantMap: Map<String, String> by lazy {
+        sharedPreferences.getString(STICKY_VARIANT_PROVIDER_KEY, null)?.split(ENTRY_DELIMITER)
+            ?.associate { entry ->
+                entry.split(KEY_VALUE_DELIMITER, limit = 2).takeIf { it.size == 2 }
+                    ?.let { it[0] to it[1] } ?: ("" to "")
+            } ?: mapOf()
+    }
+
+    internal fun update(baseEmoji: String, variantClicked: String) {
+        stickyVariantMap.toMutableMap().apply {
+            if (baseEmoji == variantClicked) {
+                this.remove(baseEmoji)
+            } else {
+                this[baseEmoji] = variantClicked
+            }
+            sharedPreferences.edit()
+                .putString(
+                    STICKY_VARIANT_PROVIDER_KEY,
+                    entries.joinToString(ENTRY_DELIMITER)
+                ).commit()
+        }
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml
new file mode 100644
index 0000000..39f3163
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true"
+    android:interpolator="@android:anim/accelerate_interpolator">
+    <alpha
+        android:duration="100"
+        android:fromAlpha="1" android:toAlpha="0" />
+
+    <scale
+        android:duration="100"
+        android:fromXScale="1.0" android:toXScale="1.0"
+        android:fromYScale="1.0" android:toYScale="0.0"
+        android:pivotX="0%"
+        android:pivotY="100%" />
+</set>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml
new file mode 100644
index 0000000..5dad0a8
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fillAfter="true"
+    android:interpolator="@android:anim/decelerate_interpolator">
+    <alpha
+        android:duration="100"
+        android:fromAlpha="0.5" android:toAlpha="1" />
+
+    <scale
+        android:duration="100"
+        android:fromXScale="1.0" android:toXScale="1.0"
+        android:fromYScale="0.0" android:toYScale="1.0"
+        android:pivotX="0%"
+        android:pivotY="100%" />
+</set>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
new file mode 100644
index 0000000..c625912
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#FFDDDDDD"/>
+    <corners android:radius="@dimen/emoji_picker_popup_view_holder_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
new file mode 100644
index 0000000..e27fe2e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M2,22h20V2L2,22z"/>
+</vector>
\ 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/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
index 0fa8c9c..76fe2b9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -15,7 +15,6 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="0dp"
     android:layout_height="0dp">
@@ -28,4 +27,11 @@
         android:importantForAccessibility="yes"
         android:textSize="30dp"
         tools:ignore="SpUsage" />
+    <ImageView
+        android:id="@+id/variant_availability_indicator"
+        android:visibility="gone"
+        android:src="@drawable/variant_availability_indicator"
+        android:layout_width="5dp"
+        android:layout_height="5dp"
+        android:layout_gravity="bottom|end"/>
 </FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
new file mode 100644
index 0000000..d61780e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+<GridLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/variant_popup"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    style="@style/EmojiPickerStylePopupViewHolder">
+</GridLayout>
\ No newline at end of file
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/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml b/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml
new file mode 100644
index 0000000..fb08439
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+
+<resources>
+    <color name="light_skin_tone">#edbd82</color>
+    <color name="medium_light_skin_tone">#ba8f63</color>
+    <color name="medium_skin_tone">#91674d</color>
+    <color name="medium_dark_skin_tone">#875334</color>
+    <color name="dark_skin_tone">#4a2f27</color>
+</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index cc79c54..17afb30 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -24,4 +24,12 @@
     <dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
     <dimen name="emoji_picker_header_height">50dp</dimen>
     <dimen name="emoji_picker_header_padding">8dp</dimen>
+    <dimen name="variant_availability_indicator_height">5dp</dimen>
+    <dimen name="variant_availability_indicator_width">5dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_vertical">3dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_start">3dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_padding_end">5dp</dimen>
+    <dimen name="emoji_picker_popup_view_elevation">2dp</dimen>
+    <dimen name="emoji_picker_popup_view_holder_corner_radius">10dp</dimen>
+    <dimen name="emoji_picker_skin_tone_circle_radius">6dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index 0e6d24e..f7df2be 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -31,4 +31,18 @@
     <style name="EmojiPickerStyleCategoryLabelText">
         <item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
     </style>
+
+    <style name="EmojiPickerStylePopupViewHolder">
+        <item name="android:padding">@dimen/emoji_picker_popup_view_holder_padding_vertical</item>
+        <item name="android:paddingStart">@dimen/emoji_picker_popup_view_holder_padding_start</item>
+        <item name="android:paddingEnd">@dimen/emoji_picker_popup_view_holder_padding_end</item>
+        <item name="android:background">@drawable/popup_view_rounded_background</item>
+        <item name="android:elevation">@dimen/emoji_picker_popup_view_elevation</item>
+    </style>
+
+    <style name="VariantPopupAnimation">
+        <item name="android:windowEnterAnimation">@anim/slide_up_and_fade_in</item>
+        <item name="android:windowExitAnimation">@anim/slide_down_and_fade_out</item>
+    </style>
+
 </resources>
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp b/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
index 35a8dfc..e712d0d 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
Binary files differ
diff --git a/gradle.properties b/gradle.properties
index c6660ac..c2082a6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,8 @@
 android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline
 # Workaround for b/162074215
 android.includeDependencyInfoInApks=false
+# Allow multiple r8 tasks at once because otherwise they can make the critical path longer: b/256187923
+android.r8.maxWorkers=2
 
 kotlin.stdlib.default.dependency=false
 # mac targets cannot be built on linux, suppress the warning.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index de0f8fc..d7c6e4d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -35,7 +35,7 @@
 incap = "0.2"
 jcodec = "0.2.5"
 kotlin = "1.7.21"
-kotlinBenchmark = "0.4.5"
+kotlinBenchmark = "0.4.6"
 kotlinNative = "1.7.21"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.6.4"
@@ -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/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 764b8b2e..25322cf 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -809,17 +809,17 @@
             <sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
-         <artifact name="kotlinx-benchmark-plugin-0.4.5.jar">
-            <sha256 value="64042e32a7c23040980d8b7cb9c2dc2281bf6e19da511d054639f44ba5eaac66" origin="Generated by Gradle because artifact wasn't signed"/>
+      <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.6">
+         <artifact name="kotlinx-benchmark-plugin-0.4.6.jar">
+            <sha256 value="9806179c613658456da5a53cb945138ee0983ecccc9185de22ece2be9c14cfc7" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
-         <artifact name="kotlinx-benchmark-plugin-0.4.5.pom">
-            <sha256 value="33a06eec241b0a20bc3f37b32574d1aaed34905005daba892a85cd832de14700" origin="Generated by Gradle because artifact wasn't signed"/>
+         <artifact name="kotlinx-benchmark-plugin-0.4.6.module">
+            <sha256 value="b2bcac512573b085113a2ad74ed339b7cdefc49094b342a463b5a51b07ef5e1e" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
-      <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
-         <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.5.pom">
-            <sha256 value="b13fc9975c2f5cfb992f423c60be042252a289e0f40390e7da2122ac2dec08a5" origin="Generated by Gradle because artifact wasn't signed"/>
+      <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.6">
+         <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.6.pom">
+            <sha256 value="3f739c3504155f5568c26c5c013aa62dc254daa7fe627d7b43b3448c79d36447" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
       <component group="org.ow2" name="ow2" version="1.5" androidx:reason="https://gitlab.ow2.org/asm/asm/-/merge_requests/354">
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 0349123..b7feac7 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -281,6 +281,18 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
+  public final class BodyWaterMassRecord implements androidx.health.connect.client.records.Record {
+    ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final androidx.health.connect.client.units.Mass mass;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
     ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
     property public final java.time.Instant time;
   }
 
+  public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.Record {
+    ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public double getHeartRateVariabilityMillis();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final double heartRateVariabilityMillis;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.Record {
     ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
   public static final class HeightRecord.Companion {
   }
 
-  public final class HipCircumferenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class HydrationRecord implements androidx.health.connect.client.records.Record {
     ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1213,18 +1225,6 @@
   public static final class Vo2MaxRecord.Companion {
   }
 
-  public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class WeightRecord implements androidx.health.connect.client.records.Record {
     ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 0349123..b7feac7 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -281,6 +281,18 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
+  public final class BodyWaterMassRecord implements androidx.health.connect.client.records.Record {
+    ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final androidx.health.connect.client.units.Mass mass;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
     ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
     property public final java.time.Instant time;
   }
 
+  public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.Record {
+    ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public double getHeartRateVariabilityMillis();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final double heartRateVariabilityMillis;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.Record {
     ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
   public static final class HeightRecord.Companion {
   }
 
-  public final class HipCircumferenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class HydrationRecord implements androidx.health.connect.client.records.Record {
     ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1213,18 +1225,6 @@
   public static final class Vo2MaxRecord.Companion {
   }
 
-  public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class WeightRecord implements androidx.health.connect.client.records.Record {
     ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 3be3faa..372b86f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -281,6 +281,18 @@
     property public java.time.ZoneOffset? zoneOffset;
   }
 
+  public final class BodyWaterMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
+    ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public androidx.health.connect.client.units.Mass getMass();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final androidx.health.connect.client.units.Mass mass;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class BoneMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
     ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
     property public final java.time.Instant time;
   }
 
+  public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.InstantaneousRecord {
+    ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public double getHeartRateVariabilityMillis();
+    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+    method public java.time.Instant getTime();
+    method public java.time.ZoneOffset? getZoneOffset();
+    property public final double heartRateVariabilityMillis;
+    property public androidx.health.connect.client.records.metadata.Metadata metadata;
+    property public java.time.Instant time;
+    property public java.time.ZoneOffset? zoneOffset;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
     ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
   public static final class HeightRecord.Companion {
   }
 
-  public final class HipCircumferenceRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class HydrationRecord implements androidx.health.connect.client.records.IntervalRecord {
     ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1236,18 +1248,6 @@
   public static final class Vo2MaxRecord.Companion {
   }
 
-  public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public androidx.health.connect.client.units.Length getCircumference();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getTime();
-    method public java.time.ZoneOffset? getZoneOffset();
-    property public final androidx.health.connect.client.units.Length circumference;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant time;
-    property public java.time.ZoneOffset? zoneOffset;
-  }
-
   public final class WeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
     ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
index c46985a..787b426 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
@@ -15,7 +15,6 @@
  */
 package androidx.health.connect.client.records
 
-import androidx.annotation.RestrictTo
 import androidx.health.connect.client.records.metadata.Metadata
 import androidx.health.connect.client.units.Mass
 import androidx.health.connect.client.units.kilograms
@@ -25,7 +24,6 @@
 /**
  * Captures the user's body water mass. Each record represents a single instantaneous measurement.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class BodyWaterMassRecord(
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
index a5379ee..4728b73 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
@@ -15,7 +15,6 @@
  */
 package androidx.health.connect.client.records
 
-import androidx.annotation.RestrictTo
 import androidx.health.connect.client.records.metadata.Metadata
 import java.time.Instant
 import java.time.ZoneOffset
@@ -23,10 +22,7 @@
 /**
  * Captures user's heart rate variability (HRV) as measured by the root mean square of successive
  * differences (RMSSD) between normal heartbeats.
- *
- * @suppress
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class HeartRateVariabilityRmssdRecord(
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
index a261871..bda2aaf 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
@@ -15,6 +15,7 @@
  */
 package androidx.health.connect.client.records
 
+import androidx.annotation.RestrictTo
 import androidx.health.connect.client.records.metadata.Metadata
 import androidx.health.connect.client.units.Length
 import androidx.health.connect.client.units.meters
@@ -22,6 +23,7 @@
 import java.time.ZoneOffset
 
 /** Captures the user's hip circumference. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class HipCircumferenceRecord(
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
index b04a2c4..c8e6244 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
@@ -15,6 +15,7 @@
  */
 package androidx.health.connect.client.records
 
+import androidx.annotation.RestrictTo
 import androidx.health.connect.client.records.metadata.Metadata
 import androidx.health.connect.client.units.Length
 import androidx.health.connect.client.units.meters
@@ -26,6 +27,7 @@
  *
  * See [Length] for supported units.
  */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class WaistCircumferenceRecord(
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt
new file mode 100644
index 0000000..2de499a
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.health.services.client.data
+
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableSet
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseCapabilitiesTest {
+    @Test
+    fun return_supportedDataTypesForSpecifiedExercise() {
+        assertThat(
+            EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+                ExerciseType.WALKING
+            ).supportedDataTypes
+        ).isEqualTo(
+            ImmutableSet.of(DataType.STEPS)
+        )
+    }
+
+    @Test
+    fun supportedGoalsForExercise() {
+        assertThat(
+            EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+                ExerciseType.RUNNING
+            ).supportedGoals
+        ).isEqualTo(
+            EXERCISE_CAPABILITIES.typeToCapabilities.get(ExerciseType.RUNNING)!!.supportedGoals
+        )
+    }
+
+    @Test
+    fun supportedMilestonesForExercise() {
+        assertThat(
+            EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+                ExerciseType.RUNNING).supportedMilestones
+        ).isEqualTo(
+            EXERCISE_CAPABILITIES.typeToCapabilities.get(ExerciseType.RUNNING)!!.supportedMilestones
+        )
+    }
+
+    @Test
+    fun exercisesSupportingAutoResumeAndPause_returnCorrectSet() {
+        val supportsAutoPauseAndResume = ExerciseTypeCapabilities(
+            supportedDataTypes = ImmutableSet.of(),
+            supportedGoals = ImmutableMap.of(),
+            supportedMilestones = ImmutableMap.of(),
+            supportsAutoPauseAndResume = true,
+        )
+        val doesNotSupportAutoPauseAndResume = ExerciseTypeCapabilities(
+            supportedDataTypes = ImmutableSet.of(),
+            supportedGoals = ImmutableMap.of(),
+            supportedMilestones = ImmutableMap.of(),
+            supportsAutoPauseAndResume = false,
+        )
+        val exerciseCapabilities = ExerciseCapabilities(
+            ImmutableMap.of(
+                ExerciseType.WALKING, supportsAutoPauseAndResume,
+                ExerciseType.RUNNING, doesNotSupportAutoPauseAndResume
+            )
+        )
+
+        assertThat(exerciseCapabilities.autoPauseAndResumeEnabledExercises)
+            .containsExactly(ExerciseType.WALKING)
+    }
+
+    @Test
+    fun parcelable_roundTrip_returnsOriginalCapabilities() {
+        val proto = EXERCISE_CAPABILITIES.proto
+        val capabilities = ExerciseCapabilities(proto)
+
+        assertThat(capabilities.supportedExerciseTypes).containsExactlyElementsIn(
+            EXERCISE_CAPABILITIES.supportedExerciseTypes
+        )
+        assertThat(capabilities.autoPauseAndResumeEnabledExercises).containsExactlyElementsIn(
+            EXERCISE_CAPABILITIES.autoPauseAndResumeEnabledExercises
+        )
+    }
+
+    @Test
+    fun parcelable_roundTrip_returnsEmptyOriginalCapabilities() {
+        val emptyCapabilities = ExerciseCapabilities(ImmutableMap.of())
+        val roundTripEmptyCapabilities = ExerciseCapabilities(emptyCapabilities.proto)
+
+        assertThat(emptyCapabilities.supportedExerciseTypes).containsExactlyElementsIn(
+            roundTripEmptyCapabilities.supportedExerciseTypes
+        )
+        assertThat(emptyCapabilities.autoPauseAndResumeEnabledExercises).containsExactlyElementsIn(
+            roundTripEmptyCapabilities.autoPauseAndResumeEnabledExercises
+        )
+    }
+
+    companion object {
+        private val WALKING_CAPABILITIES = ExerciseTypeCapabilities(
+            supportedDataTypes = ImmutableSet.of(DataType.STEPS),
+            supportedGoals = ImmutableMap.of(
+                DataType.STEPS_TOTAL,
+                ImmutableSet.of(ComparisonType.GREATER_THAN)
+            ),
+            supportedMilestones = ImmutableMap.of(
+                DataType.STEPS_TOTAL,
+                ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+            ),
+            supportsAutoPauseAndResume = false,
+        )
+        private val RUNNING_CAPABILITIES = ExerciseTypeCapabilities(
+            supportedDataTypes = ImmutableSet.of(DataType.HEART_RATE_BPM, DataType.SPEED),
+            supportedGoals = ImmutableMap.of(
+                DataType.HEART_RATE_BPM_STATS,
+                ImmutableSet.of(ComparisonType.GREATER_THAN, ComparisonType.LESS_THAN),
+                DataType.SPEED_STATS,
+                ImmutableSet.of(ComparisonType.LESS_THAN)
+            ),
+            supportedMilestones = ImmutableMap.of(
+                DataType.HEART_RATE_BPM_STATS,
+                ImmutableSet.of(ComparisonType.GREATER_THAN_OR_EQUAL),
+                DataType.SPEED_STATS,
+                ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+            ),
+            supportsAutoPauseAndResume = true,
+        )
+
+        private val SWIMMING_CAPABILITIES = ExerciseTypeCapabilities(
+            supportedDataTypes = emptySet(),
+            supportedGoals = emptyMap(),
+            supportedMilestones = emptyMap(),
+            supportsAutoPauseAndResume = true,
+        )
+
+        private val EXERCISE_TYPE_TO_EXERCISE_CAPABILITIES_MAPPING =
+            ImmutableMap.of(
+                ExerciseType.WALKING, WALKING_CAPABILITIES,
+                ExerciseType.RUNNING, RUNNING_CAPABILITIES,
+                ExerciseType.SWIMMING_POOL, SWIMMING_CAPABILITIES
+            )
+
+        private val EXERCISE_CAPABILITIES: ExerciseCapabilities =
+            ExerciseCapabilities(EXERCISE_TYPE_TO_EXERCISE_CAPABILITIES_MAPPING)
+    }
+}
\ No newline at end of file
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/lifecycle/integration-tests/incrementality/build.gradle b/lifecycle/integration-tests/incrementality/build.gradle
index f048361..21dcdf0 100644
--- a/lifecycle/integration-tests/incrementality/build.gradle
+++ b/lifecycle/integration-tests/incrementality/build.gradle
@@ -32,7 +32,11 @@
 SdkResourceGenerator.generateForHostTest(project)
 
 // lifecycle-common and annotation are the dependencies of lifecycle-compiler
-tasks.findByPath("test").dependsOn(tasks.findByPath(":lifecycle:lifecycle-compiler:publish"),
-        tasks.findByPath(":lifecycle:lifecycle-common:publish"),
-        tasks.findByPath(":lifecycle:lifecycle-runtime:publish"),
-        tasks.findByPath(":annotation:annotation:publish"))
+tasks.findByPath("test").configure {
+    dependsOn(":lifecycle:lifecycle-compiler:publish")
+    dependsOn(":lifecycle:lifecycle-common:publish")
+    dependsOn(":lifecycle:lifecycle-runtime:publish")
+    dependsOn(":annotation:annotation:publish")
+    dependsOn(":arch:core:core-common:publish")
+    dependsOn(":arch:core:core-runtime:publish")
+}
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
index cda538c..87799dd 100644
--- a/lifecycle/settings.gradle
+++ b/lifecycle/settings.gradle
@@ -29,6 +29,8 @@
     selectProjectsFromAndroidX({ name ->
         if (name.startsWith(":lifecycle")) return true
         if (name.startsWith(":annotation")) return true
+        if (name.startsWith(":arch:core:core-common")) return true
+        if (name.startsWith(":arch:core:core-runtime")) return true
         if (name == ":internal-testutils-runtime") return true
         if (name == ":internal-testutils-truth") return true
         if (isNeededForComposePlayground(name)) return true
diff --git a/navigation/settings.gradle b/navigation/settings.gradle
index 1850a6f..ad3248f 100644
--- a/navigation/settings.gradle
+++ b/navigation/settings.gradle
@@ -31,7 +31,7 @@
         if (name.startsWith(":navigation")) return true
         if (name.startsWith(":annotation")) return true
         if (name == ":compose:integration-tests:demos:common") return true
-        if (name.startsWith(":lifecycle")) return true
+        if (name.startsWith(":lifecycle") && !name.contains("integration-tests")) return true
         if (name.startsWith(":savedstate")) return true
         if (name == ":internal-testutils-navigation") return true
         if (name == ":internal-testutils-runtime") return true
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index 06fc710..4606a745 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -36,7 +36,7 @@
 ) : SymbolProcessor {
     companion object {
         const val AIDL_COMPILER_PATH_OPTIONS_KEY = "aidl_compiler_path"
-        const val USE_COMPAT_LIBRARY_OPTIONS_KEY = "use_sdk_runtime_compat_library"
+        const val SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY = "skip_sdk_runtime_compat_library"
     }
 
     override fun process(resolver: Resolver): List<KSAnnotated> {
@@ -56,9 +56,11 @@
             return emptyList()
         }
 
-        val target = if (options[USE_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase() == "true") {
-            SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
-        } else SandboxApiVersion.API_33
+        val skipCompatLibrary =
+            options[SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase().toBoolean()
+        val target = if (skipCompatLibrary) {
+            SandboxApiVersion.API_33
+        } else SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
 
         val parsedApi = ApiParser(resolver, logger).parseApi()
 
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/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 23fb1b7..5782e08 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -39,8 +39,9 @@
 
 /** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
 class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
-    private val interfaceParser = InterfaceParser(logger)
-    private val valueParser = ValueParser(logger)
+    private val typeParser = TypeParser(logger)
+    private val interfaceParser = InterfaceParser(logger, typeParser)
+    private val valueParser = ValueParser(logger, typeParser)
 
     fun parseApi(): ParsedApi {
         val services = parseAllServices()
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
deleted file mode 100644
index 2c0f555..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
+++ /dev/null
@@ -1,29 +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.privacysandbox.tools.apicompiler.parser
-
-import androidx.privacysandbox.tools.core.model.Type
-import com.google.devtools.ksp.symbol.KSDeclaration
-
-internal object Converters {
-    fun typeFromDeclaration(declaration: KSDeclaration): Type {
-        return Type(
-            packageName = declaration.packageName.getFullName(),
-            simpleName = declaration.simpleName.getShortName(),
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index 79c69c9..cb2b9d0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -19,7 +19,6 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.Method
 import androidx.privacysandbox.tools.core.model.Parameter
-import androidx.privacysandbox.tools.core.model.Type
 import com.google.devtools.ksp.getDeclaredFunctions
 import com.google.devtools.ksp.getDeclaredProperties
 import com.google.devtools.ksp.isPublic
@@ -27,12 +26,10 @@
 import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSValueParameter
 import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
 
-internal class InterfaceParser(private val logger: KSPLogger) {
+internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
     private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
     private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
 
@@ -47,13 +44,15 @@
         }
         if (interfaceDeclaration.getDeclaredProperties().any()) {
             logger.error(
-                "Error in $name: annotated interfaces cannot declare properties.")
+                "Error in $name: annotated interfaces cannot declare properties."
+            )
         }
         if (interfaceDeclaration.declarations.filterIsInstance<KSClassDeclaration>()
                 .any(KSClassDeclaration::isCompanionObject)
         ) {
             logger.error(
-                "Error in $name: annotated interfaces cannot declare companion objects.")
+                "Error in $name: annotated interfaces cannot declare companion objects."
+            )
         }
         val invalidModifiers =
             interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
@@ -75,7 +74,7 @@
 
         val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
         return AnnotatedInterface(
-            type = Converters.typeFromDeclaration(interfaceDeclaration),
+            type = typeParser.parseFromDeclaration(interfaceDeclaration),
             methods = methods,
         )
     }
@@ -102,7 +101,7 @@
         if (method.returnType == null) {
             logger.error("Error in $name: failed to resolve return type.")
         }
-        val returnType = parseType(method, method.returnType!!.resolve())
+        val returnType = typeParser.parseFromTypeReference(method.returnType!!, name)
 
         val parameters =
             method.parameters.map { parameter -> parseParameter(method, parameter) }.toList()
@@ -128,15 +127,7 @@
 
         return Parameter(
             name = parameter.name!!.getFullName(),
-            type = parseType(method, parameter.type.resolve()),
+            type = typeParser.parseFromTypeReference(parameter.type, name),
         )
     }
-
-    private fun parseType(method: KSFunctionDeclaration, type: KSType): Type {
-        val name = method.qualifiedName?.getFullName() ?: method.simpleName.getFullName()
-        if (type.nullability == Nullability.NULLABLE) {
-            logger.error("Error in $name: nullable types are not supported.")
-        }
-        return Converters.typeFromDeclaration(type.declaration)
-    }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
new file mode 100644
index 0000000..38a96b6
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.core.model.Type
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
+import com.google.devtools.ksp.symbol.Variance
+
+internal class TypeParser(private val logger: KSPLogger) {
+    fun parseFromDeclaration(declaration: KSDeclaration): Type {
+        return Type(
+            packageName = declaration.packageName.getFullName(),
+            simpleName = declaration.simpleName.getShortName(),
+        )
+    }
+
+    fun parseFromTypeReference(typeReference: KSTypeReference, debugName: String): Type {
+        val resolvedType = typeReference.resolve()
+        if (resolvedType.isError) {
+            logger.error("Failed to resolve type for $debugName.")
+        }
+        if (resolvedType.nullability == Nullability.NULLABLE) {
+            logger.error("Error in $debugName: nullable types are not supported.")
+        }
+        val typeArguments = typeReference.element?.typeArguments?.mapNotNull {
+            if (it.type == null) {
+                logger.error("Error in $debugName: null type argument.")
+            }
+            if (it.variance != Variance.INVARIANT) {
+                logger.error("Error in $debugName: only invariant type arguments are supported.")
+            }
+            it.type
+        } ?: emptyList()
+        return Type(
+            packageName = resolvedType.declaration.packageName.getFullName(),
+            simpleName = resolvedType.declaration.simpleName.getShortName(),
+            typeParameters = typeArguments.map { parseFromTypeReference(it, debugName) },
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 39d8a26..64f9c6b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -25,13 +25,13 @@
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
 
-internal class ValueParser(private val logger: KSPLogger) {
+internal class ValueParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
     fun parseValue(value: KSAnnotated): AnnotatedValue? {
         if (value !is KSClassDeclaration ||
             value.classKind != ClassKind.CLASS ||
-            !value.modifiers.contains(Modifier.DATA)) {
+            !value.modifiers.contains(Modifier.DATA)
+        ) {
             logger.error(
                 "Only data classes can be annotated with @PrivacySandboxValue."
             )
@@ -61,7 +61,7 @@
         }
 
         return AnnotatedValue(
-            type = Converters.typeFromDeclaration(value),
+            type = typeParser.parseFromDeclaration(value),
             properties = value.getAllProperties().map(::parseProperty).toList()
         )
     }
@@ -71,17 +71,9 @@
         if (property.isMutable) {
             logger.error("Error in $name: properties cannot be mutable.")
         }
-        val type = property.type.resolve()
-        if (type.isError) {
-            logger.error("Failed to resolve type for property $name.")
-        }
-        if (type.nullability == Nullability.NULLABLE) {
-            logger.error("Error in $name: nullable types are not supported.")
-        }
-
         return ValueProperty(
             name = property.simpleName.getShortName(),
-            type = Converters.typeFromDeclaration(type.declaration),
+            type = typeParser.parseFromTypeReference(property.type, name),
         )
     }
 }
\ 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 78f636c..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
@@ -34,7 +34,11 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(inputSources)
+        val result = compileWithPrivacySandboxKspCompiler(
+            inputSources,
+            platformStubs = PlatformStubs.API_33,
+            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+        )
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
@@ -48,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/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
index cda0661..1ef27b0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
@@ -32,11 +32,7 @@
         val inputSources = loadSourcesFromDirectory(inputTestDataDir)
         val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
 
-        val result = compileWithPrivacySandboxKspCompiler(
-            inputSources,
-            platformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
-            extraProcessorOptions = mapOf("use_sdk_runtime_compat_library" to "true")
-        )
+        val result = compileWithPrivacySandboxKspCompiler(inputSources)
         assertThat(result).succeeds()
 
         val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index 5de917b..e6b6e3d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
  */
 fun compileWithPrivacySandboxKspCompiler(
     sources: List<Source>,
-    platformStubs: PlatformStubs = PlatformStubs.API_33,
+    platformStubs: PlatformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
     extraProcessorOptions: Map<String, String> = mapOf(),
 ): TestCompilationResult {
     val provider = PrivacySandboxKspCompiler.Provider()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index afda28e..f37b09bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -43,6 +43,7 @@
                     @PrivacySandboxService
                     interface MySdk {
                         suspend fun doStuff(x: Int, y: Int): String
+                        suspend fun processList(list: List<Int>): List<String>
                         fun doMoreStuff()
                     }
                 """,
@@ -66,6 +67,17 @@
                                 ),
                                 returnType = Types.string,
                                 isSuspend = true,
+                            ),
+                            Method(
+                                name = "processList",
+                                parameters = listOf(
+                                    Parameter(
+                                        name = "list",
+                                        type = Types.list(Types.int),
+                                    )
+                                ),
+                                returnType = Types.list(Types.string),
+                                isSuspend = true,
                             ), Method(
                                 name = "doMoreStuff",
                                 parameters = listOf(),
@@ -220,9 +232,18 @@
     fun parameterWithGenerics_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: MutableList<Int>)"))
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
-                    "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
-                    "or @PrivacySandboxInterface are supported as parameter types."
+                "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+                    "with @PrivacySandboxValue and interfaces annotated with " +
+                    "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                    "parameter types."
+            )
+    }
+
+    @Test
+    fun listParameterWithNonInvariantTypeArgument_fails() {
+        checkSourceFails(serviceMethod("suspend fun foo(x: List<in Int>)"))
+            .containsExactlyErrors(
+                "Error in com.mysdk.MySdk.foo: only invariant type arguments are supported."
             )
     }
 
@@ -230,9 +251,10 @@
     fun parameterLambda_fails() {
         checkSourceFails(serviceMethod("suspend fun foo(x: (Int) -> Int)"))
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
-                    "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
-                    "or @PrivacySandboxInterface are supported as parameter types."
+                "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+                    "with @PrivacySandboxValue and interfaces annotated with " +
+                    "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                    "parameter types."
             )
     }
 
@@ -251,7 +273,7 @@
                 """
         )
         checkSourceFails(source).containsExactlyErrors(
-            "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
+            "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated with " +
                 "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
                 "supported as return types."
         )
@@ -361,12 +383,14 @@
                                         name = "request",
                                         type = Type(
                                             packageName = "com.mysdk",
-                                            simpleName = "MyInterface"),
+                                            simpleName = "MyInterface"
+                                        ),
                                     )
                                 ),
                                 returnType = Type(
                                     packageName = "com.mysdk",
-                                    simpleName = "MyInterface"),
+                                    simpleName = "MyInterface"
+                                ),
                                 isSuspend = true,
                             ),
                         )
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 5dd00bf..132a95a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -52,7 +52,7 @@
                     data class MySdkResponse(
                         val magicPayload: MagicPayload, val isTrulyMagic: Boolean)
                     @PrivacySandboxValue
-                    data class MagicPayload(val magicNumber: Long)
+                    data class MagicPayload(val magicList: List<Long>)
                 """
         )
         assertThat(parseSources(source)).isEqualTo(
@@ -92,9 +92,7 @@
                     ),
                     AnnotatedValue(
                         type = Type(packageName = "com.mysdk", simpleName = "MagicPayload"),
-                        properties = listOf(
-                            ValueProperty("magicNumber", Types.long),
-                        )
+                        properties = listOf(ValueProperty("magicList", Types.list(Types.long)))
                     ),
                 )
             )
@@ -178,7 +176,7 @@
         )
         checkSourceFails(dataClass)
             .containsExactlyErrors(
-                "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+                "Error in com.mysdk.MySdkRequest.foo: only primitives, lists, data classes " +
                     "annotated with @PrivacySandboxValue and interfaces annotated with " +
                     "@PrivacySandboxInterface are supported as properties."
             )
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/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 1db70b6..e588bbd 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -109,10 +109,14 @@
         if (classifier !is KmClassifier.Class) {
             throw PrivacySandboxParsingException("Unsupported type in API description: $type")
         }
-        return parseClassName(classifier.name)
+        val typeArguments = type.arguments.map { parseType(it.type!!) }
+        return parseClassName(classifier.name, typeArguments)
     }
 
-    private fun parseClassName(className: ClassName): Type {
+    private fun parseClassName(
+        className: ClassName,
+        typeArguments: List<Type> = emptyList()
+    ): Type {
         // Package names are separated with slashes and nested classes are separated with dots.
         // (e.g com/example/OuterClass.InnerClass).
         val (packageName, simpleName) = className.split('/').run {
@@ -126,7 +130,7 @@
             )
         }
 
-        return Type(packageName, simpleName)
+        return Type(packageName, simpleName, typeArguments)
     }
 
     private fun validate(api: ParsedApi) {
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/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index cc006e8..bb3c24f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -49,6 +49,7 @@
                       fun doSomething(magicNumber: Int, awesomeString: String)
                       suspend fun getPayload(request: PayloadRequest): PayloadResponse
                       suspend fun getInterface(): MyInterface
+                      suspend fun processList(list: List<Long>): List<Long>
                     }
                     @PrivacySandboxInterface
                     interface MyInterface {
@@ -64,7 +65,7 @@
                     interface CustomCallback {
                       fun onComplete(status: Int)
                     }
-                """
+                """,
         )
 
         val expectedPayloadType = AnnotatedValue(
@@ -118,7 +119,18 @@
                         ),
                         returnType = expectedPayloadResponse.type,
                         isSuspend = true,
-                    )
+                    ),
+                    Method(
+                        name = "processList",
+                        parameters = listOf(
+                            Parameter(
+                                "list",
+                                Types.list(Types.long)
+                            )
+                        ),
+                        returnType = Types.list(Types.long),
+                        isSuspend = true,
+                    ),
                 )
             )
         val expectedInterface =
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/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index 467d8ac..0ee111d 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -41,4 +41,9 @@
     val char = Type(packageName = "kotlin", simpleName = "Char")
     val short = Type(packageName = "kotlin", simpleName = "Short")
     val primitiveTypes = setOf(unit, boolean, int, long, float, double, string, char, short)
+    fun list(elementType: Type) = Type(
+        packageName = "kotlin.collections",
+        simpleName = "List",
+        typeParameters = listOf(elementType)
+    )
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
index 71e5cd2..3442e0f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
@@ -19,6 +19,7 @@
 data class Type(
   val packageName: String,
   val simpleName: String,
+  val typeParameters: List<Type> = emptyList(),
 ) {
   val qualifiedName = "$packageName.$simpleName"
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index 0f6e3f8..3b5f012 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -19,9 +19,14 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
 import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
 import androidx.privacysandbox.tools.core.model.Types
 
 class ModelValidator private constructor(val api: ParsedApi) {
+    private val values = api.values.map(AnnotatedValue::type)
+    private val interfaces = api.interfaces.map(AnnotatedInterface::type)
+    private val callbacks = api.callbacks.map(AnnotatedInterface::type)
+
     private val errors: MutableList<String> = mutableListOf()
 
     companion object {
@@ -61,33 +66,24 @@
     }
 
     private fun validateServiceAndInterfaceMethods() {
-        val allowedParameterTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.callbacks.map(AnnotatedInterface::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-        val allowedReturnValueTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         val annotatedInterfaces = api.services + api.interfaces
         for (annotatedInterface in annotatedInterfaces) {
             for (method in annotatedInterface.methods) {
-                if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+                if (method.parameters.any { !(isValidInterfaceParameterType(it.type)) }) {
                     errors.add(
                         "Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with @PrivacySandboxValue " +
-                            "and interfaces annotated with @PrivacySandboxCallback or " +
-                            "@PrivacySandboxInterface are supported as parameter types."
+                            "only primitives, lists, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxCallback or @PrivacySandboxInterface are supported " +
+                            "as parameter types."
                     )
                 }
-                if (!allowedReturnValueTypes.contains(method.returnType)) {
+                if (!isValidInterfaceReturnType(method.returnType)) {
                     errors.add(
                         "Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with @PrivacySandboxValue " +
-                            "and interfaces annotated with @PrivacySandboxInterface are " +
-                            "supported as return types."
+                            "only primitives, lists, data classes annotated with " +
+                            "@PrivacySandboxValue and interfaces annotated with " +
+                            "@PrivacySandboxInterface are supported as return types."
                     )
                 }
             }
@@ -95,17 +91,12 @@
     }
 
     private fun validateValuePropertyTypes() {
-        val allowedValuePropertyTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         for (value in api.values) {
             for (property in value.properties) {
-                if (!allowedValuePropertyTypes.contains(property.type)) {
+                if (!isValidValuePropertyType(property.type)) {
                     errors.add(
                         "Error in ${value.type.qualifiedName}.${property.name}: " +
-                            "only primitives, data classes annotated with " +
+                            "only primitives, lists, data classes annotated with " +
                             "@PrivacySandboxValue and interfaces annotated with " +
                             "@PrivacySandboxInterface are supported as properties."
                     )
@@ -115,17 +106,12 @@
     }
 
     private fun validateCallbackMethods() {
-        val allowedParameterTypes =
-            (api.values.map(AnnotatedValue::type) +
-                api.interfaces.map(AnnotatedInterface::type) +
-                Types.primitiveTypes).toSet()
-
         for (callback in api.callbacks) {
             for (method in callback.methods) {
-                if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+                if (method.parameters.any { !isValidCallbackParameterType(it.type) }) {
                     errors.add(
                         "Error in ${callback.type.qualifiedName}.${method.name}: " +
-                            "only primitives, data classes annotated with " +
+                            "only primitives, lists, data classes annotated with " +
                             "@PrivacySandboxValue and interfaces annotated with " +
                             "@PrivacySandboxInterface are supported as callback parameter types."
                     )
@@ -139,6 +125,29 @@
             }
         }
     }
+
+    private fun isValidInterfaceParameterType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) || isCallback(type)
+    private fun isValidInterfaceReturnType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+    private fun isValidValuePropertyType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+    private fun isValidCallbackParameterType(type: Type) =
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+
+    private fun isValue(type: Type) = values.contains(type)
+    private fun isInterface(type: Type) = interfaces.contains(type)
+    private fun isCallback(type: Type) = callbacks.contains(type)
+    private fun isPrimitive(type: Type) = Types.primitiveTypes.contains(type)
+    private fun isList(type: Type): Boolean {
+        if (type.qualifiedName == "kotlin.collections.List") {
+            require(type.typeParameters.size == 1) {
+                "List type should have one type parameter, found ${type.typeParameters}."
+            }
+            return type.typeParameters[0].let { isValue(it) || isPrimitive(it) }
+        }
+        return false
+    }
 }
 
 data class ValidationResult(val errors: List<String>) {
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/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index c3a654d..d13fd0e 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -239,12 +239,45 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdk.returnFoo: only primitives, data classes annotated with " +
-                "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
-                "supported as return types.",
-            "Error in com.mysdk.MySdk.receiveFoo: only primitives, data classes annotated with " +
-                "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback or " +
-                "@PrivacySandboxInterface are supported as parameter types."
+            "Error in com.mysdk.MySdk.returnFoo: only primitives, lists, data classes annotated " +
+                "with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxInterface are supported as return types.",
+            "Error in com.mysdk.MySdk.receiveFoo: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as parameter " +
+                "types."
+        )
+    }
+
+    @Test
+    fun nestedList_throws() {
+        val api = ParsedApi(
+            services = setOf(
+                AnnotatedInterface(
+                    type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+                    methods = listOf(
+                        Method(
+                            name = "processNestedList",
+                            parameters = listOf(
+                                Parameter(
+                                    name = "foo",
+                                    type = Types.list(Types.list(Types.int))
+                                )
+                            ),
+                            returnType = Types.unit,
+                            isSuspend = true,
+                        ),
+                    ),
+                ),
+            )
+        )
+        val validationResult = ModelValidator.validate(api)
+        assertThat(validationResult.isFailure).isTrue()
+        assertThat(validationResult.errors).containsExactly(
+            "Error in com.mysdk.MySdk.processNestedList: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
+                "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+                "parameter types."
         )
     }
 
@@ -266,7 +299,7 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+            "Error in com.mysdk.Foo.bar: only primitives, lists, data classes annotated with " +
                 "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
                 "are supported as properties."
         )
@@ -333,8 +366,8 @@
         val validationResult = ModelValidator.validate(api)
         assertThat(validationResult.isFailure).isTrue()
         assertThat(validationResult.errors).containsExactly(
-            "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
-                "with @PrivacySandboxValue and interfaces annotated with " +
+            "Error in com.mysdk.MySdkCallback.foo: only primitives, lists, data classes " +
+                "annotated with @PrivacySandboxValue and interfaces annotated with " +
                 "@PrivacySandboxInterface are supported as callback parameter types."
         )
     }
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 f73b573..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
@@ -149,14 +149,44 @@
     val MUTABLE_SET = Set::class.asMutableClassName()
     val HASH_SET = XClassName.get("java.util", "HashSet")
     val STRING = String::class.asClassName()
-    val OPTIONAL = ClassName.get("java.util", "Optional")
+    val OPTIONAL = XClassName.get("java.util", "Optional")
     val UUID = XClassName.get("java.util", "UUID")
     val BYTE_BUFFER = XClassName.get("java.nio", "ByteBuffer")
     val JAVA_CLASS = XClassName.get("java.lang", "Class")
 }
 
-object GuavaBaseTypeNames {
-    val OPTIONAL = ClassName.get("com.google.common.base", "Optional")
+object GuavaTypeNames {
+    val OPTIONAL = XClassName.get("com.google.common.base", "Optional")
+    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",
+        "ImmutableList",
+        "Builder"
+    )
 }
 
 object GuavaUtilConcurrentTypeNames {
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 07f549b..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.typeName == 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()
@@ -498,7 +497,7 @@
                 typeArg = typeArg,
                 resultAdapter = SingleItemQueryResultAdapter(rowAdapter)
             )
-        } else if (typeMirror.rawType.typeName == CommonTypeNames.OPTIONAL) {
+        } else if (typeMirror.rawType.asTypeName() == CommonTypeNames.OPTIONAL) {
             // Handle java.util.Optional similarly.
             val typeArg = typeMirror.typeArguments.first()
             // use nullable when finding row adapter as non-null adapters might return
@@ -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 190b633..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
@@ -16,12 +16,10 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaBaseTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
 
 /**
  * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
@@ -36,15 +34,20 @@
         cursorVarName: String,
         scope: CodeGenScope
     ) {
-        scope.builder().apply {
+        scope.builder.apply {
             val valueVarName = scope.getTmpVar("_value")
             resultAdapter.convert(valueVarName, cursorVarName, scope)
-            addStatement(
-                "final $T $L = $T.fromNullable($L)",
-                ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, typeArg.typeName),
-                outVarName,
-                GuavaBaseTypeNames.OPTIONAL,
-                valueVarName
+            addLocalVariable(
+                name = outVarName,
+                typeName = GuavaTypeNames.OPTIONAL.parametrizedBy(
+                    typeArg.asTypeName()
+                ),
+                assignExpr = XCodeBlock.of(
+                    language = language,
+                    format = "%T.fromNullable(%L)",
+                    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 146b474..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
@@ -16,41 +16,53 @@
 
 package androidx.room.solver.query.result
 
+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.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
 import androidx.room.solver.CodeGenScope
-import com.google.common.collect.ImmutableList
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
 
 class ImmutableListQueryResultAdapter(
     private val typeArg: XType,
     private val rowAdapter: RowAdapter
 ) : QueryResultAdapter(listOf(rowAdapter)) {
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
-            val collectionType = ParameterizedTypeName
-                .get(ClassName.get(ImmutableList::class.java), typeArg.typeName)
-            val immutableListBuilderType = ParameterizedTypeName
-                .get(ClassName.get(ImmutableList.Builder::class.java), typeArg.typeName)
-            val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
-            addStatement(
-                "final $T $L = $T.<$T>builder()",
-                immutableListBuilderType, immutableListBuilderName,
-                ClassName.get(ImmutableList::class.java), typeArg.typeName
+            val collectionType = GuavaTypeNames.IMMUTABLE_LIST.parametrizedBy(
+                typeArg.asTypeName()
             )
+            val immutableListBuilderType = GuavaTypeNames
+                .IMMUTABLE_LIST_BUILDER.parametrizedBy(typeArg.asTypeName())
+            val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
+            addLocalVariable(
+                name = immutableListBuilderName,
+                typeName = immutableListBuilderType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language = language,
+                    GuavaTypeNames.IMMUTABLE_LIST_BUILDER
+                )
+            )
+
             val tmpVarName = scope.getTmpVar("_item")
-            beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", typeArg.typeName, tmpVarName)
+            beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+                addLocalVariable(
+                    name = tmpVarName,
+                    typeName = typeArg.asTypeName()
+                )
                 rowAdapter.convert(tmpVarName, cursorVarName, scope)
-                addStatement("$L.add($L)", immutableListBuilderName, tmpVarName)
+                addStatement(
+                    "%L.add(%L)",
+                    immutableListBuilderName,
+                    tmpVarName
+                )
             }
             endControlFlow()
-            addStatement(
-                "final $T $L = $L.build()",
-                collectionType, outVarName, immutableListBuilderName
+            addLocalVal(
+                name = outVarName,
+                typeName = collectionType,
+                assignExprFormat = "%L.build()",
+                immutableListBuilderName
             )
         }
     }
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/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
index 72001e2..913e44e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
@@ -16,12 +16,10 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
 
 /**
  * Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
@@ -38,15 +36,20 @@
         cursorVarName: String,
         scope: CodeGenScope
     ) {
-        scope.builder().apply {
+        scope.builder.apply {
             val valueVarName = scope.getTmpVar("_value")
             resultAdapter.convert(valueVarName, cursorVarName, scope)
-            addStatement(
-                "final $T $L = $T.ofNullable($L)",
-                ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, typeArg.typeName),
-                outVarName,
-                CommonTypeNames.OPTIONAL,
-                valueVarName
+            addLocalVariable(
+                name = outVarName,
+                typeName = CommonTypeNames.OPTIONAL.parametrizedBy(
+                    typeArg.asTypeName()
+                ),
+                assignExpr = XCodeBlock.of(
+                    language = language,
+                    format = "%T.ofNullable(%L)",
+                    CommonTypeNames.OPTIONAL,
+                    valueVarName
+                )
             )
         }
     }
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 e89a1d0..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
@@ -1221,6 +1221,93 @@
             expectedFilePath = getTestGoldenPath(testName)
         )
     }
+    @Test
+    fun queryResultAdapter_optional() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfOptional(): java.util.Optional<MyEntity>
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val other: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun queryResultAdapter_guavaOptional() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfOptional(): com.google.common.base.Optional<MyEntity>
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val other: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun queryResultAdapter_immutable_list() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+            import com.google.common.collect.ImmutableList
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfList(): ImmutableList<MyEntity>
+
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfNullableEntityList(): ImmutableList<MyEntity?>
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val other: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
 
     @Test
     fun queryResultAdapter_map() {
@@ -1281,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/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt
new file mode 100644
index 0000000..32538d8
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt
@@ -0,0 +1,56 @@
+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.base.Optional
+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 queryOfOptional(): Optional<MyEntity> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _value: MyEntity?
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _value = MyEntity(_tmpPk,_tmpOther)
+            } else {
+                _value = null
+            }
+            val _result: Optional<MyEntity> = Optional.fromNullable(_value)
+            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_immutable_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
new file mode 100644
index 0000000..18f252f
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
@@ -0,0 +1,82 @@
+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.ImmutableList
+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 queryOfList(): ImmutableList<MyEntity> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _immutableListBuilder: ImmutableList.Builder<MyEntity> = ImmutableList.Builder()
+            while (_cursor.moveToNext()) {
+                val _item: MyEntity
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _item = MyEntity(_tmpPk,_tmpOther)
+                _immutableListBuilder.add(_item)
+            }
+            val _result: ImmutableList<MyEntity> = _immutableListBuilder.build()
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun queryOfNullableEntityList(): ImmutableList<MyEntity?> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _immutableListBuilder: ImmutableList.Builder<MyEntity?> = ImmutableList.Builder()
+            while (_cursor.moveToNext()) {
+                val _item: MyEntity?
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _item = MyEntity(_tmpPk,_tmpOther)
+                _immutableListBuilder.add(_item)
+            }
+            val _result: ImmutableList<MyEntity?> = _immutableListBuilder.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/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt
new file mode 100644
index 0000000..a9f9b28
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt
@@ -0,0 +1,56 @@
+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 java.lang.Class
+import java.util.Optional
+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 queryOfOptional(): Optional<MyEntity> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _value: MyEntity?
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _value = MyEntity(_tmpPk,_tmpOther)
+            } else {
+                _value = null
+            }
+            val _result: Optional<MyEntity> = Optional.ofNullable(_value)
+            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/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -117,10 +117,10 @@
     ctor public StaleObjectException();
   }
 
-  public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
-    ctor public UiAutomatorInstrumentationTestRunner();
-    method protected android.test.AndroidTestRunner! getAndroidTestRunner();
-    method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+  @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+    ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+    method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+    method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
   }
 
   @Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
     method public void removeWatcher(String?);
     method public void resetWatcherTriggers();
     method public void runWatchers();
-    method public void setCompressedLayoutHeirarchy(boolean);
+    method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+    method public void setCompressedLayoutHierarchy(boolean);
     method public void setOrientationLeft() throws android.os.RemoteException;
     method public void setOrientationNatural() throws android.os.RemoteException;
     method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
+++ b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
@@ -117,10 +117,10 @@
     ctor public StaleObjectException();
   }
 
-  public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
-    ctor public UiAutomatorInstrumentationTestRunner();
-    method protected android.test.AndroidTestRunner! getAndroidTestRunner();
-    method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+  @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+    ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+    method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+    method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
   }
 
   @Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
     method public void removeWatcher(String?);
     method public void resetWatcherTriggers();
     method public void runWatchers();
-    method public void setCompressedLayoutHeirarchy(boolean);
+    method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+    method public void setCompressedLayoutHierarchy(boolean);
     method public void setOrientationLeft() throws android.os.RemoteException;
     method public void setOrientationNatural() throws android.os.RemoteException;
     method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -117,10 +117,10 @@
     ctor public StaleObjectException();
   }
 
-  public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
-    ctor public UiAutomatorInstrumentationTestRunner();
-    method protected android.test.AndroidTestRunner! getAndroidTestRunner();
-    method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+  @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+    ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+    method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+    method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
   }
 
   @Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
     method public void removeWatcher(String?);
     method public void resetWatcherTriggers();
     method public void runWatchers();
-    method public void setCompressedLayoutHeirarchy(boolean);
+    method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+    method public void setCompressedLayoutHierarchy(boolean);
     method public void setOrientationLeft() throws android.os.RemoteException;
     method public void setOrientationNatural() throws android.os.RemoteException;
     method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
deleted file mode 100644
index b69d9d2..0000000
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2013 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.test.uiautomator;
-
-import android.app.Instrumentation;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-
-/**
- * A wrapper around {@link Instrumentation} to provide sendStatus function
- *
- * Provided for backwards compatibility purpose. New code should use
- * {@link Instrumentation#sendStatus(int, Bundle)} instead.
- *
- */
-class InstrumentationAutomationSupport implements IAutomationSupport {
-
-    private final Instrumentation mInstrumentation;
-
-    InstrumentationAutomationSupport(Instrumentation instrumentation) {
-        mInstrumentation = instrumentation;
-    }
-
-    @Override
-    public void sendStatus(int resultCode, @NonNull Bundle status) {
-        mInstrumentation.sendStatus(resultCode, status);
-    }
-}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
index 274e7d9..cc4ea0c 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
@@ -26,7 +26,11 @@
 /**
  * Test runner for {@link UiAutomatorTestCase}s. Such tests are executed
  * on the device and have access to an applications context.
+ * @deprecated as it only handles deprecated {@link UiAutomatorTestCase}s. You should use
+ * {@link UiDevice#getInstance(Instrumentation)} from any test class as long as you have access to
+ * an {@link Instrumentation} instance.
  */
+@Deprecated
 public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner {
 
     /**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
index c3e7bf1..bbde508 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
@@ -21,6 +21,8 @@
 import android.os.SystemClock;
 import android.test.InstrumentationTestCase;
 
+import androidx.annotation.NonNull;
+
 /**
  * UI Automator test case that is executed on the device.
  * @deprecated It is no longer necessary to extend UiAutomatorTestCase. You can use
@@ -60,7 +62,10 @@
     @Deprecated
     public IAutomationSupport getAutomationSupport() {
         if (mAutomationSupport == null) {
-            mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation());
+            Instrumentation instrumentation =  getInstrumentation();
+            mAutomationSupport = (int resultCode, @NonNull Bundle status) -> {
+                instrumentation.sendStatus(resultCode, status);
+            };
         }
         return mAutomationSupport;
     }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index d2e6cbf..6dc1982 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -222,8 +222,25 @@
      * and searching the hierarchy inefficient are removed.
      *
      * @param compressed true to enable compression; else, false to disable
+     * @deprecated Typo in function name, should use {@link #setCompressedLayoutHierarchy(boolean)}
+     * instead.
      */
+    @Deprecated
     public void setCompressedLayoutHeirarchy(boolean compressed) {
+        this.setCompressedLayoutHierarchy(compressed);
+    }
+
+    /**
+     * Enables or disables layout hierarchy compression.
+     *
+     * If compression is enabled, the layout hierarchy derived from the Accessibility
+     * framework will only contain nodes that are important for uiautomator
+     * testing. Any unnecessary surrounding layout nodes that make viewing
+     * and searching the hierarchy inefficient are removed.
+     *
+     * @param compressed true to enable compression; else, false to disable
+     */
+    public void setCompressedLayoutHierarchy(boolean compressed) {
         mCompressed = compressed;
         mCachedServiceFlags = -1; // Reset cached accessibility service flags to force an update.
     }
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 9cda913..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
@@ -34,10 +34,11 @@
 import androidx.tv.material.TabDefaults
 import androidx.tv.material.TabRow
 import androidx.tv.material.TabRowDefaults
+import kotlinx.coroutines.delay
 
 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() }),
 }
 
@@ -55,7 +56,12 @@
     updateSelectedTab = { selectedTabIndex = it }
   )
 
-  LaunchedEffect(selectedTabIndex) { updateSelectedTab(Navigation.values()[selectedTabIndex]) }
+  LaunchedEffect(selectedTabIndex) {
+    // Only update the tab after 250 milliseconds to avoid loading intermediate tabs while
+    // fast scrolling in the TabRow
+    delay(250)
+    updateSelectedTab(Navigation.values()[selectedTabIndex])
+  }
 }
 
 /**
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/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
index a20b5bc..936958e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
@@ -123,9 +123,9 @@
             )
         }
         rule.waitForIdle()
-        // by default fully filled progress approximately takes 23-26% of the control
+        // by default fully filled progress approximately takes 23-27% of the control
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Yellow, 23f..26f)
+            .assertColorInPercentageRange(Color.Yellow, 23f..27f)
         rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Red)
     }
 
@@ -142,9 +142,9 @@
         }
         rule.waitForIdle()
         rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Yellow)
-        // by default progress track approximately takes 23-26% of the control
+        // by default progress track approximately takes 23-27% of the control
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Red, 23f..26f)
+            .assertColorInPercentageRange(Color.Red, 23f..27f)
     }
 
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -241,7 +241,7 @@
         rule.waitForIdle()
         // Because of the stroke cap, progress color takes a little bit more space than track color
         rule.onNodeWithTag(TEST_TAG).captureToImage()
-            .assertColorInPercentageRange(Color.Yellow, 24f..27f)
+            .assertColorInPercentageRange(Color.Yellow, 24f..28f)
         rule.onNodeWithTag(TEST_TAG).captureToImage()
             .assertColorInPercentageRange(Color.Red, 18f..23f)
     }
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index ce90129..f3e30ee 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -353,53 +353,56 @@
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportOddHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 51, 199)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, false)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportEvenHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 50, 199)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, false)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportOddHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 51, 200)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, true)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportEvenHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 50, 200)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, true)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportOddHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 51, 199)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, false)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportEvenHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 50, 199)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, false)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportOddHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 51, 200)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, true)
     }
 
     @Test
     fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportEvenHeightItems() {
-        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 50, 200)
+        visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, true)
     }
 
     private fun visibleItemsAreCorrectAfterScrollingPastEndOfItems(
         autoCenterItem: Int,
         localItemSizePx: Int,
-        viewportSizePx: Int
+        viewPortSizeEven: Boolean
     ) {
         lateinit var state: ScalingLazyListState
         lateinit var scope: CoroutineScope
         rule.setContent {
             with(LocalDensity.current) {
+                val viewportSizePx =
+                    (((localItemSizePx * 4 + defaultItemSpacingPx * 3) / 2) * 2) +
+                        if (viewPortSizeEven) 0 else 1
                 scope = rememberCoroutineScope()
                 ScalingLazyColumn(
                     state = rememberScalingLazyListState(
@@ -504,7 +507,7 @@
             ScalingLazyColumn(
                 state = rememberScalingLazyListState(centerItemIndex).also { state = it },
                 modifier = Modifier.requiredSize(
-                    itemSizeDp * 4
+                    itemSizeDp * 4 + defaultItemSpacingDp * 3
                 ),
             ) {
                 items(6) {
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
index 2eb04f1..f2a2297 100644
--- a/wear/compose/compose-material/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -1,5 +1,5 @@
 HPLandroidx/wear/compose/material/AbstractPlaceholderModifier;->**(**)**
-PLandroidx/wear/compose/material/AnimationKt**->**(**)**
+SPLandroidx/wear/compose/material/AnimationKt**->**(**)**
 PLandroidx/wear/compose/material/AutoCenteringParams;->**(**)**
 Landroidx/wear/compose/material/ButtonBorder;
 Landroidx/wear/compose/material/ButtonColors;
@@ -8,7 +8,7 @@
 HSPLandroidx/wear/compose/material/CardDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/CardKt**->**(**)**
 Landroidx/wear/compose/material/CheckboxColors;
-HPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
 Landroidx/wear/compose/material/ChipBorder;
 Landroidx/wear/compose/material/ChipColors;
 HSPLandroidx/wear/compose/material/ChipDefaults;->**(**)**
@@ -22,16 +22,16 @@
 HSPLandroidx/wear/compose/material/CurvedTextKt**->**(**)**
 HSPLandroidx/wear/compose/material/DefaultButtonBorder;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultButtonColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultChipColors;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultInlineSliderColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingLazyListItemInfo;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingLazyListLayoutInfo;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultScalingParams;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultSplitToggleChipColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
 SPLandroidx/wear/compose/material/DefaultTimeSource;->**(**)**
 HSPLandroidx/wear/compose/material/DefaultTimeSourceKt**->**(**)**
 SPLandroidx/wear/compose/material/DefaultToggleButtonColors;->**(**)**
@@ -53,17 +53,17 @@
 HSPLandroidx/wear/compose/material/MaterialTheme;->**(**)**
 SPLandroidx/wear/compose/material/MaterialThemeKt**->**(**)**
 HSPLandroidx/wear/compose/material/Modifiers;->**(**)**
-PLandroidx/wear/compose/material/PainterWithBrushOverlay;->**(**)**
 HSPLandroidx/wear/compose/material/PickerDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/PickerKt**->**(**)**
 SPLandroidx/wear/compose/material/PickerScopeImpl;->**(**)**
 HSPLandroidx/wear/compose/material/PickerState;->**(**)**
+PLandroidx/wear/compose/material/PlaceholderBackgroundPainter;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderDefaults;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
 PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
 HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
 PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderState;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
 HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
 Landroidx/wear/compose/material/PositionIndicatorState;
@@ -71,8 +71,9 @@
 PLandroidx/wear/compose/material/ProgressIndicatorDefaults;->**(**)**
 HPLandroidx/wear/compose/material/ProgressIndicatorKt**->**(**)**
 Landroidx/wear/compose/material/R;
-HPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
-HSPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
+Landroidx/wear/compose/material/RadioButtonColors;
+HSPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
+SPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
 SPLandroidx/wear/compose/material/RangeDefaultsKt**->**(**)**
 SPLandroidx/wear/compose/material/RangeIcons;->**(**)**
 HSPLandroidx/wear/compose/material/ResistanceConfig;->**(**)**
@@ -100,7 +101,6 @@
 HSPLandroidx/wear/compose/material/ShapesKt**->**(**)**
 HSPLandroidx/wear/compose/material/SliderKt**->**(**)**
 Landroidx/wear/compose/material/SplitToggleChipColors;
-SPLandroidx/wear/compose/material/SqueezeMotion;->**(**)**
 SPLandroidx/wear/compose/material/StepperDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/StepperKt**->**(**)**
 Landroidx/wear/compose/material/SwipeProgress;
@@ -113,7 +113,7 @@
 HSPLandroidx/wear/compose/material/SwipeableKt**->**(**)**
 HSPLandroidx/wear/compose/material/SwipeableState;->**(**)**
 Landroidx/wear/compose/material/SwitchColors;
-HPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/TextKt**->**(**)**
 Landroidx/wear/compose/material/ThresholdConfig;
 SPLandroidx/wear/compose/material/TimeBroadcastReceiver;->**(**)**
@@ -126,8 +126,8 @@
 Landroidx/wear/compose/material/ToggleChipColors;
 HSPLandroidx/wear/compose/material/ToggleChipDefaults;->**(**)**
 HSPLandroidx/wear/compose/material/ToggleChipKt**->**(**)**
-HPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
-PLandroidx/wear/compose/material/ToggleStage;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
+SPLandroidx/wear/compose/material/ToggleStage;->**(**)**
 HSPLandroidx/wear/compose/material/Typography;->**(**)**
 HSPLandroidx/wear/compose/material/TypographyKt**->**(**)**
 HSPLandroidx/wear/compose/material/VignetteKt**->**(**)**
diff --git a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
index e343b99..5068468 100644
--- a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
@@ -1,4 +1,3 @@
-Landroidx/wear/compose/navigation/ComposableSingletons;
 HSPLandroidx/wear/compose/navigation/NavGraphBuilderKt**->**(**)**
 SPLandroidx/wear/compose/navigation/SwipeDismissableNavHostControllerKt**->**(**)**
 HSPLandroidx/wear/compose/navigation/SwipeDismissableNavHostKt**->**(**)**
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-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index dc02bd9..85af687 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -39,7 +39,6 @@
 import androidx.test.screenshot.AndroidXScreenshotTestRule
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotBoundsType
 import androidx.wear.watchface.ContentDescriptionLabel
 import androidx.wear.watchface.DrawMode
@@ -242,30 +241,6 @@
         return value!!
     }
 
-    /**
-     * Updates the complications for [interactiveInstance] and waits until they have been applied.
-     */
-    protected fun updateComplicationsBlocking(
-        interactiveInstance: InteractiveWatchFaceClient,
-        slotIdToComplicationData: Map<Int, ComplicationData>
-    ) {
-        val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
-        var slot: ComplicationSlot
-
-        runBlocking {
-            slot = engine.deferredWatchFaceImpl.await()
-                .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
-        }
-
-        val updateCountDownLatch = CountDownLatch(1)
-        handlerCoroutineScope.launch {
-            slot.complicationData.collect { updateCountDownLatch.countDown() }
-        }
-
-        interactiveInstance.updateComplicationData(slotIdToComplicationData)
-        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
-    }
-
     protected fun tapOnComplication(
         interactiveInstance: InteractiveWatchFaceClient,
         slotId: Int
@@ -471,8 +446,7 @@
     fun updateComplicationData() {
         val interactiveInstance = getOrCreateTestSubject()
 
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     rangedValueComplicationBuilder().build(),
@@ -1132,8 +1106,7 @@
             surfaceHolder
         )
         val interactiveInstance = getOrCreateTestSubject(wallpaperService)
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     rangedValueComplicationBuilder()
@@ -1194,8 +1167,7 @@
             )
         )
 
-        updateComplicationsBlocking(
-            interactiveInstance,
+        interactiveInstance.updateComplicationData(
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
                     timelineComplication.toApiComplicationData()
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
index 14afc68..4dccf73 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
@@ -131,24 +131,27 @@
     public void timeEntryToString() {
         assertThat(TIMELINE_A.toString()).isEqualTo(
                 "ComplicationDataTimeline(defaultComplicationData=ShortTextComplicationData("
-                        + "text=ComplicationText{mSurroundingText=Hello, mTimeDependentText=null}, "
-                        + "title=null, monochromaticImage=null, smallImage=null, "
+                        + "text=ComplicationText{mSurroundingText=Hello, mTimeDependentText=null, "
+                        + "mStringExpression=null}, title=null, monochromaticImage=null, "
+                        + "smallImage=null, contentDescription=ComplicationText{"
+                        + "mSurroundingText=, mTimeDependentText=null, mStringExpression=null}, "
+                        + "tapActionLostDueToSerialization=false, tapAction=null, "
+                        + "validTimeRange=TimeRange(startDateTimeMillis="
+                        + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
+                        + "+1000000000-12-31T23:59:59.999999999Z), dataSource=null, "
+                        + "persistencePolicy=0, displayPolicy=0), timelineEntries=["
+                        + "TimelineEntry(validity=TimeInterval(start=1970-01-02T03:46:40Z, "
+                        + "end=1970-01-03T07:33:20Z), complicationData=ShortTextComplicationData("
+                        + "text=ComplicationText{mSurroundingText=Updated, "
+                        + "mTimeDependentText=null, mStringExpression=null}, title=null, "
+                        + "monochromaticImage=null, smallImage=null, "
                         + "contentDescription=ComplicationText{mSurroundingText=, "
-                        + "mTimeDependentText=null}, tapActionLostDueToSerialization=false, "
-                        + "tapAction=null, validTimeRange=TimeRange("
-                        + "startDateTimeMillis=-1000000000-01-01T00:00:00Z, "
-                        + "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), "
-                        + "dataSource=null, persistencePolicy=0, displayPolicy=0), "
-                        + "timelineEntries=[TimelineEntry(validity"
-                        + "=TimeInterval(start=1970-01-02T03:46:40Z, end=1970-01-03T07:33:20Z), "
-                        + "complicationData=ShortTextComplicationData(text=ComplicationText{"
-                        + "mSurroundingText=Updated, mTimeDependentText=null}, title=null, "
-                        + "monochromaticImage=null, smallImage=null, contentDescription="
-                        + "ComplicationText{mSurroundingText=, mTimeDependentText=null}, "
-                        + "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange="
-                        + "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, "
-                        + "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), "
-                        + "dataSource=null, persistencePolicy=0, displayPolicy=0))])"
+                        + "mTimeDependentText=null, mStringExpression=null}, "
+                        + "tapActionLostDueToSerialization=false, tapAction=null, "
+                        + "validTimeRange=TimeRange(startDateTimeMillis="
+                        + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
+                        + "+1000000000-12-31T23:59:59.999999999Z), dataSource=null, "
+                        + "persistencePolicy=0, displayPolicy=0))])"
         );
     }
 
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index c473a58..8d23b7f 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -86,14 +86,14 @@
   public final class DataKt {
   }
 
-  public final class DynamicFloatKt {
-  }
-
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
+  public final class FloatExpressionKt {
+  }
+
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index fdbd262..9078277 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -89,23 +89,17 @@
   public final class DataKt {
   }
 
-  @androidx.wear.watchface.complications.data.ComplicationExperimental public abstract class DynamicFloat {
-    ctor public DynamicFloat();
-    method public abstract byte[] asByteArray();
-  }
-
-  public final class DynamicFloatKt {
-  }
-
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
+  public final class FloatExpressionKt {
+  }
+
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
     method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
     method public float getTargetValue();
@@ -114,7 +108,6 @@
     method public float getValue();
     property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
-    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
     property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
     property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
     property public final float targetValue;
@@ -127,7 +120,6 @@
 
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class GoalProgressComplicationData.Builder {
     ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
-    ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public GoalProgressComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
     method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
@@ -276,7 +268,6 @@
   public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
-    method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
     method public float getMax();
     method public float getMin();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -287,7 +278,6 @@
     method public int getValueType();
     property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
     property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
-    property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
     property public final float max;
     property public final float min;
     property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
@@ -305,7 +295,6 @@
 
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
-    ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public RangedValueComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
     method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
     method public final T setDataSource(android.content.ComponentName? dataSource);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index 83c88a5..c3d723ff 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -86,14 +86,14 @@
   public final class DataKt {
   }
 
-  public final class DynamicFloatKt {
-  }
-
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
+  public final class FloatExpressionKt {
+  }
+
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 19f21c4..23a2fc0 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -37,8 +37,8 @@
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicy
-import androidx.wear.watchface.complications.data.DynamicFloat
-import androidx.wear.watchface.complications.data.toDynamicFloat
+import androidx.wear.watchface.complications.data.FloatExpression
+import androidx.wear.watchface.complications.data.toFloatExpression
 import java.io.IOException
 import java.io.InvalidObjectException
 import java.io.ObjectInputStream
@@ -177,8 +177,8 @@
             if (isFieldValidForType(FIELD_VALUE, type)) {
                 oos.writeFloat(complicationData.rangedValue)
             }
-            if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
-                oos.writeNullable(complicationData.rangedDynamicValue) {
+            if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
+                oos.writeNullable(complicationData.rangedValueExpression) {
                     oos.writeByteArray(it.asByteArray())
                 }
             }
@@ -317,9 +317,9 @@
             if (isFieldValidForType(FIELD_VALUE, type)) {
                 fields.putFloat(FIELD_VALUE, ois.readFloat())
             }
-            if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+            if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
                 ois.readNullable { ois.readByteArray() }?.let {
-                    fields.putByteArray(FIELD_DYNAMIC_VALUE, it)
+                    fields.putByteArray(FIELD_VALUE_EXPRESSION, it)
                 }
             }
             if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
@@ -594,21 +594,21 @@
         }
 
     /**
-     * Returns true if the ComplicationData contains a ranged dynamic value. I.e. if
-     * [rangedDynamicValue] can succeed.
+     * Returns true if the ComplicationData contains a ranged value expression. I.e. if
+     * [rangedValueExpression] can succeed.
      */
-    fun hasRangedDynamicValue(): Boolean = isFieldValidForType(FIELD_DYNAMIC_VALUE, type)
+    fun hasRangedValueExpression(): Boolean = isFieldValidForType(FIELD_VALUE_EXPRESSION, type)
 
     /**
-     * Returns the *dynamicValue* field for this complication.
+     * Returns the *valueExpression* field for this complication.
      *
      * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
      * [TYPE_GOAL_PROGRESS].
      */
-    val rangedDynamicValue: DynamicFloat?
+    val rangedValueExpression: FloatExpression?
         get() {
-            checkFieldValidForTypeWithoutThrowingException(FIELD_DYNAMIC_VALUE, type)
-            return fields.getByteArray(FIELD_DYNAMIC_VALUE)?.toDynamicFloat()
+            checkFieldValidForTypeWithoutThrowingException(FIELD_VALUE_EXPRESSION, type)
+            return fields.getByteArray(FIELD_VALUE_EXPRESSION)?.toFloatExpression()
         }
 
     /**
@@ -1264,15 +1264,15 @@
         fun setRangedValue(value: Float?) = apply { putOrRemoveField(FIELD_VALUE, value) }
 
         /**
-         * Sets the *dynamicValue* field. It is evaluated to a value with the same limitations as
+         * Sets the *valueExpression* field. It is evaluated to a value with the same limitations as
          * [setRangedValue].
          *
          * Returns this Builder to allow chaining.
          *
          * @throws IllegalStateException if this field is not valid for the complication type
          */
-        fun setRangedDynamicValue(value: DynamicFloat?) =
-            apply { putOrRemoveField(FIELD_DYNAMIC_VALUE, value?.asByteArray()) }
+        fun setRangedValueExpression(value: FloatExpression?) =
+            apply { putOrRemoveField(FIELD_VALUE_EXPRESSION, value?.asByteArray()) }
 
         /**
          * Sets the *value type* field which provides meta data about the value. This is
@@ -1923,7 +1923,7 @@
         private const val FIELD_TIMELINE_ENTRIES = "TIMELINE"
         private const val FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE"
         private const val FIELD_VALUE = "VALUE"
-        private const val FIELD_DYNAMIC_VALUE = "DYNAMIC_VALUE"
+        private const val FIELD_VALUE_EXPRESSION = "VALUE_EXPRESSION"
         private const val FIELD_VALUE_TYPE = "VALUE_TYPE"
 
         // Experimental fields, these are subject to change without notice.
@@ -1991,7 +1991,7 @@
             TYPE_EMPTY to setOf(),
             TYPE_SHORT_TEXT to setOf(),
             TYPE_LONG_TEXT to setOf(),
-            TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+            TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_VALUE_EXPRESSION)),
             TYPE_ICON to setOf(),
             TYPE_SMALL_IMAGE to setOf(),
             TYPE_LARGE_IMAGE to setOf(),
@@ -1999,7 +1999,7 @@
             TYPE_NO_DATA to setOf(),
             EXP_TYPE_PROTO_LAYOUT to setOf(),
             EXP_TYPE_LIST to setOf(),
-            TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+            TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_VALUE_EXPRESSION)),
             TYPE_WEIGHTED_ELEMENTS to setOf(),
         )
 
@@ -2105,7 +2105,7 @@
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_TAP_ACTION,
                 FIELD_VALUE,
-                FIELD_DYNAMIC_VALUE,
+                FIELD_VALUE_EXPRESSION,
                 FIELD_VALUE_TYPE,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
index 16f9352..171680f 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
@@ -37,6 +37,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.wear.watchface.complications.data.StringExpression;
 
 import java.io.IOException;
 import java.io.InvalidObjectException;
@@ -75,21 +76,25 @@
             return false;
         }
         ComplicationText that = (ComplicationText) o;
+        if (!Objects.equals(mStringExpression, that.mStringExpression)) {
+            return false;
+        }
         if (!Objects.equals(mTimeDependentText, that.mTimeDependentText)) {
             return false;
         }
         if (mSurroundingText == null) {
+            return that.mSurroundingText == null;
+        } else {
             if (that.mSurroundingText != null) {
-                return false;
+                return mSurroundingText.toString().contentEquals(that.mSurroundingText);
             }
-            return true;
         }
-        return mSurroundingText.toString().contentEquals(that.mSurroundingText);
+        return true;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mSurroundingText, mTimeDependentText);
+        return Objects.hash(mSurroundingText, mTimeDependentText, mStringExpression);
     }
 
     @NonNull
@@ -97,7 +102,8 @@
     public String toString() {
         return "ComplicationText{" + "mSurroundingText="
                 + ComplicationData.maybeRedact(mSurroundingText)
-                + ", mTimeDependentText=" + mTimeDependentText + '}';
+                + ", mTimeDependentText=" + mTimeDependentText + ", mStringExpression="
+                + mStringExpression + "}";
     }
 
     /** @hide */
@@ -229,6 +235,7 @@
     private static final String KEY_DIFFERENCE_STYLE = "difference_style";
     private static final String KEY_DIFFERENCE_SHOW_NOW_TEXT = "show_now_text";
     private static final String KEY_DIFFERENCE_MINIMUM_UNIT = "minimum_unit";
+    private static final String KEY_DYNAMIC_STRING = "dynamic_string";
     private static final String KEY_FORMAT_FORMAT_STRING = "format_format_string";
     private static final String KEY_FORMAT_STYLE = "format_style";
     private static final String KEY_FORMAT_TIME_ZONE = "format_time_zone";
@@ -264,8 +271,13 @@
      * must be not null and {@link #getTextAt} will return just the time-dependent value relative to
      * the given time.
      */
+    @Nullable
     private final TimeDependentText mTimeDependentText;
 
+    /** A [StringExpression] which will be evaluated by the system on the WatchFace's behalf. */
+    @Nullable
+    private final StringExpression mStringExpression;
+
     /** Used to replace occurrences of ^1 with time dependent text and ignore ^[2-9]. */
     private final CharSequence[] mTemplateValues =
             new CharSequence[]{"", "^2", "^3", "^4", "^5", "^6", "^7", "^8", "^9"};
@@ -277,16 +289,28 @@
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     public ComplicationText(@Nullable CharSequence surroundingText,
-            @Nullable TimeDependentText timeDependentText) {
+            @Nullable TimeDependentText timeDependentText,
+            @Nullable StringExpression stringExpression) {
         mSurroundingText = surroundingText;
         mTimeDependentText = timeDependentText;
+        mStringExpression = stringExpression;
         checkFields();
     }
 
+    public ComplicationText(@NonNull StringExpression stringExpression) {
+        this(/* surroundingText = */ null, /* timeDependentText = */ null, stringExpression);
+    }
+
     private ComplicationText(@NonNull Parcel in) {
         Bundle bundle = in.readBundle(getClass().getClassLoader());
         mSurroundingText = bundle.getCharSequence(KEY_SURROUNDING_STRING);
 
+        if (bundle.containsKey(KEY_DYNAMIC_STRING)) {
+            mStringExpression = new StringExpression(bundle.getByteArray(KEY_DYNAMIC_STRING));
+        } else {
+            mStringExpression = null;
+        }
+
         if (bundle.containsKey(KEY_DIFFERENCE_STYLE)
                 && bundle.containsKey(KEY_DIFFERENCE_PERIOD_START)
                 && bundle.containsKey(KEY_DIFFERENCE_PERIOD_END)) {
@@ -317,30 +341,48 @@
     private static class SerializedForm implements Serializable {
         CharSequence mSurroundingText;
         TimeDependentText mTimeDependentText;
+        StringExpression mStringExpression;
 
         SerializedForm(@Nullable CharSequence surroundingText,
-                @Nullable TimeDependentText timeDependentText) {
+                @Nullable TimeDependentText timeDependentText,
+                @Nullable StringExpression stringExpression) {
             mSurroundingText = surroundingText;
             mTimeDependentText = timeDependentText;
+            mStringExpression = stringExpression;
         }
 
         private void writeObject(ObjectOutputStream oos) throws IOException {
             CharSequenceSerializableHelper.writeToStream(mSurroundingText, oos);
             oos.writeObject(mTimeDependentText);
+            if (mStringExpression == null) {
+                oos.writeInt(0);
+            } else {
+                byte[] bytes = mStringExpression.asByteArray();
+                oos.writeInt(bytes.length);
+                oos.write(bytes);
+            }
         }
 
         private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
             mSurroundingText = CharSequenceSerializableHelper.readFromStream(ois);
             mTimeDependentText = (TimeDependentText) ois.readObject();
+            int length = ois.readInt();
+            if (length == 0) {
+                mStringExpression = null;
+            } else {
+                byte[] bytes = new byte[length];
+                ois.readFully(bytes);
+                mStringExpression = new StringExpression(bytes);
+            }
         }
 
         Object readResolve() {
-            return new ComplicationText(mSurroundingText, mTimeDependentText);
+            return new ComplicationText(mSurroundingText, mTimeDependentText, mStringExpression);
         }
     }
 
     Object writeReplace() {
-        return new SerializedForm(mSurroundingText, mTimeDependentText);
+        return new SerializedForm(mSurroundingText, mTimeDependentText, mStringExpression);
     }
 
     private void readObject(ObjectInputStream stream) throws InvalidObjectException {
@@ -364,9 +406,10 @@
     }
 
     private void checkFields() {
-        if (mSurroundingText == null && mTimeDependentText == null) {
+        if (mSurroundingText == null && mTimeDependentText == null && mStringExpression == null) {
             throw new IllegalStateException(
-                    "One of mSurroundingText and mTimeDependentText must be non-null");
+                    "One of mSurroundingText, mTimeDependentText and mStringExpression must be"
+                            + " non-null");
         }
     }
 
@@ -381,22 +424,29 @@
         Bundle bundle = new Bundle();
         bundle.putCharSequence(KEY_SURROUNDING_STRING, mSurroundingText);
 
-        if (mTimeDependentText instanceof TimeDifferenceText) {
-            TimeDifferenceText timeDiffText = (TimeDifferenceText) mTimeDependentText;
-            bundle.putLong(KEY_DIFFERENCE_PERIOD_START, timeDiffText.getReferencePeriodStart());
-            bundle.putLong(KEY_DIFFERENCE_PERIOD_END, timeDiffText.getReferencePeriodEnd());
-            bundle.putInt(KEY_DIFFERENCE_STYLE, timeDiffText.getStyle());
-            bundle.putBoolean(KEY_DIFFERENCE_SHOW_NOW_TEXT, timeDiffText.shouldShowNowText());
-            if (timeDiffText.getMinimumUnit() != null) {
-                bundle.putString(KEY_DIFFERENCE_MINIMUM_UNIT, timeDiffText.getMinimumUnit().name());
-            }
-        } else if (mTimeDependentText instanceof TimeFormatText) {
-            TimeFormatText timeFormatText = (TimeFormatText) mTimeDependentText;
-            bundle.putString(KEY_FORMAT_FORMAT_STRING, timeFormatText.getFormatString());
-            bundle.putInt(KEY_FORMAT_STYLE, timeFormatText.getStyle());
-            TimeZone timeZone = timeFormatText.getTimeZone();
-            if (timeZone != null) {
-                bundle.putString(KEY_FORMAT_TIME_ZONE, timeZone.getID());
+        if (mStringExpression != null) {
+            bundle.putByteArray(KEY_DYNAMIC_STRING, mStringExpression.asByteArray());
+        }
+
+        if (mTimeDependentText != null) {
+            if (mTimeDependentText instanceof TimeDifferenceText) {
+                TimeDifferenceText timeDiffText = (TimeDifferenceText) mTimeDependentText;
+                bundle.putLong(KEY_DIFFERENCE_PERIOD_START, timeDiffText.getReferencePeriodStart());
+                bundle.putLong(KEY_DIFFERENCE_PERIOD_END, timeDiffText.getReferencePeriodEnd());
+                bundle.putInt(KEY_DIFFERENCE_STYLE, timeDiffText.getStyle());
+                bundle.putBoolean(KEY_DIFFERENCE_SHOW_NOW_TEXT, timeDiffText.shouldShowNowText());
+                if (timeDiffText.getMinimumUnit() != null) {
+                    bundle.putString(KEY_DIFFERENCE_MINIMUM_UNIT,
+                            timeDiffText.getMinimumUnit().name());
+                }
+            } else if (mTimeDependentText instanceof TimeFormatText) {
+                TimeFormatText timeFormatText = (TimeFormatText) mTimeDependentText;
+                bundle.putString(KEY_FORMAT_FORMAT_STRING, timeFormatText.getFormatString());
+                bundle.putInt(KEY_FORMAT_STYLE, timeFormatText.getStyle());
+                TimeZone timeZone = timeFormatText.getTimeZone();
+                if (timeZone != null) {
+                    bundle.putString(KEY_FORMAT_TIME_ZONE, timeZone.getID());
+                }
             }
         }
 
@@ -411,6 +461,11 @@
     @NonNull
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public TimeDependentText getTimeDependentText() {
+        if (mStringExpression != null) {
+            throw new UnsupportedOperationException("getTimeDependentText not supported for "
+                    + "StringExpressions");
+        }
+        assert mTimeDependentText != null;
         return mTimeDependentText;
     }
 
@@ -430,6 +485,11 @@
     @NonNull
     @Override
     public CharSequence getTextAt(@NonNull Resources resources, long dateTimeMillis) {
+        if (mStringExpression != null && mTimeDependentText == null && mSurroundingText == null) {
+            throw new UnsupportedOperationException("getTextAt not supported for "
+                    + "StringExpressions");
+        }
+
         if (mTimeDependentText == null) {
             return mSurroundingText;
         }
@@ -522,7 +582,8 @@
      */
     @NonNull
     public static ComplicationText plainText(@NonNull CharSequence text) {
-        return new ComplicationText(text, null);
+        return new ComplicationText(
+                text, /* timeDependentText= */ null, /* stringExpression= */ null);
     }
 
     /**
@@ -703,7 +764,8 @@
                             mReferencePeriodEndMillis,
                             mStyle,
                             showNowText,
-                            mMinimumUnit));
+                            mMinimumUnit),
+                    /* stringExpression= */ null);
         }
 
         /** Returns the default value for the 'show now text' option for the given {@code style}. */
@@ -791,7 +853,8 @@
         @SuppressLint("SyntheticAccessor")
         public ComplicationText build() {
             return new ComplicationText(
-                    mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone));
+                    mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone),
+                    /* stringExpression= */ null);
         }
     }
 }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index d774957..4b5c05a 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -910,7 +910,7 @@
  * The value may be accompanied by an icon and/or short text and title.
  *
  * The [min] and [max] fields are required for this type, as well as one of [value] or
- * [dynamicValue]. The value within the range is expected to always be displayed.
+ * [valueExpression]. The value within the range is expected to always be displayed.
  *
  * The icon, title, and text fields are optional and the watch face may choose which of these
  * fields to display, if any.
@@ -925,9 +925,6 @@
  * [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
  * than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
  * by the renderer. The semantic meaning of value is described by [valueType].
- * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
- * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
- * ignore this field.
  * @property min The minimum [Float] value for this complication.
  * @property max The maximum [Float] value for this complication.
  * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -964,9 +961,7 @@
 public class RangedValueComplicationData internal constructor(
     public val value: Float,
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @ComplicationExperimental
-    @get:ComplicationExperimental
-    public val dynamicValue: DynamicFloat?,
+    valueExpression: FloatExpression?,
     public val min: Float,
     public val max: Float,
     public val monochromaticImage: MonochromaticImage?,
@@ -991,6 +986,15 @@
     persistencePolicy = persistencePolicy,
     displayPolicy = displayPolicy
 ) {
+    /**
+     * The [FloatExpression] optionally set by the data source. If present the system will
+     * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
+     * this field.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public val valueExpression: FloatExpression? = valueExpression
+
     /** @hide */
     @IntDef(value = [TYPE_UNDEFINED, TYPE_RATING, TYPE_PERCENTAGE])
     public annotation class RangedValueType
@@ -999,14 +1003,14 @@
      * Builder for [RangedValueComplicationData].
      *
      * You must at a minimum set the [min], [max] and [contentDescription] fields, at least one of
-     * [value] or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text]
+     * [value] or [valueExpression], and at least one of [monochromaticImage], [smallImage], [text]
      * or [title].
      */
     public class Builder
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public constructor(
         private val value: Float,
-        private val dynamicValue: DynamicFloat?,
+        private val valueExpression: FloatExpression?,
         private val min: Float,
         private val max: Float,
         private var contentDescription: ComplicationText
@@ -1026,26 +1030,27 @@
             min: Float,
             max: Float,
             contentDescription: ComplicationText
-        ) : this(value, dynamicValue = null, min, max, contentDescription)
+        ) : this(value, valueExpression = null, min, max, contentDescription)
 
         /**
-         * Creates a [Builder] for a [RangedValueComplicationData] with a [DynamicFloat] value.
+         * Creates a [Builder] for a [RangedValueComplicationData] with a [FloatExpression] value.
          *
-         * @param dynamicValue The [DynamicFloat] of the ranged complication which will be evaluated
-         * into a value dynamically, and should be in the range [[min]] .. [[max]]. The semantic
-         * meaning of value can be specified via [setValueType].
+         * @param valueExpression The [FloatExpression] of the ranged complication which will be
+         * evaluated into a value dynamically, and should be in the range [[min]] .. [[max]]. The
+         * semantic meaning of value can be specified via [setValueType].
          * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
          * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
          * [TYPE_PERCENTAGE] this must be 0f.
          * @param contentDescription Localized description for use by screen readers
+         * @hide
          */
-        @ComplicationExperimental
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public constructor(
-            dynamicValue: DynamicFloat,
+            valueExpression: FloatExpression,
             min: Float,
             max: Float,
             contentDescription: ComplicationText
-        ) : this(value = min /* sensible default */, dynamicValue, min, max, contentDescription)
+        ) : this(value = min /* sensible default */, valueExpression, min, max, contentDescription)
 
         private var tapAction: PendingIntent? = null
         private var validTimeRange: TimeRange? = null
@@ -1125,7 +1130,7 @@
             }
             return RangedValueComplicationData(
                 value,
-                dynamicValue,
+                valueExpression,
                 min,
                 max,
                 monochromaticImage,
@@ -1158,7 +1163,7 @@
 
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
-        builder.setRangedDynamicValue(dynamicValue)
+        builder.setRangedValueExpression(valueExpression)
         builder.setRangedMinValue(min)
         builder.setRangedMaxValue(max)
         monochromaticImage?.addToWireComplicationData(builder)
@@ -1188,7 +1193,7 @@
         other as RangedValueComplicationData
 
         if (value != other.value) return false
-        if (dynamicValue != other.dynamicValue) return false
+        if (valueExpression != other.valueExpression) return false
         if (valueType != other.valueType) return false
         if (min != other.min) return false
         if (max != other.max) return false
@@ -1210,7 +1215,7 @@
 
     override fun hashCode(): Int {
         var result = value.hashCode()
-        result = 31 * result + (dynamicValue?.hashCode() ?: 0)
+        result = 31 * result + (valueExpression?.hashCode() ?: 0)
         result = 31 * result + valueType
         result = 31 * result + min.hashCode()
         result = 31 * result + max.hashCode()
@@ -1235,13 +1240,13 @@
         } else {
             value.toString()
         }
-        val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+        val valueExpressionString = if (WireComplicationData.shouldRedact()) {
             "REDACTED"
         } else {
-            dynamicValue.toString()
+            valueExpression.toString()
         }
         return "RangedValueComplicationData(value=$valueString, " +
-            "dynamicValue=$dynamicValueString, valueType=$valueType, min=$min, " +
+            "valueExpression=$valueExpressionString, valueType=$valueType, min=$min, " +
             "max=$max, monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
             "title=$title, text=$text, contentDescription=$contentDescription), " +
             "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1309,7 +1314,7 @@
  * text and title.
  *
  * The [targetValue] field is required for this type, as well as one of [value] or
- * [dynamicValue]. The progress is expected to always be displayed.
+ * [valueExpression]. The progress is expected to always be displayed.
  *
  * The icon, title, and text fields are optional and the watch face may choose which of these
  * fields to display, if any.
@@ -1329,9 +1334,6 @@
  * than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
  * rather than rendering normally, its suggested to be drawn as a grey arc with a percentage value
  * selected by the renderer.
- * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
- * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
- * ignore this field.
  * @property targetValue The target [Float] value for this complication.
  * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
  * face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
@@ -1366,9 +1368,7 @@
 internal constructor(
     public val value: Float,
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @ComplicationExperimental
-    @get:ComplicationExperimental
-    public val dynamicValue: DynamicFloat?,
+    valueExpression: FloatExpression?,
     public val targetValue: Float,
     public val monochromaticImage: MonochromaticImage?,
     public val smallImage: SmallImage?,
@@ -1392,10 +1392,19 @@
     displayPolicy = displayPolicy
 ) {
     /**
+     * The [FloatExpression] optionally set by the data source. If present the system will
+     * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
+     * this field.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public val valueExpression: FloatExpression? = valueExpression
+
+    /**
      * Builder for [GoalProgressComplicationData].
      *
      * You must at a minimum set the [targetValue] and [contentDescription] fields, one of [value]
-     * or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text] or
+     * or [valueExpression], and at least one of [monochromaticImage], [smallImage], [text] or
      * [title].
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -1403,7 +1412,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public constructor(
         private val value: Float,
-        private val dynamicValue: DynamicFloat?,
+        private val valueExpression: FloatExpression?,
         private val targetValue: Float,
         private var contentDescription: ComplicationText
     ) : BaseBuilder<Builder, GoalProgressComplicationData>() {
@@ -1418,24 +1427,25 @@
             value: Float,
             targetValue: Float,
             contentDescription: ComplicationText
-        ) : this(value, dynamicValue = null, targetValue, contentDescription)
+        ) : this(value, valueExpression = null, targetValue, contentDescription)
 
         /**
-         * Creates a [Builder] for a [GoalProgressComplicationData] with a [DynamicFloat] value.
+         * Creates a [Builder] for a [GoalProgressComplicationData] with a [FloatExpression] value.
          *
-         * @param dynamicValue The [DynamicFloat] of the goal complication which will be evaluated
-         * into a value dynamically, and should be >= 0.
+         * @param valueExpression The [FloatExpression] of the goal complication which will be
+         * evaluated into a value dynamically, and should be >= 0.
          * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
          * @param contentDescription Localized description for use by screen readers
+         * @hide
          */
-        @ComplicationExperimental
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public constructor(
-            dynamicValue: DynamicFloat,
+            valueExpression: FloatExpression,
             targetValue: Float,
             contentDescription: ComplicationText
         ) : this(
             value = 0f /* sensible default */,
-            dynamicValue,
+            valueExpression,
             targetValue,
             contentDescription
         )
@@ -1502,7 +1512,7 @@
             }
             return GoalProgressComplicationData(
                 value,
-                dynamicValue,
+                valueExpression,
                 targetValue,
                 monochromaticImage,
                 smallImage,
@@ -1533,7 +1543,7 @@
 
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
-        builder.setRangedDynamicValue(dynamicValue)
+        builder.setRangedValueExpression(valueExpression)
         builder.setTargetValue(targetValue)
         monochromaticImage?.addToWireComplicationData(builder)
         smallImage?.addToWireComplicationData(builder)
@@ -1561,7 +1571,7 @@
         other as GoalProgressComplicationData
 
         if (value != other.value) return false
-        if (dynamicValue != other.dynamicValue) return false
+        if (valueExpression != other.valueExpression) return false
         if (targetValue != other.targetValue) return false
         if (monochromaticImage != other.monochromaticImage) return false
         if (smallImage != other.smallImage) return false
@@ -1581,7 +1591,7 @@
 
     override fun hashCode(): Int {
         var result = value.hashCode()
-        result = 31 * result + (dynamicValue?.hashCode() ?: 0)
+        result = 31 * result + (valueExpression?.hashCode() ?: 0)
         result = 31 * result + targetValue.hashCode()
         result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
         result = 31 * result + (smallImage?.hashCode() ?: 0)
@@ -1604,13 +1614,13 @@
         } else {
             value.toString()
         }
-        val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+        val valueExpressionString = if (WireComplicationData.shouldRedact()) {
             "REDACTED"
         } else {
-            dynamicValue.toString()
+            valueExpression.toString()
         }
         return "GoalProgressComplicationData(value=$valueString, " +
-            "dynamicValue=$dynamicValueString, targetValue=$targetValue, " +
+            "valueExpression=$valueExpressionString, targetValue=$targetValue, " +
             "monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
             "text=$text, contentDescription=$contentDescription), " +
             "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -2669,7 +2679,7 @@
             RangedValueComplicationData.TYPE.toWireComplicationType() ->
                 RangedValueComplicationData.Builder(
                     value = rangedValue,
-                    dynamicValue = rangedDynamicValue,
+                    valueExpression = rangedValueExpression,
                     min = rangedMinValue,
                     max = rangedMaxValue,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
@@ -2728,7 +2738,7 @@
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
                 GoalProgressComplicationData.Builder(
                     value = rangedValue,
-                    dynamicValue = rangedDynamicValue,
+                    valueExpression = rangedValueExpression,
                     targetValue = targetValue,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
@@ -2846,7 +2856,7 @@
             RangedValueComplicationData.TYPE.toWireComplicationType() ->
                 RangedValueComplicationData.Builder(
                     value = rangedValue,
-                    dynamicValue = rangedDynamicValue,
+                    valueExpression = rangedValueExpression,
                     min = rangedMinValue,
                     max = rangedMaxValue,
                     contentDescription = contentDescription?.toApiComplicationText()
@@ -2922,7 +2932,7 @@
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
                 GoalProgressComplicationData.Builder(
                     value = rangedValue,
-                    dynamicValue = rangedDynamicValue,
+                    valueExpression = rangedValueExpression,
                     targetValue = targetValue,
                     contentDescription = contentDescription?.toApiComplicationText()
                         ?: ComplicationText.EMPTY
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
similarity index 69%
rename from wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
rename to wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
index 0374f72..6ac63a8 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
@@ -18,27 +18,32 @@
 
 import androidx.annotation.RestrictTo
 
-/** Placeholder for DynamicFloat implementation by tiles. */
+/**
+ * Placeholder for FloatExpression implementation by tiles.
+ * @hide
+ */
 // TODO(b/257413268): Replace this with the real implementation.
-@ComplicationExperimental
-abstract class DynamicFloat {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class FloatExpression {
     abstract fun asByteArray(): ByteArray
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         // Not checking for exact same class because it's not implemented yet.
-        if (other !is DynamicFloat) return false
+        if (other !is FloatExpression) return false
         return asByteArray().contentEquals(other.asByteArray())
     }
 
     override fun hashCode() = asByteArray().contentHashCode()
 
-    override fun toString() = "DynamicFloatPlaceholder${asByteArray().contentToString()}"
+    override fun toString() = "FloatExpressionPlaceholder${asByteArray().contentToString()}"
 }
 
-/** Placeholder parser for [DynamicFloat] from [ByteArray]. */
-@ComplicationExperimental
+/**
+ * Placeholder parser for [FloatExpression] from [ByteArray].
+ * @hide
+ */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun ByteArray.toDynamicFloat() = object : DynamicFloat() {
-    override fun asByteArray() = this@toDynamicFloat
+fun ByteArray.toFloatExpression() = object : FloatExpression() {
+    override fun asByteArray() = this@toFloatExpression
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index f335e6a..f318733 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -564,13 +564,9 @@
         return false
     }
 
-    override fun hashCode(): Int {
-        return delegate.hashCode()
-    }
+    override fun hashCode() = delegate.hashCode()
 
-    override fun toString(): String {
-        return delegate.toString()
-    }
+    override fun toString() = delegate.toString()
 }
 
 /** Converts a [WireComplicationText] into an equivalent [ComplicationText] instead. */
@@ -627,16 +623,92 @@
         return true
     }
 
-    override fun hashCode(): Int {
-        return delegate.hashCode()
-    }
+    override fun hashCode() = delegate.hashCode()
 
-    override fun toString(): String {
-        return delegate.toString()
-    }
+    override fun toString() = delegate.toString()
 }
 
 /** @hide */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public fun WireTimeDependentText.toApiComplicationText(): ComplicationText =
     DelegatingTimeDependentText(this)
+
+/**
+ * This is a placeholder for the tiles StringExpression which isn't currently available. We'll
+ * remove this later in favor of the real thing.
+ * @hide
+ */
+// TODO(b/260065006): Remove this in favor of the real thing when available.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class StringExpression(private val expression: ByteArray) {
+    fun asByteArray() = expression
+
+    override fun toString(): String {
+        return "StringExpression(expression=${expression.contentToString()})"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as StringExpression
+
+        if (!expression.contentEquals(other.expression)) return false
+
+        return true
+    }
+
+    override fun hashCode() = expression.contentHashCode()
+}
+
+/**
+ * A [ComplicationText] where the system evaluates a [StringExpression] on behalf of the watch face.
+ * By the time this reaches the watch face's Renderer, it'll have been converted to a plain
+ * ComplicationText.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class StringExpressionComplicationText(
+    public val expression: StringExpression
+) : ComplicationText {
+    private val delegate = DelegatingComplicationText(WireComplicationText(expression))
+
+    override fun getTextAt(resources: Resources, instant: Instant) =
+        delegate.getTextAt(resources, instant)
+
+    override fun returnsSameText(firstInstant: Instant, secondInstant: Instant) =
+        delegate.returnsSameText(firstInstant, secondInstant)
+
+    override fun getNextChangeTime(afterInstant: Instant): Instant =
+        delegate.getNextChangeTime(afterInstant)
+
+    override fun isAlwaysEmpty() = delegate.isAlwaysEmpty()
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    override fun isPlaceholder(): Boolean = delegate.isPlaceholder()
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.SUBCLASSES)
+    override fun getTimeDependentText() = delegate.getTimeDependentText()
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    override fun toWireComplicationText() = delegate.toWireComplicationText()
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as StringExpressionComplicationText
+
+        if (delegate != other.delegate) return false
+
+        return true
+    }
+
+    override fun hashCode() = delegate.hashCode()
+
+    override fun toString() = delegate.toString()
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index 4494346..fefb8be 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -27,7 +27,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
-import androidx.wear.watchface.complications.data.toDynamicFloat
+import androidx.wear.watchface.complications.data.toFloatExpression
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Assert.assertThrows
@@ -97,7 +97,7 @@
         // WHEN the relevant getters are called on the resulting data
         // THEN the correct values are returned.
         Assert.assertEquals(data.rangedValue, 57f, 0f)
-        Assert.assertNull(data.rangedDynamicValue)
+        Assert.assertNull(data.rangedValueExpression)
         Assert.assertEquals(data.rangedMinValue, 5f, 0f)
         Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
         Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
@@ -111,7 +111,7 @@
         // GIVEN complication data of the RANGED_VALUE type created by the Builder...
         val data =
             ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
-                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                 .setRangedMinValue(5f)
                 .setRangedMaxValue(150f)
                 .setShortTitle(ComplicationText.plainText("title"))
@@ -120,7 +120,7 @@
 
         // WHEN the relevant getters are called on the resulting data
         // THEN the correct values are returned.
-        Truth.assertThat(data.rangedDynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        Truth.assertThat(data.rangedValueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
         Assert.assertEquals(data.rangedMinValue, 5f, 0f)
         Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
         Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 2a85fef..75dda2c 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -21,6 +21,7 @@
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.watchface.complications.data.StringExpression
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
 import com.google.common.truth.Truth
 import org.junit.Assert
@@ -799,4 +800,30 @@
         Truth.assertThat(text.getNextChangeTime(600000123))
             .isEqualTo(600060000)
     }
-}
\ No newline at end of file
+
+    @Test
+    public fun stringExpressionToParcelRoundTrip() {
+        val text = ComplicationText(StringExpression(byteArrayOf(1, 2, 3)))
+
+        Truth.assertThat(text.toParcelRoundTrip()).isEqualTo(text)
+    }
+
+    @Test
+    public fun getTextAt_ignoresStringExpressionIfSurroundingStringPresent() {
+        val text = ComplicationText(
+            "hello" as CharSequence,
+            /* timeDependentText = */ null,
+            StringExpression(byteArrayOf(1, 2, 3))
+        )
+
+        Truth.assertThat(text.getTextAt(mResources, 132456789).toString())
+            .isEqualTo("hello")
+    }
+}
+
+fun ComplicationText.toParcelRoundTrip(): ComplicationText {
+    val parcel = Parcel.obtain()
+    writeToParcel(parcel, /* flags = */ 0)
+    parcel.setDataPosition(0)
+    return ComplicationText.CREATOR.createFromParcel(parcel)
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 44c76f8..14e3a3d 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -84,9 +84,9 @@
         assertThat(data).isEqualTo(NoDataComplicationData())
         assertThat(data.hashCode()).isEqualTo(NoDataComplicationData().hashCode())
         assertThat(data.toString()).isEqualTo(
-            "NoDataComplicationData(placeholder=null, " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+            "NoDataComplicationData(placeholder=null, tapActionLostDueToSerialization=false," +
+                " tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
                 "+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -178,13 +178,14 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
-                "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
-                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
-                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, persistencePolicy=1, displayPolicy=1)"
+                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=1, displayPolicy=1)"
         )
     }
 
@@ -251,13 +252,14 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
-                "mTimeDependentText=null}, monochromaticImage=MonochromaticImage(" +
-                "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
-                "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
-                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
+                "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
+                "type=PHOTO, ambientImage=null), contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
@@ -313,13 +315,13 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
-                "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -387,15 +389,16 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
-                "mTimeDependentText=null}, monochromaticImage=MonochromaticImage(image=" +
-                "Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(image=" +
-                "Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
+                "mTimeDependentText=null, mStringExpression=null}, " +
+                "title=ComplicationText{mSurroundingText=title, mTimeDependentText=null, " +
+                "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+                "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
+                "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -428,7 +431,7 @@
         assertThat(deserialized.max).isEqualTo(100f)
         assertThat(deserialized.min).isEqualTo(0f)
         assertThat(deserialized.value).isEqualTo(95f)
-        assertThat(deserialized.dynamicValue).isNull()
+        assertThat(deserialized.valueExpression).isNull()
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
             .isEqualTo("content description")
         assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
@@ -455,24 +458,23 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
-                "valueType=0, min=0.0, max=100.0, " +
-                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
-                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+            "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
+                "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=battery, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
     }
 
     @Test
-    public fun rangedValueComplicationData_withDynamicValue() {
+    public fun rangedValueComplicationData_withValueExpression() {
         val data = RangedValueComplicationData.Builder(
-            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
             min = 5f,
             max = 100f,
             contentDescription = "content description".complicationText
@@ -483,7 +485,7 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-                    .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                    .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                     .setRangedValue(5f) // min as a sensible default
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(5f)
@@ -499,7 +501,7 @@
         val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
         assertThat(deserialized.max).isEqualTo(100f)
         assertThat(deserialized.min).isEqualTo(5f)
-        assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
         assertThat(deserialized.value).isEqualTo(5f) // min as a sensible default
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
             .isEqualTo("content description")
@@ -507,7 +509,7 @@
             .isEqualTo("battery")
 
         val sameData = RangedValueComplicationData.Builder(
-            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
             min = 5f,
             max = 100f,
             contentDescription = "content description".complicationText
@@ -524,8 +526,8 @@
             .setTitle("battery".complicationText)
             .setDataSource(dataSourceA)
             .build()
-        val diffDataDynamicValue = RangedValueComplicationData.Builder(
-            dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+        val diffDataValueExpression = RangedValueComplicationData.Builder(
+            valueExpression = byteArrayOf(43, 108).toFloatExpression(),
             min = 5f,
             max = 100f,
             contentDescription = "content description".complicationText
@@ -536,26 +538,97 @@
 
         assertThat(data).isEqualTo(sameData)
         assertThat(data).isNotEqualTo(diffDataFixedValue)
-        assertThat(data).isNotEqualTo(diffDataDynamicValue)
+        assertThat(data).isNotEqualTo(diffDataValueExpression)
         assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(diffDataFixedValue.hashCode())
-        assertThat(data.hashCode()).isNotEqualTo(diffDataDynamicValue.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(diffDataValueExpression.hashCode())
         assertThat(data.toString()).isEqualTo(
             "RangedValueComplicationData(value=5.0, " +
-                "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
-                "valueType=0, min=5.0, max=100.0, " +
-                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "valueExpression=FloatExpressionPlaceholder[42, 107], valueType=0, min=5.0, " +
+                "max=100.0, monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+                "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+                "text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
                 "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
     }
 
+    @Test
+    public fun rangedValueComplicationData_withStringExpression() {
+        val data = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle(
+                StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+            )
+            .setDataSource(dataSourceA)
+            .build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                    .setRangedValue(95f)
+                    .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
+                    .setRangedMinValue(0f)
+                    .setRangedMaxValue(100f)
+                    .setShortTitle(
+                        WireComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+                    )
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
+        assertThat(deserialized.max).isEqualTo(100f)
+        assertThat(deserialized.min).isEqualTo(0f)
+        assertThat(deserialized.value).isEqualTo(95f)
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+
+        val sameData = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle(
+                StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+            )
+            .setDataSource(dataSourceA)
+            .build()
+
+        val differentData = RangedValueComplicationData.Builder(
+            value = 95f, min = 0f, max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle(StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 4, 5))))
+            .setDataSource(dataSourceB)
+            .build()
+
+        assertThat(data).isEqualTo(sameData)
+        assertThat(data).isNotEqualTo(differentData)
+        assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(differentData.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
+                "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=(null), mTimeDependentText=null, " +
+                "mStringExpression=StringExpression(expression=[1, 2, 3, 4, 5])}, text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+                "displayPolicy=0)"
+        )
+    }
+
     @RequiresApi(Build.VERSION_CODES.P)
     @Test
     public fun rangedValueComplicationData_withImages() {
@@ -626,15 +699,16 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+            "RangedValueComplicationData(value=95.0, valueExpression=null, " +
                 "valueType=1, min=0.0, max=100.0, " +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
-                " mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                " mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
@@ -665,7 +739,7 @@
         testRoundTripConversions(data)
         val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
         assertThat(deserialized.value).isEqualTo(1200f)
-        assertThat(deserialized.dynamicValue).isNull()
+        assertThat(deserialized.valueExpression).isNull()
         assertThat(deserialized.targetValue).isEqualTo(10000f)
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
             .isEqualTo("content description")
@@ -693,24 +767,23 @@
         assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+            "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
-                "text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
-                "persistencePolicy=0, displayPolicy=0)"
+                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+                "displayPolicy=0)"
         )
     }
 
     @Test
-    public fun goalProgressComplicationData_withDynamicValue() {
+    public fun goalProgressComplicationData_withValueExpression() {
         val data = GoalProgressComplicationData.Builder(
-            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
             targetValue = 10000f,
             contentDescription = "content description".complicationText
         )
@@ -720,7 +793,7 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
-                    .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                    .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                     .setRangedValue(0f) // sensible default
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -732,7 +805,7 @@
             )
         testRoundTripConversions(data)
         val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
-        assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
         assertThat(deserialized.value).isEqualTo(0f) // sensible default
         assertThat(deserialized.targetValue).isEqualTo(10000f)
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
@@ -741,7 +814,7 @@
             .isEqualTo("steps")
 
         val sameData = GoalProgressComplicationData.Builder(
-            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
             targetValue = 10000f,
             contentDescription = "content description".complicationText
         )
@@ -750,7 +823,7 @@
             .build()
 
         val diffData = GoalProgressComplicationData.Builder(
-            dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+            valueExpression = byteArrayOf(43, 108).toFloatExpression(),
             targetValue = 10000f,
             contentDescription = "content description".complicationText
         )
@@ -763,18 +836,17 @@
         assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=0.0, " +
-                "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
-                "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
-                "text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
-                "persistencePolicy=0, displayPolicy=0)"
+            "GoalProgressComplicationData(value=0.0, valueExpression=" +
+                "FloatExpressionPlaceholder[42, 107], targetValue=10000.0, " +
+                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+                "mSurroundingText=steps, mTimeDependentText=null, mStringExpression=null}, " +
+                "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
+                "description, mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+                "displayPolicy=0)"
         )
     }
 
@@ -834,17 +906,16 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+            "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
-                "text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, -16711936, " +
-                "-16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
+                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+                "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
         )
     }
 
@@ -917,19 +988,19 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+            "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
                 "targetValue=10000.0, " +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=steps, " +
-                "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
-                "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
-                "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
-                "persistencePolicy=0, displayPolicy=0)"
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+                "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
         )
     }
 
@@ -992,17 +1063,17 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+            "RangedValueComplicationData(value=95.0, valueExpression=null, " +
                 "valueType=0, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
-                "ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, -16711936, " +
-                "-16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
+                "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+                "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
+                "description, mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+                "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
         )
     }
 
@@ -1094,13 +1165,12 @@
             "WeightedElementsComplicationData(elements=Element(color=-65536, weight=0.5)," +
                 " Element(color=-16711936, weight=1.0), Element(color=-16776961, weight=2.0), " +
                 "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null}, " +
-                "text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -1193,12 +1263,12 @@
                 "image=Icon(typ=URI uri=someuri), ambientImage=null), " +
                 "smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
                 "ambientImage=null), title=ComplicationText{mSurroundingText=calories, " +
-                "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
+                "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
-                "validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -1239,12 +1309,12 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "MonochromaticImageComplicationData(monochromaticImage=MonochromaticImage(image=" +
-                "Icon(typ=URI uri=someuri), ambientImage=null), contentDescription=" +
-                "ComplicationText{mSurroundingText=content description, mTimeDependentText=null})" +
-                ", tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+            "MonochromaticImageComplicationData(monochromaticImage=MonochromaticImage(" +
+                "image=Icon(typ=URI uri=someuri), ambientImage=null), contentDescription=" +
+                "ComplicationText{mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -1290,12 +1360,13 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "SmallImageComplicationData(smallImage=SmallImage(image=Icon(typ=URI uri=someuri)" +
-                ", type=PHOTO, ambientImage=null), contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+            "SmallImageComplicationData(smallImage=SmallImage(image=Icon(" +
+                "typ=URI uri=someuri), type=PHOTO, ambientImage=null), " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -1372,11 +1443,12 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "PhotoImageComplicationData(photoImage=Icon(typ=URI uri=someuri), contentDescription=" +
-                "ComplicationText{mSurroundingText=content description, mTimeDependentText=null})" +
-                ", tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+            "PhotoImageComplicationData(photoImage=Icon(typ=URI uri=someuri), " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
@@ -1416,12 +1488,13 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=needs location," +
-                " mTimeDependentText=null}, title=null, monochromaticImage=null, smallImage=null," +
-                " tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
-                "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
+            "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=needs location, " +
+                "mTimeDependentText=null, mStringExpression=null}, title=null, " +
+                "monochromaticImage=null, smallImage=null, tapActionLostDueToSerialization=false," +
+                " tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+                "+1000000000-12-31T23:59:59.999999999Z), dataSource=ComponentInfo{" +
+                "com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
     }
 
@@ -1467,12 +1540,13 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=" +
-                "needs location, mTimeDependentText=null}, title=null, monochromaticImage=" +
-                "MonochromaticImage(image=Icon(typ=URI uri=someuri), ambientImage=null), " +
-                "smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
-                "ambientImage=null), tapActionLostDueToSerialization=false, tapAction=null, " +
-                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+            "NoPermissionComplicationData(text=ComplicationText{" +
+                "mSurroundingText=needs location, mTimeDependentText=null, " +
+                "mStringExpression=null}, title=null, monochromaticImage=MonochromaticImage(" +
+                "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
+                "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
         )
@@ -1539,18 +1613,19 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=ShortTextComplicationData(text=" +
-                "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
-                "title=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText" +
-                "=null}, monochromaticImage=MonochromaticImage(image=Icon(typ=RESOURCE pkg= " +
-                "id=0xffffffff), ambientImage=null), smallImage=null, " +
-                "contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}, " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+                "mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+                "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+                "image=Icon(typ=RESOURCE pkg= id=0xffffffff), ambientImage=null), " +
+                "smallImage=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
-                "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null," +
-                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
@@ -1605,13 +1680,14 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "NoDataComplicationData(placeholder=LongTextComplicationData(text=" +
-                "ComplicationText{mSurroundingText=text, mTimeDependentText=null}, title=null, " +
-                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+            "NoDataComplicationData(placeholder=LongTextComplicationData(" +
+                "text=ComplicationText{mSurroundingText=text, mTimeDependentText=null, " +
+                "mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -1688,13 +1764,13 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=RangedValueComplicationData(" +
-                "value=3.4028235E38, dynamicValue=null, valueType=0, min=0.0, max=100.0, " +
+                "value=3.4028235E38, valueExpression=null, valueType=0, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
-                "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
-                "contentDescription=ComplicationText{mSurroundingText=" +
-                "content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
@@ -1771,19 +1847,19 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=GoalProgressComplicationData(" +
-                "value=3.4028235E38, dynamicValue=null, targetValue=10000.0, " +
+                "value=3.4028235E38, valueExpression=null, targetValue=10000.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
-                "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
-                "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
-                "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=false)," +
-                " persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=false), " +
+                "persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
+                "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+                "-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
@@ -1866,12 +1942,12 @@
             "NoDataComplicationData(placeholder=WeightedElementsComplicationData(" +
                 "elements=Element(color=-65536, weight=0.5), Element(color=-16711936, " +
                 "weight=1.0), Element(color=-16776961, weight=2.0), " +
-                "elementBackgroundColor=-7829368, monochromaticImage=null, " +
-                "smallImage=null, title=ComplicationText{mSurroundingText=calories, " +
-                "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
@@ -1955,19 +2031,18 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=RangedValueComplicationData(" +
-                "value=3.4028235E38, dynamicValue=null, valueType=1, min=0.0, max=100.0, " +
+                "value=3.4028235E38, valueExpression=null, valueType=1, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
-                "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
-                "contentDescription=ComplicationText{mSurroundingText=" +
-                "content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, " +
-                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
-                "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
-                "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
-                "persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+                "-16711936, -16776961], interpolated=true), persistencePolicy=0, " +
+                "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
@@ -2027,10 +2102,10 @@
             "NoDataComplicationData(placeholder=MonochromaticImageComplicationData(" +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=RESOURCE pkg= " +
                 "id=0xffffffff), ambientImage=null), contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "mSurroundingText=content description, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2094,10 +2169,10 @@
             "NoDataComplicationData(placeholder=SmallImageComplicationData(smallImage=" +
                 "SmallImage(image=Icon(typ=RESOURCE pkg= id=0xffffffff), type=ICON, " +
                 "ambientImage=null), contentDescription=ComplicationText{mSurroundingText=" +
-                "content description, mTimeDependentText=null}), tapActionLostDueToSerialization=" +
-                "false, tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
-                "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+                "content description, mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2156,12 +2231,13 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "NoDataComplicationData(placeholder=PhotoImageComplicationData(photoImage=" +
-                "Icon(typ=RESOURCE pkg= id=0xffffffff), contentDescription=ComplicationText{" +
-                "mSurroundingText=content description, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
-                "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
-                "+1000000000-12-31T23:59:59.999999999Z), " +
+            "NoDataComplicationData(placeholder=PhotoImageComplicationData(" +
+                "photoImage=Icon(typ=RESOURCE pkg= id=0xffffffff), " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+                "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2267,10 +2343,10 @@
     }
 
     @Test
-    public fun rangedValueComplicationData_withDynamicValue() {
+    public fun rangedValueComplicationData_withValueExpression() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
-                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                 .setRangedMinValue(0f)
                 .setRangedMaxValue(100f)
                 .setShortTitle(WireComplicationText.plainText("battery"))
@@ -2316,10 +2392,10 @@
     }
 
     @Test
-    public fun goalProgressComplicationData_withDynamicValue() {
+    public fun goalProgressComplicationData_withValueExpression() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
-                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
                 .setTargetValue(10000f)
                 .setShortTitle(WireComplicationText.plainText("steps"))
                 .setContentDescription(WireComplicationText.plainText("content description"))
@@ -3251,11 +3327,12 @@
 
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
-                " validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
@@ -3273,13 +3350,14 @@
             .build()
 
         assertThat(data.toString()).isEqualTo(
-            "LongTextComplicationData(text=" +
-                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
-                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
-                "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
-                "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
+            "LongTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
+                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+                "displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=4, mFields=REDACTED}"
@@ -3299,14 +3377,15 @@
             .build()
 
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+            "RangedValueComplicationData(value=REDACTED, valueExpression=REDACTED, " +
                 "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-                "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-                "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
-                "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
+                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=ComplicationText{mSurroundingText=REDACTED, " +
+                "mTimeDependentText=null, mStringExpression=null}, contentDescription=" +
+                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                " validTimeRange=TimeRange(REDACTED), dataSource=null, colorRamp=null, " +
+                "persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=5, mFields=REDACTED}"
@@ -3325,14 +3404,14 @@
             .build()
 
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+            "GoalProgressComplicationData(value=REDACTED, valueExpression=REDACTED, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
-                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-                "text=null, contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-                "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
-                "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
-                "persistencePolicy=0, displayPolicy=0)"
+                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
+                "TimeRange(REDACTED), dataSource=null, colorRamp=ColorRamp(colors=[-65536, " +
+                "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=13, mFields=REDACTED}"
@@ -3349,13 +3428,14 @@
         )
 
         assertThat(data.toString()).isEqualTo(
-            "NoDataComplicationData(placeholder=LongTextComplicationData(text=" +
-                "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
-                "title=null, monochromaticImage=null, smallImage=null, contentDescription=" +
-                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
-                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
-                "TimeRange(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0), " +
+            "NoDataComplicationData(placeholder=LongTextComplicationData(" +
+                "text=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null," +
+                " mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+                "mTimeDependentText=null, mStringExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
+                "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+                "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(REDACTED), persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
index b691df3..e16b877 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
@@ -238,7 +238,9 @@
             ComplicationText.FORMAT_STYLE_DEFAULT,
             null
         )
-        val text = TimeDifferenceComplicationText(ComplicationText("test", tft))
+        val text = TimeDifferenceComplicationText(
+            ComplicationText("test", tft, /* stringExpression = */ null)
+        )
 
         assertNull(text.getMinimumTimeUnit())
     }
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/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 1d98429..d5c8fff 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -684,15 +684,20 @@
     }
 
     /**
-     * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
-     * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
-     * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
-     * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
-     * it returns `null`.
+     * When a UserStyleSchema contains hierarchical styles, only part of it is deemed to be active
+     * based on the user’s options in [userStyle]. Conversely if the UserStyleSchema doesn’t contain
+     * any hierarchical styles then all of it is considered to be active all the time.
+     *
+     * From the active portion of the UserStyleSchema we only allow there to be at most one
+     * [UserStyleSetting.ComplicationSlotsUserStyleSetting]. This function searches the active
+     * portion of the UserStyleSchema for the [UserStyleSetting.ComplicationSlotsUserStyleSetting],
+     * if one is found then it returns the selected [ComplicationSlotsOption] from that, based on
+     * the [userStyle]. If a [UserStyleSetting.ComplicationSlotsUserStyleSetting] is not found in
+     * the active portion of the UserStyleSchema it returns `null`.
      *
      * @param userStyle The [UserStyle] for which the function will search for the selected
      * [ComplicationSlotsOption], if any.
-     * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+     * @return The selected [ComplicationSlotsOption] based on the [userStyle] if any, or `null`
      * otherwise.
      */
     public fun findComplicationSlotsOptionForUserStyle(
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/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 0e94efe..2eb58be 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -350,10 +350,20 @@
         public val XML_WATCH_FACE_METADATA =
             "androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
 
-        internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
-            engine: WatchFaceService.EngineWrapper?,
+        internal enum class ExecutionThread {
+            UI,
+            CURRENT
+        }
+        /**
+         * Waits for deferredValue using runBlocking, then executes the task on the thread
+         * specified by executionThread param.
+         */
+        private fun <R, V> awaitDeferredThenRunTaskOnThread(
+            engine: EngineWrapper?,
             traceName: String,
-            task: (watchFaceImpl: WatchFaceImpl) -> R
+            executionThread: ExecutionThread,
+            task: (deferredValue: V) -> R,
+            waitDeferred: suspend (engine: EngineWrapper) -> V
         ): R? = TraceEvent(traceName).use {
             if (engine == null) {
                 Log.w(TAG, "Task $traceName posted after close(), ignoring.")
@@ -362,9 +372,16 @@
             runBlocking {
                 try {
                     withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        val watchFaceImpl = engine.deferredWatchFaceImpl.await()
-                        withContext(engine.uiThreadCoroutineScope.coroutineContext) {
-                            task(watchFaceImpl)
+                        val deferredValue = waitDeferred(engine)
+                        when (executionThread) {
+                            ExecutionThread.UI -> {
+                                withContext(engine.uiThreadCoroutineScope.coroutineContext) {
+                                    task(deferredValue)
+                                }
+                            }
+                            ExecutionThread.CURRENT -> {
+                                task(deferredValue)
+                            }
                         }
                     }
                 } catch (e: Exception) {
@@ -374,29 +391,24 @@
             }
         }
 
+        internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
+            engine: EngineWrapper?,
+            traceName: String,
+            task: (watchFaceImpl: WatchFaceImpl) -> R
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+            it.deferredWatchFaceImpl.await()
+        }
+
         /**
          * During startup tasks will run before those posted by
          * [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking].
          */
-        internal fun <R> awaitDeferredWatchFaceThenRunOnBinderThread(
-            engine: WatchFaceService.EngineWrapper?,
+        internal fun <R> awaitDeferredWatchFaceThenRunOnUiThread(
+            engine: EngineWrapper?,
             traceName: String,
             task: (watchFace: WatchFace) -> R
-        ): R? = TraceEvent(traceName).use {
-            if (engine == null) {
-                Log.w(TAG, "Task $traceName posted after close(), ignoring.")
-                return null
-            }
-            runBlocking {
-                try {
-                    withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        task(engine.deferredWatchFace.await())
-                    }
-                } catch (e: Exception) {
-                    Log.e(TAG, "Operation $traceName failed", e)
-                    throw e
-                }
-            }
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+            it.deferredWatchFace.await()
         }
 
         /**
@@ -404,25 +416,13 @@
          * [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking] and
          * [awaitDeferredWatchFaceThenRunOnBinderThread].
          */
-        internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine: WatchFaceService.EngineWrapper?,
+        internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engine: EngineWrapper?,
             traceName: String,
+            executionThread: ExecutionThread,
             task: (earlyInitDetails: EarlyInitDetails) -> R
-        ): R? = TraceEvent(traceName).use {
-            if (engine == null) {
-                Log.w(TAG, "Task $traceName ignored due to null engine.")
-                return null
-            }
-            runBlocking {
-                try {
-                    withTimeout(AWAIT_DEFERRED_TIMEOUT) {
-                        task(engine.deferredEarlyInitDetails.await())
-                    }
-                } catch (e: Exception) {
-                    Log.e(TAG, "Operation $traceName failed", e)
-                    throw e
-                }
-            }
+        ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, executionThread, task) {
+            it.deferredEarlyInitDetails.await()
         }
     }
 
@@ -819,30 +819,31 @@
     ) = TraceEvent(
         "WatchFaceService.writeComplicationCache"
     ).use {
-        // File IO can be slow so perform the write from a background thread.
-        getBackgroundThreadHandler().post {
-            try {
-                val stream = ByteArrayOutputStream()
-                val objectOutputStream = ObjectOutputStream(stream)
-                objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
-                for (slot in complicationSlotsManager.complicationSlots) {
-                    objectOutputStream.writeInt(slot.key)
-                    objectOutputStream.writeObject(
-                        if ((slot.value.complicationData.value.persistencePolicy and
-                              ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
-                        ) {
-                            NoDataComplicationData().asWireComplicationData()
-                        } else {
-                            slot.value.complicationData.value.asWireComplicationData()
-                        }
-                    )
-                }
-                objectOutputStream.close()
-                val byteArray = stream.toByteArray()
-                writeComplicationDataCacheByteArray(context, fileName, byteArray)
-            } catch (e: Exception) {
-                Log.w(TAG, "Failed to write to complication cache due to exception", e)
+        try {
+            val stream = ByteArrayOutputStream()
+            val objectOutputStream = ObjectOutputStream(stream)
+            objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+            for (slot in complicationSlotsManager.complicationSlots) {
+                objectOutputStream.writeInt(slot.key)
+                objectOutputStream.writeObject(
+                    if ((slot.value.complicationData.value.persistencePolicy and
+                            ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+                    ) {
+                        NoDataComplicationData().asWireComplicationData()
+                    } else {
+                        slot.value.complicationData.value.asWireComplicationData()
+                    }
+                )
             }
+            objectOutputStream.close()
+            val byteArray = stream.toByteArray()
+
+            // File IO can be slow so perform the write from a background thread.
+            getBackgroundThreadHandler().post {
+                writeComplicationDataCacheByteArray(context, fileName, byteArray)
+            }
+        } catch (e: Exception) {
+            Log.w(TAG, "Failed to write to complication cache due to exception", e)
         }
     }
 
@@ -1137,8 +1138,7 @@
     internal class EarlyInitDetails(
         val complicationSlotsManager: ComplicationSlotsManager,
         val userStyleRepository: CurrentUserStyleRepository,
-        val userStyleFlavors: UserStyleFlavors,
-        val surfaceHolder: SurfaceHolder
+        val userStyleFlavors: UserStyleFlavors
     )
 
     /** @hide */
@@ -1182,7 +1182,7 @@
          * [deferredSurfaceHolder] will complete after [onSurfaceChanged], before then it's not
          * safe to create a UiThread OpenGL context.
          */
-        internal var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
+        private var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
 
         internal val mutableWatchState = getMutableWatchState().apply {
             isVisible.value = this@EngineWrapper.isVisible || forceIsVisibleForTesting()
@@ -2028,6 +2028,15 @@
             asyncWatchFaceConstructionPending = true
             createdBy = _createdBy
 
+            // In case of overrideSurfaceHolder provided (tests) return its size instead of real
+            // metrics.
+            screenBounds = if (overrideSurfaceHolder != null) {
+                overrideSurfaceHolder.surfaceFrame
+            } else {
+                val displayMetrics = resources.displayMetrics
+                Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
+            }
+
             backgroundThreadCoroutineScope.launch {
                 val timeBefore = System.currentTimeMillis()
                 val currentUserStyleRepository =
@@ -2049,6 +2058,14 @@
                         createUserStyleFlavors(currentUserStyleRepository, complicationSlotsManager)
                     }
 
+                deferredEarlyInitDetails.complete(
+                    EarlyInitDetails(
+                        complicationSlotsManager,
+                        currentUserStyleRepository,
+                        userStyleFlavors
+                    )
+                )
+
                 val deferredWatchFace = CompletableDeferred<WatchFace>()
                 val initComplicationsDone = CompletableDeferred<Unit>()
 
@@ -2067,14 +2084,6 @@
 
                 try {
                     val surfaceHolder = overrideSurfaceHolder ?: deferredSurfaceHolder.await()
-                    deferredEarlyInitDetails.complete(
-                        EarlyInitDetails(
-                            complicationSlotsManager,
-                            currentUserStyleRepository,
-                            userStyleFlavors,
-                            surfaceHolder
-                        )
-                    )
 
                     val watchFace = TraceEvent("WatchFaceService.createWatchFace").use {
                         // Note by awaiting deferredSurfaceHolder we ensure onSurfaceChanged has
@@ -2659,6 +2668,9 @@
             }
         }
 
+        internal lateinit var screenBounds: Rect
+            private set
+
         @UiThread
         internal fun dump(writer: IndentingPrintWriter) {
             require(uiThreadHandler.looper.isCurrentThread) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index badd1a8..a5f3897 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -85,11 +85,14 @@
             "HeadlessWatchFaceImpl.getPreviewReferenceTimeMillis"
         ) { it.previewReferenceInstant.toEpochMilli() } ?: 0
 
-    override fun getComplicationState() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine,
-            "HeadlessWatchFaceImpl.getComplicationState"
-        ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+    override fun getComplicationState() = run {
+        val engineCopy = engine
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engineCopy,
+            "HeadlessWatchFaceImpl.getComplicationState",
+            WatchFaceService.Companion.ExecutionThread.UI
+        ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
+    }
 
     override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
         WatchFaceService.awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
@@ -98,21 +101,24 @@
         ) { it.renderComplicationToBitmap(params) }
 
     override fun getUserStyleSchema() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.getUserStyleSchema"
+            "HeadlessWatchFaceImpl.getUserStyleSchema",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.toWireFormat() }
 
     override fun computeUserStyleSchemaDigestHash() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash"
+            "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.getDigestHash() }
 
     override fun getUserStyleFlavors() =
-        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "HeadlessWatchFaceImpl.getUserStyleFlavors"
+            "HeadlessWatchFaceImpl.getUserStyleFlavors",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleFlavors.toWireFormat() }
 
     override fun release() {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 3134a78..ba6fce5 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -87,7 +87,7 @@
     }
 
     override fun getWatchFaceOverlayStyle(): WatchFaceOverlayStyleWireFormat? =
-        WatchFaceService.awaitDeferredWatchFaceThenRunOnBinderThread(
+        WatchFaceService.awaitDeferredWatchFaceThenRunOnUiThread(
             engine,
             "InteractiveWatchFaceImpl.getWatchFaceOverlayStyle"
         ) { WatchFaceOverlayStyleWireFormat(
@@ -189,16 +189,19 @@
     }
 
     override fun getComplicationDetails(): List<IdAndComplicationStateWireFormat>? {
-        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
-            engine,
-            "InteractiveWatchFaceImpl.getComplicationDetails"
-        ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+        val engineCopy = engine
+        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+            engineCopy,
+            "InteractiveWatchFaceImpl.getComplicationDetails",
+            WatchFaceService.Companion.ExecutionThread.UI
+        ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
     }
 
     override fun getUserStyleSchema(): UserStyleSchemaWireFormat? {
-        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+        return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
             engine,
-            "InteractiveWatchFaceImpl.getUserStyleSchema"
+            "InteractiveWatchFaceImpl.getUserStyleSchema",
+            WatchFaceService.Companion.ExecutionThread.CURRENT
         ) { it.userStyleRepository.schema.toWireFormat() }
     }
 
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.