Merge "Add isClosed() method to the Recording class" into androidx-main
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index dd1dd37..3fa2a9a 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -141,8 +141,8 @@
     field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult!> CREATOR;
   }
 
-  public interface ActivityResultCallback<O> {
-    method public void onActivityResult(O!);
+  public fun interface ActivityResultCallback<O> {
+    method public void onActivityResult(O result);
   }
 
   public interface ActivityResultCaller {
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index dd1dd37..3fa2a9a 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -141,8 +141,8 @@
     field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult!> CREATOR;
   }
 
-  public interface ActivityResultCallback<O> {
-    method public void onActivityResult(O!);
+  public fun interface ActivityResultCallback<O> {
+    method public void onActivityResult(O result);
   }
 
   public interface ActivityResultCaller {
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 64662e2..39310a7a 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -140,8 +140,8 @@
     field public static final android.os.Parcelable.Creator<androidx.activity.result.ActivityResult!> CREATOR;
   }
 
-  public interface ActivityResultCallback<O> {
-    method public void onActivityResult(O!);
+  public fun interface ActivityResultCallback<O> {
+    method public void onActivityResult(O result);
   }
 
   public interface ActivityResultCaller {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
index dad575f..a6af035 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -59,12 +59,10 @@
 
         // register for the result
         val activityResult = registry.register(
-            "test", lifecycleOwner,
-            TakePicturePreview(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {
+            resultReturned = true
+        }
 
         // move the state to started
         lifecycleOwner.currentState = Lifecycle.State.STARTED
@@ -82,8 +80,8 @@
         // register for the result
         val activityResult = registry.register(
             "test", lifecycleOwner,
-            TakePicturePreview(), ActivityResultCallback {}
-        )
+            TakePicturePreview()
+        ) {}
 
         // saved the state of the registry
         val state = Bundle()
@@ -101,11 +99,10 @@
         var resultReturned = false
         // re-register for the result that should have been saved
         registry.register(
-            "test", lifecycleOwner, TakePicturePreview(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {
+            resultReturned = true
+        }
 
         lifecycleOwner.currentState = Lifecycle.State.STARTED
 
@@ -119,9 +116,8 @@
         try {
             // register for the result
             registry.register(
-                "test", lifecycleOwner,
-                TakePicturePreview(), ActivityResultCallback {}
-            )
+                "test", lifecycleOwner, TakePicturePreview()
+            ) {}
             fail("Registering for activity result after Lifecycle ON_CREATE should fail")
         } catch (e: IllegalStateException) {
             assertThat(e).hasMessageThat().contains(
@@ -138,9 +134,8 @@
 
         // register for the result
         val activityResult = registry.register(
-            "test", lifecycleOwner,
-            TakePicturePreview(), ActivityResultCallback {}
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {}
 
         // saved the state of the registry
         val state = Bundle()
@@ -155,11 +150,10 @@
         var resultReturned = false
         // re-register for the result that should have been saved
         registry.register(
-            "test", lifecycleOwner, TakePicturePreview(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {
+            resultReturned = true
+        }
 
         // launch the result
         activityResult.launch(null)
@@ -200,10 +194,9 @@
         var resultReturned = false
         val activityResult = dispatchResultRegistry.register(
             "test", lifecycleOwner, TakePicture(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+        ) {
+            resultReturned = true
+        }
 
         // launch the result
         activityResult.launch(null)
@@ -244,10 +237,9 @@
         var resultReturned = false
         val activityResult = dispatchResultRegistry.register(
             "test", lifecycleOwner, TakePicturePreview(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+        ) {
+            resultReturned = true
+        }
 
         // launch the result
         activityResult.launch(null)
@@ -276,9 +268,8 @@
 
         // register for the result
         val activityResult = registry.register(
-            "test", lifecycleOwner,
-            TakePicturePreview(), ActivityResultCallback {}
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {}
 
         // saved the state of the registry
         val state = Bundle()
@@ -296,11 +287,10 @@
         var resultReturned = false
         // re-register for the result that should have been saved
         registry.register(
-            "test", lifecycleOwner, TakePicturePreview(),
-            ActivityResultCallback {
-                resultReturned = true
-            }
-        )
+            "test", lifecycleOwner, TakePicturePreview()
+        ) {
+            resultReturned = true
+        }
 
         // move to CREATED and make sure the callback is not fired
         lifecycleOwner.currentState = Lifecycle.State.CREATED
diff --git a/activity/activity/src/main/java/androidx/activity/Cancellable.java b/activity/activity/src/main/java/androidx/activity/Cancellable.kt
similarity index 90%
rename from activity/activity/src/main/java/androidx/activity/Cancellable.java
rename to activity/activity/src/main/java/androidx/activity/Cancellable.kt
index a5cb90a..33af37c 100644
--- a/activity/activity/src/main/java/androidx/activity/Cancellable.java
+++ b/activity/activity/src/main/java/androidx/activity/Cancellable.kt
@@ -13,17 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.activity;
+package androidx.activity
 
 /**
  * Token representing a cancellable operation.
  */
-interface Cancellable {
-
+internal interface Cancellable {
     /**
      * Cancel the subscription. This call should be idempotent, making it safe to
      * call multiple times.
      */
-    void cancel();
-}
+    fun cancel()
+}
\ No newline at end of file
diff --git a/activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.java b/activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.kt
similarity index 66%
rename from activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.java
rename to activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.kt
index fde2497..048ef0e 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultCallback.kt
@@ -13,23 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package androidx.activity.result
 
-
-package androidx.activity.result;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
+import android.app.Activity
 
 /**
- * A type-safe callback to be called when an {@link Activity#onActivityResult activity result}
+ * A type-safe callback to be called when an [activity result][Activity.onActivityResult]
  * is available.
- *
- * @param <O> result type
  */
-public interface ActivityResultCallback<O> {
-
+fun interface ActivityResultCallback<O : Any> {
     /**
      * Called when result is available
      */
-    void onActivityResult(@SuppressLint("UnknownNullness") O result);
+    fun onActivityResult(result: O)
 }
diff --git a/benchmark/benchmark-common/api/public_plus_experimental_current.txt b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
index 24e4123..54c1187 100644
--- a/benchmark/benchmark-common/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
@@ -55,9 +55,30 @@
 
 package androidx.benchmark.perfetto {
 
+  @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
+  }
+
   public final class PerfettoConfigKt {
   }
 
+  @RequiresApi(21) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
+    ctor public PerfettoTrace(String path);
+    method public String getPath();
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    property public final String path;
+    field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
+  }
+
+  public static final class PerfettoTrace.Companion {
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+  }
+
   public final class UiStateKt {
   }
 
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
new file mode 100644
index 0000000..73d01c3
--- /dev/null
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/PerfettoTraceTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 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.benchmark
+
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalPerfettoCaptureApi::class)
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class PerfettoTraceTest {
+    @Test
+    fun record_basic() {
+        assumeTrue(PerfettoHelper.isAbiSupported())
+        var perfettoTrace: PerfettoTrace? = null
+        PerfettoTrace.record(
+            fileLabel = "testTrace",
+            traceCallback = { trace ->
+                perfettoTrace = trace
+            }
+        ) {
+            // noop
+        }
+        assertNotNull(perfettoTrace)
+        assert(perfettoTrace!!.path.matches(Regex(".*/testTrace_[0-9-]+.perfetto-trace"))) {
+            "$perfettoTrace didn't match!"
+        }
+    }
+    @Test
+    fun record_reentrant() {
+        assumeTrue(PerfettoHelper.isAbiSupported())
+        var perfettoTrace: PerfettoTrace? = null
+        PerfettoTrace.record(
+            fileLabel = "outer",
+            traceCallback = { trace ->
+                perfettoTrace = trace
+            }
+        ) {
+            // tracing while tracing should fail
+            assertFailsWith<IllegalStateException> {
+                PerfettoTrace.record(
+                    fileLabel = "inner",
+                    traceCallback = { _ ->
+                        fail("inner trace should not complete / record")
+                    }
+                ) {
+                    // noop
+                }
+            }
+        }
+        assertNotNull(perfettoTrace)
+    }
+}
diff --git a/activity/activity/src/main/java/androidx/activity/Cancellable.java b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
similarity index 63%
copy from activity/activity/src/main/java/androidx/activity/Cancellable.java
copy to benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
index a5cb90a..28d3e1f 100644
--- a/activity/activity/src/main/java/androidx/activity/Cancellable.java
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,16 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.activity;
+package androidx.benchmark.perfetto
 
 /**
- * Token representing a cancellable operation.
+ * Annotation indicating experimental API for capturing Perfetto traces.
  */
-interface Cancellable {
-
-    /**
-     * Cancel the subscription. This call should be idempotent, making it safe to
-     * call multiple times.
-     */
-    void cancel();
-}
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+public annotation class ExperimentalPerfettoCaptureApi
\ No newline at end of file
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index ebbe441..198ab7a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -39,6 +39,17 @@
         }
     }
 
+    companion object {
+        val inUseLock = Object()
+
+        /**
+         * Prevents re-entrance of perfetto trace capture, as it doesn't handle this correctly
+         *
+         * (Single file output location, process cleanup, etc.)
+         */
+        var inUse = false
+    }
+
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
     private fun start(
         appTagPackages: List<String>,
@@ -63,53 +74,62 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    private fun stop(benchmarkName: String, iteration: Int?): String {
-        val traceName: String
-        val reportKey: String
-        if (iteration != null) {
-            val iterString = iteration.toString().padStart(3, '0')
-            traceName = "${benchmarkName}_iter${iterString}_${dateToFileName()}.perfetto-trace"
-            reportKey = "perfetto_trace_$iterString"
-        } else {
-            traceName = "${benchmarkName}_${dateToFileName()}.perfetto-trace"
-            reportKey = "perfetto_trace"
-        }
-        return Outputs.writeFile(fileName = traceName, reportKey = reportKey) {
+    private fun stop(traceLabel: String): String {
+        return Outputs.writeFile(
+            fileName = "${traceLabel}_${dateToFileName()}.perfetto-trace",
+            reportKey = "perfetto_trace_$traceLabel"
+        ) {
             capture!!.stop(it.absolutePath)
         }
     }
 
     fun record(
-        benchmarkName: String,
+        fileLabel: String,
         appTagPackages: List<String>,
         userspaceTracingPackage: String?,
-        iteration: Int? = null,
+        traceCallback: ((String) -> Unit)? = null,
         block: () -> Unit
     ): String? {
         // skip if Perfetto not supported, or on Cuttlefish (where tracing doesn't work)
         if (Build.VERSION.SDK_INT < 21 || !isAbiSupported()) {
             block()
-            return null // tracing not supported
+            return null
         }
 
+        synchronized(inUseLock) {
+            if (inUse) {
+                throw IllegalStateException(
+                    "Reentrant Perfetto Tracing is not supported." +
+                        " This means you cannot use more than one of" +
+                        " BenchmarkRule/MacrobenchmarkRule/PerfettoTraceRule/PerfettoTrace.record" +
+                        " together."
+                )
+            }
+            inUse = true
+        }
         // Prior to Android 11 (R), a shell property must be set to enable perfetto tracing, see
         // https://perfetto.dev/docs/quickstart/android-tracing#starting-the-tracing-services
         val propOverride = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
             PropOverride(TRACE_ENABLE_PROP, "1")
         } else null
+
+        val path: String
         try {
             propOverride?.forceValue()
             start(appTagPackages, userspaceTracingPackage)
-            val path: String
             try {
                 block()
             } finally {
                 // finally here to ensure trace is fully recorded if block throws
-                path = stop(benchmarkName, iteration)
+                path = stop(fileLabel)
+                traceCallback?.invoke(path)
             }
             return path
         } finally {
             propOverride?.resetIfOverridden()
+            synchronized(inUseLock) {
+                inUse = false
+            }
         }
     }
 }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
new file mode 100644
index 0000000..98ed9d8
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.benchmark.perfetto
+
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+
+@RequiresApi(21)
+@ExperimentalPerfettoCaptureApi
+class PerfettoTrace(
+    /**
+     * Absolute file path of the trace.
+     *
+     * Note that the trace is not guaranteed to be placed into an app-accessible directory, and
+     * may require shell commands to access.
+     */
+    val path: String
+) {
+    companion object {
+        /**
+         * Record a Perfetto System Trace for the specified [block].
+         *
+         * ```
+         * PerfettoTrace.record("myTrace") {
+         *     // content in here is traced to myTrace_<timestamp>.perfetto_trace
+         * }
+         * ```
+         *
+         * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
+         * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTraceRule`.
+         *
+         * If the block throws, the trace is still captured and passed to [traceCallback].
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun record(
+            /**
+             * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
+             *
+             * This timestamp is used for uniqueness when trace files are pulled automatically to
+             * Studio.
+             */
+            fileLabel: String,
+            /**
+             * Target process to trace with app tag (enables android.os.Trace / androidx.Trace).
+             *
+             * By default, traces this process.
+             */
+            appTagPackages: List<String> = listOf(
+                InstrumentationRegistry.getInstrumentation().targetContext.packageName
+            ),
+            /**
+             * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`,
+             * ignored below API 30.
+             *
+             * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is
+             * currently experimental.
+             */
+            userspaceTracingPackage: String? = null,
+            /**
+             * Callback for trace capture.
+             *
+             * This callback allows you to process the trace even if the block throws, e.g. during
+             * a test failure.
+             */
+            traceCallback: ((PerfettoTrace) -> Unit)? = null,
+            /**
+             * Block to be traced.
+             */
+            block: () -> Unit
+        ) {
+            PerfettoCaptureWrapper().record(
+                fileLabel = fileLabel,
+                appTagPackages = appTagPackages,
+                userspaceTracingPackage = userspaceTracingPackage,
+                traceCallback = { path ->
+                    // emphasize the first package in the package list, or target package otherwise
+                    val highlightPackage = appTagPackages.firstOrNull()
+                        ?: InstrumentationRegistry.getInstrumentation().targetContext.packageName
+                    File(path).appendUiState(
+                        UiState(
+                            timelineStart = null,
+                            timelineEnd = null,
+                            highlightPackage = highlightPackage
+                        )
+                    )
+                    traceCallback?.invoke(PerfettoTrace(path))
+                },
+                block = block
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index e98a49b..a016541 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -20,6 +20,8 @@
 import java.util.concurrent.atomic.AtomicBoolean
 import org.gradle.api.Plugin
 import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.provider.Provider
 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
@@ -109,15 +111,47 @@
             it.xcResultPath.set(runDarwinBenchmarks.flatMap { task ->
                 task.xcResultPath
             })
+            val resultFileName = "${extension.xcodeProjectName.get()}-benchmark-result.json"
             it.outputFile.set(
                 project.layout.buildDirectory.file(
-                    "${extension.xcodeProjectName.get()}-benchmark-result.json"
+                    resultFileName
                 )
             )
+            val provider = project.benchmarksDistributionDirectory(extension)
+            val resultFile = provider.map { directory ->
+                directory.file(resultFileName)
+            }
+            it.distFile.set(resultFile)
+        }
+    }
+
+    private fun Project.benchmarksDistributionDirectory(
+        extension: DarwinBenchmarkPluginExtension
+    ): Provider<Directory> {
+        val distProvider = project.distributionDirectory()
+        val benchmarksDirProvider = distProvider.flatMap { distDir ->
+            extension.xcodeProjectName.map { projectName ->
+                val projectPath = project.path.replace(":", "/")
+                val benchmarksDirectory = File(distDir, DARWIN_BENCHMARKS_DIR)
+                File(benchmarksDirectory, "$projectPath/$projectName")
+            }
+        }
+        return project.layout.dir(benchmarksDirProvider)
+    }
+
+    private fun Project.distributionDirectory(): Provider<File> {
+        // We want to write metrics to library metrics specific location
+        // Context: b/257326666
+        return providers.environmentVariable(DIST_DIR).map { value ->
+            File(value, LIBRARY_METRICS)
         }
     }
 
     private companion object {
+        // Environment variables
+        const val DIST_DIR = "DIST_DIR"
+        const val LIBRARY_METRICS = "librarymetrics"
+        const val DARWIN_BENCHMARKS_DIR = "darwinBenchmarks"
         // Gradle Properties
         const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri"
 
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
index b95352d..7d4136d 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -27,6 +27,7 @@
 import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.tasks.CacheableTask
 import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -44,6 +45,10 @@
     @get:OutputFile
     abstract val outputFile: RegularFileProperty
 
+    @get:OutputFile
+    @get:Optional
+    abstract val distFile: RegularFileProperty
+
     @TaskAction
     fun benchmarkResults() {
         val xcResultFile = xcResultPath.get().asFile
@@ -68,5 +73,10 @@
             .toJson(metrics)
 
         outputFile.get().asFile.writeText(output)
+
+        // Add output to the DIST_DIR when specified
+        if (distFile.isPresent) {
+            distFile.get().asFile.writeText(output)
+        }
     }
 }
diff --git a/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt b/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
index 873f105..b376c11 100644
--- a/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
@@ -19,5 +19,16 @@
     method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
   }
 
+  @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
+    ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public boolean getEnableAppTagTracing();
+    method public boolean getEnableUserspaceTracing();
+    method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
+    property public final boolean enableAppTagTracing;
+    property public final boolean enableUserspaceTracing;
+    property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
+  }
+
 }
 
diff --git a/benchmark/benchmark-junit4/api/restricted_current.ignore b/benchmark/benchmark-junit4/api/restricted_current.ignore
index 7b0440e..b2f8308 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.ignore
+++ b/benchmark/benchmark-junit4/api/restricted_current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-RemovedMethod: androidx.benchmark.junit4.PerfettoRule#PerfettoRule():
-    Removed constructor androidx.benchmark.junit4.PerfettoRule()
+RemovedClass: androidx.benchmark.junit4.PerfettoRule:
+    Removed class androidx.benchmark.junit4.PerfettoRule
diff --git a/benchmark/benchmark-junit4/api/restricted_current.txt b/benchmark/benchmark-junit4/api/restricted_current.txt
index e6624c1..c2d8056 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-junit4/api/restricted_current.txt
@@ -20,14 +20,5 @@
     method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerfettoRule implements org.junit.rules.TestRule {
-    ctor public PerfettoRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public boolean getEnableAppTagTracing();
-    method public boolean getEnableUserspaceTracing();
-    property public final boolean enableAppTagTracing;
-    property public final boolean enableUserspaceTracing;
-  }
-
 }
 
diff --git a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt
deleted file mode 100644
index 37934ad..0000000
--- a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2021 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.benchmark.junit4
-
-import android.os.Build
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SmallTest
-import androidx.testutils.verifyWithPolling
-import androidx.tracing.Trace
-import androidx.tracing.trace
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest // recording is expensive
-@RunWith(AndroidJUnit4::class)
-class PerfettoRuleTest {
-    @get:Rule
-    val perfettoRule = PerfettoRule()
-
-    @Ignore("b/217256936")
-    @Test
-    fun tracingEnabled() {
-        trace("PerfettoCaptureTest") {
-            val traceShouldBeEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
-            verifyTraceEnable(traceShouldBeEnabled)
-        }
-
-        // NOTE: ideally, we'd validate the output file, but it's difficult to assert the
-        // behavior of the rule, since we can't really assert the result of a rule, which
-        // occurs after both @Test and @After
-    }
-}
-
-@SmallTest // not recording is cheap
-@RunWith(AndroidJUnit4::class)
-class PerfettoRuleControlTest {
-    @Test
-    fun tracingNotEnabled() {
-        verifyTraceEnable(false)
-    }
-}
-
-private fun verifyTraceEnable(enabled: Boolean) {
-    // We poll here, since we may need to wait for enable flags to propagate to apps
-    verifyWithPolling(
-        "Timeout waiting for Trace.isEnabled == $enabled",
-        periodMs = 50,
-        timeoutMs = 5000
-    ) {
-        Trace.isEnabled() == enabled
-    }
-}
\ No newline at end of file
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 9a33fe9..676ce86 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -212,7 +212,7 @@
             var userspaceTrace: perfetto.protos.Trace? = null
 
             val tracePath = PerfettoCaptureWrapper().record(
-                benchmarkName = uniqueName,
+                fileLabel = uniqueName,
                 appTagPackages = packages,
                 userspaceTracingPackage = null
             ) {
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
deleted file mode 100644
index 97fd9d6..0000000
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2021 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.benchmark.junit4
-
-import androidx.annotation.RestrictTo
-import androidx.benchmark.perfetto.PerfettoCaptureWrapper
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Add this rule to record a Perfetto trace for each test on Q+ devices.
- *
- * Relies on either AGP's additionalTestOutputDir copying, or (in Jetpack CI),
- * `additionalTestOutputFile_***` copying.
- *
- * When invoked locally with Gradle, file will be copied to host path like the following:
- *
- * ```
- * out/androidx/benchmark/benchmark-macro/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/<deviceName>/androidx.mypackage.TestClass_testMethod.perfetto-trace
- * ```
- *
- * Note: if run from Studio, the file must be `adb pull`-ed manually, e.g.:
- * ```
- * > adb pull /storage/emulated/0/Android/data/androidx.mypackage.test/files/test_data/androidx.mypackage.TestClass_testMethod.trace
- * ```
- *
- * You can check logcat for messages tagged "PerfettoCapture:" for the path of each perfetto trace.
- * ```
- * > adb pull /storage/emulated/0/Android/data/mypackage.test/files/PerfettoCaptureTest.trace
- * ```
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class PerfettoRule(
-    /**
-     * Pass false to disable android.os.Trace API tracing in this process
-     *
-     * Defaults to true.
-     */
-    val enableAppTagTracing: Boolean = true,
-    /**
-     * Pass true to enable userspace tracing (androidx.tracing.tracing-perfetto APIs)
-     *
-     * Defaults to false.
-     */
-    val enableUserspaceTracing: Boolean = false
-) : TestRule {
-    override fun apply(
-        base: Statement,
-        description: Description
-    ): Statement = object : Statement() {
-        override fun evaluate() {
-            val thisPackage = InstrumentationRegistry.getInstrumentation().context.packageName
-            PerfettoCaptureWrapper().record(
-                benchmarkName = "${description.className}_${description.methodName}",
-                appTagPackages = if (enableAppTagTracing) listOf(thisPackage) else emptyList(),
-                userspaceTracingPackage = if (enableUserspaceTracing) thisPackage else null
-            ) {
-                base.evaluate()
-            }
-        }
-    }
-}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
new file mode 100644
index 0000000..aa35558
--- /dev/null
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 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.benchmark.junit4
+
+import android.os.Build
+import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Add this rule to record a Perfetto trace for each test on Android Lollipop (API 21)+ devices.
+ *
+ * ```
+ * @RunWith(AndroidJUnit4::class)
+ * class PerfettoOverheadBenchmark {
+ *     // traces all tests in file
+ *     @get:Rule
+ *     val perfettoRule = PerfettoTraceRule()
+ *
+ *     @Test
+ *     fun test() {}
+ * }
+ * ```
+ * Captured traces can be observed through any of:
+ * * Android Studio trace linking under `Benchmark` in test output tab
+ * * The optional `traceCallback` parameter
+ * * Android Gradle defining and pulling the file via additionalTestOutputDir.
+ *
+ * When invoked via Gradle, files will be copied to host path like the following:
+ * ```
+ * out/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/<deviceName>/androidx.mypackage.TestClass_testMethod.perfetto-trace
+ * ```
+ *
+ * You can additionally check logcat for messages tagged "PerfettoCapture:" for the path of each
+ * perfetto trace.
+ * ```
+ * > adb pull /storage/emulated/0/Android/data/mypackage.test/files/PerfettoCaptureTest.trace
+ * ```
+ *
+ * Reentrant Perfetto trace capture is not supported, so this API may not be combined with
+ * `BenchmarkRule`, `MacrobenchmarkRule`, or `PerfettoTrace.record`.
+ */
+@ExperimentalPerfettoCaptureApi
+class PerfettoTraceRule(
+    /**
+     * Pass false to disable android.os.Trace API tracing in this process
+     *
+     * Defaults to true.
+     */
+    val enableAppTagTracing: Boolean = true,
+    /**
+     * Pass true to enable userspace tracing (androidx.tracing.tracing-perfetto APIs)
+     *
+     * Defaults to false.
+     */
+    val enableUserspaceTracing: Boolean = false,
+
+    /**
+     * Callback for each captured trace.
+     */
+    val traceCallback: ((PerfettoTrace) -> Unit)? = null
+) : TestRule {
+    override fun apply(
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        base: Statement,
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        description: Description
+    ): Statement = object : Statement() {
+        override fun evaluate() {
+            val thisPackage = InstrumentationRegistry.getInstrumentation().context.packageName
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                val label = "${description.className}_${description.methodName}"
+                PerfettoTrace.record(
+                    fileLabel = label,
+                    appTagPackages = if (enableAppTagTracing) listOf(thisPackage) else emptyList(),
+                    userspaceTracingPackage = if (enableUserspaceTracing) thisPackage else null,
+                    traceCallback = {
+                        val relativePath = Outputs.relativePathFor(it.path)
+                            .replace("(", "\\(")
+                            .replace(")", "\\)")
+                        InstrumentationResults.instrumentationReport {
+                            ideSummaryRecord(
+                                // Can't link, simply print path
+                                summaryV1 = "Trace written to device at ${it.path}",
+                                // Link the trace within Studio
+                                summaryV2 = "[$label Trace](file://$relativePath)"
+                            )
+                        }
+                        traceCallback?.invoke(it)
+                    }
+                ) {
+                    base.evaluate()
+                }
+            } else {
+                base.evaluate()
+            }
+        }
+    }
+}
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 2ca3ad2..ccf5a4c 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -65,6 +65,7 @@
     implementation(libs.testUiautomator)
     implementation(libs.wireRuntime)
 
+    androidTestImplementation(project(":benchmark:benchmark-junit4"))
     androidTestImplementation(project(":internal-testutils-ktx"))
     androidTestImplementation("androidx.activity:activity-ktx:1.3.1")
     androidTestImplementation(project(":tracing:tracing-perfetto-common"))
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
new file mode 100644
index 0000000..4b2a453
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.benchmark.macro
+
+import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tracing.trace
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+/**
+ * NOTE: This test is in `benchmark-macro` since validating trace content requires
+ * `benchmark-macro`, which has a minAPI of 23, and benchmark-junit4 can't work with that.
+ *
+ * This should be moved to `benchmark-junit` once trace validation can be done in its tests.
+ */
+@LargeTest // recording is expensive
+@OptIn(ExperimentalPerfettoCaptureApi::class)
+@RunWith(AndroidJUnit4::class)
+class PerfettoTraceRuleTest {
+    companion object {
+        const val UNIQUE_SLICE_NAME = "PerfettoRuleTestUnique"
+    }
+    var trace: PerfettoTrace? = null
+
+    // wrap the perfetto rule with another which consumes + validates the trace
+    @get:Rule
+    val rule: RuleChain = RuleChain.outerRule { base, _ ->
+        object : Statement() {
+            override fun evaluate() {
+                base.evaluate()
+                if (PerfettoHelper.isAbiSupported()) {
+                    assertNotNull(trace)
+                    val sliceNameInstances = PerfettoTraceProcessor.runServer(trace!!.path) {
+                        querySlices(UNIQUE_SLICE_NAME)
+                    }.map { slice -> slice.name }
+                    assertEquals(listOf(UNIQUE_SLICE_NAME), sliceNameInstances)
+                }
+            }
+        }
+    }.around(
+        PerfettoTraceRule {
+            trace = it
+        }
+    )
+
+    @Test
+    fun simple() {
+        trace(UNIQUE_SLICE_NAME) {}
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun exception() {
+        // trace works even if test throws
+        trace(UNIQUE_SLICE_NAME) {}
+        throw IllegalStateException()
+    }
+}
\ No newline at end of file
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 b2ca1b9..b3561e8 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
@@ -281,7 +281,7 @@
     val metric = StartupTimingMetric()
     metric.configure(packageName)
     val tracePath = PerfettoCaptureWrapper().record(
-        benchmarkName = packageName,
+        fileLabel = packageName,
         // note - packageName may be this package, so we convert to set then list to make unique
         // and on API 23 and below, we use reflection to trace instead within this process
         appTagPackages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index ed2982f..689cf89 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -208,9 +208,9 @@
                     setupBlock(scope)
                 }
 
+                val iterString = iteration.toString().padStart(3, '0')
                 val tracePath = perfettoCollector.record(
-                    benchmarkName = uniqueName,
-                    iteration = iteration,
+                    fileLabel = "${uniqueName}_iter$iterString",
 
                     /**
                      * Prior to API 24, every package name was joined into a single setprop which
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
index cb014b1..830a342 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
@@ -164,7 +164,7 @@
      *
      * Note that sliceNames may include wildcard matches, such as `foo%`
      */
-    internal fun querySlices(
+    fun querySlices(
         vararg sliceNames: String
     ): List<Slice> {
         require(perfettoHttpServer.isRunning()) { "Perfetto trace_shell_process is not running." }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
index 4cc0db3..7ee5044 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
@@ -16,9 +16,11 @@
 
 package androidx.benchmark.macro.perfetto
 
+import androidx.annotation.RestrictTo
 import androidx.benchmark.macro.perfetto.server.QueryResultIterator
 
-internal data class Slice(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+data class Slice(
     val name: String,
     val ts: Long,
     val dur: Long
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
index f2bfd74..449e495 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
@@ -17,10 +17,10 @@
 package androidx.benchmark.benchmark
 
 import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.PerfettoRule
 import androidx.benchmark.junit4.measureRepeated
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tracing.Trace
 import androidx.tracing.trace
 import org.junit.Rule
@@ -30,11 +30,11 @@
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class PerfettoOverheadBenchmark {
-    @get:Rule
-    val benchmarkRule = BenchmarkRule()
+    private val targetPackage =
+        InstrumentationRegistry.getInstrumentation().targetContext.packageName
 
     @get:Rule
-    val perfettoRule = PerfettoRule()
+    val benchmarkRule = BenchmarkRule(packages = listOf(targetPackage))
 
     /**
      * Empty baseline, no tracing. Expect similar results to [TrivialJavaBenchmark.nothing].
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
index 7f0a639..0da127f 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
@@ -18,7 +18,6 @@
 
 import android.os.Build
 import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.PerfettoRule
 import androidx.benchmark.junit4.measureRepeated
 import androidx.benchmark.perfetto.PerfettoCapture
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
@@ -49,9 +48,6 @@
     @get:Rule
     val benchmarkRule = BenchmarkRule(packages = listOf(targetPackage))
 
-    @get:Rule
-    val perfettoRule = PerfettoRule()
-
     private val testData = Array(50_000) { UUID.randomUUID().toString() }
 
     @Before
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index f084924..0598270 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -73,7 +73,7 @@
     ctor public CustomTabsCallback();
     method public void extraCallback(String, android.os.Bundle?);
     method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
-    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
     method public void onMessageChannelReady(android.os.Bundle?);
     method public void onNavigationEvent(int, android.os.Bundle?);
     method public void onPostMessage(String, android.os.Bundle?);
@@ -118,7 +118,7 @@
     field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
     field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
     field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
-    field public static final String EXTRA_ACTIVITY_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
     field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
     field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
diff --git a/browser/browser/api/public_plus_experimental_current.txt b/browser/browser/api/public_plus_experimental_current.txt
index f084924..0598270 100644
--- a/browser/browser/api/public_plus_experimental_current.txt
+++ b/browser/browser/api/public_plus_experimental_current.txt
@@ -73,7 +73,7 @@
     ctor public CustomTabsCallback();
     method public void extraCallback(String, android.os.Bundle?);
     method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
-    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
     method public void onMessageChannelReady(android.os.Bundle?);
     method public void onNavigationEvent(int, android.os.Bundle?);
     method public void onPostMessage(String, android.os.Bundle?);
@@ -118,7 +118,7 @@
     field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
     field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
     field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
-    field public static final String EXTRA_ACTIVITY_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
     field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
     field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 09759da..de4bb0c 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -84,7 +84,7 @@
     ctor public CustomTabsCallback();
     method public void extraCallback(String, android.os.Bundle?);
     method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
-    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
     method public void onMessageChannelReady(android.os.Bundle?);
     method public void onNavigationEvent(int, android.os.Bundle?);
     method public void onPostMessage(String, android.os.Bundle?);
@@ -129,7 +129,7 @@
     field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
     field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
     field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
-    field public static final String EXTRA_ACTIVITY_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
     field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
     field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
     field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
diff --git a/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsCallbackTest.java b/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsCallbackTest.java
index 2caee24..6df7e8b 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsCallbackTest.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/CustomTabsCallbackTest.java
@@ -45,7 +45,7 @@
 
     @Test
     public void testOnActivityResized() throws Throwable {
-        mToken.getCallback().onActivityResized(75239, new Bundle());
+        mToken.getCallback().onActivityResized(75239, 1200, new Bundle());
         assertTrue(mCallback.hasActivityBeenResized());
     }
 }
diff --git a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
index 3363e0f..9293212 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsCallback.java
@@ -70,8 +70,8 @@
         }
 
         @Override
-        public void onActivityResized(int size, Bundle extras) throws RemoteException {
-            TestCustomTabsCallback.this.onActivityResized(size, extras);
+        public void onActivityResized(int height, int width, Bundle extras) throws RemoteException {
+            TestCustomTabsCallback.this.onActivityResized(height, width, extras);
         }
     };
 
@@ -104,7 +104,7 @@
     }
 
     @Override
-    public void onActivityResized(int size, Bundle extras) {
+    public void onActivityResized(int height, int width, @NonNull Bundle extras) {
         mOnResizedReceived = true;
     }
 
diff --git a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
index 38bb3e1..951e8ee 100644
--- a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
@@ -29,5 +29,5 @@
     void onPostMessage(String message, in Bundle extras) = 4;
     void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
     Bundle extraCallbackWithResult(String callbackName, in Bundle args) = 6;
-    void onActivityResized(int size, in Bundle extras) = 7;
+    void onActivityResized(int height, int width, in Bundle extras) = 7;
 }
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
index 3ac0a3c..a111f89 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
@@ -161,12 +161,12 @@
             boolean result, @Nullable Bundle extras) {}
 
     /**
-     * Called when the tab is resized in its height. This is applicable when users resize a tab
-     * launched with {@link CustomTabsIntent#ACTIVITY_HEIGHT_ADJUSTABLE} for the {@link
-     * CustomTabsIntent#ActivityResizeBehavior}.
+     * Called when the tab is resized.
      *
-     * @param size The updated height in px.
+     * @param height The updated height in px.
+     * @param width The updated width in px.
      * @param extras Reserved for future use.
      */
-    public void onActivityResized(@Dimension(unit = PX) int size, @NonNull Bundle extras) {}
+    public void onActivityResized(@Dimension(unit = PX) int height,
+            @Dimension(unit = PX) int width, @NonNull Bundle extras) {}
 }
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
index e45d218..36d1393 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -398,13 +398,14 @@
             }
 
             @Override
-            public void onActivityResized(final int size, final @Nullable Bundle extras)
+            public void onActivityResized(final int height, final int width,
+                    final @Nullable Bundle extras)
                     throws RemoteException {
                 if (callback == null) return;
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        callback.onActivityResized(size, extras);
+                        callback.onActivityResized(height, width, extras);
                     }
                 });
             }
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
index 9792aff..53c77a0 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
@@ -357,24 +357,24 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef({ACTIVITY_HEIGHT_DEFAULT, ACTIVITY_HEIGHT_ADJUSTABLE, ACTIVITY_HEIGHT_FIXED})
     @Retention(RetentionPolicy.SOURCE)
-    public @interface ActivityResizeBehavior {
+    public @interface ActivityHeightResizeBehavior {
     }
 
     /**
-     * Applies the default resize behavior for the Custom Tab Activity when it behaves as a
+     * Applies the default height resize behavior for the Custom Tab Activity when it behaves as a
      * bottom sheet.
      */
     public static final int ACTIVITY_HEIGHT_DEFAULT = 0;
 
     /**
-     * The Custom Tab Activity, when it behaves as a bottom sheet, can be manually resized by the
-     * user.
+     * The Custom Tab Activity, when it behaves as a bottom sheet, can have its height manually
+     * resized by the user.
      */
     public static final int ACTIVITY_HEIGHT_ADJUSTABLE = 1;
 
     /**
-     * The Custom Tab Activity, when it behaves as a bottom sheet, cannot be manually resized by
-     * the user.
+     * The Custom Tab Activity, when it behaves as a bottom sheet, cannot have its height manually
+     * resized by the user.
      */
     public static final int ACTIVITY_HEIGHT_FIXED = 2;
 
@@ -385,12 +385,12 @@
 
     /**
      * Extra that, if set in combination with
-     * {@link CustomTabsIntent#EXTRA_INITIAL_ACTIVITY_HEIGHT_PX}, defines the resize behavior of
-     * the Custom Tab Activity when it behaves as a bottom sheet.
+     * {@link CustomTabsIntent#EXTRA_INITIAL_ACTIVITY_HEIGHT_PX}, defines the height resize
+     * behavior of the Custom Tab Activity when it behaves as a bottom sheet.
      * Default is {@link CustomTabsIntent#ACTIVITY_HEIGHT_DEFAULT}.
      */
-    public static final String EXTRA_ACTIVITY_RESIZE_BEHAVIOR =
-            "androidx.browser.customtabs.extra.ACTIVITY_RESIZE_BEHAVIOR";
+    public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR =
+            "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
 
     /**
      * Extra that sets the toolbar's top corner radii in dp. This will only have
@@ -980,27 +980,28 @@
          * The Custom Tab will behave as a bottom sheet.
          *
          * @param initialHeightPx The Custom Tab Activity's initial height in pixels.
-         * @param activityResizeBehavior Desired height behavior.
+         * @param activityHeightResizeBehavior Desired height behavior.
          * @see CustomTabsIntent#EXTRA_INITIAL_ACTIVITY_HEIGHT_PX
-         * @see CustomTabsIntent#EXTRA_ACTIVITY_RESIZE_BEHAVIOR
+         * @see CustomTabsIntent#EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR
          * @see CustomTabsIntent#ACTIVITY_HEIGHT_DEFAULT
          * @see CustomTabsIntent#ACTIVITY_HEIGHT_ADJUSTABLE
          * @see CustomTabsIntent#ACTIVITY_HEIGHT_FIXED
          */
         @NonNull
         public Builder setInitialActivityHeightPx(@Dimension(unit = PX) int initialHeightPx,
-                @ActivityResizeBehavior int activityResizeBehavior) {
+                @ActivityHeightResizeBehavior int activityHeightResizeBehavior) {
             if (initialHeightPx <= 0) {
                 throw new IllegalArgumentException("Invalid value for the initialHeightPx "
                         + "argument");
             }
-            if (activityResizeBehavior < 0 || activityResizeBehavior > ACTIVITY_HEIGHT_MAX) {
-                throw new IllegalArgumentException("Invalid value for the activityResizeBehavior "
-                        + "argument");
+            if (activityHeightResizeBehavior < 0
+                    || activityHeightResizeBehavior > ACTIVITY_HEIGHT_MAX) {
+                throw new IllegalArgumentException(
+                        "Invalid value for the activityHeightResizeBehavior argument");
             }
 
             mIntent.putExtra(EXTRA_INITIAL_ACTIVITY_HEIGHT_PX, initialHeightPx);
-            mIntent.putExtra(EXTRA_ACTIVITY_RESIZE_BEHAVIOR, activityResizeBehavior);
+            mIntent.putExtra(EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR, activityHeightResizeBehavior);
             return this;
         }
 
@@ -1184,14 +1185,14 @@
      * @param intent Intent to retrieve the resize behavior from.
      * @return The resize behavior. If {@link CustomTabsIntent#EXTRA_INITIAL_ACTIVITY_HEIGHT_PX}
      *         is not set as part of the same intent, the value has no effect.
-     * @see CustomTabsIntent#EXTRA_ACTIVITY_RESIZE_BEHAVIOR
+     * @see CustomTabsIntent#EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR
      * @see CustomTabsIntent#ACTIVITY_HEIGHT_DEFAULT
      * @see CustomTabsIntent#ACTIVITY_HEIGHT_ADJUSTABLE
      * @see CustomTabsIntent#ACTIVITY_HEIGHT_FIXED
      */
-    @ActivityResizeBehavior
+    @ActivityHeightResizeBehavior
     public static int getActivityResizeBehavior(@NonNull Intent intent) {
-        return intent.getIntExtra(EXTRA_ACTIVITY_RESIZE_BEHAVIOR,
+        return intent.getIntExtra(EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR,
                 CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT);
     }
 
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
index e495fc9..435e242 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSessionToken.java
@@ -74,7 +74,7 @@
                 Uri requestedOrigin, boolean result, Bundle extras) {}
 
         @Override
-        public void onActivityResized(int size, Bundle extras) {}
+        public void onActivityResized(int height, int width, Bundle extras) {}
 
         @Override
         public IBinder asBinder() {
@@ -191,9 +191,9 @@
 
             @SuppressWarnings("NullAway")  // TODO: b/142938599
             @Override
-            public void onActivityResized(int size, @NonNull Bundle extras) {
+            public void onActivityResized(int height, int width, @NonNull Bundle extras) {
                 try {
-                    mCallbackBinder.onActivityResized(size, extras);
+                    mCallbackBinder.onActivityResized(height, width, extras);
                 } catch (RemoteException e) {
                     Log.e(TAG, "RemoteException during ICustomTabsCallback transaction");
                 }
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
index 18e3aa6..1bba7b6 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/CustomTabsIntentTest.java
@@ -157,7 +157,7 @@
     }
 
     @Test
-    public void testActivityInitialFixedResizeBehavior() {
+    public void testActivityInitialFixedHeightResizeBehavior() {
         int heightFixedResizeBehavior = CustomTabsIntent.ACTIVITY_HEIGHT_FIXED;
         int initialActivityHeight = 200;
 
@@ -166,9 +166,10 @@
                 .build()
                 .intent;
 
-        assertEquals("The value of EXTRA_ACTIVITY_FIXED_HEIGHT should be ACTIVITY_HEIGHT_FIXED.",
+        assertEquals("The value of EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR should be "
+                        + "ACTIVITY_HEIGHT_FIXED.",
                 heightFixedResizeBehavior,
-                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_RESIZE_BEHAVIOR,
+                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR,
                         CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT));
         assertEquals("The height should be the same as the one that was set.",
                 initialActivityHeight,
@@ -182,7 +183,7 @@
     }
 
     @Test
-    public void testActivityInitialAdjustableResizeBehavior() {
+    public void testActivityInitialAdjustableHeightResizeBehavior() {
         int heightAdjustableResizeBehavior = CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE;
         int initialActivityHeight = 200;
 
@@ -191,10 +192,10 @@
                 .build()
                 .intent;
 
-        assertEquals("The value of EXTRA_ACTIVITY_FIXED_HEIGHT should be "
+        assertEquals("The value of EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR should be "
                         + "ACTIVITY_HEIGHT_ADJUSTABLE.",
                 heightAdjustableResizeBehavior,
-                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_RESIZE_BEHAVIOR,
+                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR,
                         CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT));
         assertEquals("The height should be the same as the one that was set.",
                 initialActivityHeight,
@@ -220,10 +221,10 @@
         assertEquals("The height should be the same as the one that was set.",
                 initialActivityHeight,
                 intent.getIntExtra(CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX, 0));
-        assertEquals("The value of EXTRA_ACTIVITY_RESIZE_BEHAVIOR should be "
+        assertEquals("The value of EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR should be "
                         + "ACTIVITY_HEIGHT_DEFAULT.",
                 defaultResizeBehavior,
-                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_RESIZE_BEHAVIOR,
+                intent.getIntExtra(CustomTabsIntent.EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR,
                         CustomTabsIntent.ACTIVITY_HEIGHT_FIXED));
         assertEquals("The height returned by the getter should be the same.",
                 initialActivityHeight,
@@ -242,8 +243,8 @@
 
         assertFalse("The EXTRA_INITIAL_ACTIVITY_HEIGHT_PX should not be set.",
                 intent.hasExtra(CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX));
-        assertFalse("The EXTRA_ACTIVITY_RESIZE_BEHAVIOR should not be set.",
-                intent.hasExtra(CustomTabsIntent.EXTRA_ACTIVITY_RESIZE_BEHAVIOR));
+        assertFalse("The EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR should not be set.",
+                intent.hasExtra(CustomTabsIntent.EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR));
         assertEquals("The getter should return the default value.",
                 defaultInitialActivityHeight,
                 CustomTabsIntent.getInitialActivityHeightPx(intent));
diff --git a/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
index 5ad9a67..653ae96 100644
--- a/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
+++ b/buildSrc-tests/project-subsets/src/test/kotlin/androidx/build/ProjectSubsetsTest.kt
@@ -88,9 +88,13 @@
         if (outDir == null || outDir == "") {
             outDir = File(projectDir, "../../out").normalize().toString()
         }
+        // --dependency-verification=off is set because we don't have to do validation of
+        // dependencies during these tests, it is already handled by the main build.
+        // Having it validate here breaks in androidx-studio-integration case where we
+        // might get new dependencies from AGP that are missinng signatures.
         GradleRunner.create()
             .withProjectDir(projectDir)
-            .withArguments("-Pandroidx.projects=$name", "tasks")
+            .withArguments("-Pandroidx.projects=$name", "tasks", "--dependency-verification=off")
             .withTestKitDir(File(outDir, ".gradle-testkit"))
             .build(); // fails the test if the build fails
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index bc95bb6e..9a796c7e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -36,7 +36,6 @@
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.XmlProvider
-import org.gradle.api.attributes.DocsType
 import org.gradle.api.component.ComponentWithVariants
 import org.gradle.api.component.SoftwareComponent
 import org.gradle.api.component.SoftwareComponentFactory
@@ -383,10 +382,7 @@
 ) {
     val targetConfigurations = mutableSetOf<Configuration>()
     configurations.configureEach {
-        if (
-            it.attributes.getAttribute(DocsType.DOCS_TYPE_ATTRIBUTE)?.name == DocsType.SOURCES &&
-            it.name in names
-        ) {
+        if (it.name in names) {
             targetConfigurations.add(it)
             if (targetConfigurations.size == names.size) {
                 action(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e137796..e6780db 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -111,6 +111,11 @@
     @Input
     var showLibraryMetadata: Boolean = false
 
+    // The base URL to create source links for classes, as a format string with placeholders for the
+    // file path and qualified class name.
+    @Input
+    lateinit var baseSourceLink: String
+
     private fun sourceSets(): List<DokkaInputModels.SourceSet> {
         val externalDocs = externalLinks.map { (name, url) ->
             DokkaInputModels.GlobalDocsLink(
@@ -119,6 +124,13 @@
                     "file://${docsProjectDir.toPath()}/package-lists/$name/package-list"
             )
         }
+        val sourceLinks = listOf(
+            DokkaInputModels.SrcLink(
+                // This is part of dokka source links but isn't needed by dackka
+                File("/"),
+                baseSourceLink
+            )
+        )
         val gson = GsonBuilder().create()
         val multiplatformSourceSets = projectStructureMetadataFile
             .takeIf { it.exists() }
@@ -147,7 +159,7 @@
                         noJdkLink = !analysisPlatform.androidOrJvm(),
                         noAndroidSdkLink = analysisPlatform != DokkaAnalysisPlatform.ANDROID,
                         noStdlibLink = false,
-                        sourceLinks = emptyList()
+                        sourceLinks = sourceLinks
                     )
                 }
         } ?: emptyList()
@@ -165,7 +177,7 @@
                 noJdkLink = false,
                 noAndroidSdkLink = false,
                 noStdlibLink = false,
-                sourceLinks = emptyList()
+                sourceLinks = sourceLinks
             )
         ) + multiplatformSourceSets
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index a4a255a..39e7417 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -623,6 +623,11 @@
                 ":compose:material:material:icons:generator",
                 ":compose:material:material-icons-extended"
             ),
+            // Link glance-appwidget macrobenchmark and its target.
+            setOf(
+                ":glance:glance-appwidget:integration-tests:macrobenchmark",
+                ":glance:glance-appwidget:integration-tests:macrobenchmark-target"
+            ),
         )
 
         val IGNORED_PATHS = setOf(
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 dbf3f76..3a6a8be 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -460,6 +460,9 @@
                 libraryMetadataFile.set(getMetadataRegularFile(project))
                 showLibraryMetadata = true
                 projectStructureMetadataFile = mergedProjectMetadata
+                // 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"
             }
         }
 
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
index 808e43d..88efac3 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ExtensionsManagerTest.kt
@@ -182,7 +182,7 @@
     @Test
     fun correctAvailability_whenExtensionIsNotAvailable() {
         // Skips the test if extensions availability is disabled by quirk.
-        assumeFalse(ExtensionsTestUtil.extensionsDisabledByQuirk(lensFacing, extensionMode))
+        assumeFalse(ExtensionsTestUtil.extensionsDisabledByQuirk())
 
         extensionsManager = ExtensionsManager.getInstanceAsync(
             context,
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
index 57d7a47..9deb408 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/util/ExtensionsTestUtil.java
@@ -207,10 +207,7 @@
     /**
      * Returns whether extensions is disabled by quirk.
      */
-    public static boolean extensionsDisabledByQuirk(@CameraSelector.LensFacing int lensFacing,
-            @ExtensionMode.Mode int extensionMode) {
-
-        return new ExtensionDisabledValidator().shouldDisableExtension(
-                CameraUtil.getCameraIdWithLensFacing(lensFacing), extensionMode);
+    public static boolean extensionsDisabledByQuirk() {
+        return new ExtensionDisabledValidator().shouldDisableExtension();
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
index 7715f57..17a7a78 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdvancedVendorExtender.java
@@ -56,10 +56,8 @@
             new ExtensionDisabledValidator();
     private final AdvancedExtenderImpl mAdvancedExtenderImpl;
     private String mCameraId;
-    private final @ExtensionMode.Mode int mMode;
 
     public AdvancedVendorExtender(@ExtensionMode.Mode int mode) {
-        mMode = mode;
         try {
             switch (mode) {
                 case ExtensionMode.BOKEH:
@@ -101,7 +99,7 @@
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
 
-        if (mExtensionDisabledValidator.shouldDisableExtension(cameraId, mMode)) {
+        if (mExtensionDisabledValidator.shouldDisableExtension()) {
             return false;
         }
 
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 2a9f2d0..37b5610 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
@@ -123,7 +123,7 @@
     public boolean isExtensionAvailable(@NonNull String cameraId,
             @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
 
-        if (mExtensionDisabledValidator.shouldDisableExtension(cameraId, mMode)) {
+        if (mExtensionDisabledValidator.shouldDisableExtension()) {
             return false;
         }
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
index 6b58434..ec3f327 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ExtensionVersion.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.extensions.internal;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
@@ -75,6 +76,38 @@
         return getInstance().isAdvancedExtenderSupportedInternal();
     }
 
+    /**
+     * Check if the Runtime Version meets the minimum compatible version requirement. This implies
+     * that the runtime version is equal to or newer than the version.
+     *
+     * <p> The compatible version is comprised of the major and minor version numbers. The patch
+     * number is ignored.
+     *
+     * @param version The minimum compatible version required
+     * @return True if the Runtime version meets the minimum version requirement and False
+     * otherwise.
+     */
+    public static boolean isMinimumCompatibleVersion(@NonNull Version version) {
+        return ExtensionVersion.getRuntimeVersion()
+                .compareTo(version.getMajor(), version.getMinor()) >= 0;
+    }
+
+    /**
+     * Check if the Runtime Version meets the maximum compatible version requirement. This implies
+     * that the runtime version is equal to or older than the version.
+     *
+     * <p> The compatible version is comprised of the major and minor version numbers. The patch
+     * number is ignored.
+     *
+     * @param version The maximum compatible version required
+     * @return True if the Runtime version meets the maximum version requirement and False
+     * otherwise.
+     */
+    public static boolean isMaximumCompatibleVersion(@NonNull Version version) {
+        return ExtensionVersion.getRuntimeVersion()
+                .compareTo(version.getMajor(), version.getMinor()) <= 0;
+    }
+
     abstract boolean isAdvancedExtenderSupportedInternal();
 
     /**
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
index c9dd6cd..992962c 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/quirk/ExtensionDisabledQuirk.java
@@ -18,46 +18,43 @@
 
 import android.os.Build;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.Quirk;
-import androidx.camera.extensions.ExtensionMode;
 import androidx.camera.extensions.internal.ExtensionVersion;
 import androidx.camera.extensions.internal.Version;
 
 
 /**
  * <p>QuirkSummary
- *     Bug Id: b/199408131, b/214130117
- *     Description: Quirk required to disable extension for some devices. An example is that
- *                  Pixel 5's availability check result of the basic extension interface should
- *                  be false, but it actually returns true. Therefore, force disable Basic
- *                  Extender capability on the device. Another example is that Motorola razr 5G's
- *                  availability check results of both back and front camera are true, but it
- *                  will cause the black preview screen issue. Therefore, force disable the bokeh
- *                  mode on the device.
- *     Device(s): Pixel 5, Motorola razr 5G
- *     @see androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator
+ * Bug Id: b/199408131, b/214130117, b/255956506
+ * Description: Quirk required to disable extension for some devices. An example is that
+ * Pixel 5's availability check result of the basic extension interface should
+ * be false, but it actually returns true. Therefore, force disable Basic
+ * Extender capability on the device. Another example is to ensure Motorola devices meet the
+ * minimum quality requirements for camera extensions support. Common issues encountered with
+ * Motorola extensions include: Bokeh not supported on some devices, SurfaceView not supported,
+ * Image doesn't appear after taking a picture, Preview is pauses after resuming.
+ * Device(s): Pixel 5, Motorola
+ *
+ * @see androidx.camera.extensions.internal.compat.workaround.ExtensionDisabledValidator
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public class ExtensionDisabledQuirk implements Quirk {
-    private boolean mIsAdvancedInterface = isAdvancedExtenderSupported();
+    private final boolean mIsAdvancedInterface = isAdvancedExtenderSupported();
 
     static boolean load() {
-        return isPixel5() || isMotoRazr5G() || isAdvancedExtenderSupported();
+        return isPixel5() || isMoto() || isAdvancedExtenderSupported();
     }
 
     /**
      * Checks whether extension should be disabled.
      */
-    public boolean shouldDisableExtension(@NonNull String cameraId,
-            @ExtensionMode.Mode int extensionMode) {
+    public boolean shouldDisableExtension() {
         if (isPixel5() && !mIsAdvancedInterface) {
             // 1. Disables Pixel 5's Basic Extender capability.
             return true;
-        } else if (isMotoRazr5G() && ("0".equals(cameraId) || "1".equals(cameraId)) && (
-                ExtensionMode.BOKEH == extensionMode)) {
-            // 2. Disables Motorola Razr 5G's bokeh capability.
+        } else if (isMoto() && ExtensionVersion.isMaximumCompatibleVersion(Version.VERSION_1_1)) {
+            // 2. Disables Motorola extensions capability for version 1.1 and older.
             return true;
         }
 
@@ -68,14 +65,12 @@
         return "google".equalsIgnoreCase(Build.BRAND) && "redfin".equalsIgnoreCase(Build.DEVICE);
     }
 
-    private static boolean isMotoRazr5G() {
-        return "motorola".equalsIgnoreCase(Build.BRAND) && "smith".equalsIgnoreCase(Build.DEVICE);
+    private static boolean isMoto() {
+        return "motorola".equalsIgnoreCase(Build.BRAND);
     }
 
     private static boolean isAdvancedExtenderSupported() {
-        if (ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_2) < 0) {
-            return false;
-        }
-        return ExtensionVersion.isAdvancedExtenderSupported();
+        return ExtensionVersion.isMinimumCompatibleVersion(Version.VERSION_1_2)
+                && ExtensionVersion.isAdvancedExtenderSupported();
     }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
index 3b9bcd1..166eabf 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidator.java
@@ -16,9 +16,7 @@
 
 package androidx.camera.extensions.internal.compat.workaround;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.camera.extensions.ExtensionMode;
 import androidx.camera.extensions.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.extensions.internal.compat.quirk.ExtensionDisabledQuirk;
 
@@ -40,8 +38,7 @@
     /**
      * Checks whether extension should be disabled.
      */
-    public boolean shouldDisableExtension(@NonNull String cameraId,
-            @ExtensionMode.Mode int extensionMode) {
-        return mQuirk != null && mQuirk.shouldDisableExtension(cameraId, extensionMode);
+    public boolean shouldDisableExtension() {
+        return mQuirk != null && mQuirk.shouldDisableExtension();
     }
 }
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMaximumCompatibleTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMaximumCompatibleTest.kt
new file mode 100644
index 0000000..b21dea5
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMaximumCompatibleTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.extensions.internal
+
+import android.os.Build
+import androidx.camera.extensions.internal.util.ExtensionsTestUtil.resetSingleton
+import androidx.camera.extensions.internal.util.ExtensionsTestUtil.setTestApiVersion
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP,
+    instrumentedPackages = arrayOf("androidx.camera.extensions.internal")
+)
+class ExtensionVersionMaximumCompatibleTest(private val config: TestConfig) {
+
+    @Before
+    @Throws(NoSuchFieldException::class, IllegalAccessException::class)
+    fun setUp() {
+        val field = VersionName::class.java.getDeclaredField("CURRENT")
+        field.isAccessible = true
+        field[null] = VersionName(config.targetVersion)
+    }
+
+    @After
+    fun tearDown() {
+        resetSingleton(ExtensionVersion::class.java, "sExtensionVersion")
+    }
+
+    @Test
+    fun isMaximumCompatibleVersion() {
+        setTestApiVersion(config.targetVersion)
+
+        val version = Version.parse(config.maximumCompatibleVersion)!!
+        assertThat(ExtensionVersion.isMaximumCompatibleVersion(version))
+            .isEqualTo(config.expectedResult)
+    }
+
+    data class TestConfig(
+        val targetVersion: String,
+        val maximumCompatibleVersion: String,
+        val expectedResult: Boolean
+    )
+
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return listOf(
+                TestConfig("1.1.0", "1.1.0", true),
+                TestConfig("1.1.0", "1.2.0", true),
+                TestConfig("1.1.0", "1.0.0", false),
+                TestConfig("1.1.0", "0.9.0", false),
+
+                // Test to ensure the patch version is ignored
+                TestConfig("1.2.1", "1.2.0", true),
+                TestConfig("1.2.0", "1.2.1", true),
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMinimumCompatibleTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMinimumCompatibleTest.kt
new file mode 100644
index 0000000..d92c03db1
--- /dev/null
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/ExtensionVersionMinimumCompatibleTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.extensions.internal
+
+import android.os.Build
+import androidx.camera.extensions.internal.util.ExtensionsTestUtil.resetSingleton
+import androidx.camera.extensions.internal.util.ExtensionsTestUtil.setTestApiVersion
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP,
+    instrumentedPackages = arrayOf("androidx.camera.extensions.internal")
+)
+class ExtensionVersionMinimumCompatibleTest(private val config: TestConfig) {
+
+    @Before
+    @Throws(NoSuchFieldException::class, IllegalAccessException::class)
+    fun setUp() {
+        val field = VersionName::class.java.getDeclaredField("CURRENT")
+        field.isAccessible = true
+        field[null] = VersionName(config.targetVersion)
+    }
+
+    @After
+    fun tearDown() {
+        resetSingleton(ExtensionVersion::class.java, "sExtensionVersion")
+    }
+
+    @Test
+    fun isMinimumCompatibleVersion() {
+        setTestApiVersion(config.targetVersion)
+        val version = Version.parse(config.minimumCompatibleVersion)!!
+        assertThat(ExtensionVersion.isMinimumCompatibleVersion(version))
+            .isEqualTo(config.expectedResult)
+    }
+
+    data class TestConfig(
+        val targetVersion: String,
+        val minimumCompatibleVersion: String,
+        val expectedResult: Boolean
+    )
+
+    companion object {
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+        fun createTestSet(): List<TestConfig> {
+            return listOf(
+                TestConfig("1.1.0", "1.1.0", true),
+                TestConfig("1.1.0", "1.0.0", true),
+                TestConfig("1.1.0", "1.2.0", false),
+
+                // Test to ensure the patch version is ignored
+                TestConfig("1.1.1", "1.1.0", true),
+                TestConfig("1.1.0", "1.1.1", true),
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
index a70923e..d34b6c6 100644
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
+++ b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/compat/workaround/ExtensionDisabledValidatorTest.kt
@@ -17,7 +17,6 @@
 package androidx.camera.extensions.internal.compat.workaround
 
 import android.os.Build
-import androidx.camera.extensions.ExtensionMode
 import androidx.camera.extensions.internal.ExtensionVersion
 import androidx.camera.extensions.internal.util.ExtensionsTestUtil.resetSingleton
 import androidx.camera.extensions.internal.util.ExtensionsTestUtil.setTestApiVersionAndAdvancedExtender
@@ -41,7 +40,7 @@
 
     @Before
     fun setUp() {
-        setTestApiVersionAndAdvancedExtender("1.2.0", config.isAdvancedInterface)
+        setTestApiVersionAndAdvancedExtender(config.version, config.isAdvancedInterface)
     }
 
     @After
@@ -56,19 +55,13 @@
         ReflectionHelpers.setStaticField(Build::class.java, "DEVICE", config.device)
 
         val validator = ExtensionDisabledValidator()
-        assertThat(
-            validator.shouldDisableExtension(
-                config.cameraId,
-                config.extensionMode
-            )
-        ).isEqualTo(config.shouldDisableExtension)
+        assertThat(validator.shouldDisableExtension()).isEqualTo(config.shouldDisableExtension)
     }
 
     class TestConfig(
         val brand: String,
         val device: String,
-        val cameraId: String,
-        val extensionMode: Int,
+        val version: String,
         val isAdvancedInterface: Boolean,
         val shouldDisableExtension: Boolean
     )
@@ -79,36 +72,24 @@
         fun createTestSet(): List<TestConfig> {
             return listOf(
                 // Pixel 5 extension capability is disabled on basic extender
-                TestConfig("Google", "Redfin", "0", ExtensionMode.BOKEH, false, true),
-                TestConfig("Google", "Redfin", "0", ExtensionMode.HDR, false, true),
-                TestConfig("Google", "Redfin", "0", ExtensionMode.NIGHT, false, true),
-                TestConfig("Google", "Redfin", "0", ExtensionMode.FACE_RETOUCH, false, true),
-                TestConfig("Google", "Redfin", "0", ExtensionMode.AUTO, false, true),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.BOKEH, false, true),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.HDR, false, true),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.NIGHT, false, true),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.FACE_RETOUCH, false, true),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.AUTO, false, true),
+                TestConfig("Google", "Redfin", "1.2.0", false, true),
 
                 // Pixel 5 extension capability is enabled on advanced extender
-                TestConfig("Google", "Redfin", "0", ExtensionMode.NIGHT, true, false),
-                TestConfig("Google", "Redfin", "1", ExtensionMode.NIGHT, true, false),
+                TestConfig("Google", "Redfin", "1.2.0", true, false),
 
-                // Motorola Razr 5G bokeh mode is disabled. Other extension modes should still work.
-                TestConfig("Motorola", "Smith", "0", ExtensionMode.BOKEH, false, true),
-                TestConfig("Motorola", "Smith", "0", ExtensionMode.HDR, false, false),
-                TestConfig("Motorola", "Smith", "1", ExtensionMode.BOKEH, false, true),
-                TestConfig("Motorola", "Smith", "1", ExtensionMode.HDR, false, false),
-                TestConfig("Motorola", "Smith", "2", ExtensionMode.BOKEH, false, false),
-                TestConfig("Motorola", "Smith", "2", ExtensionMode.HDR, false, false),
+                // All Motorola devices should be disabled for version 1.1.0 and older.
+                TestConfig("Motorola", "Smith", "1.1.0", false, true),
+                TestConfig("Motorola", "Hawaii P", "1.1.0", false, true),
+
+                // Make sure Motorola device would still be enabled for newer versions
+                // Motorola doesn't support this today but making sure there is a path to enable
+                TestConfig("Motorola", "Hawaii P", "1.2.0", false, false),
 
                 // Other cases should be kept normal.
-                TestConfig("", "", "0", ExtensionMode.BOKEH, false, false),
-                TestConfig("", "", "1", ExtensionMode.BOKEH, false, false),
+                TestConfig("", "", "1.2.0", false, false),
 
                 // Advanced extender is enabled for all devices
-                TestConfig("", "", "0", ExtensionMode.BOKEH, true, false),
-                TestConfig("", "", "1", ExtensionMode.BOKEH, true, false),
+                TestConfig("", "", "1.2.0", true, false),
             )
         }
     }
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/AdvancedExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/AdvancedExtenderValidationTest.kt
index 7cdee86..5fbc9a3 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/AdvancedExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/AdvancedExtenderValidationTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -39,16 +40,13 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 28)
-class AdvancedExtenderValidationTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
-    private val validation = AdvancedExtenderValidation(cameraId, extensionMode)
+class AdvancedExtenderValidationTest(config: CameraIdExtensionModePair) {
+    private val validation = AdvancedExtenderValidation(config.cameraId, config.extensionMode)
 
     companion object {
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/BindUnbindUseCasesStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/BindUnbindUseCasesStressTest.kt
index 31fb57d..168e040 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/BindUnbindUseCasesStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/BindUnbindUseCasesStressTest.kt
@@ -34,6 +34,7 @@
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.VERIFICATION_TARGET_IMAGE_CAPTURE
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.VERIFICATION_TARGET_PREVIEW
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -67,10 +68,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class BindUnbindUseCasesStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class BindUnbindUseCasesStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -96,6 +94,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
+        val (cameraId, extensionMode) = config
         baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
 
@@ -133,8 +132,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
index 1475998..d294dbc 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureExtenderValidationTest.kt
@@ -31,6 +31,7 @@
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
 import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
 import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -60,10 +61,7 @@
 @SmallTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureExtenderValidationTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class ImageCaptureExtenderValidationTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -86,6 +84,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
+        val (cameraId, extensionMode) = config
         baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
 
@@ -119,8 +118,8 @@
 
     companion object {
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
 
@@ -133,8 +132,8 @@
         // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
         // from vendor library for the target effect mode.
         val impl = CameraXExtensionsTestUtil.createImageCaptureExtenderImpl(
-            extensionMode,
-            cameraId,
+            config.extensionMode,
+            config.cameraId,
             cameraCharacteristics
         )
 
@@ -149,8 +148,8 @@
         // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
         // API level is older than 28.
         val impl = CameraXExtensionsTestUtil.createImageCaptureExtenderImpl(
-            extensionMode,
-            cameraId,
+            config.extensionMode,
+            config.cameraId,
             cameraCharacteristics
         )
         assertThat(impl.onPresetSession()).isNull()
@@ -166,7 +165,7 @@
         // the getEstimatedCaptureLatencyRange function.
         val latencyInfo = extensionsManager.getEstimatedCaptureLatencyRange(
             baseCameraSelector,
-            extensionMode
+            config.extensionMode
         )
 
         // Calls bind to lifecycle to get the selected camera
@@ -179,7 +178,7 @@
 
         // Creates ImageCaptureExtenderImpl directly to retrieve the capture latency range info
         val impl = CameraXExtensionsTestUtil.createImageCaptureExtenderImpl(
-            extensionMode,
+            config.extensionMode,
             cameraId,
             characteristics
         )
@@ -201,7 +200,10 @@
             setOrientationNatural()
         }
 
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(
+            config.cameraId,
+            config.extensionMode
+        )
 
         with(activityScenario) {
             use {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index 9cf35f4..941ad66 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -25,6 +25,7 @@
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
 import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
 import androidx.camera.integration.extensions.util.takePictureAndWaitForImageSavedIdle
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -48,7 +49,7 @@
  */
 @LargeTest
 @RunWith(Parameterized::class)
-class ImageCaptureTest(private val cameraId: String, private val extensionMode: Int) {
+class ImageCaptureTest(private val config: CameraIdExtensionModePair) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
@@ -62,7 +63,7 @@
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
     companion object {
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -87,7 +88,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
-        assumeExtensionModeSupported(extensionsManager, cameraId, extensionMode)
+        assumeExtensionModeSupported(extensionsManager, config.cameraId, config.extensionMode)
     }
 
     @After
@@ -114,7 +115,7 @@
      */
     @Test
     fun takePictureWithExtensionMode() {
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(config.cameraId, config.extensionMode)
 
         with(activityScenario) {
             use {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
index 51c1624..04404e4 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
@@ -28,6 +28,7 @@
 import androidx.camera.integration.extensions.util.takePictureAndWaitForImageSavedIdle
 import androidx.camera.integration.extensions.util.waitForPreviewViewIdle
 import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
@@ -57,10 +58,7 @@
  */
 @LargeTest
 @RunWith(Parameterized::class)
-class LifecycleStatusChangeStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class LifecycleStatusChangeStressTest(private val config: CameraIdExtensionModePair) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
@@ -74,7 +72,7 @@
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
     companion object {
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -95,8 +93,8 @@
         // Checks whether the extension mode can be supported first before launching the activity.
         CameraXExtensionsTestUtil.assumeExtensionModeSupported(
             extensionsManager,
-            cameraId,
-            extensionMode
+            config.cameraId,
+            config.extensionMode
         )
 
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
@@ -147,7 +145,7 @@
         verificationTarget: Int,
         repeatCount: Int = CameraXExtensionsTestUtil.getStressTestRepeatingCount()
     ) {
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(config.cameraId, config.extensionMode)
 
         with(activityScenario) {
             use {
@@ -188,7 +186,7 @@
         verificationTarget: Int,
         repeatCount: Int = CameraXExtensionsTestUtil.getStressTestRepeatingCount()
     ) {
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(config.cameraId, config.extensionMode)
 
         with(activityScenario) {
             use {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCameraStressTest.kt
index d0a1279..44aa1be 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCameraStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCameraStressTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.core.UseCase
 import androidx.camera.extensions.ExtensionsManager
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -56,10 +57,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class OpenCloseCameraStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class OpenCloseCameraStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -79,6 +77,7 @@
     @Before
     fun setUp(): Unit = runBlocking {
         assumeTrue(CameraXExtensionsTestUtil.isTargetDeviceAvailableForExtensions())
+        val (cameraId, extensionMode) = config
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
         extensionsManager = ExtensionsManager.getInstanceAsync(
             context,
@@ -125,8 +124,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
index 5dedbdf..902b8ad 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
@@ -41,6 +41,7 @@
 import androidx.camera.extensions.ExtensionMode
 import androidx.camera.extensions.ExtensionsManager
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -69,10 +70,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class OpenCloseCaptureSessionStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class OpenCloseCaptureSessionStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -101,6 +99,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
+        val (cameraId, extensionMode) = config
         baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
 
@@ -175,7 +174,7 @@
                 val extensionEnabledCameraEventMonitorCameraSelector =
                     getExtensionsCameraEventMonitorCameraSelector(
                         extensionsManager,
-                        extensionMode,
+                        config.extensionMode,
                         baseCameraSelector
                     )
 
@@ -205,8 +204,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
 
         /**
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
index c0a25f0..9cf7c98 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewExtenderValidationTest.kt
@@ -29,6 +29,7 @@
 import androidx.camera.extensions.internal.ExtensionVersion
 import androidx.camera.extensions.internal.Version
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -53,10 +54,7 @@
 @SmallTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewExtenderValidationTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class PreviewExtenderValidationTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -79,6 +77,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
+        val (cameraId, extensionMode) = config
         baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
 
@@ -110,8 +109,8 @@
 
     companion object {
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
 
@@ -124,8 +123,8 @@
         // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
         // from vendor library for the target effect mode.
         val impl = CameraXExtensionsTestUtil.createPreviewExtenderImpl(
-            extensionMode,
-            cameraId,
+            config.extensionMode,
+            config.cameraId,
             cameraCharacteristics
         )
 
@@ -140,8 +139,8 @@
         // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
         // API level is older than 28.
         val impl = CameraXExtensionsTestUtil.createPreviewExtenderImpl(
-            extensionMode,
-            cameraId,
+            config.extensionMode,
+            config.cameraId,
             cameraCharacteristics
         )
         assertThat(impl.onPresetSession()).isNull()
@@ -150,8 +149,8 @@
     @Test
     fun returnCorrectProcessor() {
         val impl = CameraXExtensionsTestUtil.createPreviewExtenderImpl(
-            extensionMode,
-            cameraId,
+            config.extensionMode,
+            config.cameraId,
             cameraCharacteristics
         )
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
index f96fbed..571c3bf 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.kt
@@ -40,6 +40,7 @@
 import androidx.camera.extensions.ExtensionMode
 import androidx.camera.extensions.ExtensionsManager
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -73,10 +74,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewProcessorTimestampTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class PreviewProcessorTimestampTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -155,6 +153,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
+        val (cameraId, extensionMode) = config
         baseCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
 
@@ -191,7 +190,7 @@
             val timestampExtensionEnabledCameraSelector =
                 getTimestampExtensionEnabledCameraSelector(
                     extensionsManager,
-                    extensionMode,
+                    config.extensionMode,
                     baseCameraSelector
                 )
 
@@ -236,8 +235,8 @@
 
     companion object {
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
-        val parameters: Collection<Array<Any>>
+        @get:Parameterized.Parameters(name = "config = {0}")
+        val parameters: Collection<CameraIdExtensionModePair>
             get() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
 
         /**
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
index 03371e2..8154c16 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewTest.kt
@@ -24,6 +24,7 @@
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil.launchCameraExtensionsActivity
 import androidx.camera.integration.extensions.util.HOME_TIMEOUT_MS
 import androidx.camera.integration.extensions.util.waitForPreviewViewStreaming
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -48,7 +49,7 @@
  */
 @LargeTest
 @RunWith(Parameterized::class)
-class PreviewTest(private val cameraId: String, private val extensionMode: Int) {
+class PreviewTest(private val config: CameraIdExtensionModePair) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
@@ -60,7 +61,7 @@
     private lateinit var extensionsManager: ExtensionsManager
 
     companion object {
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = CameraXExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -86,7 +87,7 @@
             cameraProvider
         )[10000, TimeUnit.MILLISECONDS]
 
-        assumeExtensionModeSupported(extensionsManager, cameraId, extensionMode)
+        assumeExtensionModeSupported(extensionsManager, config.cameraId, config.extensionMode)
     }
 
     @After
@@ -114,7 +115,7 @@
      */
     @Test
     fun previewWithExtensionModeCanEnterStreamingState() {
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(config.cameraId, config.extensionMode)
 
         with(activityScenario) {
             use {
@@ -128,7 +129,7 @@
             ApplicationProvider.getApplicationContext(),
             extensionsManager,
             cameraId,
-            extensionMode
+            config.extensionMode
         )
         assumeTrue(
             "Cannot find next camera id that supports extensions mode($extensionsMode)",
@@ -140,8 +141,8 @@
      */
     @Test
     fun previewCanEnterStreamingStateAfterSwitchingCamera() {
-        assumeNextCameraIdExtensionModeSupported(cameraId, extensionMode)
-        val activityScenario = launchCameraExtensionsActivity(cameraId, extensionMode)
+        assumeNextCameraIdExtensionModeSupported(config.cameraId, config.extensionMode)
+        val activityScenario = launchCameraExtensionsActivity(config.cameraId, config.extensionMode)
 
         with(activityScenario) {
             use {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
index 2ba7a62..f689467 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
@@ -30,6 +30,7 @@
 import androidx.camera.integration.extensions.util.waitForImageSavedIdle
 import androidx.camera.integration.extensions.util.waitForPreviewIdle
 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.isCamera2ExtensionModeSupported
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -58,10 +59,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 31)
-class Camera2ExtensionsActivityTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class Camera2ExtensionsActivityTest(private val config: CameraIdExtensionModePair) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
@@ -101,7 +99,7 @@
         @ClassRule
         @JvmField val stressTest = StressTestRule()
 
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = Camera2ExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -110,8 +108,7 @@
     @Test
     fun checkPreviewUpdated() {
         val activityScenario = launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-            cameraId,
-            extensionMode
+            config
         )
         with(activityScenario) { // Launches activity
             use { // Ensures that ActivityScenario is cleaned up properly
@@ -125,8 +122,7 @@
     @Test
     fun canCaptureSingleImage() {
         val activityScenario = launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-            cameraId,
-            extensionMode
+            config
         )
         with(activityScenario) { // Launches activity
             use { // Ensures that ActivityScenario is cleaned up properly
@@ -140,8 +136,7 @@
     @Test
     fun checkPreviewUpdated_afterPauseResume() {
         val activityScenario = launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-            cameraId,
-            extensionMode
+            config
         )
         with(activityScenario) { // Launches activity
             use { // Ensures that ActivityScenario is cleaned up properly
@@ -162,8 +157,7 @@
     @Test
     fun canCaptureImage_afterPauseResume() {
         val activityScenario = launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-            cameraId,
-            extensionMode
+           config
         )
         with(activityScenario) { // Launches activity
             use { // Ensures that ActivityScenario is cleaned up properly
@@ -187,8 +181,7 @@
     @Test
     fun canCaptureMultipleImages() {
         val activityScenario = launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-            cameraId,
-            extensionMode
+            config
         )
         with(activityScenario) { // Launches activity
             use { // Ensures that ActivityScenario is cleaned up properly
@@ -201,9 +194,9 @@
     }
 
     private fun launchCamera2ExtensionsActivityAndWaitForCaptureSessionConfigured(
-        cameraId: String,
-        extensionMode: Int
+        config: CameraIdExtensionModePair
     ): ActivityScenario<Camera2ExtensionsActivity> {
+        val (cameraId, extensionMode) = config
         val context = ApplicationProvider.getApplicationContext<Context>()
         assumeTrue(isCamera2ExtensionModeSupported(context, cameraId, extensionMode))
         val intent = context.packageManager
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsOpenCloseStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsOpenCloseStressTest.kt
index c42d674..8c9f89d 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsOpenCloseStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsOpenCloseStressTest.kt
@@ -21,6 +21,7 @@
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.assertCanOpenExtensionsSession
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.StressTestRule
 import androidx.test.core.app.ApplicationProvider
@@ -38,10 +39,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 31)
-class Camera2ExtensionsOpenCloseStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class Camera2ExtensionsOpenCloseStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -51,7 +49,7 @@
         @ClassRule
         @JvmField val stressTest = StressTestRule()
 
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = Camera2ExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -65,6 +63,7 @@
     }
     @Test
     fun openCloseExtensionSession(): Unit = runBlocking {
+        val (cameraId, extensionMode) = config
         repeat(Camera2ExtensionsTestUtil.getStressTestRepeatingCount()) {
             assertCanOpenExtensionsSession(cameraManager, cameraId, extensionMode)
         }
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchCameraStressTest.kt
index 9756e72..bc0dfdd 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchCameraStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchCameraStressTest.kt
@@ -22,6 +22,7 @@
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.assertCanOpenExtensionsSession
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.findNextSupportedCameraId
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.StressTestRule
 import androidx.test.core.app.ApplicationProvider
@@ -39,10 +40,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 31)
-class Camera2ExtensionsSwitchCameraStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class Camera2ExtensionsSwitchCameraStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -52,7 +50,7 @@
         @ClassRule
         @JvmField val stressTest = StressTestRule()
 
-        @Parameterized.Parameters(name = "cameraId = {0}, extensionMode = {1}")
+        @Parameterized.Parameters(name = "config = {0}")
         @JvmStatic
         fun parameters() = Camera2ExtensionsTestUtil.getAllCameraIdExtensionModeCombinations()
     }
@@ -67,6 +65,7 @@
 
     @Test
     fun switchCameras(): Unit = runBlocking {
+        val (cameraId, extensionMode) = config
         val nextCameraId = findNextSupportedCameraId(context, cameraId, extensionMode)
         assumeTrue(nextCameraId != null)
         repeat(Camera2ExtensionsTestUtil.getStressTestRepeatingCount()) {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchModeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchModeStressTest.kt
index 47b7114..6cffd8f 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchModeStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsSwitchModeStressTest.kt
@@ -22,6 +22,7 @@
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.EXTENSION_NOT_FOUND
 import androidx.camera.integration.extensions.util.Camera2ExtensionsTestUtil.findNextEffectMode
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.StressTestRule
 import androidx.test.core.app.ApplicationProvider
@@ -39,10 +40,7 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 31)
-class Camera2ExtensionsSwitchModeStressTest(
-    private val cameraId: String,
-    private val extensionMode: Int
-) {
+class Camera2ExtensionsSwitchModeStressTest(private val config: CameraIdExtensionModePair) {
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
         CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
@@ -68,6 +66,7 @@
 
     @Test
     fun switchModes(): Unit = runBlocking {
+        val (cameraId, extensionMode) = config
         val nextMode = findNextEffectMode(context, cameraId, extensionMode)
         assumeTrue(nextMode != EXTENSION_NOT_FOUND)
         repeat(Camera2ExtensionsTestUtil.getStressTestRepeatingCount()) {
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
index 5a32a44..18051b9 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/Camera2ExtensionsTestUtil.kt
@@ -37,6 +37,7 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.AVAILABLE_CAMERA2_EXTENSION_MODES
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.SurfaceTextureProvider
@@ -73,12 +74,10 @@
      * Gets a list of all camera id and extension mode combinations.
      */
     @JvmStatic
-    fun getAllCameraIdExtensionModeCombinations(): List<Array<Any>> =
-        arrayListOf<Array<Any>>().apply {
-            CameraUtil.getBackwardCompatibleCameraIdListOrThrow().forEach { cameraId ->
-                AVAILABLE_CAMERA2_EXTENSION_MODES.forEach { mode ->
-                    add(arrayOf(cameraId, mode))
-                }
+    fun getAllCameraIdExtensionModeCombinations(): List<CameraIdExtensionModePair> =
+        CameraUtil.getBackwardCompatibleCameraIdListOrThrow().flatMap { cameraId ->
+            AVAILABLE_CAMERA2_EXTENSION_MODES.map { extensionMode ->
+                CameraIdExtensionModePair(cameraId, extensionMode)
             }
         }
 
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/CameraXExtensionsTestUtil.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/CameraXExtensionsTestUtil.kt
index 70f7440..09a7be9 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/CameraXExtensionsTestUtil.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/util/CameraXExtensionsTestUtil.kt
@@ -49,6 +49,7 @@
 import androidx.camera.extensions.internal.Version
 import androidx.camera.integration.extensions.CameraExtensionsActivity
 import androidx.camera.integration.extensions.IntentExtraKey
+import androidx.camera.integration.extensions.utils.CameraIdExtensionModePair
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES
@@ -66,12 +67,10 @@
      * Gets a list of all camera id and extension mode combinations.
      */
     @JvmStatic
-    fun getAllCameraIdExtensionModeCombinations(): List<Array<Any>> =
-        arrayListOf<Array<Any>>().apply {
-            CameraUtil.getBackwardCompatibleCameraIdListOrThrow().forEach { cameraId ->
-                ExtensionModeUtil.AVAILABLE_EXTENSION_MODES.forEach { mode ->
-                    add(arrayOf(cameraId, mode))
-                }
+    fun getAllCameraIdExtensionModeCombinations(): List<CameraIdExtensionModePair> =
+        CameraUtil.getBackwardCompatibleCameraIdListOrThrow().flatMap { cameraId ->
+            ExtensionModeUtil.AVAILABLE_EXTENSION_MODES.map { extensionMode ->
+                CameraIdExtensionModePair(cameraId, extensionMode)
             }
         }
 
diff --git a/activity/activity/src/main/java/androidx/activity/Cancellable.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/CameraIdExtensionModePair.kt
similarity index 65%
copy from activity/activity/src/main/java/androidx/activity/Cancellable.java
copy to camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/CameraIdExtensionModePair.kt
index a5cb90a..629e678 100644
--- a/activity/activity/src/main/java/androidx/activity/Cancellable.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/CameraIdExtensionModePair.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 The Android Open Source Project
+ * 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.
@@ -14,16 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.activity;
+package androidx.camera.integration.extensions.utils
 
 /**
- * Token representing a cancellable operation.
+ * Represents a pair of Camera ID and Camera Extension Mode type
  */
-interface Cancellable {
-
-    /**
-     * Cancel the subscription. This call should be idempotent, making it safe to
-     * call multiple times.
-     */
-    void cancel();
-}
+data class CameraIdExtensionModePair(val cameraId: String, val extensionMode: Int)
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 1cec99e..122d25a 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -106,7 +106,7 @@
 
 androidx {
     name = "AndroidX Collections Benchmarks (Android / iOS)"
-    mavenGroup = LibraryGroups.COLLECTIONS
+    mavenGroup = LibraryGroups.COLLECTION
     inceptionYear = "2022"
     description = "AndroidX Collections Benchmarks (Android / iOS)"
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
index ea32dce..cef6022 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
@@ -33,10 +33,8 @@
 import org.jetbrains.kotlin.cli.common.messages.MessageCollector
 import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
 import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
-import org.jetbrains.kotlin.codegen.ClassBuilderFactories
 import org.jetbrains.kotlin.codegen.ClassFileFactory
 import org.jetbrains.kotlin.codegen.GeneratedClassLoader
 import org.jetbrains.kotlin.config.CommonConfigurationKeys
@@ -172,10 +170,7 @@
             try {
                 val environment = myEnvironment ?: error("Environment not initialized")
                 val files = myFiles ?: error("Files not initialized")
-                val generationState = GenerationUtils.compileFiles(
-                    files.psiFiles, environment, ClassBuilderFactories.TEST,
-                    NoScopeRecordCliBindingTrace()
-                )
+                val generationState = GenerationUtils.compileFiles(environment, files.psiFiles)
                 generationState.factory.also { classFileFactory = it }
             } catch (e: TestsCompilerError) {
                 if (reportProblems) {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
index cfc467a..8806902 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
@@ -16,15 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
-import org.jetbrains.kotlin.checkers.DiagnosedRange
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.config.JvmTarget
-import org.jetbrains.kotlin.diagnostics.Diagnostic
 import java.io.File
+import org.jetbrains.kotlin.checkers.DiagnosedRange
+import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.diagnostics.Diagnostic
 
 abstract class AbstractComposeDiagnosticsTest : AbstractCompilerTest() {
 
@@ -40,16 +36,7 @@
         val files = listOf(file)
 
         // Use the JVM version of the analyzer to allow using classes in .jar files
-        val moduleTrace = NoScopeRecordCliBindingTrace()
-        val result = TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
-            environment.project,
-            files,
-            moduleTrace,
-            environment.configuration.copy().apply {
-                this.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-            },
-            environment::createPackagePartProvider
-        )
+        val result = JvmResolveUtil.analyze(environment, files)
 
         // Collect the errors
         val errors = result.bindingContext.diagnostics.all().toMutableList()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 7fa4afd..594bc5d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -343,7 +343,7 @@
         ComposeComponentRegistrar.registerCommonExtensions(environment.project)
         IrGenerationExtension.registerExtension(environment.project, extension)
 
-        val analysisResult = JvmResolveUtil.analyze(files, environment)
+        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
         val codegenFactory = JvmIrCodegenFactory(
             configuration,
             configuration.get(CLIConfigurationKeys.PHASE_CONFIG) ?: PhaseConfig(jvmPhases)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
index e52cc06..9b45bd8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
@@ -221,9 +221,9 @@
         val environment = myEnvironment ?: error("Environment not initialized")
 
         val ktFile = KtPsiFactory(environment.project).createFile(text)
-        val bindingContext = JvmResolveUtil.analyze(
-            ktFile,
-            environment
+        val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
+            environment,
+            listOf(ktFile)
         ).bindingContext
 
         carets.forEachIndexed { index, (offset, calltype) ->
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
index 0df3b5e..d77cf5d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
@@ -16,80 +16,41 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.psi.search.GlobalSearchScope
 import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
 import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
 import org.jetbrains.kotlin.backend.jvm.jvmPhases
 import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.codegen.ClassBuilderFactories
-import org.jetbrains.kotlin.codegen.ClassBuilderFactory
-import org.jetbrains.kotlin.codegen.DefaultCodegenFactory
 import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
 import org.jetbrains.kotlin.codegen.state.GenerationState
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.load.kotlin.PackagePartProvider
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.resolve.AnalyzingUtils
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 object GenerationUtils {
-    @JvmStatic
-    @JvmOverloads
     fun compileFiles(
-        files: List<KtFile>,
         environment: KotlinCoreEnvironment,
-        classBuilderFactory: ClassBuilderFactory = ClassBuilderFactories.TEST,
-        trace: BindingTrace = NoScopeRecordCliBindingTrace()
-    ): GenerationState =
-        compileFiles(
-            files,
-            environment.configuration,
-            classBuilderFactory,
-            environment::createPackagePartProvider,
-            trace
-        )
-
-    @JvmStatic
-    @JvmOverloads
-    fun compileFiles(
         files: List<KtFile>,
-        configuration: CompilerConfiguration,
-        classBuilderFactory: ClassBuilderFactory,
-        packagePartProvider: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = NoScopeRecordCliBindingTrace()
     ): GenerationState {
-        val analysisResult =
-            JvmResolveUtil.analyzeAndCheckForErrors(
-                files.first().project,
-                files,
-                configuration,
-                packagePartProvider,
-                trace
-            )
+        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
         analysisResult.throwIfError()
 
         val state = GenerationState.Builder(
-            files.first().project,
-            classBuilderFactory,
+            environment.project,
+            ClassBuilderFactories.TEST,
             analysisResult.moduleDescriptor,
             analysisResult.bindingContext,
             files,
-            configuration
+            environment.configuration
         ).codegenFactory(
-            if (configuration.getBoolean(JVMConfigurationKeys.IR))
-                JvmIrCodegenFactory(
-                    configuration,
-                    configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
-                        ?: PhaseConfig(jvmPhases)
-                )
-            else DefaultCodegenFactory
-        ).build()
-        if (analysisResult.shouldGenerateCode) {
-            KotlinCodegenFacade.compileCorrectFiles(state)
-        }
+            JvmIrCodegenFactory(
+                environment.configuration,
+                environment.configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
+                    ?: PhaseConfig(jvmPhases)
+            )
+        ).isIrBackend(true).build()
+
+        KotlinCodegenFacade.compileCorrectFiles(state)
 
         // For JVM-specific errors
         try {
@@ -100,4 +61,4 @@
 
         return state
     }
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
index adadec6..629bfca 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
@@ -16,26 +16,17 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.openapi.project.Project
-import com.intellij.psi.search.GlobalSearchScope
 import org.jetbrains.kotlin.analyzer.AnalysisResult
-import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.load.kotlin.PackagePartProvider
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.resolve.AnalyzingUtils
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 object JvmResolveUtil {
-    @JvmStatic
     fun analyzeAndCheckForErrors(
-        project: Project,
-        files: Collection<KtFile>,
-        configuration: CompilerConfiguration,
-        packagePartProvider: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = CliBindingTrace()
+        environment: KotlinCoreEnvironment,
+        files: Collection<KtFile>
     ): AnalysisResult {
         for (file in files) {
             try {
@@ -45,13 +36,7 @@
             }
         }
 
-        return analyze(
-            project,
-            files,
-            configuration,
-            packagePartProvider,
-            trace
-        ).apply {
+        return analyze(environment, files).apply {
             try {
                 AnalyzingUtils.throwExceptionOnErrors(bindingContext)
             } catch (e: Exception) {
@@ -60,40 +45,12 @@
         }
     }
 
-    @JvmStatic
-    fun analyze(file: KtFile, environment: KotlinCoreEnvironment): AnalysisResult =
-        analyze(setOf(file), environment)
-
-    @JvmStatic
-    fun analyze(files: Collection<KtFile>, environment: KotlinCoreEnvironment): AnalysisResult =
-        analyze(
-            files,
-            environment,
-            environment.configuration
-        )
-
-    @JvmStatic
-    fun analyze(
-        files: Collection<KtFile>,
-        environment: KotlinCoreEnvironment,
-        configuration: CompilerConfiguration
-    ): AnalysisResult =
-        analyze(
+    fun analyze(environment: KotlinCoreEnvironment, files: Collection<KtFile>): AnalysisResult =
+        TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
             environment.project,
             files,
-            configuration,
+            NoScopeRecordCliBindingTrace(),
+            environment.configuration,
             environment::createPackagePartProvider
         )
-
-    private fun analyze(
-        project: Project,
-        files: Collection<KtFile>,
-        configuration: CompilerConfiguration,
-        packagePartProviderFactory: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = CliBindingTrace()
-    ): AnalysisResult {
-        return TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
-            project, files, trace, configuration, packagePartProviderFactory
-        )
-    }
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
index e201d0a..5f00b26 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
@@ -152,10 +152,8 @@
 
         val ktFile = KtPsiFactory(environment.project).createFile(text)
         val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
-            environment.project,
+            environment,
             listOf(ktFile),
-            environment.configuration,
-            environment::createPackagePartProvider
         ).bindingContext
 
         carets.forEachIndexed { index, (offset, marking) ->
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
index fc6c275..2f620f9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
@@ -178,7 +178,7 @@
             interface Bar {
                 @Composable
                 fun composableFunction(param: Boolean): Boolean
-                val composableProperty: Boolean @Composable get()
+                @get:Composable val composableProperty: Boolean
                 fun nonComposableFunction(param: Boolean): Boolean
                 val nonComposableProperty: Boolean
             }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index 9d9672b..92acae1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -1569,6 +1569,110 @@
         }
     }
 
+    @Test
+    fun animatingItemsWithPreviousIndexLargerThanTheNewItemCount() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6, 7))
+        val gridSize = itemSize * 2 - 1
+        rule.setContent {
+            LazyGrid(2, maxSize = with(rule.density) { gridSize.toDp() }) {
+                items(list, key = { it }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertLayoutInfoPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 4, 6)
+        }
+
+        onAnimationFrame { fraction ->
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                add(
+                    2 to AxisIntOffset(
+                        (itemSize * fraction).roundToInt(),
+                        (itemSize * (1f - fraction)).roundToInt()
+                    )
+                )
+                    val item4MainAxis = itemSize + (itemSize * (1f - fraction)).roundToInt()
+                if (item4MainAxis < gridSize) {
+                    add(
+                        4 to AxisIntOffset(0, item4MainAxis)
+                    )
+                }
+                val item6MainAxis = itemSize + (itemSize * 2 * (1f - fraction)).roundToInt()
+                if (item6MainAxis < gridSize) {
+                    add(
+                        6 to AxisIntOffset(itemSize, item6MainAxis)
+                    )
+                }
+            }
+
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
+    @Test
+    fun animatingItemsWithPreviousIndexLargerThanTheNewItemCount_differentSpans() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3, 4, 5, 6))
+        val gridSize = itemSize * 2 - 1
+        rule.setContent {
+            LazyGrid(2, maxSize = with(rule.density) { gridSize.toDp() }) {
+                items(list, key = { it }, span = {
+                    GridItemSpan(if (it == 6) maxLineSpan else 1)
+                }) {
+                    Item(it)
+                }
+            }
+        }
+
+        assertLayoutInfoPositions(
+            0 to AxisIntOffset(0, 0),
+            1 to AxisIntOffset(itemSize, 0),
+            2 to AxisIntOffset(0, itemSize),
+            3 to AxisIntOffset(itemSize, itemSize)
+        )
+
+        rule.runOnIdle {
+            list = listOf(0, 4, 6)
+        }
+
+        onAnimationFrame { fraction ->
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                val item4MainAxis = (itemSize * 2 * (1f - fraction)).roundToInt()
+                if (item4MainAxis < gridSize) {
+                    add(
+                        4 to AxisIntOffset(itemSize, item4MainAxis)
+                    )
+                }
+                if (fraction == 1f) {
+                    val item6MainAxis = itemSize + (itemSize * 2 * (1f - fraction)).roundToInt()
+                    if (item6MainAxis < gridSize) {
+                        add(
+                            6 to AxisIntOffset(0, item6MainAxis)
+                        )
+                    }
+                }
+            }
+
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
     private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
         if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
new file mode 100644
index 0000000..042769b
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.text
+
+import android.os.Build
+import androidx.compose.foundation.text.matchers.assertThat
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@MediumTest
+class DrawPhaseAttributesToggleTest(private val config: Config) {
+
+    private val textTag = "text"
+
+    class Config(
+        private val description: String,
+        val updateStyle: (TextStyle) -> TextStyle,
+        val initializeStyle: (TextStyle) -> TextStyle = { it }
+    ) {
+        override fun toString(): String = "toggling $description"
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            Config(
+                "color unspecified/color/unspecified",
+                initializeStyle = { it.copy(color = Color.Unspecified) },
+                updateStyle = { it.copy(color = Color.Blue) },
+            ),
+            Config(
+                "color colorA/colorB/colorA",
+                initializeStyle = { it.copy(color = Color.Black) },
+                updateStyle = { it.copy(color = Color.Blue) },
+            ),
+            Config(
+                "color colorA/brushA/colorA",
+                initializeStyle = {
+                    it.copy(color = Color.Red)
+                },
+                updateStyle = {
+                    it.copy(brush = Brush.verticalGradient(listOf(Color.Blue, Color.Magenta)))
+                }
+            ),
+            Config(
+                "brush brushA/brushB/brushA",
+                initializeStyle = {
+                    it.copy(brush = Brush.horizontalGradient(listOf(Color.Black, Color.Blue)))
+                },
+                updateStyle = {
+                    it.copy(brush = Brush.verticalGradient(listOf(Color.Red, Color.Blue)))
+                }
+            ),
+            Config(
+                "brush brushA/colorA/brushA",
+                initializeStyle = {
+                    it.copy(brush = Brush.horizontalGradient(listOf(Color.Black, Color.Blue)))
+                },
+                updateStyle = {
+                    it.copy(color = Color.Red)
+                }
+            ),
+            Config(
+                "alpha",
+                initializeStyle = {
+                    it.copy(
+                        alpha = 1f,
+                        brush = Brush.verticalGradient(0f to Color.Blue, 1f to Color.Magenta)
+                    )
+                },
+                updateStyle = { it.copy(alpha = 0.5f, brush = it.brush) },
+            ),
+            Config(
+                "textDecoration none/lineThrough/none",
+                initializeStyle = { it.copy(textDecoration = TextDecoration.None) },
+                updateStyle = { it.copy(textDecoration = TextDecoration.LineThrough) }
+            ),
+            Config(
+                "textDecoration lineThrough/none/lineThrough",
+                initializeStyle = { it.copy(textDecoration = TextDecoration.LineThrough) },
+                updateStyle = { it.copy(textDecoration = TextDecoration.None) }
+            ),
+            Config(
+                "textDecoration null/lineThrough/null",
+                initializeStyle = { it.copy(textDecoration = null) },
+                updateStyle = { it.copy(textDecoration = TextDecoration.LineThrough) }
+            ),
+            Config(
+                "shadow null/shadow/null",
+                initializeStyle = { it.copy(shadow = null) },
+                updateStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 4f)) }
+            ),
+            Config(
+                "shadow shadowA/shadowB/shadowA",
+                initializeStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 1f)) },
+                updateStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 4f)) }
+            ),
+            Config(
+                "shadow shadowA/null/shadowA",
+                initializeStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 1f)) },
+                updateStyle = { it.copy(shadow = null) }
+            ),
+            Config(
+                "drawStyle null/drawStyle/null",
+                initializeStyle = { it.copy(drawStyle = null) },
+                updateStyle = { it.copy(drawStyle = Stroke(width = 2f)) }
+            ),
+            Config(
+                "drawStyle drawStyleA/drawStyleB/drawStyleA",
+                initializeStyle = { it.copy(drawStyle = Stroke(width = 1f)) },
+                updateStyle = { it.copy(drawStyle = Stroke(width = 2f)) }
+            ),
+            Config(
+                "drawStyle drawStyle/null/drawStyle",
+                initializeStyle = { it.copy(drawStyle = Stroke(width = 1f)) },
+                updateStyle = { it.copy(drawStyle = null) }
+            ),
+            Config(
+                "drawStyle stroke/fill/stroke",
+                initializeStyle = { it.copy(drawStyle = Stroke(width = 1f)) },
+                updateStyle = { it.copy(drawStyle = Fill) }
+            )
+        )
+    }
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun basicText() {
+        var style by mutableStateOf(
+            TextStyle(
+                color = Color.Black,
+                textDecoration = null,
+                shadow = null
+            ).let(config.initializeStyle)
+        )
+
+        rule.setContent {
+            BasicText(
+                "ABC",
+                style = style,
+                modifier = Modifier.testTag(textTag)
+            )
+        }
+
+        rule.waitForIdle()
+        val initialBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+
+        style = config.updateStyle(style)
+
+        rule.waitForIdle()
+        val updatedBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+        assertThat(initialBitmap).isNotEqualToBitmap(updatedBitmap)
+
+        style = config.initializeStyle(style)
+
+        rule.waitForIdle()
+        val finalBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+        assertThat(finalBitmap).isNotEqualToBitmap(updatedBitmap)
+
+        assertThat(finalBitmap).isEqualToBitmap(initialBitmap)
+    }
+
+    @Test
+    fun basicTextField() {
+        var style by mutableStateOf(config.initializeStyle(TextStyle(color = Color.Black)))
+
+        rule.setContent {
+            BasicTextField(
+                "ABC",
+                onValueChange = {},
+                textStyle = style,
+                modifier = Modifier.testTag(textTag)
+            )
+        }
+
+        rule.waitForIdle()
+        val initialBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+
+        style = config.updateStyle(style)
+
+        rule.waitForIdle()
+        val updatedBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+        assertThat(initialBitmap).isNotEqualToBitmap(updatedBitmap)
+
+        style = config.initializeStyle(style)
+
+        rule.waitForIdle()
+        val finalBitmap = rule.onNodeWithTag(textTag).captureToImage().asAndroidBitmap()
+        assertThat(finalBitmap).isNotEqualToBitmap(updatedBitmap)
+
+        assertThat(finalBitmap).isEqualToBitmap(initialBitmap)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
index cab90e0..f6ea83a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
@@ -16,10 +16,11 @@
 
 package androidx.compose.foundation.text
 
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
 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.layout.FirstBaseline
 import androidx.compose.ui.layout.IntrinsicMeasurable
@@ -31,134 +32,83 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.node.Ref
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
-import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class TextLayoutTest {
-    @Suppress("DEPRECATION")
     @get:Rule
-    internal val activityTestRule = androidx.test.rule.ActivityTestRule(
-        ComponentActivity::class.java
-    )
-    private lateinit var activity: ComponentActivity
-    private lateinit var density: Density
-
-    @Before
-    fun setup() {
-        activity = activityTestRule.activity
-        density = Density(activity)
-    }
+    val rule = createComposeRule()
 
     @Test
-    fun testTextLayout() = with(density) {
-        val layoutLatch = CountDownLatch(2)
+    fun textLayout() {
         val textSize = Ref<IntSize>()
         val doubleTextSize = Ref<IntSize>()
-        show {
+        rule.setContent {
             TestingText(
                 "aa",
                 modifier = Modifier.onGloballyPositioned { coordinates ->
                     textSize.value = coordinates.size
-                    layoutLatch.countDown()
                 }
             )
             TestingText(
                 "aaaa",
                 modifier = Modifier.onGloballyPositioned { coordinates ->
                     doubleTextSize.value = coordinates.size
-                    layoutLatch.countDown()
                 }
             )
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(textSize.value).isNotNull()
-        assertThat(doubleTextSize.value).isNotNull()
-        assertThat(textSize.value!!.width).isGreaterThan(0)
-        assertThat(textSize.value!!.height).isGreaterThan(0)
-        assertThat(textSize.value!!.width * 2).isEqualTo(doubleTextSize.value!!.width)
-        assertThat(textSize.value!!.height).isEqualTo(doubleTextSize.value!!.height)
+
+        rule.runOnIdle {
+            assertThat(textSize.value).isNotNull()
+            assertThat(doubleTextSize.value).isNotNull()
+            assertThat(textSize.value!!.width).isGreaterThan(0)
+            assertThat(textSize.value!!.height).isGreaterThan(0)
+            assertThat(textSize.value!!.width * 2).isEqualTo(doubleTextSize.value!!.width)
+            assertThat(textSize.value!!.height).isEqualTo(doubleTextSize.value!!.height)
+        }
     }
 
     @Test
-    fun testTextLayout_intrinsicMeasurements() = with(density) {
-        val layoutLatch = CountDownLatch(2)
+    fun textLayout_intrinsicMeasurements() {
         val textSize = Ref<IntSize>()
         val doubleTextSize = Ref<IntSize>()
-        show {
+        var textMeasurable by mutableStateOf<Measurable?>(null)
+
+        rule.setContent {
             TestingText(
                 "aa ",
-                modifier = Modifier.onGloballyPositioned { coordinates ->
-                    textSize.value = coordinates.size
-                    layoutLatch.countDown()
-                }
+                modifier = Modifier.onSizeChanged { textSize.value = it }
             )
             TestingText(
                 "aa aa ",
-                modifier = Modifier.onGloballyPositioned { coordinates ->
-                    doubleTextSize.value = coordinates.size
-                    layoutLatch.countDown()
-                }
+                modifier = Modifier.onSizeChanged { doubleTextSize.value = it }
             )
-        }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        val textWidth = textSize.value!!.width
-        val textHeight = textSize.value!!.height
-        val doubleTextWidth = doubleTextSize.value!!.width
 
-        val intrinsicsLatch = CountDownLatch(1)
-        show {
-            val text = @Composable {
-                TestingText("aa aa ")
-            }
-            val measurePolicy = remember {
-                object : MeasurePolicy {
+            Layout(
+                content = {
+                    TestingText("aa aa ")
+                },
+                measurePolicy = object : MeasurePolicy {
                     override fun MeasureScope.measure(
                         measurables: List<Measurable>,
                         constraints: Constraints
                     ): MeasureResult {
-                        val textMeasurable = measurables.first()
-                        // Min width.
-                        assertThat(textWidth).isEqualTo(textMeasurable.minIntrinsicWidth(0))
-                        // Min height.
-                        assertThat(textMeasurable.minIntrinsicHeight(textWidth))
-                            .isGreaterThan(textHeight)
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.minIntrinsicHeight(doubleTextWidth))
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.minIntrinsicHeight(Constraints.Infinity))
-                        // Max width.
-                        assertThat(doubleTextWidth).isEqualTo(textMeasurable.maxIntrinsicWidth(0))
-                        // Max height.
-                        assertThat(textMeasurable.maxIntrinsicHeight(textWidth))
-                            .isGreaterThan(textHeight)
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.maxIntrinsicHeight(doubleTextWidth))
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.maxIntrinsicHeight(Constraints.Infinity))
-
-                        intrinsicsLatch.countDown()
-
+                        textMeasurable = measurables.first()
                         return layout(0, 0) {}
                     }
 
@@ -182,79 +132,94 @@
                         width: Int
                     ) = 0
                 }
-            }
-            Layout(
-                content = text,
-                measurePolicy = measurePolicy
             )
         }
-        assertThat(intrinsicsLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            val textWidth = textSize.value!!.width
+            val textHeight = textSize.value!!.height
+            val doubleTextWidth = doubleTextSize.value!!.width
+
+            textMeasurable!!.let { textMeasurable ->
+                // Min width.
+                assertThat(textWidth).isEqualTo(textMeasurable.minIntrinsicWidth(0))
+                // Min height.
+                assertThat(textMeasurable.minIntrinsicHeight(textWidth))
+                    .isGreaterThan(textHeight)
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.minIntrinsicHeight(doubleTextWidth))
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.minIntrinsicHeight(Constraints.Infinity))
+
+                // Max width.
+                assertThat(doubleTextWidth).isEqualTo(textMeasurable.maxIntrinsicWidth(0))
+                // Max height.
+                assertThat(textMeasurable.maxIntrinsicHeight(textWidth))
+                    .isGreaterThan(textHeight)
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.maxIntrinsicHeight(doubleTextWidth))
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.maxIntrinsicHeight(Constraints.Infinity))
+            }
+        }
     }
 
     @Test
-    fun testTextLayout_providesBaselines() = with(density) {
-        val layoutLatch = CountDownLatch(2)
-        show {
-            val text = @Composable {
+    fun textLayout_providesBaselines_whenUnconstrained() {
+        var firstBaseline by mutableStateOf(-1)
+        var lastBaseline by mutableStateOf(-1)
+
+        rule.setContent {
+            Layout({
                 TestingText("aa")
-            }
-            Layout(text) { measurables, _ ->
+            }) { measurables, _ ->
                 val placeable = measurables.first().measure(Constraints())
-                assertThat(placeable[FirstBaseline]).isNotNull()
-                assertThat(placeable[LastBaseline]).isNotNull()
-                assertThat(placeable[FirstBaseline]).isEqualTo(placeable[LastBaseline])
-                layoutLatch.countDown()
-                layout(0, 0) {}
-            }
-            Layout(text) { measurables, _ ->
-                val placeable = measurables.first().measure(Constraints(maxWidth = 0))
-                assertThat(placeable[FirstBaseline]).isNotNull()
-                assertThat(placeable[LastBaseline]).isNotNull()
-                assertThat(placeable[FirstBaseline])
-                    .isLessThan(placeable[LastBaseline])
-                layoutLatch.countDown()
+                firstBaseline = placeable[FirstBaseline]
+                lastBaseline = placeable[LastBaseline]
                 layout(0, 0) {}
             }
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            assertThat(firstBaseline).isGreaterThan(-1)
+            assertThat(lastBaseline).isGreaterThan(-1)
+            assertThat(firstBaseline).isEqualTo(lastBaseline)
+        }
     }
 
     @Test
-    fun testOnTextLayout() = with(density) {
-        val layoutLatch = CountDownLatch(1)
-        val callback = mock<(TextLayoutResult) -> Unit>()
-        show {
-            val text = @Composable {
-                TestingText("aa", onTextLayout = callback)
-            }
-            Layout(text) { measurables, _ ->
-                measurables.first().measure(Constraints())
-                layoutLatch.countDown()
+    fun textLayout_providesBaselines_whenZeroMaxWidth() {
+        var firstBaseline by mutableStateOf(-1)
+        var lastBaseline by mutableStateOf(-1)
+
+        rule.setContent {
+            Layout({
+                TestingText("aa")
+            }) { measurables, _ ->
+                val placeable = measurables.first().measure(Constraints(maxWidth = 0))
+                firstBaseline = placeable[FirstBaseline]
+                lastBaseline = placeable[LastBaseline]
                 layout(0, 0) {}
             }
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        verify(callback, times(1)).invoke(any())
+
+        rule.runOnIdle {
+            assertThat(firstBaseline).isGreaterThan(-1)
+            assertThat(lastBaseline).isGreaterThan(-1)
+            assertThat(firstBaseline).isLessThan(lastBaseline)
+        }
     }
 
-    private fun show(composable: @Composable () -> Unit) {
-        val runnable = Runnable {
-            activity.setContent {
-                Layout(composable) { measurables, constraints ->
-                    val placeables = measurables.map {
-                        it.measure(constraints.copy(minWidth = 0, minHeight = 0))
-                    }
-                    layout(constraints.maxWidth, constraints.maxHeight) {
-                        var top = 0
-                        placeables.forEach {
-                            it.placeRelative(0, top)
-                            top += it.height
-                        }
-                    }
-                }
-            }
+    @Test
+    fun textLayout_OnTextLayoutCallback() {
+        val resultsFromCallback = mutableListOf<TextLayoutResult>()
+        rule.setContent {
+            TestingText("aa", onTextLayout = { resultsFromCallback += it })
         }
-        activityTestRule.runOnUiThread(runnable)
+
+        rule.runOnIdle {
+            assertThat(resultsFromCallback).hasSize(1)
+        }
     }
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index aab5ed0..7aac595 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -350,6 +350,9 @@
 
     fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this
 
+    /**
+     * @return the amount of scroll that was consumed
+     */
     fun ScrollScope.dispatchScroll(availableDelta: Offset, source: NestedScrollSource): Offset {
         val scrollDelta = availableDelta.singleAxisOffset()
         val overscrollPreConsumed = overscrollPreConsumeDelta(scrollDelta, source)
@@ -376,7 +379,7 @@
             source
         )
 
-        return leftForParent - parentConsumed
+        return overscrollPreConsumed + preConsumedByParent + axisConsumed + parentConsumed
     }
 
     fun overscrollPreConsumeDelta(
@@ -447,8 +450,7 @@
         var result: Velocity = available
         scrollableState.scroll {
             val outerScopeScroll: (Offset) -> Offset = { delta ->
-                val consumed = this.dispatchScroll(delta.reverseIfNeeded(), Fling)
-                delta - consumed.reverseIfNeeded()
+                dispatchScroll(delta.reverseIfNeeded(), Fling).reverseIfNeeded()
             }
             val scope = object : ScrollScope {
                 override fun scrollBy(pixels: Float): Float {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 78679fe..351a8a1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.animation.core.VisibilityThreshold
 import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -361,12 +362,9 @@
             if (!reverseLayout) viewportStartItemIndex > index else viewportStartItemIndex < index
         return when {
             afterViewportEnd -> {
-                val fromIndex = if (!reverseLayout) {
-                    // viewportEndItemIndex is the last item in the line already
-                    viewportEndItemIndex + 1
-                } else {
-                    spanLayoutProvider.firstIndexInNextLineAfter(index)
-                }
+                val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
+                    if (!reverseLayout) viewportEndItemIndex else index
+                )
                 val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
                     if (!reverseLayout) index else viewportEndItemIndex
                 )
@@ -383,12 +381,9 @@
                 val fromIndex = spanLayoutProvider.firstIndexInNextLineAfter(
                     if (!reverseLayout) index else viewportStartItemIndex
                 )
-                val toIndex = if (!reverseLayout) {
-                    // viewportStartItemIndex is the first item in the line already
-                    viewportStartItemIndex - 1
-                } else {
-                    spanLayoutProvider.lastIndexInPreviousLineBefore(index)
-                }
+                val toIndex = spanLayoutProvider.lastIndexInPreviousLineBefore(
+                    if (!reverseLayout) viewportStartItemIndex else index
+                )
                 viewportStartItemNotVisiblePartSize + scrolledBy.mainAxis +
                     // minus the size of this item as we are looking for the start offset of it.
                     -mainAxisSizeWithSpacings +
@@ -485,18 +480,6 @@
     visibilityThreshold = IntOffset.VisibilityThreshold
 )
 
-private fun LazyGridSpanLayoutProvider.lastIndexInPreviousLineBefore(index: Int): Int {
-    val lineIndex = getLineIndexOfItem(index)
-    val lineConfiguration = getLineConfiguration(lineIndex.value)
-    return lineConfiguration.firstItemIndex - 1
-}
-
-private fun LazyGridSpanLayoutProvider.firstIndexInNextLineAfter(index: Int): Int {
-    val lineIndex = getLineIndexOfItem(index)
-    val lineConfiguration = getLineConfiguration(lineIndex.value)
-    return lineConfiguration.firstItemIndex + lineConfiguration.spans.size
-}
-
 private fun LazyGridSpanLayoutProvider.getLinesMainAxisSizesSum(
     fromIndex: Int,
     toIndex: Int,
@@ -532,3 +515,45 @@
     }
     return fallback
 }
+
+private fun LazyGridSpanLayoutProvider.lastIndexInPreviousLineBefore(index: Int) =
+    firstIndexInLineContaining(index) - 1
+
+private fun LazyGridSpanLayoutProvider.firstIndexInNextLineAfter(index: Int) =
+    if (index >= totalSize) {
+        // after totalSize we just approximate with 1 slot per item
+        firstIndexInLineContaining(index) + slotsPerLine
+    } else {
+        val lineIndex = getLineIndexOfItem(index)
+        val lineConfiguration = getLineConfiguration(lineIndex.value)
+        lineConfiguration.firstItemIndex + lineConfiguration.spans.size
+    }
+
+private fun LazyGridSpanLayoutProvider.firstIndexInLineContaining(index: Int): Int {
+    return if (index >= totalSize) {
+        val firstIndexForLastKnowLine = getFirstIndexInNextLineAfterTheLastKnownOne()
+        // after totalSize we just approximate with 1 slot per item
+        val linesBetween = (index - firstIndexForLastKnowLine) / slotsPerLine
+        firstIndexForLastKnowLine + slotsPerLine * linesBetween
+    } else {
+        val lineIndex = getLineIndexOfItem(index)
+        val lineConfiguration = getLineConfiguration(lineIndex.value)
+        lineConfiguration.firstItemIndex
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyGridSpanLayoutProvider.getFirstIndexInNextLineAfterTheLastKnownOne(): Int {
+    // first we find the line for the `totalSize - 1` item
+    val lineConfiguration = getLineConfiguration(getLineIndexOfItem(totalSize - 1).value)
+    var currentSpan = 0
+    var currentIndex = lineConfiguration.firstItemIndex - 1
+    // then we go through all the known spans
+    lineConfiguration.spans.fastForEach {
+        currentSpan += it.currentLineSpan
+        currentIndex++
+    }
+    // and increment index as if we had more items with slot == 1 until we switch to the next line
+    currentIndex += slotsPerLine - currentSpan + 1
+    return currentIndex
+}
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
index be6e663..4e62a3d 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconSourceTasks.kt
@@ -119,6 +119,10 @@
     val sourceSet = project.getMultiplatformSourceSet(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
     val generatedSrcMainDirectory = buildDirectory.resolve(IconGenerationTask.GeneratedSrcMain)
     sourceSet.kotlin.srcDir(project.files(generatedSrcMainDirectory).builtBy(task))
+    // add it to the multiplatform sources as well.
+    project.tasks.named("multiplatformSourceJar", Jar::class.java).configure {
+        it.from(task.map { generatedSrcMainDirectory })
+    }
     project.addToSourceJar(generatedSrcMainDirectory, task)
 }
 
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index bc57514..f96b55a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -103,6 +103,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -681,6 +684,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 906e393..c815b0b 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -132,6 +132,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -1013,6 +1016,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index bc57514..f96b55a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -103,6 +103,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -681,6 +684,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
new file mode 100644
index 0000000..9b5997e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.material3
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.material3.internal.CalendarModelImpl
+import androidx.compose.material3.internal.LegacyCalendarModelImpl
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.Locale
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalMaterial3Api::class)
+@RequiresApi(Build.VERSION_CODES.O)
+internal class CalendarModelTest(private val model: CalendarModel) {
+
+    @Test
+    fun dateCreation() {
+        val date = model.getDate(January2022Millis) // 1/1/2022
+        assertThat(date.year).isEqualTo(2022)
+        assertThat(date.month).isEqualTo(1)
+        assertThat(date.dayOfMonth).isEqualTo(1)
+        assertThat(date.utcTimeMillis).isEqualTo(January2022Millis)
+    }
+
+    @Test
+    fun dateRestore() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        assertThat(model.getDate(date.utcTimeMillis)).isEqualTo(date)
+    }
+
+    @Test
+    fun monthCreation() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val monthFromDate = model.getMonth(date)
+        val monthFromMilli = model.getMonth(January2022Millis)
+        val monthFromYearMonth = model.getMonth(year = 2022, month = 1)
+        assertThat(monthFromDate).isEqualTo(monthFromMilli)
+        assertThat(monthFromDate).isEqualTo(monthFromYearMonth)
+    }
+
+    @Test
+    fun monthRestore() {
+        val month = model.getMonth(year = 1999, month = 12)
+        assertThat(model.getMonth(month.startUtcTimeMillis)).isEqualTo(month)
+    }
+
+    @Test
+    fun plusMinusMonth() {
+        val month = model.getMonth(January2022Millis) // 1/1/2022
+        val expectedNextMonth = model.getMonth(month.endUtcTimeMillis + 1) // 2/1/2022
+        val plusMonth = model.plusMonths(from = month, addedMonthsCount = 1)
+        assertThat(plusMonth).isEqualTo(expectedNextMonth)
+        assertThat(model.minusMonths(from = plusMonth, subtractedMonthsCount = 1)).isEqualTo(month)
+    }
+
+    @Test
+    fun parseDate() {
+        val expectedDate =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val parsedDate = model.parse("1/1/2022", "M/d/yyyy")
+        assertThat(parsedDate).isEqualTo(expectedDate)
+    }
+
+    @Test
+    fun formatDate() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val month = model.plusMonths(model.getMonth(date), 2)
+        assertThat(model.format(date, "MM/dd/yyyy")).isEqualTo("01/01/2022")
+        assertThat(model.format(month, "MM/dd/yyyy")).isEqualTo("03/01/2022")
+    }
+
+    @Test
+    fun weekdayNames() {
+        // Ensure we are running on a US locale for this test.
+        Locale.setDefault(Locale.US)
+        val weekDays = model.weekdayNames
+        assertThat(weekDays).hasSize(DaysInWeek)
+        // Check that the first day is always "Monday", per ISO-8601 standard.
+        assertThat(weekDays.first().first).ignoringCase().contains("Monday")
+        weekDays.forEach {
+            assertThat(it.second.first().lowercaseChar()).isEqualTo(
+                it.first.first().lowercaseChar()
+            )
+        }
+    }
+
+    @Test
+    fun equalModelsOutput() {
+        // Note: This test ignores the parameters and just runs a few equality tests for the output.
+        // It will execute twice, but that should to tolerable :)
+        val newModel = CalendarModelImpl()
+        val legacyModel = LegacyCalendarModelImpl()
+
+        val date = newModel.getDate(January2022Millis) // 1/1/2022
+        val legacyDate = legacyModel.getDate(January2022Millis)
+        val month = newModel.getMonth(date)
+        val legacyMonth = legacyModel.getMonth(date)
+
+        assertThat(newModel.today).isEqualTo(legacyModel.today)
+        assertThat(month).isEqualTo(legacyMonth)
+        assertThat(newModel.plusMonths(month, 3)).isEqualTo(legacyModel.plusMonths(month, 3))
+        assertThat(date).isEqualTo(legacyDate)
+        assertThat(newModel.getDayOfWeek(date)).isEqualTo(legacyModel.getDayOfWeek(date))
+        assertThat(newModel.format(date, "MMM d, yyyy")).isEqualTo(
+            legacyModel.format(
+                date,
+                "MMM d, yyyy"
+            )
+        )
+        assertThat(newModel.format(month, "MMM yyyy")).isEqualTo(
+            legacyModel.format(
+                month,
+                "MMM yyyy"
+            )
+        )
+    }
+
+    internal companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            CalendarModelImpl(),
+            LegacyCalendarModelImpl()
+        )
+    }
+}
+
+private const val January2022Millis = 1640995200000
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt
new file mode 100644
index 0000000..089aeb0
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt
@@ -0,0 +1,357 @@
+/*
+ * 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.material3.internal
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.material3.CalendarDate
+import androidx.compose.material3.CalendarModel
+import androidx.compose.material3.CalendarMonth
+import androidx.compose.material3.DaysInWeek
+import androidx.compose.material3.ExperimentalMaterial3Api
+import java.text.DateFormatSymbols
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.time.DayOfWeek
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
+import java.time.format.TextStyle
+import java.time.temporal.WeekFields
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * Creates a [CalendarModel] to be used by the date picker.
+ */
+@ExperimentalMaterial3Api
+internal fun createDefaultCalendarModel(): CalendarModel {
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        CalendarModelImpl()
+    } else {
+        LegacyCalendarModelImpl()
+    }
+}
+
+/**
+ * A [CalendarModel] implementation for API < 26.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+internal class LegacyCalendarModelImpl : CalendarModel {
+
+    override val today
+        get(): CalendarDate {
+            val systemCalendar = Calendar.getInstance()
+            systemCalendar[Calendar.HOUR_OF_DAY] = 0
+            systemCalendar[Calendar.MINUTE] = 0
+            systemCalendar[Calendar.SECOND] = 0
+            systemCalendar[Calendar.MILLISECOND] = 0
+            val utcOffset =
+                systemCalendar.get(Calendar.ZONE_OFFSET) + systemCalendar.get(Calendar.DST_OFFSET)
+            return CalendarDate(
+                year = systemCalendar[Calendar.YEAR],
+                month = systemCalendar[Calendar.MONTH] + 1,
+                dayOfMonth = systemCalendar[Calendar.DAY_OF_MONTH],
+                utcTimeMillis = systemCalendar.timeInMillis + utcOffset
+            )
+        }
+
+    override val firstDayOfWeek: Int = dayInISO8601(Calendar.getInstance().firstDayOfWeek)
+
+    override val weekdayNames: List<Pair<String, String>> = buildList {
+        val weekdays = DateFormatSymbols(Locale.getDefault()).weekdays
+        val shortWeekdays = DateFormatSymbols(Locale.getDefault()).shortWeekdays
+        // Skip the first item, as it's empty, and the second item, as it represents Sunday while it
+        // should be last according to ISO-8601.
+        weekdays.drop(2).forEachIndexed { index, day ->
+            add(Pair(day, shortWeekdays[index + 2]))
+        }
+        // Add Sunday to the end.
+        add(Pair(weekdays[1], shortWeekdays[1]))
+    }
+
+    override fun getDate(timeInMillis: Long): CalendarDate {
+        val calendar = Calendar.getInstance(utcTimeZone)
+        calendar.timeInMillis = timeInMillis
+        return CalendarDate(
+            year = calendar[Calendar.YEAR],
+            month = calendar[Calendar.MONTH] + 1,
+            dayOfMonth = calendar[Calendar.DAY_OF_MONTH],
+            utcTimeMillis = timeInMillis
+        )
+    }
+
+    override fun getMonth(timeInMillis: Long): CalendarMonth {
+        val firstDayCalendar = Calendar.getInstance(utcTimeZone)
+        firstDayCalendar.timeInMillis = timeInMillis
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = 1
+        return getMonth(firstDayCalendar)
+    }
+
+    override fun getMonth(date: CalendarDate): CalendarMonth {
+        return getMonth(date.year, date.month)
+    }
+
+    override fun getMonth(year: Int, month: Int): CalendarMonth {
+        val firstDayCalendar = Calendar.getInstance(utcTimeZone)
+        firstDayCalendar.clear()
+        firstDayCalendar[Calendar.YEAR] = year
+        firstDayCalendar[Calendar.MONTH] = month - 1
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = 1
+        return getMonth(firstDayCalendar)
+    }
+
+    override fun getDayOfWeek(date: CalendarDate): Int {
+        return dayInISO8601(date.toCalendar(TimeZone.getDefault())[Calendar.DAY_OF_WEEK])
+    }
+
+    override fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth {
+        if (addedMonthsCount <= 0) return from
+
+        val laterMonth = from.toCalendar()
+        laterMonth.add(Calendar.MONTH, addedMonthsCount)
+        return getMonth(laterMonth)
+    }
+
+    override fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth {
+        if (subtractedMonthsCount <= 0) return from
+
+        val earlierMonth = from.toCalendar()
+        earlierMonth.add(Calendar.MONTH, -subtractedMonthsCount)
+        return getMonth(earlierMonth)
+    }
+
+    override fun format(month: CalendarMonth, pattern: String): String {
+        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return dateFormat.format(month.toCalendar().timeInMillis)
+    }
+
+    override fun format(date: CalendarDate, pattern: String): String {
+        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return dateFormat.format(date.toCalendar(utcTimeZone).timeInMillis)
+    }
+
+    override fun parse(date: String, pattern: String): CalendarDate? {
+        val dateFormat = SimpleDateFormat(pattern)
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return try {
+            val parsedDate = dateFormat.parse(date) ?: return null
+            val calendar = Calendar.getInstance(utcTimeZone)
+            calendar.time = parsedDate
+            CalendarDate(
+                year = calendar[Calendar.YEAR],
+                month = calendar[Calendar.MONTH] + 1,
+                dayOfMonth = calendar[Calendar.DAY_OF_MONTH],
+                utcTimeMillis = calendar.timeInMillis
+            )
+        } catch (pe: ParseException) {
+            null
+        }
+    }
+
+    /**
+     * Returns a given [Calendar] day number as a day representation under ISO-8601, where the first
+     * day is defined as Monday.
+     */
+    private fun dayInISO8601(day: Int): Int {
+        val shiftedDay = (day + 6) % 7
+        return if (shiftedDay == 0) return /* Sunday */ 7 else shiftedDay
+    }
+
+    private fun getMonth(firstDayCalendar: Calendar): CalendarMonth {
+        val difference = dayInISO8601(firstDayCalendar[Calendar.DAY_OF_WEEK]) - firstDayOfWeek
+        val daysFromStartOfWeekToFirstOfMonth = if (difference < 0) {
+            difference + DaysInWeek
+        } else {
+            difference
+        }
+        return CalendarMonth(
+            year = firstDayCalendar[Calendar.YEAR],
+            month = firstDayCalendar[Calendar.MONTH] + 1,
+            numberOfDays = firstDayCalendar.getActualMaximum(Calendar.DAY_OF_MONTH),
+            daysFromStartOfWeekToFirstOfMonth = daysFromStartOfWeekToFirstOfMonth,
+            startUtcTimeMillis = firstDayCalendar.timeInMillis
+        )
+    }
+
+    private fun CalendarMonth.toCalendar(): Calendar {
+        val calendar = Calendar.getInstance(utcTimeZone)
+        calendar.timeInMillis = this.startUtcTimeMillis
+        return calendar
+    }
+
+    private fun CalendarDate.toCalendar(timeZone: TimeZone): Calendar {
+        val calendar = Calendar.getInstance(timeZone)
+        calendar.clear()
+        calendar[Calendar.YEAR] = this.year
+        calendar[Calendar.MONTH] = this.month - 1
+        calendar[Calendar.DAY_OF_MONTH] = this.dayOfMonth
+        return calendar
+    }
+
+    private var utcTimeZone = TimeZone.getTimeZone("UTC")
+}
+
+/**
+ * A [CalendarModel] implementation for API >= 26.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@RequiresApi(Build.VERSION_CODES.O)
+internal class CalendarModelImpl : CalendarModel {
+
+    override val today
+        get(): CalendarDate {
+            val systemLocalDate = LocalDate.now()
+            return CalendarDate(
+                year = systemLocalDate.year,
+                month = systemLocalDate.monthValue,
+                dayOfMonth = systemLocalDate.dayOfMonth,
+                utcTimeMillis = systemLocalDate.atTime(LocalTime.MIDNIGHT)
+                    .atZone(utcTimeZoneId).toInstant().toEpochMilli()
+            )
+        }
+
+    override val firstDayOfWeek: Int = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value
+
+    override val weekdayNames: List<Pair<String, String>> =
+        // This will start with Monday as the first day, according to ISO-8601.
+        with(Locale.getDefault()) {
+            DayOfWeek.values().map {
+                it.getDisplayName(
+                    TextStyle.FULL,
+                    /* locale = */ this
+                ) to it.getDisplayName(
+                    TextStyle.NARROW,
+                    /* locale = */ this
+                )
+            }
+        }
+
+    override fun getDate(timeInMillis: Long): CalendarDate {
+        val localDate =
+            Instant.ofEpochMilli(timeInMillis).atZone(utcTimeZoneId).toLocalDate()
+        return CalendarDate(
+            year = localDate.year,
+            month = localDate.monthValue,
+            dayOfMonth = localDate.dayOfMonth,
+            utcTimeMillis = timeInMillis
+        )
+    }
+
+    override fun getMonth(timeInMillis: Long): CalendarMonth {
+        return getMonth(
+            Instant.ofEpochMilli(timeInMillis).atZone(utcTimeZoneId).toLocalDate()
+        )
+    }
+
+    override fun getMonth(date: CalendarDate): CalendarMonth {
+        return getMonth(LocalDate.of(date.year, date.month, 1))
+    }
+
+    override fun getMonth(year: Int, month: Int): CalendarMonth {
+        return getMonth(LocalDate.of(year, month, 1))
+    }
+
+    override fun getDayOfWeek(date: CalendarDate): Int {
+        return date.toLocalDate().dayOfWeek.value
+    }
+
+    override fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth {
+        if (addedMonthsCount <= 0) return from
+
+        val firstDayLocalDate = from.toLocalDate()
+        val laterMonth = firstDayLocalDate.plusMonths(addedMonthsCount.toLong())
+        return getMonth(laterMonth)
+    }
+
+    override fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth {
+        if (subtractedMonthsCount <= 0) return from
+
+        val firstDayLocalDate = from.toLocalDate()
+        val earlierMonth = firstDayLocalDate.minusMonths(subtractedMonthsCount.toLong())
+        return getMonth(earlierMonth)
+    }
+
+    override fun format(month: CalendarMonth, pattern: String): String {
+        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(pattern)
+        return month.toLocalDate().format(formatter)
+    }
+
+    override fun format(date: CalendarDate, pattern: String): String {
+        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(pattern)
+        return date.toLocalDate().format(formatter)
+    }
+
+    override fun parse(date: String, pattern: String): CalendarDate? {
+        // TODO: A DateTimeFormatter can be reused.
+        val formatter = DateTimeFormatter.ofPattern(pattern)
+        return try {
+            val localDate = LocalDate.parse(date, formatter)
+            CalendarDate(
+                year = localDate.year,
+                month = localDate.month.value,
+                dayOfMonth = localDate.dayOfMonth,
+                utcTimeMillis = localDate.atTime(LocalTime.MIDNIGHT)
+                    .atZone(utcTimeZoneId).toInstant().toEpochMilli()
+            )
+        } catch (pe: DateTimeParseException) {
+            null
+        }
+    }
+
+    private fun getMonth(firstDayLocalDate: LocalDate): CalendarMonth {
+        val difference = firstDayLocalDate.dayOfWeek.value - firstDayOfWeek
+        val daysFromStartOfWeekToFirstOfMonth = if (difference < 0) {
+            difference + DaysInWeek
+        } else {
+            difference
+        }
+        val firstDayEpochMillis =
+            firstDayLocalDate.atTime(LocalTime.MIDNIGHT).atZone(utcTimeZoneId).toInstant()
+                .toEpochMilli()
+        return CalendarMonth(
+            year = firstDayLocalDate.year,
+            month = firstDayLocalDate.monthValue,
+            numberOfDays = firstDayLocalDate.lengthOfMonth(),
+            daysFromStartOfWeekToFirstOfMonth = daysFromStartOfWeekToFirstOfMonth,
+            startUtcTimeMillis = firstDayEpochMillis
+        )
+    }
+
+    private fun CalendarMonth.toLocalDate(): LocalDate {
+        return Instant.ofEpochMilli(startUtcTimeMillis).atZone(utcTimeZoneId).toLocalDate()
+    }
+
+    private fun CalendarDate.toLocalDate(): LocalDate {
+        return LocalDate.of(
+            this.year,
+            this.month,
+            this.dayOfMonth
+        )
+    }
+
+    private val utcTimeZoneId = ZoneId.of("UTC")
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
new file mode 100644
index 0000000..54cbd7b
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.material3
+
+@ExperimentalMaterial3Api
+internal interface CalendarModel {
+
+    /**
+     * A [CalendarDate] representing the current day.
+     */
+    val today: CalendarDate
+
+    /**
+     * Hold the first day of the week at the current `Locale` as an integer. The integer value
+     * follows the ISO-8601 standard and refer to Monday as 1, and Sunday as 7.
+     */
+    val firstDayOfWeek: Int
+
+    /**
+     * Holds a list of weekday names, starting from Monday as the first day in the list.
+     *
+     * Each item in this list is a [Pair] that holds the full name of the day, and its short
+     * abbreviation letter(s).
+     *
+     * Newer APIs (i.e. API 26+), a [Pair] will hold a full name and the first letter of the
+     * day.
+     * Older APIs that predate API 26 will hold a full name and the first three letters of the day.
+     */
+    val weekdayNames: List<Pair<String, String>>
+
+    /**
+     * Returns a [CalendarDate] from a given _UTC_ time in milliseconds.
+     *
+     * @param timeInMillis UTC milliseconds from the epoch
+     */
+    fun getDate(timeInMillis: Long): CalendarDate
+
+    /**
+     * Returns a [CalendarMonth] from a given _UTC_ time in milliseconds.
+     *
+     * @param timeInMillis UTC milliseconds from the epoch for the first day the month
+     */
+    fun getMonth(timeInMillis: Long): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] from a given [CalendarDate].
+     *
+     * Note: This function ignores the [CalendarDate.dayOfMonth] value and just uses the date's
+     * year and month to resolve a [CalendarMonth].
+     *
+     * @param date a [CalendarDate] to resolve into a month
+     */
+    fun getMonth(date: CalendarDate): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] from a given [year] and [month].
+     *
+     * @param year the month's year
+     * @param month an integer representing a month (e.g. JANUARY as 1, December as 12)
+     */
+    fun getMonth(year: Int, /* @IntRange(from = 1, to = 12) */ month: Int): CalendarMonth
+
+    /**
+     * Returns a day of week from a given [CalendarDate].
+     *
+     * @param date a [CalendarDate] to resolve
+     */
+    fun getDayOfWeek(date: CalendarDate): Int
+
+    /**
+     * Returns a [CalendarMonth] that is computed by adding a number of months, given as
+     * [addedMonthsCount], to a given month.
+     *
+     * @param from the [CalendarMonth] to add to
+     * @param addedMonthsCount the number of months to add
+     */
+    fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] that is computed by subtracting a number of months, given as
+     * [subtractedMonthsCount], from a given month.
+     *
+     * @param from the [CalendarMonth] to subtract from
+     * @param subtractedMonthsCount the number of months to subtract
+     */
+    fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth
+
+    /**
+     * Formats a [CalendarMonth] into a string with a given date format pattern.
+     *
+     * @param month a [CalendarMonth] to format
+     * @param pattern a date format pattern
+     */
+    fun format(month: CalendarMonth, pattern: String): String
+
+    /**
+     * Formats a [CalendarDate] into a string with a given date format pattern.
+     *
+     * @param date a [CalendarDate] to format
+     * @param pattern a date format pattern
+     */
+    fun format(date: CalendarDate, pattern: String): String
+
+    /**
+     * Parses a date string into a [CalendarDate].
+     *
+     * @param date a date string
+     * @param pattern the expected date pattern to be used for parsing the date string
+     * @return a [CalendarDate], or a `null` in case the parsing failed
+     */
+    fun parse(date: String, pattern: String): CalendarDate?
+}
+
+/**
+ * Represents a calendar date.
+ *
+ * @param year the date's year
+ * @param month the date's month
+ * @param dayOfMonth the date's day of month
+ * @param utcTimeMillis the date representation in _UTC_ milliseconds from the epoch
+ */
+@ExperimentalMaterial3Api
+internal data class CalendarDate(
+    val year: Int,
+    val month: Int,
+    val dayOfMonth: Int,
+    val utcTimeMillis: Long
+) : Comparable<CalendarDate> {
+    override operator fun compareTo(other: CalendarDate): Int =
+        this.utcTimeMillis.compareTo(other.utcTimeMillis)
+}
+
+/**
+ * Represents a calendar month.
+ *
+ * @param year the month's year
+ * @param month the calendar month as an integer (e.g. JANUARY as 1, December as 12)
+ * @param numberOfDays the number of days in the month
+ * @param daysFromStartOfWeekToFirstOfMonth the number of days from the start of the week to the
+ * first day of the month
+ * @param startUtcTimeMillis the first day of the month in _UTC_ milliseconds from the epoch
+ */
+@ExperimentalMaterial3Api
+internal data class CalendarMonth(
+    val year: Int,
+    val month: Int,
+    val numberOfDays: Int,
+    val daysFromStartOfWeekToFirstOfMonth: Int,
+    val startUtcTimeMillis: Long
+) {
+
+    /**
+     * The last _UTC_ milliseconds from the epoch of the month (i.e. the last millisecond of the
+     * last day of the month)
+     */
+    val endUtcTimeMillis: Long = startUtcTimeMillis + (numberOfDays * MillisecondsIn24Hours) - 1
+
+    /**
+     * Returns the position of a [CalendarMonth] within given years range.
+     */
+    internal fun indexIn(years: IntRange): Int {
+        return (year - years.first) * 12 + month - 1
+    }
+}
+
+internal const val DaysInWeek: Int = 7
+internal const val MillisecondsIn24Hours = 86400000L
diff --git a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
deleted file mode 100644
index 33af974..0000000
--- a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2020 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.graphics.benchmark
-
-import androidx.benchmark.junit4.PerfettoRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.runner.RunWith
-
-/**
- * Duplicate of [VectorBenchmark], but which adds tracing.
- *
- * Note: Per PerfettoRule, these benchmarks will be ignored < API 29
- */
-@Suppress("ClassName")
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class VectorBenchmarkWithTracing : VectorBenchmark() {
-    @get:Rule
-    val perfettoRule = PerfettoRule()
-}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidPargraphExt.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
similarity index 83%
rename from compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidPargraphExt.kt
rename to compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
index 0a1cbd0..a76fc2b 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidPargraphExt.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphExt.kt
@@ -18,17 +18,14 @@
 
 import android.graphics.Bitmap
 import android.graphics.Canvas
-import androidx.compose.ui.text.style.TextDecoration
 import kotlin.math.ceil
 
-internal fun AndroidParagraph.bitmap(
-    textDecoration: TextDecoration? = null
-): Bitmap {
+internal fun AndroidParagraph.bitmap(): Bitmap {
     val bitmap = Bitmap.createBitmap(
         ceil(this.width).toInt(),
         ceil(this.height).toInt(),
         Bitmap.Config.ARGB_8888
     )
-    this.paint(androidx.compose.ui.graphics.Canvas(Canvas(bitmap)), textDecoration = textDecoration)
+    this.paint(androidx.compose.ui.graphics.Canvas(Canvas(bitmap)))
     return bitmap
 }
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 946e6c0..663d460 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1436,29 +1436,25 @@
     }
 
     @Test
-    fun testSpanStyle_textDecoration_underline_appliedAsSpan() {
-        val text = "abc"
+    fun testSpanStyle_textDecoration_underline_appliedOnTextPaint() {
         val paragraph = simpleParagraph(
-            text = text,
+            text = "",
             style = TextStyle(textDecoration = TextDecoration.Underline),
             width = 0.0f
         )
 
-        assertThat(paragraph.charSequence)
-            .hasSpan(TextDecorationSpan::class, 0, text.length) { it.isUnderlineText }
+        assertThat(paragraph.textPaint.isUnderlineText).isTrue()
     }
 
     @Test
-    fun testSpanStyle_textDecoration_lineThrough_appliedAsSpan() {
-        val text = "abc"
+    fun testSpanStyle_textDecoration_lineThrough_appliedOnTextPaint() {
         val paragraph = simpleParagraph(
-            text = text,
+            text = "",
             style = TextStyle(textDecoration = TextDecoration.LineThrough),
             width = 0.0f
         )
 
-        assertThat(paragraph.charSequence)
-            .hasSpan(TextDecorationSpan::class, 0, text.length) { it.isStrikethroughText }
+        assertThat(paragraph.textPaint.isStrikeThruText).isTrue()
     }
 
     @OptIn(ExperimentalTextApi::class)
@@ -1519,6 +1515,7 @@
             style = TextStyle(textDecoration = null),
             width = 0.0f
         )
+        assertThat(paragraph.textPaint.isUnderlineText).isFalse()
 
         val canvas = Canvas(android.graphics.Canvas())
         paragraph.paint(canvas, textDecoration = TextDecoration.Underline)
@@ -1534,12 +1531,9 @@
             ),
             width = 0.0f
         )
-        // Underline text is not applied on TextPaint initially. It is set as a span.
-        // Once drawn, this span gets applied on the TextPaint.
-        val canvas = Canvas(android.graphics.Canvas())
-        paragraph.paint(canvas, textDecoration = TextDecoration.Underline)
         assertThat(paragraph.textPaint.isUnderlineText).isTrue()
 
+        val canvas = Canvas(android.graphics.Canvas())
         paragraph.paint(canvas, textDecoration = TextDecoration.None)
         assertThat(paragraph.textPaint.isUnderlineText).isFalse()
     }
@@ -1553,12 +1547,9 @@
             ),
             width = 0.0f
         )
-        // Underline text is not applied on TextPaint initially. It is set as a span.
-        // Once drawn, this span gets applied on the TextPaint.
-        val canvas = Canvas(android.graphics.Canvas())
-        paragraph.paint(canvas, textDecoration = TextDecoration.Underline)
         assertThat(paragraph.textPaint.isUnderlineText).isTrue()
 
+        val canvas = Canvas(android.graphics.Canvas())
         paragraph.paint(canvas, textDecoration = null)
         assertThat(paragraph.textPaint.isUnderlineText).isTrue()
     }
@@ -1953,38 +1944,6 @@
         )
     }
 
-    @Test
-    fun drawText_withUnderlineStyle_equalToUnderlinePaint() = with(defaultDensity) {
-        val fontSize = 30.sp
-        val fontSizeInPx = fontSize.toPx()
-        val text = "レンズ(単焦点)"
-        val spanStyle = SpanStyle(textDecoration = TextDecoration.Underline)
-        val paragraph = simpleParagraph(
-            text = text,
-            style = TextStyle(fontSize = fontSize),
-            spanStyles = listOf(AnnotatedString.Range(spanStyle, 0, text.length)),
-            width = fontSizeInPx * 20
-        )
-
-        val paragraph2 = simpleParagraph(
-            text = text,
-            style = TextStyle(
-                fontSize = fontSize,
-                textDecoration = TextDecoration.Underline
-            ),
-            width = fontSizeInPx * 20
-        )
-
-        val bitmapWithSpan = paragraph.bitmap()
-        // Our text rendering stack relies on the fact that given textstyle is also passed to draw
-        // functions of TextLayoutResult, MultiParagraph, Paragraph. If Underline is not specified
-        // here, it would be removed while drawing the MultiParagraph. We are simply mimicking
-        // what TextPainter does.
-        val bitmapNoSpan = paragraph2.bitmap(textDecoration = TextDecoration.Underline)
-
-        assertThat(bitmapWithSpan).isEqualToBitmap(bitmapNoSpan)
-    }
-
     @OptIn(ExperimentalTextApi::class)
     @Test
     fun shaderBrushSpan_createsShaderOnlyOnce() {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index c2d97d7..fce67fc 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -31,7 +31,6 @@
 import androidx.compose.ui.text.matchers.isZero
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextDirection
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.unit.Constraints
@@ -43,6 +42,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.filters.Suppress
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -1155,35 +1155,6 @@
     }
 
     @Test
-    fun drawText_withUnderlineStyle_equalToUnderlinePaint() = with(defaultDensity) {
-        val fontSize = 30.sp
-        val fontSizeInPx = fontSize.toPx()
-        val multiParagraph = simpleMultiParagraph(
-            text = buildAnnotatedString {
-                withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) {
-                    append("レンズ(単焦点)")
-                }
-            },
-            style = TextStyle(fontSize = fontSize),
-            width = fontSizeInPx * 20
-        )
-
-        val multiParagraph2 = simpleMultiParagraph(
-            text = AnnotatedString("レンズ(単焦点)"),
-            style = TextStyle(
-                fontSize = fontSize,
-                textDecoration = TextDecoration.Underline
-            ),
-            width = fontSizeInPx * 20
-        )
-
-        val bitmapWithSpan = multiParagraph.bitmap()
-        val bitmapNoSpan = multiParagraph2.bitmap()
-
-        assertThat(bitmapWithSpan).isEqualToBitmap(bitmapNoSpan)
-    }
-
-    @Test
     fun textIndent_onFirstLine() {
         with(defaultDensity) {
             val text = createAnnotatedString("aaa", "\u05D0\u05D0\u05D0")
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
index 0f04167..3b2cca6 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
@@ -28,7 +28,6 @@
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.platform.extensions.applySpanStyle
 import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextGeometricTransform
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.TextUnit
@@ -258,7 +257,7 @@
         assertThat(notApplied.background).isEqualTo(Color.Unspecified)
     }
 
-    @Test
+    /*@Test
     fun textDecorationUnderline_shouldBeLeftAsSpan() {
         val textDecoration = TextDecoration.Underline
         val spanStyle = SpanStyle(textDecoration = textDecoration)
@@ -297,7 +296,7 @@
         assertThat(tp.isUnderlineText).isEqualTo(false)
         assertThat(tp.isStrikeThruText).isEqualTo(false)
         assertThat(notApplied.textDecoration).isNull()
-    }
+    }*/
 
     @Test
     fun shadow_shouldBeAppliedTo_shadowLayer() {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
index 4a57356..2c9eb85 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.platform.AndroidTextPaint
 import androidx.compose.ui.text.style.BaselineShift
-import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextGeometricTransform
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.TextUnit
@@ -106,7 +105,7 @@
     // Paragraph.paint will receive a proper Size after layout is completed.
     setBrush(style.brush, Size.Unspecified, style.alpha)
     setShadow(style.shadow)
-    // Skip textDecoration (b/199939617). TextDecoration should be applied as a span.
+    setTextDecoration(style.textDecoration)
     setDrawStyle(style.drawStyle)
 
     // letterSpacing with unit Sp needs to be handled by span.
@@ -129,9 +128,6 @@
             null
         } else {
             style.baselineShift
-        },
-        textDecoration = style.textDecoration.takeIf {
-            style.textDecoration != TextDecoration.None
         }
     )
 }
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
index a0f47fc..82f7d09 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
@@ -57,24 +57,27 @@
             canvas.save()
             canvas.clipRect(bounds)
         }
+        val resolvedSpanStyle = resolveSpanStyleDefaults(
+            textLayoutResult.layoutInput.style.spanStyle
+        )
         try {
-            val brush = textLayoutResult.layoutInput.style.brush
+            val brush = resolvedSpanStyle.brush
             if (brush != null) {
                 textLayoutResult.multiParagraph.paint(
-                    canvas,
-                    brush,
-                    textLayoutResult.layoutInput.style.alpha,
-                    textLayoutResult.layoutInput.style.shadow,
-                    textLayoutResult.layoutInput.style.textDecoration,
-                    textLayoutResult.layoutInput.style.drawStyle
+                    canvas = canvas,
+                    brush = brush,
+                    alpha = resolvedSpanStyle.alpha,
+                    shadow = resolvedSpanStyle.shadow,
+                    decoration = resolvedSpanStyle.textDecoration,
+                    drawStyle = resolvedSpanStyle.drawStyle
                 )
             } else {
                 textLayoutResult.multiParagraph.paint(
-                    canvas,
-                    textLayoutResult.layoutInput.style.color,
-                    textLayoutResult.layoutInput.style.shadow,
-                    textLayoutResult.layoutInput.style.textDecoration,
-                    textLayoutResult.layoutInput.style.drawStyle
+                    canvas = canvas,
+                    color = resolvedSpanStyle.color,
+                    shadow = resolvedSpanStyle.shadow,
+                    decoration = resolvedSpanStyle.textDecoration,
+                    drawStyle = resolvedSpanStyle.drawStyle
                 )
             }
         } finally {
@@ -323,7 +326,8 @@
 
 private fun DrawTransform.clip(textLayoutResult: TextLayoutResult) {
     if (textLayoutResult.hasVisualOverflow &&
-        textLayoutResult.layoutInput.overflow != TextOverflow.Visible) {
+        textLayoutResult.layoutInput.overflow != TextOverflow.Visible
+    ) {
         clipRect(
             left = 0f,
             top = 0f,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 46de247..28504c3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.platform
 
+import android.accessibilityservice.AccessibilityServiceInfo
 import android.content.Context
 import android.graphics.RectF
 import android.graphics.Region
@@ -82,6 +83,7 @@
 import androidx.compose.ui.text.platform.toAccessibilitySpannableString
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.core.view.AccessibilityDelegateCompat
@@ -188,10 +190,44 @@
     private val accessibilityManager: AccessibilityManager =
         view.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
     internal var accessibilityForceEnabledForTesting = false
-    private val isAccessibilityEnabled
+
+    /**
+     * True if any accessibility service enabled in the system, except the UIAutomator (as it
+     * doesn't appear in the list of enabled services)
+     */
+    private val isEnabled: Boolean
+        get() {
+            // checking the list allows us to filter out the UIAutomator which doesn't appear in it
+            val enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
+                AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+            )
+            return accessibilityForceEnabledForTesting ||
+                accessibilityManager.isEnabled && enabledServices.isNotEmpty()
+        }
+
+    /**
+     * True if accessibility service with the touch exploration (e.g. Talkback) is enabled in the
+     * system.
+     * Note that UIAutomator doesn't request touch exploration therefore returns false
+     */
+    private val isTouchExplorationEnabled
         get() = accessibilityForceEnabledForTesting ||
-            accessibilityManager.isEnabled &&
-            accessibilityManager.isTouchExplorationEnabled
+                accessibilityManager.isEnabled && accessibilityManager.isTouchExplorationEnabled
+
+    /** True if an accessibility service that listens for the event of type [eventType] is enabled
+     * in the system.
+     * Note that UIAutomator will always return false as it doesn't appear in the list of enabled
+     * services
+     */
+    private fun isEnabledForEvent(eventType: Int): Boolean {
+        val enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
+            AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+        )
+        return enabledServices.fastFirstOrNull {
+            it.eventTypes.and(eventType) != 0
+        } != null || accessibilityForceEnabledForTesting
+    }
+
     private val handler = Handler(Looper.getMainLooper())
     private var nodeProvider: AccessibilityNodeProviderCompat =
         AccessibilityNodeProviderCompat(MyNodeProvider())
@@ -978,7 +1014,7 @@
      * @return Whether this virtual view actually took accessibility focus.
      */
     private fun requestAccessibilityFocus(virtualViewId: Int): Boolean {
-        if (!isAccessibilityEnabled) {
+        if (!isTouchExplorationEnabled) {
             return false
         }
         // TODO: Check virtual view visibility.
@@ -1029,7 +1065,7 @@
         contentChangeType: Int? = null,
         contentDescription: List<String>? = null
     ): Boolean {
-        if (virtualViewId == InvalidId || !isAccessibilityEnabled) {
+        if (virtualViewId == InvalidId || !isEnabledForEvent(eventType)) {
             return false
         }
 
@@ -1051,7 +1087,8 @@
      * @return true if the event was sent successfully.
      */
     private fun sendEvent(event: AccessibilityEvent): Boolean {
-        if (!isAccessibilityEnabled) {
+        // only send an event if there's an enabled service listening for events of this type
+        if (!isEnabledForEvent(event.eventType)) {
             return false
         }
 
@@ -1499,7 +1536,7 @@
      * @return Whether the hover event was handled.
      */
     fun dispatchHoverEvent(event: MotionEvent): Boolean {
-        if (!isAccessibilityEnabled) {
+        if (!isTouchExplorationEnabled) {
             return false
         }
 
@@ -1637,7 +1674,7 @@
         // later, we can refresh currentSemanticsNodes if currentSemanticsNodes is stale.
         currentSemanticsNodesInvalidated = true
 
-        if (isAccessibilityEnabled && !checkingForSemanticsChanges) {
+        if (isEnabled && !checkingForSemanticsChanges) {
             checkingForSemanticsChanges = true
             handler.post(semanticsChangeChecker)
         }
@@ -1652,7 +1689,7 @@
         try {
             val subtreeChangedSemanticsNodesIds = ArraySet<Int>()
             for (notification in boundsUpdateChannel) {
-                if (isAccessibilityEnabled) {
+                if (isEnabled) {
                     for (i in subtreeChangedLayoutNodes.indices) {
                         @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
                         sendSubtreeChangeAccessibilityEvents(
@@ -1693,7 +1730,7 @@
         // currentSemanticsNodesInvalidated up to date so that when accessibility is turned on
         // later, we can refresh currentSemanticsNodes if currentSemanticsNodes is stale.
         currentSemanticsNodesInvalidated = true
-        if (!isAccessibilityEnabled) {
+        if (!isEnabled) {
             return
         }
         // The layout change of a LayoutNode will also affect its children, so even if it doesn't
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 888c103..0c83242 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -36,6 +36,7 @@
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
     method public androidx.constraintlayout.compose.Dimension getHeight();
+    method public float getHorizontalBias();
     method public float getHorizontalChainWeight();
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
     method public float getPivotX();
@@ -50,6 +51,7 @@
     method public float getTranslationX();
     method public float getTranslationY();
     method public float getTranslationZ();
+    method public float getVerticalBias();
     method public float getVerticalChainWeight();
     method public androidx.constraintlayout.compose.Visibility getVisibility();
     method public androidx.constraintlayout.compose.Dimension getWidth();
@@ -60,6 +62,7 @@
     method public void resetTransforms();
     method public void setAlpha(float);
     method public void setHeight(androidx.constraintlayout.compose.Dimension);
+    method public void setHorizontalBias(float);
     method public void setHorizontalChainWeight(float);
     method public void setPivotX(float);
     method public void setPivotY(float);
@@ -71,6 +74,7 @@
     method public void setTranslationX(float);
     method public void setTranslationY(float);
     method public void setTranslationZ(float);
+    method public void setVerticalBias(float);
     method public void setVerticalChainWeight(float);
     method public void setVisibility(androidx.constraintlayout.compose.Visibility);
     method public void setWidth(androidx.constraintlayout.compose.Dimension);
@@ -81,6 +85,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
     property public final androidx.constraintlayout.compose.VerticalAnchorable end;
     property public final androidx.constraintlayout.compose.Dimension height;
+    property public final float horizontalBias;
     property public final float horizontalChainWeight;
     property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
     property public final float pivotX;
@@ -95,6 +100,7 @@
     property public final float translationX;
     property public final float translationY;
     property public final float translationZ;
+    property public final float verticalBias;
     property public final float verticalChainWeight;
     property public final androidx.constraintlayout.compose.Visibility visibility;
     property public final androidx.constraintlayout.compose.Dimension width;
@@ -151,6 +157,9 @@
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
     method protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> getTasks();
     method public void reset();
+    method public final androidx.constraintlayout.compose.LayoutReference withChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withHorizontalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withVerticalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional float weight);
     property protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> tasks;
   }
 
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index 9bd9dc8..fd40d95 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -67,6 +67,7 @@
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
     method public androidx.constraintlayout.compose.Dimension getHeight();
+    method public float getHorizontalBias();
     method public float getHorizontalChainWeight();
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
     method public float getPivotX();
@@ -81,6 +82,7 @@
     method public float getTranslationX();
     method public float getTranslationY();
     method public float getTranslationZ();
+    method public float getVerticalBias();
     method public float getVerticalChainWeight();
     method public androidx.constraintlayout.compose.Visibility getVisibility();
     method public androidx.constraintlayout.compose.Dimension getWidth();
@@ -91,6 +93,7 @@
     method public void resetTransforms();
     method public void setAlpha(float);
     method public void setHeight(androidx.constraintlayout.compose.Dimension);
+    method public void setHorizontalBias(float);
     method public void setHorizontalChainWeight(float);
     method public void setPivotX(float);
     method public void setPivotY(float);
@@ -102,6 +105,7 @@
     method public void setTranslationX(float);
     method public void setTranslationY(float);
     method public void setTranslationZ(float);
+    method public void setVerticalBias(float);
     method public void setVerticalChainWeight(float);
     method public void setVisibility(androidx.constraintlayout.compose.Visibility);
     method public void setWidth(androidx.constraintlayout.compose.Dimension);
@@ -112,6 +116,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
     property public final androidx.constraintlayout.compose.VerticalAnchorable end;
     property public final androidx.constraintlayout.compose.Dimension height;
+    property public final float horizontalBias;
     property public final float horizontalChainWeight;
     property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
     property public final float pivotX;
@@ -126,6 +131,7 @@
     property public final float translationX;
     property public final float translationY;
     property public final float translationZ;
+    property public final float verticalBias;
     property public final float verticalChainWeight;
     property public final androidx.constraintlayout.compose.Visibility visibility;
     property public final androidx.constraintlayout.compose.Dimension width;
@@ -182,6 +188,9 @@
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
     method protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> getTasks();
     method public void reset();
+    method public final androidx.constraintlayout.compose.LayoutReference withChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withHorizontalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withVerticalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional float weight);
     property protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> tasks;
   }
 
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index b2a7877..d11ace4 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -36,6 +36,7 @@
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
     method public androidx.constraintlayout.compose.Dimension getHeight();
+    method public float getHorizontalBias();
     method public float getHorizontalChainWeight();
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference getParent();
     method public float getPivotX();
@@ -50,6 +51,7 @@
     method public float getTranslationX();
     method public float getTranslationY();
     method public float getTranslationZ();
+    method public float getVerticalBias();
     method public float getVerticalChainWeight();
     method public androidx.constraintlayout.compose.Visibility getVisibility();
     method public androidx.constraintlayout.compose.Dimension getWidth();
@@ -60,6 +62,7 @@
     method public void resetTransforms();
     method public void setAlpha(float);
     method public void setHeight(androidx.constraintlayout.compose.Dimension);
+    method public void setHorizontalBias(float);
     method public void setHorizontalChainWeight(float);
     method public void setPivotX(float);
     method public void setPivotY(float);
@@ -71,6 +74,7 @@
     method public void setTranslationX(float);
     method public void setTranslationY(float);
     method public void setTranslationZ(float);
+    method public void setVerticalBias(float);
     method public void setVerticalChainWeight(float);
     method public void setVisibility(androidx.constraintlayout.compose.Visibility);
     method public void setWidth(androidx.constraintlayout.compose.Dimension);
@@ -81,6 +85,7 @@
     property public final androidx.constraintlayout.compose.HorizontalAnchorable bottom;
     property public final androidx.constraintlayout.compose.VerticalAnchorable end;
     property public final androidx.constraintlayout.compose.Dimension height;
+    property public final float horizontalBias;
     property public final float horizontalChainWeight;
     property public final androidx.constraintlayout.compose.ConstrainedLayoutReference parent;
     property public final float pivotX;
@@ -95,6 +100,7 @@
     property public final float translationX;
     property public final float translationY;
     property public final float translationZ;
+    property public final float verticalBias;
     property public final float verticalChainWeight;
     property public final androidx.constraintlayout.compose.Visibility visibility;
     property public final androidx.constraintlayout.compose.Dimension width;
@@ -151,6 +157,9 @@
     method public final androidx.constraintlayout.compose.VerticalChainReference createVerticalChain(androidx.constraintlayout.compose.LayoutReference![] elements, optional androidx.constraintlayout.compose.ChainStyle chainStyle);
     method protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> getTasks();
     method public void reset();
+    method public final androidx.constraintlayout.compose.LayoutReference withChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float topMargin, optional float endMargin, optional float bottomMargin, optional float startGoneMargin, optional float topGoneMargin, optional float endGoneMargin, optional float bottomGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withHorizontalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float startMargin, optional float endMargin, optional float startGoneMargin, optional float endGoneMargin, optional float weight);
+    method public final androidx.constraintlayout.compose.LayoutReference withVerticalChainParams(androidx.constraintlayout.compose.LayoutReference, optional float topMargin, optional float bottomMargin, optional float topGoneMargin, optional float bottomGoneMargin, optional float weight);
     property protected final java.util.List<kotlin.jvm.functions.Function1<androidx.constraintlayout.compose.State,kotlin.Unit>> tasks;
     field @kotlin.PublishedApi internal int helpersHashCode;
   }
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
index bbfe49b..5fb7268 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ChainsTest.kt
@@ -21,7 +21,9 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.dp
@@ -114,7 +116,11 @@
         val boxSizes = arrayOf(10.dp, 20.dp, 30.dp)
         val margin = 10.dp
         rule.setContent {
-            ConstraintLayout(Modifier.size(rootSize)) {
+            ConstraintLayout(
+                Modifier
+                    .background(Color.LightGray)
+                    .size(rootSize)
+            ) {
                 val (box0, box1, box2) = createRefs()
                 val chain0 = createHorizontalChain(box0, box1, chainStyle = ChainStyle.Packed)
                 constrain(chain0) {
@@ -170,4 +176,288 @@
         rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(box1Left, box1Top)
         rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(margin, margin)
     }
+
+    @Test
+    fun testHorizontalPacked_withMargins() {
+        val rootSize = 100.dp
+        val boxSizes = arrayOf(10.dp, 20.dp, 30.dp)
+        val boxMargin = 5.dp
+        val boxGoneMargin = 7.dp
+        val constraintSet = ConstraintSet {
+            val box0 = createRefFor("box0")
+            val boxGone = createRefFor("boxGone")
+            val box1 = createRefFor("box1")
+            val box2 = createRefFor("box2")
+
+            createHorizontalChain(
+                box0.withChainParams(
+                    startMargin = 0.dp,
+                    endMargin = boxMargin, // Not applied since the next box is Gone
+                    endGoneMargin = boxGoneMargin
+                ),
+                boxGone.withChainParams(
+                    // None of these margins should apply since it's Gone
+                    startMargin = 100.dp,
+                    endMargin = 100.dp,
+                    startGoneMargin = 100.dp,
+                    endGoneMargin = 100.dp
+                ),
+                box1,
+                box2.withHorizontalChainParams(startMargin = boxMargin, endMargin = 0.dp),
+                chainStyle = ChainStyle.Packed
+            )
+
+            constrain(box0) {
+                width = Dimension.value(boxSizes[0])
+                height = Dimension.value(boxSizes[0])
+                centerVerticallyTo(parent)
+            }
+            constrain(boxGone) {
+                width = Dimension.value(boxSizes[1])
+                height = Dimension.value(boxSizes[1])
+                centerVerticallyTo(box0)
+
+                visibility = Visibility.Gone
+            }
+            constrain(box1) {
+                width = Dimension.value(boxSizes[1])
+                height = Dimension.value(boxSizes[1])
+                centerVerticallyTo(box0)
+            }
+            constrain(box2) {
+                width = Dimension.value(boxSizes[2])
+                height = Dimension.value(boxSizes[2])
+                centerVerticallyTo(box0)
+            }
+        }
+        rule.setContent {
+            ConstraintLayout(
+                modifier = Modifier
+                    .background(Color.LightGray)
+                    .size(rootSize),
+                constraintSet = constraintSet
+            ) {
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .layoutTestId("box0")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Blue)
+                        .layoutTestId("box1")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Green)
+                        .layoutTestId("box2")
+                )
+            }
+        }
+        rule.waitForIdle()
+
+        val totalMargins = boxMargin + boxGoneMargin
+        val totalChainSpace = boxSizes[0] + boxSizes[1] + boxSizes[2] + totalMargins
+
+        val box0Left = (rootSize - totalChainSpace) * 0.5f
+        val box0Top = (rootSize - boxSizes[0]) * 0.5f
+
+        val box1Left = box0Left + boxSizes[0] + boxGoneMargin
+        val box1Top = (rootSize - boxSizes[1]) * 0.5f
+
+        val box2Left = box1Left + boxSizes[1] + boxMargin
+        val box2Top = (rootSize - boxSizes[2]) * 0.5f
+
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(box0Left, box0Top)
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(box1Left, box1Top)
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(box2Left, box2Top)
+    }
+
+    @Test
+    fun testVerticalPacked_withMargins() {
+        val rootSize = 100.dp
+        val boxSizes = arrayOf(10.dp, 20.dp, 30.dp)
+        val boxMargin = 5.dp
+        val boxGoneMargin = 7.dp
+        val constraintSet = ConstraintSet {
+            val box0 = createRefFor("box0")
+            val boxGone = createRefFor("boxGone")
+            val box1 = createRefFor("box1")
+            val box2 = createRefFor("box2")
+
+            createVerticalChain(
+                box0.withChainParams(
+                    topMargin = 0.dp,
+                    bottomMargin = boxMargin, // Not applied since the next box is Gone
+                    bottomGoneMargin = boxGoneMargin
+                ),
+                boxGone.withChainParams(
+                    // None of these margins should apply since it's Gone
+                    topMargin = 100.dp,
+                    bottomMargin = 100.dp,
+                    topGoneMargin = 100.dp,
+                    bottomGoneMargin = 100.dp
+                ),
+                box1,
+                box2.withVerticalChainParams(topMargin = boxMargin, bottomMargin = 0.dp),
+                chainStyle = ChainStyle.Packed
+            )
+
+            constrain(box0) {
+                width = Dimension.value(boxSizes[0])
+                height = Dimension.value(boxSizes[0])
+                centerHorizontallyTo(parent)
+            }
+            constrain(boxGone) {
+                width = Dimension.value(100.dp) // Dimensions won't matter since it's Gone
+                height = Dimension.value(100.dp) // Dimensions won't matter since it's Gone
+                centerHorizontallyTo(box0)
+
+                visibility = Visibility.Gone
+            }
+            constrain(box1) {
+                width = Dimension.value(boxSizes[1])
+                height = Dimension.value(boxSizes[1])
+                centerHorizontallyTo(box0)
+            }
+            constrain(box2) {
+                width = Dimension.value(boxSizes[2])
+                height = Dimension.value(boxSizes[2])
+                centerHorizontallyTo(box0)
+            }
+        }
+        rule.setContent {
+            ConstraintLayout(
+                modifier = Modifier
+                    .background(Color.LightGray)
+                    .size(rootSize),
+                constraintSet = constraintSet
+            ) {
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .layoutTestId("box0")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Blue)
+                        .layoutTestId("box1")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Green)
+                        .layoutTestId("box2")
+                )
+            }
+        }
+        rule.waitForIdle()
+
+        val totalMargins = boxMargin + boxGoneMargin
+        val totalChainSpace = boxSizes[0] + boxSizes[1] + boxSizes[2] + totalMargins
+
+        val box0Left = (rootSize - boxSizes[0]) * 0.5f
+        val box0Top = (rootSize - totalChainSpace) * 0.5f
+
+        val box1Left = (rootSize - boxSizes[1]) * 0.5f
+        val box1Top = box0Top + boxSizes[0] + boxGoneMargin
+
+        val box2Left = (rootSize - boxSizes[2]) * 0.5f
+        val box2Top = box1Top + boxSizes[1] + boxMargin
+
+        rule.onNodeWithTag("box0").assertPositionInRootIsEqualTo(box0Left, box0Top)
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(box1Left, box1Top)
+        rule.onNodeWithTag("box2").assertPositionInRootIsEqualTo(box2Left, box2Top)
+    }
+
+    @Test
+    fun testHorizontalWeight_withConstraintSet() {
+        val rootSize = 100.dp
+        val boxSize = 10.dp
+
+        rule.setContent {
+            ConstraintLayout(
+                modifier = Modifier
+                    .background(Color.LightGray)
+                    .size(rootSize),
+                constraintSet = ConstraintSet {
+                    val box0 = createRefFor("box0")
+                    val box1 = createRefFor("box1")
+
+                    constrain(box0) {
+                        width = Dimension.fillToConstraints
+                        height = Dimension.value(boxSize)
+
+                        horizontalChainWeight = 1.0f
+                        verticalChainWeight = 2.0f // Ignored in horizontal chain
+                    }
+                    constrain(box1) {
+                        width = Dimension.fillToConstraints
+                        height = Dimension.value(boxSize)
+                    }
+                    createHorizontalChain(box0, box1.withChainParams(weight = 0.5f))
+                }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .layoutTestId("box0")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Blue)
+                        .layoutTestId("box1")
+                )
+            }
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag("box0").assertWidthIsEqualTo(rootSize * 0.667f)
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(rootSize * 0.667f, 0.dp)
+        rule.onNodeWithTag("box1").assertWidthIsEqualTo(rootSize * 0.334f)
+    }
+
+    @Test
+    fun testVerticalWeight_withConstraintSet() {
+        val rootSize = 100.dp
+        val boxSize = 10.dp
+
+        rule.setContent {
+            ConstraintLayout(
+                modifier = Modifier
+                    .background(Color.LightGray)
+                    .size(rootSize),
+                constraintSet = ConstraintSet {
+                    val box0 = createRefFor("box0")
+                    val box1 = createRefFor("box1")
+
+                    constrain(box0) {
+                        width = Dimension.value(boxSize)
+                        height = Dimension.fillToConstraints
+
+                        horizontalChainWeight = 2.0f // Ignored in vertical chain
+                        verticalChainWeight = 1.0f
+                    }
+                    constrain(box1) {
+                        width = Dimension.value(boxSize)
+                        height = Dimension.fillToConstraints
+                    }
+                    createVerticalChain(box0, box1.withChainParams(weight = 0.5f))
+                }
+            ) {
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .layoutTestId("box0")
+                )
+                Box(
+                    modifier = Modifier
+                        .background(Color.Blue)
+                        .layoutTestId("box1")
+                )
+            }
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag("box0").assertHeightIsEqualTo(rootSize * 0.667f)
+        rule.onNodeWithTag("box1").assertPositionInRootIsEqualTo(0.dp, rootSize * 0.667f)
+        rule.onNodeWithTag("box1").assertHeightIsEqualTo(rootSize * 0.334f)
+    }
 }
\ No newline at end of file
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 5c2b507..c93b13f 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
@@ -1663,4 +1663,68 @@
         rule.onNodeWithTag(boxTag1).assertPositionInRootIsEqualTo(29.5.dp, 0.dp)
         rule.onNodeWithTag(boxTag2).assertPositionInRootIsEqualTo(60.dp, 0.dp)
     }
+
+    @Test
+    fun testBias_withConstraintSet() {
+        val rootSize = 100.dp
+        val boxSize = 10.dp
+        val horBias = 0.2f
+        val verBias = 1f - horBias
+        rule.setContent {
+            ConstraintLayout(
+                modifier = Modifier.size(rootSize),
+                constraintSet = ConstraintSet {
+                    constrain(createRefFor("box")) {
+                        width = Dimension.value(boxSize)
+                        height = Dimension.value(boxSize)
+
+                        centerTo(parent)
+                        horizontalBias = horBias
+                        verticalBias = verBias
+                    }
+                }) {
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .layoutTestId("box")
+                )
+            }
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag("box").assertPositionInRootIsEqualTo(
+            (rootSize - boxSize) * 0.2f,
+            (rootSize - boxSize) * 0.8f
+        )
+    }
+
+    @Test
+    fun testBias_withInlineDsl() {
+        val rootSize = 100.dp
+        val boxSize = 10.dp
+        val horBias = 0.2f
+        val verBias = 1f - horBias
+        rule.setContent {
+            ConstraintLayout(Modifier.size(rootSize)) {
+                val box = createRef()
+                Box(
+                    modifier = Modifier
+                        .background(Color.Red)
+                        .constrainAs(box) {
+                            width = Dimension.value(boxSize)
+                            height = Dimension.value(boxSize)
+
+                            centerTo(parent)
+                            horizontalBias = horBias
+                            verticalBias = verBias
+                        }
+                        .layoutTestId("box")
+                )
+            }
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag("box").assertPositionInRootIsEqualTo(
+            (rootSize - boxSize) * 0.2f,
+            (rootSize - boxSize) * 0.8f
+        )
+    }
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
index 4e171e2..fb1c981 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
@@ -134,7 +134,7 @@
         set(value) {
             field = value
             addTransform {
-                if (visibility != Visibility.Invisible) {
+                if (this@ConstrainScope.visibility != Visibility.Invisible) {
                     // A bit of a hack, this behavior is not defined in :core
                     // Invisible should override alpha
                     alpha(value)
@@ -260,6 +260,44 @@
             }
         }
 
+    /**
+     * Applied when the widget has constraints on the [start] and [end] anchors. It defines the
+     * position of the widget relative to the space within the constraints, where `0f` is the
+     * left-most position and `1f` is the right-most position.
+     *
+     * &nbsp;
+     *
+     * When layout direction is RTL, the value of the bias is effectively inverted.
+     *
+     * E.g.: For `horizontalBias = 0.3f`, `0.7f` is used for RTL.
+     *
+     * &nbsp;
+     *
+     * Note that the bias may also be applied with calls such as [linkTo].
+     */
+    @FloatRange(0.0, 1.0)
+    var horizontalBias: Float = 0.5f
+        set(value) {
+            field = value
+            tasks.add { state ->
+                state.constraints(id).horizontalBias(value)
+            }
+        }
+
+    /**
+     * Applied when the widget has constraints on the [top] and [bottom] anchors. It defines the
+     * position of the widget relative to the space within the constraints, where `0f` is the
+     * top-most position and `1f` is the bottom-most position.
+     */
+    @FloatRange(0.0, 1.0)
+    var verticalBias: Float = 0.5f
+        set(value) {
+            field = value
+            tasks.add { state ->
+                state.constraints(id).verticalBias(value)
+            }
+        }
+
     private fun addTransform(change: ConstraintReference.() -> Unit) =
         tasks.add { state -> change(state.constraints(id)) }
 
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
index 3193e55..d086df2 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayoutBaseScope.kt
@@ -583,7 +583,7 @@
         updateHelpersHashCode(16)
         elements.forEach { updateHelpersHashCode(it.hashCode()) }
 
-         return ConstrainedLayoutReference(id)
+        return ConstrainedLayoutReference(id)
     }
 
     /**
@@ -592,7 +592,6 @@
      * Use [constrain] with the resulting [HorizontalChainReference] to modify the start/left and
      * end/right constraints of this chain.
      */
-    // TODO(popam, b/157783937): this API should be improved
     fun createHorizontalChain(
         vararg elements: LayoutReference,
         chainStyle: ChainStyle = ChainStyle.Spread
@@ -603,7 +602,17 @@
                 id,
                 androidx.constraintlayout.core.state.State.Helper.HORIZONTAL_CHAIN
             ) as androidx.constraintlayout.core.state.helpers.HorizontalChainReference
-            helper.add(*(elements.map { it.id }.toTypedArray()))
+            elements.forEach { chainElement ->
+                val elementParams = chainElement.getHelperParams() ?: ChainParams.Default
+                helper.addChainElement(
+                    chainElement.id,
+                    elementParams.weight,
+                    state.convertDimension(elementParams.startMargin).toFloat(),
+                    state.convertDimension(elementParams.endMargin).toFloat(),
+                    state.convertDimension(elementParams.startGoneMargin).toFloat(),
+                    state.convertDimension(elementParams.endGoneMargin).toFloat()
+                )
+            }
             helper.style(chainStyle.style)
             helper.apply()
             if (chainStyle.bias != null) {
@@ -622,7 +631,6 @@
      * Use [constrain] with the resulting [VerticalChainReference] to modify the top and
      * bottom constraints of this chain.
      */
-    // TODO(popam, b/157783937): this API should be improved
     fun createVerticalChain(
         vararg elements: LayoutReference,
         chainStyle: ChainStyle = ChainStyle.Spread
@@ -633,7 +641,17 @@
                 id,
                 androidx.constraintlayout.core.state.State.Helper.VERTICAL_CHAIN
             ) as androidx.constraintlayout.core.state.helpers.VerticalChainReference
-            helper.add(*(elements.map { it.id }.toTypedArray()))
+            elements.forEach { chainElement ->
+                val elementParams = chainElement.getHelperParams() ?: ChainParams.Default
+                helper.addChainElement(
+                    chainElement.id,
+                    elementParams.weight,
+                    state.convertDimension(elementParams.topMargin).toFloat(),
+                    state.convertDimension(elementParams.bottomMargin).toFloat(),
+                    state.convertDimension(elementParams.topGoneMargin).toFloat(),
+                    state.convertDimension(elementParams.bottomGoneMargin).toFloat()
+                )
+            }
             helper.style(chainStyle.style)
             helper.apply()
             if (chainStyle.bias != null) {
@@ -645,6 +663,147 @@
         updateHelpersHashCode(chainStyle.hashCode())
         return VerticalChainReference(id)
     }
+
+    /**
+     * Sets the parameters that are used by chains to customize the resulting layout.
+     *
+     * Use margins to customize the space between widgets in the chain.
+     *
+     * Use weight to distribute available space to each widget when their dimensions are not
+     * fixed.
+     *
+     * &nbsp;
+     *
+     * Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
+     * Chains.
+     *
+     * Since margins are only for widgets within the chain: Top, Start and End, Bottom margins are
+     * ignored when the widget is the first or the last element in the chain, respectively.
+     *
+     * @param startMargin Added space from the start of this widget to the previous widget
+     * @param topMargin Added space from the top of this widget to the previous widget
+     * @param endMargin Added space from the end of this widget to the next widget
+     * @param bottomMargin Added space from the bottom of this widget to the next widget
+     * @param startGoneMargin Added space from the start of this widget when the previous widget has [Visibility.Gone]
+     * @param topGoneMargin Added space from the top of this widget when the previous widget has [Visibility.Gone]
+     * @param endGoneMargin Added space from the end of this widget when the next widget has [Visibility.Gone]
+     * @param bottomGoneMargin Added space from the bottom of this widget when the next widget has [Visibility.Gone]
+     * @param weight Defines the proportion of space (relative to the total weight) occupied by this
+     * layout when the corresponding dimension is not a fixed value.
+     * @return The same [LayoutReference] instance with the applied values
+     */
+    fun LayoutReference.withChainParams(
+        startMargin: Dp = 0.dp,
+        topMargin: Dp = 0.dp,
+        endMargin: Dp = 0.dp,
+        bottomMargin: Dp = 0.dp,
+        startGoneMargin: Dp = 0.dp,
+        topGoneMargin: Dp = 0.dp,
+        endGoneMargin: Dp = 0.dp,
+        bottomGoneMargin: Dp = 0.dp,
+        weight: Float = Float.NaN,
+    ): LayoutReference =
+        this.apply {
+            setHelperParams(
+                ChainParams(
+                    startMargin = startMargin,
+                    topMargin = topMargin,
+                    endMargin = endMargin,
+                    bottomMargin = bottomMargin,
+                    startGoneMargin = startGoneMargin,
+                    topGoneMargin = topGoneMargin,
+                    endGoneMargin = endGoneMargin,
+                    bottomGoneMargin = bottomGoneMargin,
+                    weight = weight
+                )
+            )
+        }
+
+    /**
+     * Sets the parameters that are used by horizontal chains to customize the resulting layout.
+     *
+     * Use margins to customize the space between widgets in the chain.
+     *
+     * Use weight to distribute available space to each widget when their horizontal dimension is
+     * not fixed.
+     *
+     * &nbsp;
+     *
+     * Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
+     * Chains.
+     *
+     * Since margins are only for widgets within the chain: Start and End margins are
+     * ignored when the widget is the first or the last element in the chain, respectively.
+     *
+     * @param startMargin Added space from the start of this widget to the previous widget
+     * @param endMargin Added space from the end of this widget to the next widget
+     * @param startGoneMargin Added space from the start of this widget when the previous widget has [Visibility.Gone]
+     * @param endGoneMargin Added space from the end of this widget when the next widget has [Visibility.Gone]
+     * @param weight Defines the proportion of space (relative to the total weight) occupied by this
+     * layout when the width is not a fixed dimension.
+     * @return The same [LayoutReference] instance with the applied values
+     */
+    fun LayoutReference.withHorizontalChainParams(
+        startMargin: Dp = 0.dp,
+        endMargin: Dp = 0.dp,
+        startGoneMargin: Dp = 0.dp,
+        endGoneMargin: Dp = 0.dp,
+        weight: Float = Float.NaN
+    ): LayoutReference =
+        withChainParams(
+            startMargin = startMargin,
+            topMargin = 0.dp,
+            endMargin = endMargin,
+            bottomMargin = 0.dp,
+            startGoneMargin = startGoneMargin,
+            topGoneMargin = 0.dp,
+            endGoneMargin = endGoneMargin,
+            bottomGoneMargin = 0.dp,
+            weight = weight
+        )
+
+    /**
+     * Sets the parameters that are used by vertical chains to customize the resulting layout.
+     *
+     * Use margins to customize the space between widgets in the chain.
+     *
+     * Use weight to distribute available space to each widget when their vertical dimension is not
+     * fixed.
+     *
+     * &nbsp;
+     *
+     * Similarly named parameters available from [ConstrainScope.linkTo] are ignored in
+     * Chains.
+     *
+     * Since margins are only for widgets within the chain: Top and Bottom margins are
+     * ignored when the widget is the first or the last element in the chain, respectively.
+     *
+     * @param topMargin Added space from the top of this widget to the previous widget
+     * @param bottomMargin Added space from the bottom of this widget to the next widget
+     * @param topGoneMargin Added space from the top of this widget when the previous widget has [Visibility.Gone]
+     * @param bottomGoneMargin Added space from the bottom of this widget when the next widget has [Visibility.Gone]
+     * @param weight Defines the proportion of space (relative to the total weight) occupied by this
+     * layout when the height is not a fixed dimension.
+     * @return The same [LayoutReference] instance with the applied values
+     */
+    fun LayoutReference.withVerticalChainParams(
+        topMargin: Dp = 0.dp,
+        bottomMargin: Dp = 0.dp,
+        topGoneMargin: Dp = 0.dp,
+        bottomGoneMargin: Dp = 0.dp,
+        weight: Float = Float.NaN
+    ): LayoutReference =
+        withChainParams(
+            startMargin = 0.dp,
+            topMargin = topMargin,
+            endMargin = 0.dp,
+            bottomMargin = bottomMargin,
+            startGoneMargin = 0.dp,
+            topGoneMargin = topGoneMargin,
+            endGoneMargin = 0.dp,
+            bottomGoneMargin = bottomGoneMargin,
+            weight = weight
+        )
 }
 
 /**
@@ -653,6 +812,11 @@
  */
 @Stable
 abstract class LayoutReference internal constructor(internal open val id: Any) {
+    /**
+     * This map should be used to store one instance of different implementations of [HelperParams].
+     */
+    private val helperParamsMap: MutableMap<String, HelperParams> = mutableMapOf()
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
@@ -667,6 +831,60 @@
     override fun hashCode(): Int {
         return id.hashCode()
     }
+
+    internal fun setHelperParams(helperParams: HelperParams) {
+        // Use the class name to force one instance per implementation
+        helperParamsMap[helperParams.javaClass.simpleName] = helperParams
+    }
+
+    /**
+     * Returns the [HelperParams] that corresponds to the class type [T]. Null if no instance of
+     * type [T] has been set.
+     */
+    internal inline fun <reified T> getHelperParams(): T? where T : HelperParams {
+        return helperParamsMap[T::class.java.simpleName] as? T
+    }
+}
+
+/**
+ * Helpers that need parameters on a per-widget basis may implement this interface to store custom
+ * parameters within [LayoutReference].
+ *
+ * @see [LayoutReference.getHelperParams]
+ * @see [LayoutReference.setHelperParams]
+ */
+internal interface HelperParams
+
+/**
+ * Parameters that may be defined for each widget within a chain.
+ *
+ * These will always be used instead of similarly named parameters defined with other calls such as
+ * [ConstrainScope.linkTo].
+ */
+internal class ChainParams(
+    val startMargin: Dp,
+    val topMargin: Dp,
+    val endMargin: Dp,
+    val bottomMargin: Dp,
+    val startGoneMargin: Dp,
+    val topGoneMargin: Dp,
+    val endGoneMargin: Dp,
+    val bottomGoneMargin: Dp,
+    val weight: Float,
+) : HelperParams {
+    companion object {
+        internal val Default = ChainParams(
+            startMargin = 0.dp,
+            topMargin = 0.dp,
+            endMargin = 0.dp,
+            bottomMargin = 0.dp,
+            startGoneMargin = 0.dp,
+            topGoneMargin = 0.dp,
+            endGoneMargin = 0.dp,
+            bottomGoneMargin = 0.dp,
+            weight = Float.NaN
+        )
+    }
 }
 
 /**
@@ -859,7 +1077,7 @@
 @Immutable
 class Wrap internal constructor(
     internal val mode: Int
-    ) {
+) {
     companion object {
         val None =
             Wrap(androidx.constraintlayout.core.widgets.Flow.WRAP_NONE)
@@ -876,7 +1094,7 @@
 @Immutable
 class VerticalAlign internal constructor(
     internal val mode: Int
-    ) {
+) {
     companion object {
         val Top = VerticalAlign(androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_TOP)
         val Bottom =
@@ -894,7 +1112,7 @@
 @Immutable
 class HorizontalAlign internal constructor(
     internal val mode: Int
-    ) {
+) {
     companion object {
         val Start =
             HorizontalAlign(androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_START)
@@ -910,7 +1128,7 @@
 @Immutable
 class FlowStyle internal constructor(
     internal val style: Int
-    ) {
+) {
     companion object {
         val Spread = FlowStyle(0)
         val SpreadInside = FlowStyle(1)
diff --git a/constraintlayout/constraintlayout-core/api/current.txt b/constraintlayout/constraintlayout-core/api/current.txt
index ff1bb30..30a9d6c 100644
--- a/constraintlayout/constraintlayout-core/api/current.txt
+++ b/constraintlayout/constraintlayout-core/api/current.txt
@@ -2477,20 +2477,22 @@
   }
 
   public class ChainReference extends androidx.constraintlayout.core.state.HelperReference {
-    ctor public ChainReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
-    method public void addChainElement(String!, float, float, float);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! bias(float);
+    ctor public ChainReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+    method public void addChainElement(String, float, float, float);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference bias(float);
     method public float getBias();
-    method protected float getPostMargin(String!);
-    method protected float getPreMargin(String!);
-    method public androidx.constraintlayout.core.state.State.Chain! getStyle();
-    method protected float getWeight(String!);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! style(androidx.constraintlayout.core.state.State.Chain!);
+    method protected float getPostGoneMargin(String);
+    method protected float getPostMargin(String);
+    method protected float getPreGoneMargin(String);
+    method protected float getPreMargin(String);
+    method public androidx.constraintlayout.core.state.State.Chain getStyle();
+    method protected float getWeight(String);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference style(androidx.constraintlayout.core.state.State.Chain);
     field protected float mBias;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPostMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPreMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapWeights;
-    field protected androidx.constraintlayout.core.state.State.Chain! mStyle;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPostMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPreMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapWeights;
+    field protected androidx.constraintlayout.core.state.State.Chain mStyle;
   }
 
   public interface Facade {
diff --git a/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
index ff1bb30..30a9d6c 100644
--- a/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
@@ -2477,20 +2477,22 @@
   }
 
   public class ChainReference extends androidx.constraintlayout.core.state.HelperReference {
-    ctor public ChainReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
-    method public void addChainElement(String!, float, float, float);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! bias(float);
+    ctor public ChainReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+    method public void addChainElement(String, float, float, float);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference bias(float);
     method public float getBias();
-    method protected float getPostMargin(String!);
-    method protected float getPreMargin(String!);
-    method public androidx.constraintlayout.core.state.State.Chain! getStyle();
-    method protected float getWeight(String!);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! style(androidx.constraintlayout.core.state.State.Chain!);
+    method protected float getPostGoneMargin(String);
+    method protected float getPostMargin(String);
+    method protected float getPreGoneMargin(String);
+    method protected float getPreMargin(String);
+    method public androidx.constraintlayout.core.state.State.Chain getStyle();
+    method protected float getWeight(String);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference style(androidx.constraintlayout.core.state.State.Chain);
     field protected float mBias;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPostMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPreMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapWeights;
-    field protected androidx.constraintlayout.core.state.State.Chain! mStyle;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPostMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPreMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapWeights;
+    field protected androidx.constraintlayout.core.state.State.Chain mStyle;
   }
 
   public interface Facade {
diff --git a/constraintlayout/constraintlayout-core/api/restricted_current.txt b/constraintlayout/constraintlayout-core/api/restricted_current.txt
index ff1bb30..161a4ec 100644
--- a/constraintlayout/constraintlayout-core/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-core/api/restricted_current.txt
@@ -2477,20 +2477,23 @@
   }
 
   public class ChainReference extends androidx.constraintlayout.core.state.HelperReference {
-    ctor public ChainReference(androidx.constraintlayout.core.state.State!, androidx.constraintlayout.core.state.State.Helper!);
-    method public void addChainElement(String!, float, float, float);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! bias(float);
+    ctor public ChainReference(androidx.constraintlayout.core.state.State, androidx.constraintlayout.core.state.State.Helper);
+    method public void addChainElement(String, float, float, float);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public void addChainElement(Object, float, float, float, float, float);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference bias(float);
     method public float getBias();
-    method protected float getPostMargin(String!);
-    method protected float getPreMargin(String!);
-    method public androidx.constraintlayout.core.state.State.Chain! getStyle();
-    method protected float getWeight(String!);
-    method public androidx.constraintlayout.core.state.helpers.ChainReference! style(androidx.constraintlayout.core.state.State.Chain!);
+    method protected float getPostGoneMargin(String);
+    method protected float getPostMargin(String);
+    method protected float getPreGoneMargin(String);
+    method protected float getPreMargin(String);
+    method public androidx.constraintlayout.core.state.State.Chain getStyle();
+    method protected float getWeight(String);
+    method public androidx.constraintlayout.core.state.helpers.ChainReference style(androidx.constraintlayout.core.state.State.Chain);
     field protected float mBias;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPostMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapPreMargin;
-    field protected java.util.HashMap<java.lang.String!,java.lang.Float!>! mMapWeights;
-    field protected androidx.constraintlayout.core.state.State.Chain! mStyle;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPostMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapPreMargin;
+    field @Deprecated protected java.util.HashMap<java.lang.String!,java.lang.Float!> mMapWeights;
+    field protected androidx.constraintlayout.core.state.State.Chain mStyle;
   }
 
   public interface Facade {
diff --git a/constraintlayout/constraintlayout-core/build.gradle b/constraintlayout/constraintlayout-core/build.gradle
index 40b4820..329d954 100644
--- a/constraintlayout/constraintlayout-core/build.gradle
+++ b/constraintlayout/constraintlayout-core/build.gradle
@@ -23,6 +23,7 @@
 }
 
 dependencies {
+    api("androidx.annotation:annotation:1.5.0")
     testImplementation(libs.junit)
 }
 
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
index edf122b..a6af993 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintSetParser.java
@@ -811,6 +811,7 @@
                                         postMargin = toPix(state, array.getFloat(3));
                                         break;
                                 }
+                                // TODO: Define how to set gone margin in JSON syntax
                                 chain.addChainElement(id, weight, preMargin, postMargin);
                             }
                         } else {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/ChainReference.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/ChainReference.java
index 4229892..5a22613 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/ChainReference.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/ChainReference.java
@@ -18,85 +18,181 @@
 
 import static androidx.constraintlayout.core.widgets.ConstraintWidget.UNKNOWN;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.constraintlayout.core.state.HelperReference;
 import androidx.constraintlayout.core.state.State;
 
 import java.util.HashMap;
 
+/**
+ * {@link HelperReference} for Chains.
+ *
+ * Elements should be added with {@link ChainReference#addChainElement}
+ */
 public class ChainReference extends HelperReference {
 
     protected float mBias = 0.5f;
-    protected HashMap<String ,Float> mMapWeights;
-    protected HashMap<String,Float> mMapPreMargin;
-    protected HashMap<String,Float> mMapPostMargin;
 
-    protected State.Chain mStyle = State.Chain.SPREAD;
+    /**
+     * @deprecated Unintended visibility, use {@link #getWeight(String)} instead
+     */
+    @Deprecated // TODO(b/253515185): Change to private visibility once we change major version
+    protected @NonNull HashMap<String, Float> mMapWeights = new HashMap<>();
 
-    public ChainReference(State state, State.Helper type) {
+    /**
+     * @deprecated Unintended visibility, use {@link #getPreMargin(String)} instead
+     */
+    @Deprecated // TODO(b/253515185): Change to private visibility once we change major version
+    protected @NonNull HashMap<String, Float> mMapPreMargin = new HashMap<>();
+
+    /**
+     * @deprecated Unintended visibility, use {@link #getPostMargin(String)} instead
+     */
+    @Deprecated // TODO(b/253515185): Change to private visibility once we change major version
+    protected @NonNull HashMap<String, Float> mMapPostMargin = new HashMap<>();
+
+    private HashMap<String, Float> mMapPreGoneMargin;
+    private HashMap<String, Float> mMapPostGoneMargin;
+
+    protected @NonNull State.Chain mStyle = State.Chain.SPREAD;
+
+    public ChainReference(@NonNull State state, @NonNull State.Helper type) {
         super(state, type);
     }
 
-    public State.Chain getStyle() {
+    public @NonNull State.Chain getStyle() {
         return State.Chain.SPREAD;
     }
 
-    // @TODO: add description
-    public ChainReference style(State.Chain style) {
+    /**
+     * Sets the {@link State.Chain style}.
+     *
+     * @param style Defines the way the chain will lay out its elements
+     * @return This same instance
+     */
+    @NonNull
+    public ChainReference style(@NonNull State.Chain style) {
         mStyle = style;
         return this;
     }
 
-    public void addChainElement(String id, float weight, float preMargin, float  postMargin ) {
-        super.add(id);
+    /**
+     * Adds the element by the given id to the Chain.
+     *
+     * The order in which the elements are added is important. It will represent the element's
+     * position in the Chain.
+     *
+     * @param id         Id of the element to add
+     * @param weight     Weight used to distribute remaining space to each element
+     * @param preMargin  Additional space in pixels between the added element and the previous one
+     *                   (if any)
+     * @param postMargin Additional space in pixels between the added element and the next one (if
+     *                   any)
+     */
+    public void addChainElement(@NonNull String id,
+            float weight,
+            float preMargin,
+            float postMargin) {
+        addChainElement(id, weight, preMargin, postMargin, 0, 0);
+    }
+
+    /**
+     * Adds the element by the given id to the Chain.
+     *
+     * The object's {@link Object#toString()} result will be used to map the given margins and
+     * weight to it, so it must stable and comparable.
+     *
+     * The order in which the elements are added is important. It will represent the element's
+     * position in the Chain.
+     *
+     * @param id             Id of the element to add
+     * @param weight         Weight used to distribute remaining space to each element
+     * @param preMargin      Additional space in pixels between the added element and the
+     *                       previous one
+     *                       (if any)
+     * @param postMargin     Additional space in pixels between the added element and the next
+     *                       one (if
+     *                       any)
+     * @param preGoneMargin  Additional space in pixels between the added element and the previous
+     *                       one (if any) when the previous element has Gone visibility
+     * @param postGoneMargin Additional space in pixels between the added element and the next
+     *                       one (if any) when the next element has Gone visibility
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public void addChainElement(@NonNull Object id,
+            float weight,
+            float preMargin,
+            float postMargin,
+            float preGoneMargin,
+            float postGoneMargin) {
+        super.add(id); // Add element id as is, it's expected to return the same given instance
+        String idString = id.toString();
         if (!Float.isNaN(weight)) {
-            if (mMapWeights == null) {
-                mMapWeights = new HashMap<>();
-            }
-            mMapWeights.put(id, weight);
+            mMapWeights.put(idString, weight);
         }
         if (!Float.isNaN(preMargin)) {
-            if (mMapPreMargin == null) {
-                mMapPreMargin = new HashMap<>();
-            }
-            mMapPreMargin.put(id, preMargin);
+            mMapPreMargin.put(idString, preMargin);
         }
         if (!Float.isNaN(postMargin)) {
-            if (mMapPostMargin == null) {
-                mMapPostMargin = new HashMap<>();
+            mMapPostMargin.put(idString, postMargin);
+        }
+        if (!Float.isNaN(preGoneMargin)) {
+            if (mMapPreGoneMargin == null) {
+                mMapPreGoneMargin = new HashMap<>();
             }
-            mMapPostMargin.put(id, postMargin);
+            mMapPreGoneMargin.put(idString, preGoneMargin);
+        }
+        if (!Float.isNaN(postGoneMargin)) {
+            if (mMapPostGoneMargin == null) {
+                mMapPostGoneMargin = new HashMap<>();
+            }
+            mMapPostGoneMargin.put(idString, postGoneMargin);
         }
     }
 
-  protected float getWeight(String id) {
-       if (mMapWeights == null) {
-           return UNKNOWN;
-       }
-       if (mMapWeights.containsKey(id)) {
-           return mMapWeights.get(id);
-       }
-       return UNKNOWN;
+    protected float getWeight(@NonNull String id) {
+        if (mMapWeights.containsKey(id)) {
+            return mMapWeights.get(id);
+        }
+        return UNKNOWN;
     }
 
-    protected float getPostMargin(String id) {
-        if (mMapPostMargin != null  && mMapPostMargin.containsKey(id)) {
+    protected float getPostMargin(@NonNull String id) {
+        if (mMapPostMargin.containsKey(id)) {
             return mMapPostMargin.get(id);
         }
         return 0;
     }
 
-    protected float getPreMargin(String id) {
-        if (mMapPreMargin != null  && mMapPreMargin.containsKey(id)) {
+    protected float getPreMargin(@NonNull String id) {
+        if (mMapPreMargin.containsKey(id)) {
             return mMapPreMargin.get(id);
         }
         return 0;
     }
 
+    protected float getPostGoneMargin(@NonNull String id) {
+        if (mMapPostGoneMargin != null && mMapPostGoneMargin.containsKey(id)) {
+            return mMapPostGoneMargin.get(id);
+        }
+        return 0;
+    }
+
+    protected float getPreGoneMargin(@NonNull String id) {
+        if (mMapPreGoneMargin != null && mMapPreGoneMargin.containsKey(id)) {
+            return mMapPreGoneMargin.get(id);
+        }
+        return 0;
+    }
+
     public float getBias() {
         return mBias;
     }
 
     // @TODO: add description
+    @NonNull
     @Override
     public ChainReference bias(float bias) {
         mBias = bias;
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/HorizontalChainReference.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/HorizontalChainReference.java
index be09236..32539da 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/HorizontalChainReference.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/HorizontalChainReference.java
@@ -58,14 +58,17 @@
                 } else {
                     // No constraint declared, default to Parent.
                     String refKey = reference.getKey().toString();
-                    first.startToStart(State.PARENT).margin(getPreMargin(refKey));
+                    first.startToStart(State.PARENT).margin(getPreMargin(refKey)).marginGone(
+                            getPreGoneMargin(refKey));
                 }
             }
             if (previous != null) {
                 String preKey = previous.getKey().toString();
                 String refKey = reference.getKey().toString();
-                previous.endToStart(reference.getKey()).margin(getPostMargin(preKey));
-                reference.startToEnd(previous.getKey()).margin(getPreMargin(refKey));
+                previous.endToStart(reference.getKey()).margin(getPostMargin(preKey)).marginGone(
+                        getPostGoneMargin(preKey));
+                reference.startToEnd(previous.getKey()).margin(getPreMargin(refKey)).marginGone(
+                        getPreGoneMargin(refKey));
             }
             float weight = getWeight(key.toString());
             if (weight != UNKNOWN) {
@@ -88,7 +91,8 @@
             } else {
                 // No constraint declared, default to Parent.
                 String preKey = previous.getKey().toString();
-                previous.endToEnd(State.PARENT).margin(getPostMargin(preKey));
+                previous.endToEnd(State.PARENT).margin(getPostMargin(preKey)).marginGone(
+                        getPostGoneMargin(preKey));
             }
         }
 
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/VerticalChainReference.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/VerticalChainReference.java
index d4fdd23..8d880b1 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/VerticalChainReference.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/helpers/VerticalChainReference.java
@@ -49,14 +49,17 @@
                 } else {
                     // No constraint declared, default to Parent.
                     String refKey = reference.getKey().toString();
-                    first.topToTop(State.PARENT).margin(getPreMargin(refKey));
+                    first.topToTop(State.PARENT).margin(getPreMargin(refKey)).marginGone(
+                            getPreGoneMargin(refKey));
                 }
             }
             if (previous != null) {
                 String preKey = previous.getKey().toString();
                 String refKey = reference.getKey().toString();
-                previous.bottomToTop(reference.getKey()).margin(getPostMargin(preKey));
-                reference.topToBottom(previous.getKey()).margin(getPreMargin(refKey));
+                previous.bottomToTop(reference.getKey()).margin(getPostMargin(preKey)).marginGone(
+                        getPostGoneMargin(preKey));
+                reference.topToBottom(previous.getKey()).margin(getPreMargin(refKey)).marginGone(
+                        getPreGoneMargin(refKey));
             }
             float weight = getWeight(key.toString());
             if (weight != UNKNOWN) {
@@ -77,7 +80,8 @@
             } else {
                 // No constraint declared, default to Parent.
                 String preKey = previous.getKey().toString();
-                previous.bottomToBottom(State.PARENT).margin(getPostMargin(preKey));
+                previous.bottomToBottom(State.PARENT).margin(getPostMargin(preKey)).marginGone(
+                        getPostGoneMargin(preKey));
             }
         }
 
diff --git a/credentials/credentials/api/current.txt b/credentials/credentials/api/current.txt
index 3544ddf9..b14a46d 100644
--- a/credentials/credentials/api/current.txt
+++ b/credentials/credentials/api/current.txt
@@ -1,12 +1,22 @@
 // Signature format: 4.0
 package androidx.credentials {
 
-  public abstract class CreateCredentialRequest {
-    ctor public CreateCredentialRequest();
+  public class CreateCredentialRequest {
+    ctor public CreateCredentialRequest(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
-  public abstract class CreateCredentialResponse {
-    ctor public CreateCredentialResponse();
+  public class CreateCredentialResponse {
+    ctor public CreateCredentialResponse(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
@@ -21,8 +31,12 @@
     ctor public CreatePasswordResponse();
   }
 
-  public abstract class Credential {
-    ctor public Credential();
+  public class Credential {
+    ctor public Credential(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CredentialManager {
@@ -51,8 +65,14 @@
     property public final CharSequence? errorMessage;
   }
 
-  public abstract class GetCredentialOption {
-    ctor public GetCredentialOption();
+  public class GetCredentialOption {
+    ctor public GetCredentialOption(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
   public final class GetCredentialRequest {
diff --git a/credentials/credentials/api/public_plus_experimental_current.txt b/credentials/credentials/api/public_plus_experimental_current.txt
index 3544ddf9..b14a46d 100644
--- a/credentials/credentials/api/public_plus_experimental_current.txt
+++ b/credentials/credentials/api/public_plus_experimental_current.txt
@@ -1,12 +1,22 @@
 // Signature format: 4.0
 package androidx.credentials {
 
-  public abstract class CreateCredentialRequest {
-    ctor public CreateCredentialRequest();
+  public class CreateCredentialRequest {
+    ctor public CreateCredentialRequest(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
-  public abstract class CreateCredentialResponse {
-    ctor public CreateCredentialResponse();
+  public class CreateCredentialResponse {
+    ctor public CreateCredentialResponse(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
@@ -21,8 +31,12 @@
     ctor public CreatePasswordResponse();
   }
 
-  public abstract class Credential {
-    ctor public Credential();
+  public class Credential {
+    ctor public Credential(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CredentialManager {
@@ -51,8 +65,14 @@
     property public final CharSequence? errorMessage;
   }
 
-  public abstract class GetCredentialOption {
-    ctor public GetCredentialOption();
+  public class GetCredentialOption {
+    ctor public GetCredentialOption(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
   public final class GetCredentialRequest {
diff --git a/credentials/credentials/api/restricted_current.txt b/credentials/credentials/api/restricted_current.txt
index 3544ddf9..b14a46d 100644
--- a/credentials/credentials/api/restricted_current.txt
+++ b/credentials/credentials/api/restricted_current.txt
@@ -1,12 +1,22 @@
 // Signature format: 4.0
 package androidx.credentials {
 
-  public abstract class CreateCredentialRequest {
-    ctor public CreateCredentialRequest();
+  public class CreateCredentialRequest {
+    ctor public CreateCredentialRequest(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
-  public abstract class CreateCredentialResponse {
-    ctor public CreateCredentialResponse();
+  public class CreateCredentialResponse {
+    ctor public CreateCredentialResponse(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CreatePasswordRequest extends androidx.credentials.CreateCredentialRequest {
@@ -21,8 +31,12 @@
     ctor public CreatePasswordResponse();
   }
 
-  public abstract class Credential {
-    ctor public Credential();
+  public class Credential {
+    ctor public Credential(String type, android.os.Bundle data);
+    method public final android.os.Bundle getData();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final String type;
   }
 
   public final class CredentialManager {
@@ -51,8 +65,14 @@
     property public final CharSequence? errorMessage;
   }
 
-  public abstract class GetCredentialOption {
-    ctor public GetCredentialOption();
+  public class GetCredentialOption {
+    ctor public GetCredentialOption(String type, android.os.Bundle data, boolean requireSystemProvider);
+    method public final android.os.Bundle getData();
+    method public final boolean getRequireSystemProvider();
+    method public final String getType();
+    property public final android.os.Bundle data;
+    property public final boolean requireSystemProvider;
+    property public final String type;
   }
 
   public final class GetCredentialRequest {
diff --git a/credentials/credentials/build.gradle b/credentials/credentials/build.gradle
index 29d733e..6bb5a52 100644
--- a/credentials/credentials/build.gradle
+++ b/credentials/credentials/build.gradle
@@ -24,6 +24,7 @@
 }
 
 dependencies {
+    api("androidx.annotation:annotation:1.5.0")
     api(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesCore)
 
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
new file mode 100644
index 0000000..953bd5a
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestJavaTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+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 CreatePasswordRequestJavaTest {
+    @Test
+    public void constructor_nullId_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new CreatePasswordRequest(null, "pwd")
+        );
+    }
+
+    @Test
+    public void constructor_nullPassword_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new CreatePasswordRequest("id", null)
+        );
+    }
+
+    @Test
+    public void constructor_emptyPassword_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new CreatePasswordRequest("id", "")
+        );
+    }
+
+    @Test
+    public void getter_id() {
+        String idExpected = "id";
+        CreatePasswordRequest request = new CreatePasswordRequest(idExpected, "password");
+        assertThat(request.getId()).isEqualTo(idExpected);
+    }
+
+    @Test
+    public void getter_password() {
+        String passwordExpected = "pwd";
+        CreatePasswordRequest request = new CreatePasswordRequest("id", passwordExpected);
+        assertThat(request.getPassword()).isEqualTo(passwordExpected);
+    }
+
+    @Test
+    public void getter_frameworkProperties() {
+        String idExpected = "id";
+        String passwordExpected = "pwd";
+        Bundle expectedData = new Bundle();
+        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected);
+        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_PASSWORD, passwordExpected);
+
+        CreatePasswordRequest request = new CreatePasswordRequest(idExpected, passwordExpected);
+
+        assertThat(request.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(request.getData(), expectedData)).isTrue();
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
new file mode 100644
index 0000000..9943eed
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordRequestTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreatePasswordRequestTest {
+    @Test
+    fun constructor_emptyPassword_throws() {
+        assertThrows<IllegalArgumentException> {
+            CreatePasswordRequest("id", "")
+        }
+    }
+
+    @Test
+    fun getter_id() {
+        val idExpected = "id"
+        val request = CreatePasswordRequest(idExpected, "password")
+        assertThat(request.id).isEqualTo(idExpected)
+    }
+
+    @Test
+    fun getter_password() {
+        val passwordExpected = "pwd"
+        val request = CreatePasswordRequest("id", passwordExpected)
+        assertThat(request.password).isEqualTo(passwordExpected)
+    }
+
+    @Test
+    fun getter_frameworkProperties() {
+        val idExpected = "id"
+        val passwordExpected = "pwd"
+        val expectedData = Bundle()
+        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_ID, idExpected)
+        expectedData.putString(CreatePasswordRequest.BUNDLE_KEY_PASSWORD, passwordExpected)
+
+        val request = CreatePasswordRequest(idExpected, passwordExpected)
+
+        assertThat(request.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+        assertThat(equals(request.data, expectedData)).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseJavaTest.java
new file mode 100644
index 0000000..5f75653
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseJavaTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+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 CreatePasswordResponseJavaTest {
+    @Test
+    public void getter_frameworkProperties() {
+        CreatePasswordResponse response = new CreatePasswordResponse();
+
+        assertThat(response.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(response.getData(), Bundle.EMPTY)).isTrue();
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseTest.kt
new file mode 100644
index 0000000..84e4102
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePasswordResponseTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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
+
+import android.os.Bundle
+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 CreatePasswordResponseTest {
+    @Test
+    fun getter_frameworkProperties() {
+        val response = CreatePasswordResponse()
+        Truth.assertThat(response.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+        Truth.assertThat(equals(response.data, Bundle.EMPTY)).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
index db1a2a9..2604d86 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestJavaTest.java
@@ -16,10 +16,15 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.CreatePublicKeyCredentialBaseRequest.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Bundle;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -80,4 +85,22 @@
         String testJsonActual = createPublicKeyCredentialReq.getRequestJson();
         assertThat(testJsonActual).isEqualTo(testJsonExpected);
     }
+
+    @Test
+    public void getter_frameworkProperties_success() {
+        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        boolean allowHybridExpected = false;
+        Bundle expectedData = new Bundle();
+        expectedData.putString(
+                BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+        expectedData.putBoolean(
+                BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+
+        CreatePublicKeyCredentialRequest request =
+                new CreatePublicKeyCredentialRequest(requestJsonExpected, allowHybridExpected);
+
+        assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(request.getData(), expectedData)).isTrue();
+        assertThat(request.getRequireSystemProvider()).isFalse();
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
index 82adc07..8558cbb 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedJavaTest.java
@@ -16,8 +16,15 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.CreatePublicKeyCredentialBaseRequest.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
+import static androidx.credentials.CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RP;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.Bundle;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -107,4 +114,26 @@
                 createPublicKeyCredentialRequestPrivileged.getClientDataHash();
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
     }
+
+    @Test
+    public void getter_frameworkProperties_success() {
+        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        String rpExpected = "RP";
+        String clientDataHashExpected = "X342%4dfd7&";
+        boolean allowHybridExpected = false;
+        Bundle expectedData = new Bundle();
+        expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+        expectedData.putString(BUNDLE_KEY_RP, rpExpected);
+        expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
+        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+
+        CreatePublicKeyCredentialRequestPrivileged request =
+                new CreatePublicKeyCredentialRequestPrivileged(
+                        requestJsonExpected, rpExpected, clientDataHashExpected,
+                        allowHybridExpected);
+
+        assertThat(request.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(request.getData(), expectedData)).isTrue();
+        assertThat(request.getRequireSystemProvider()).isFalse();
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
index faffbcb..e485003 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -110,4 +111,37 @@
         val clientDataHashActual = createPublicKeyCredentialRequestPrivileged.clientDataHash
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected)
     }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val rpExpected = "RP"
+        val clientDataHashExpected = "X342%4dfd7&"
+        val allowHybridExpected = false
+        val expectedData = Bundle()
+        expectedData.putString(
+            CreatePublicKeyCredentialBaseRequest.BUNDLE_KEY_REQUEST_JSON,
+            requestJsonExpected
+        )
+        expectedData.putString(CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_RP, rpExpected)
+        expectedData.putString(
+            CreatePublicKeyCredentialRequestPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
+            clientDataHashExpected
+        )
+        expectedData.putBoolean(
+            CreatePublicKeyCredentialRequest.BUNDLE_KEY_ALLOW_HYBRID,
+            allowHybridExpected
+        )
+
+        val request = CreatePublicKeyCredentialRequestPrivileged(
+            requestJsonExpected,
+            rpExpected,
+            clientDataHashExpected,
+            allowHybridExpected
+        )
+
+        assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+        assertThat(equals(request.data, expectedData)).isTrue()
+        assertThat(request.requireSystemProvider).isFalse()
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
index 4058526..32d4365 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestTest.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.credentials.CreatePublicKeyCredentialBaseRequest.Companion.BUNDLE_KEY_REQUEST_JSON
+import androidx.credentials.CreatePublicKeyCredentialRequest.Companion.BUNDLE_KEY_ALLOW_HYBRID
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -69,4 +72,26 @@
         val testJsonActual = createPublicKeyCredentialReq.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val allowHybridExpected = false
+        val expectedData = Bundle()
+        expectedData.putString(
+            BUNDLE_KEY_REQUEST_JSON, requestJsonExpected
+        )
+        expectedData.putBoolean(
+            BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected
+        )
+
+        val request = CreatePublicKeyCredentialRequest(
+            requestJsonExpected,
+            allowHybridExpected
+        )
+
+        assertThat(request.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+        assertThat(equals(request.data, expectedData)).isTrue()
+        assertThat(request.requireSystemProvider).isFalse()
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseJavaTest.java
index dc7bdb26..099f367 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseJavaTest.java
@@ -20,6 +20,8 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Bundle;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -59,4 +61,19 @@
         String testJsonActual = createPublicKeyCredentialResponse.getRegistrationResponseJson();
         assertThat(testJsonActual).isEqualTo(testJsonExpected);
     }
+
+    @Test
+    public void getter_frameworkProperties_success() {
+        String registrationResponseJsonExpected = "{\"input\":5}";
+        Bundle expectedData = new Bundle();
+        expectedData.putString(
+                CreatePublicKeyCredentialResponse.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON,
+                registrationResponseJsonExpected);
+
+        CreatePublicKeyCredentialResponse response =
+                new CreatePublicKeyCredentialResponse(registrationResponseJsonExpected);
+
+        assertThat(response.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(response.getData(), expectedData)).isTrue();
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseTest.kt
index 9311d59..e2179b9 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialResponseTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -47,4 +48,19 @@
         val testJsonActual = createPublicKeyCredentialResponse.registrationResponseJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val registrationResponseJsonExpected = "{\"input\":5}"
+        val expectedData = Bundle()
+        expectedData.putString(
+            CreatePublicKeyCredentialResponse.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON,
+            registrationResponseJsonExpected
+        )
+
+        val response = CreatePublicKeyCredentialResponse(registrationResponseJsonExpected)
+
+        assertThat(response.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+        assertThat(equals(response.data, expectedData)).isTrue()
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
new file mode 100644
index 0000000..d8050da
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionJavaTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+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 GetPasswordOptionJavaTest {
+    @Test
+    public void getter_frameworkProperties() {
+        GetPasswordOption option = new GetPasswordOption();
+
+        assertThat(option.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(option.getData(), Bundle.EMPTY)).isTrue();
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
new file mode 100644
index 0000000..2c155af
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPasswordOptionTest.kt
@@ -0,0 +1,35 @@
+/*
+ * 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
+
+import android.os.Bundle
+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 GetPasswordOptionTest {
+    @Test
+    fun getter_frameworkProperties() {
+        val option = GetPasswordOption()
+        Truth.assertThat(option.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+        Truth.assertThat(equals(option.data, Bundle.EMPTY)).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
index ec3083c..7575d2a 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionJavaTest.java
@@ -16,10 +16,15 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.GetPublicKeyCredentialBaseOption.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Bundle;
+
 import org.junit.Test;
 
 public class GetPublicKeyCredentialOptionJavaTest {
@@ -73,4 +78,20 @@
         String testJsonActual = getPublicKeyCredentialOpt.getRequestJson();
         assertThat(testJsonActual).isEqualTo(testJsonExpected);
     }
+
+    @Test
+    public void getter_frameworkProperties_success() {
+        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        boolean allowHybridExpected = false;
+        Bundle expectedData = new Bundle();
+        expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+
+        GetPublicKeyCredentialOption option =
+                new GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected);
+
+        assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(option.getData(), expectedData)).isTrue();
+        assertThat(option.getRequireSystemProvider()).isFalse();
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
index 2b18b0d..54efd71 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedJavaTest.java
@@ -16,8 +16,15 @@
 
 package androidx.credentials;
 
+import static androidx.credentials.GetPublicKeyCredentialBaseOption.BUNDLE_KEY_REQUEST_JSON;
+import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID;
+import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH;
+import static androidx.credentials.GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RP;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.Bundle;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -106,4 +113,26 @@
         String clientDataHashActual = getPublicKeyCredentialOptionPrivileged.getClientDataHash();
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected);
     }
+
+    @Test
+    public void getter_frameworkProperties_success() {
+        String requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        String rpExpected = "RP";
+        String clientDataHashExpected = "X342%4dfd7&";
+        boolean allowHybridExpected = false;
+        Bundle expectedData = new Bundle();
+        expectedData.putString(BUNDLE_KEY_REQUEST_JSON, requestJsonExpected);
+        expectedData.putString(BUNDLE_KEY_RP, rpExpected);
+        expectedData.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHashExpected);
+        expectedData.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybridExpected);
+
+        GetPublicKeyCredentialOptionPrivileged option =
+                new GetPublicKeyCredentialOptionPrivileged(
+                        requestJsonExpected, rpExpected, clientDataHashExpected,
+                        allowHybridExpected);
+
+        assertThat(option.getType()).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(option.getData(), expectedData)).isTrue();
+        assertThat(option.getRequireSystemProvider()).isFalse();
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
index 024070d..e1c1079 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -107,4 +108,35 @@
         val clientDataHashActual = getPublicKeyCredentialOptionPrivileged.clientDataHash
         assertThat(clientDataHashActual).isEqualTo(clientDataHashExpected)
     }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val rpExpected = "RP"
+        val clientDataHashExpected = "X342%4dfd7&"
+        val allowHybridExpected = false
+        val expectedData = Bundle()
+        expectedData.putString(
+            GetPublicKeyCredentialBaseOption.BUNDLE_KEY_REQUEST_JSON,
+            requestJsonExpected
+        )
+        expectedData.putString(GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_RP, rpExpected)
+        expectedData.putString(
+            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_CLIENT_DATA_HASH,
+            clientDataHashExpected
+        )
+        expectedData.putBoolean(
+            GetPublicKeyCredentialOptionPrivileged.BUNDLE_KEY_ALLOW_HYBRID,
+            allowHybridExpected
+        )
+
+        val option = GetPublicKeyCredentialOptionPrivileged(
+            requestJsonExpected, rpExpected, clientDataHashExpected,
+            allowHybridExpected
+        )
+
+        assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+        assertThat(equals(option.data, expectedData)).isTrue()
+        assertThat(option.requireSystemProvider).isFalse()
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
index 7cc8392..821b580 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -68,4 +69,25 @@
         val testJsonActual = createPublicKeyCredentialReq.requestJson
         assertThat(testJsonActual).isEqualTo(testJsonExpected)
     }
+
+    @Test
+    fun getter_frameworkProperties_success() {
+        val requestJsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val allowHybridExpected = false
+        val expectedData = Bundle()
+        expectedData.putString(
+            GetPublicKeyCredentialBaseOption.BUNDLE_KEY_REQUEST_JSON,
+            requestJsonExpected
+        )
+        expectedData.putBoolean(
+            GetPublicKeyCredentialOption.BUNDLE_KEY_ALLOW_HYBRID,
+            allowHybridExpected
+        )
+
+        val option = GetPublicKeyCredentialOption(requestJsonExpected, allowHybridExpected)
+
+        assertThat(option.type).isEqualTo(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
+        assertThat(equals(option.data, expectedData)).isTrue()
+        assertThat(option.requireSystemProvider).isFalse()
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
new file mode 100644
index 0000000..c41d985
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialJavaTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Bundle;
+
+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 PasswordCredentialJavaTest {
+    @Test
+    public void constructor_nullId_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new PasswordCredential(null, "pwd")
+        );
+    }
+
+    @Test
+    public void constructor_nullPassword_throws() {
+        assertThrows(
+                NullPointerException.class,
+                () -> new PasswordCredential("id", null)
+        );
+    }
+
+    @Test
+    public void constructor_emptyPassword_throws() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> new PasswordCredential("id", "")
+        );
+    }
+
+    @Test
+    public void getter_id() {
+        String idExpected = "id";
+        PasswordCredential credential = new PasswordCredential(idExpected, "password");
+        assertThat(credential.getId()).isEqualTo(idExpected);
+    }
+
+    @Test
+    public void getter_password() {
+        String passwordExpected = "pwd";
+        PasswordCredential credential = new PasswordCredential("id", passwordExpected);
+        assertThat(credential.getPassword()).isEqualTo(passwordExpected);
+    }
+
+    @Test
+    public void getter_frameworkProperties() {
+        String idExpected = "id";
+        String passwordExpected = "pwd";
+        Bundle expectedData = new Bundle();
+        expectedData.putString(PasswordCredential.BUNDLE_KEY_ID, idExpected);
+        expectedData.putString(PasswordCredential.BUNDLE_KEY_PASSWORD, passwordExpected);
+
+        CreatePasswordRequest credential = new CreatePasswordRequest(idExpected, passwordExpected);
+
+        assertThat(credential.getType()).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(credential.getData(), expectedData)).isTrue();
+    }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
new file mode 100644
index 0000000..c8f425c
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PasswordCredentialTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PasswordCredentialTest {
+    @Test
+    fun constructor_emptyPassword_throws() {
+        assertThrows<IllegalArgumentException> {
+            PasswordCredential("id", "")
+        }
+    }
+
+    @Test
+    fun getter_id() {
+        val idExpected = "id"
+        val credential = PasswordCredential(idExpected, "password")
+        assertThat(credential.id).isEqualTo(idExpected)
+    }
+
+    @Test
+    fun getter_password() {
+        val passwordExpected = "pwd"
+        val credential = PasswordCredential("id", passwordExpected)
+        assertThat(credential.password).isEqualTo(passwordExpected)
+    }
+
+    @Test
+    fun getter_frameworkProperties() {
+        val idExpected = "id"
+        val passwordExpected = "pwd"
+        val expectedData = Bundle()
+        expectedData.putString(PasswordCredential.BUNDLE_KEY_ID, idExpected)
+        expectedData.putString(PasswordCredential.BUNDLE_KEY_PASSWORD, passwordExpected)
+
+        val credential = PasswordCredential(idExpected, passwordExpected)
+
+        assertThat(credential.type).isEqualTo(PasswordCredential.TYPE_PASSWORD_CREDENTIAL)
+        assertThat(equals(credential.data, expectedData)).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
index 86847fa..04223aa 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialJavaTest.java
@@ -20,6 +20,8 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.os.Bundle;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -64,6 +66,20 @@
     }
 
     @Test
+    public void getter_frameworkProperties() {
+        String jsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}";
+        Bundle expectedData = new Bundle();
+        expectedData.putString(
+                PublicKeyCredential.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, jsonExpected);
+
+        PublicKeyCredential publicKeyCredential = new PublicKeyCredential(jsonExpected);
+
+        assertThat(publicKeyCredential.getType()).isEqualTo(
+                PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL);
+        assertThat(TestUtilsKt.equals(publicKeyCredential.getData(), expectedData)).isTrue();
+    }
+
+    @Test
     public void staticProperty_hasCorrectTypeConstantValue() {
         String typeExpected = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL";
         String typeActual = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL;
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
index 47aecb4..0c67994 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/PublicKeyCredentialTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.credentials
 
+import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -54,6 +55,22 @@
     }
 
     @Test
+    fun getter_frameworkProperties() {
+        val jsonExpected = "{\"hi\":{\"there\":{\"lol\":\"Value\"}}}"
+        val expectedData = Bundle()
+        expectedData.putString(
+            PublicKeyCredential.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, jsonExpected
+        )
+
+        val publicKeyCredential = PublicKeyCredential(jsonExpected)
+
+        assertThat(publicKeyCredential.type).isEqualTo(
+            PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+        )
+        assertThat(equals(publicKeyCredential.data, expectedData)).isTrue()
+    }
+
+    @Test
     fun staticProperty_hasCorrectTypeConstantValue() {
         val typeExpected = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
         val typeActual = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.kt
new file mode 100644
index 0000000..4567380
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/TestUtils.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
+
+import android.os.Bundle
+
+/** True if the two Bundles contain the same elements, and false otherwise. */
+@Suppress("DEPRECATION")
+fun equals(a: Bundle, b: Bundle): Boolean {
+    if (a.keySet().size != b.keySet().size) {
+        return false
+    }
+    for (key in a.keySet()) {
+        if (!b.keySet().contains(key)) {
+            return false
+        }
+
+        val valA = a.get(key)
+        val valB = b.get(key)
+        if (valA is Bundle && valB is Bundle && !equals(valA, valB)) {
+            return false
+        } else {
+            val isEqual = (valA?.equals(valB) ?: (valB == null))
+            if (!isEqual) {
+                return false
+            }
+        }
+    }
+    return true
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
index d630fa7..7c373fe 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialRequest.kt
@@ -16,11 +16,22 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+
 /**
  * Base request class for registering a credential.
  *
  * An application can construct a subtype request and call [CredentialManager.executeCreateCredential] to
  * launch framework UI flows to collect consent and any other metadata needed from the user to
  * register a new user credential.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * @property data the request data in the [Bundle] format
+ * @property requireSystemProvider true if must only be fulfilled by a system provider and false
+ *                              otherwise
  */
-abstract class CreateCredentialRequest
\ No newline at end of file
+open class CreateCredentialRequest(
+    val type: String,
+    val data: Bundle,
+    val requireSystemProvider: Boolean,
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
index 192d472..5267997 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreateCredentialResponse.kt
@@ -16,5 +16,13 @@
 
 package androidx.credentials
 
-/** Base response class for registering a credential. */
-abstract class CreateCredentialResponse
\ No newline at end of file
+import android.os.Bundle
+
+/**
+ * Base response class for the credential creation operation made with the
+ * [CreateCredentialRequest].
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * @property data the response data in the [Bundle] format
+ */
+open class CreateCredentialResponse(val type: String, val data: Bundle)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
index 2fc0116..f03c633 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordRequest.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A request to save the user password credential with their password provider.
  *
@@ -28,9 +31,29 @@
 class CreatePasswordRequest constructor(
     val id: String,
     val password: String,
-) : CreateCredentialRequest() {
+) : CreateCredentialRequest(
+    PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+    toBundle(id, password),
+    false,
+) {
 
     init {
         require(password.isNotEmpty()) { "password should not be empty" }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
+
+        @JvmStatic
+        internal fun toBundle(id: String, password: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_ID, id)
+            bundle.putString(BUNDLE_KEY_PASSWORD, password)
+            return bundle
+        }
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordResponse.kt
index d70468c..436db69 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePasswordResponse.kt
@@ -16,5 +16,10 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+
 /** A response of a password saving flow. */
-class CreatePasswordResponse : CreateCredentialResponse()
\ No newline at end of file
+class CreatePasswordResponse : CreateCredentialResponse(
+    PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+    Bundle(),
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialBaseRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialBaseRequest.kt
index e78b5ac..7fbe48a 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialBaseRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialBaseRequest.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * Base request class for registering a public key credential.
  *
@@ -30,10 +33,19 @@
  * @hide
  */
 abstract class CreatePublicKeyCredentialBaseRequest constructor(
-    val requestJson: String
-) : CreateCredentialRequest() {
+    val requestJson: String,
+    type: String,
+    data: Bundle,
+    requireSystemProvider: Boolean,
+) : CreateCredentialRequest(type, data, requireSystemProvider) {
 
     init {
         require(requestJson.isNotEmpty()) { "request json must not be empty" }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
index d0b2d24..fe79aa99 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequest.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A request to register a passkey from the user's public key credential provider.
  *
@@ -32,4 +35,23 @@
     requestJson: String,
     @get:JvmName("allowHybrid")
     val allowHybrid: Boolean = true
-) : CreatePublicKeyCredentialBaseRequest(requestJson)
\ No newline at end of file
+) : CreatePublicKeyCredentialBaseRequest(
+    requestJson,
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(requestJson, allowHybrid),
+    false,
+) {
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+
+        @JvmStatic
+        internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
index 23abbfb..1641f43 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivileged.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A privileged request to register a passkey from the user’s public key credential provider, where
  * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
@@ -38,7 +41,12 @@
     val clientDataHash: String,
     @get:JvmName("allowHybrid")
     val allowHybrid: Boolean = true
-) : CreatePublicKeyCredentialBaseRequest(requestJson) {
+) : CreatePublicKeyCredentialBaseRequest(
+    requestJson,
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(requestJson, rp, clientDataHash, allowHybrid),
+    false,
+) {
 
     init {
         require(rp.isNotEmpty()) { "rp must not be empty" }
@@ -88,4 +96,30 @@
                 this.rp, this.clientDataHash, this.allowHybrid)
         }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+            "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+
+        @JvmStatic
+        internal fun toBundle(
+            requestJson: String,
+            rp: String,
+            clientDataHash: String,
+            allowHybrid: Boolean
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putString(BUNDLE_KEY_RP, rp)
+            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+    }
 }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialResponse.kt b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialResponse.kt
index f4cf4b9..7b5cd1d 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialResponse.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CreatePublicKeyCredentialResponse.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A response of a public key credential (passkey) flow.
  *
@@ -28,10 +31,27 @@
  */
 class CreatePublicKeyCredentialResponse(
     val registrationResponseJson: String
-) : CreateCredentialResponse() {
+) : CreateCredentialResponse(
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(registrationResponseJson)
+) {
 
     init {
         require(registrationResponseJson.isNotEmpty()) { "registrationResponseJson must not be " +
             "empty" }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_REGISTRATION_RESPONSE_JSON =
+            "androidx.credentials.BUNDLE_KEY_REGISTRATION_RESPONSE_JSON"
+
+        @JvmStatic
+        internal fun toBundle(registrationResponseJson: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REGISTRATION_RESPONSE_JSON, registrationResponseJson)
+            return bundle
+        }
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
index eb19ff8..d1d9629 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/Credential.kt
@@ -16,5 +16,12 @@
 
 package androidx.credentials
 
-/** Base class for a credential with which the user consented to authenticate to the app. */
-abstract class Credential
\ No newline at end of file
+import android.os.Bundle
+
+/**
+ * Base class for a credential with which the user consented to authenticate to the app.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * @property data the credential data in the [Bundle] format.
+ */
+open class Credential(val type: String, val data: Bundle)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt
index dfc262c..e5c2bbf 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/FederatedCredential.kt
@@ -16,6 +16,8 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+
 /**
  * A federated credential fetched from a federated identity provider (FedCM).
  *
@@ -24,7 +26,10 @@
  *
  * @hide
  */
-class FederatedCredential private constructor() {
+class FederatedCredential private constructor() : Credential(
+    TYPE_FEDERATED_CREDENTIAL,
+    Bundle(),
+) {
     companion object {
         /** The type value for federated credential related operations. */
         const val TYPE_FEDERATED_CREDENTIAL: String = "type.federated_credential"
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt
index 2fcd72a..c75a157 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetCredentialOption.kt
@@ -16,5 +16,21 @@
 
 package androidx.credentials
 
-/** Base request class for getting a registered credential. */
-abstract class GetCredentialOption
\ No newline at end of file
+import android.os.Bundle
+
+/**
+ * Base class for getting a specific type of credentials.
+ *
+ * [GetCredentialRequest] will be composed of a list of [GetCredentialOption] subclasses to indicate
+ * the specific credential types and configurations that your app accepts.
+ *
+ * @property type the credential type determined by the credential-type-specific subclass
+ * @property data the request data in the [Bundle] format
+ * @property requireSystemProvider true if must only be fulfilled by a system provider and false
+ *                              otherwise
+ */
+open class GetCredentialOption(
+    val type: String,
+    val data: Bundle,
+    val requireSystemProvider: Boolean,
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
index 920cbf2..a43927e 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPasswordOption.kt
@@ -16,5 +16,11 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+
 /** A request to retrieve the user's saved application password from their password provider. */
-class GetPasswordOption : GetCredentialOption()
\ No newline at end of file
+class GetPasswordOption : GetCredentialOption(
+    PasswordCredential.TYPE_PASSWORD_CREDENTIAL,
+    Bundle(),
+    false,
+)
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialBaseOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialBaseOption.kt
index d42c089..23cd3a3 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialBaseOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialBaseOption.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * Base request class for getting a registered public key credential.
  *
@@ -27,10 +30,19 @@
  * @hide
  */
 abstract class GetPublicKeyCredentialBaseOption constructor(
-    val requestJson: String
-) : GetCredentialOption() {
+    val requestJson: String,
+    type: String,
+    data: Bundle,
+    requireSystemProvider: Boolean,
+) : GetCredentialOption(type, data, requireSystemProvider) {
 
     init {
         require(requestJson.isNotEmpty()) { "request json must not be empty" }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
index d372ffa..7f449d4 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOption.kt
@@ -1,4 +1,4 @@
-package androidx.credentials/*
+/*
  * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,6 +14,11 @@
  * limitations under the License.
  */
 
+package androidx.credentials
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A request to get passkeys from the user's public key credential provider.
  *
@@ -30,4 +35,23 @@
     requestJson: String,
     @get:JvmName("allowHybrid")
     val allowHybrid: Boolean = true,
-) : GetPublicKeyCredentialBaseOption(requestJson)
\ No newline at end of file
+) : GetPublicKeyCredentialBaseOption(
+    requestJson,
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(requestJson, allowHybrid),
+    false
+) {
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+
+        @JvmStatic
+        internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
index a8fc6a2..2228e7b 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/GetPublicKeyCredentialOptionPrivileged.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * A privileged request to get passkeys from the user's public key credential provider. The caller
  * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
@@ -38,7 +41,12 @@
     val clientDataHash: String,
     @get:JvmName("allowHybrid")
     val allowHybrid: Boolean = true
-) : GetPublicKeyCredentialBaseOption(requestJson) {
+) : GetPublicKeyCredentialBaseOption(
+    requestJson,
+    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(requestJson, rp, clientDataHash, allowHybrid),
+    false,
+) {
 
     init {
         require(rp.isNotEmpty()) { "rp must not be empty" }
@@ -88,4 +96,30 @@
                 this.rp, this.clientDataHash, this.allowHybrid)
         }
     }
+
+    /** @hide */
+    companion object {
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+            "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+
+        @JvmStatic
+        internal fun toBundle(
+            requestJson: String,
+            rp: String,
+            clientDataHash: String,
+            allowHybrid: Boolean
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putString(BUNDLE_KEY_RP, rp)
+            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
index 252f4a1..3c42d37 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/PasswordCredential.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * Represents the user's password credential granted by the user for app sign-in.
  *
@@ -28,9 +31,30 @@
 class PasswordCredential constructor(
     val id: String,
     val password: String,
-) : Credential() {
+) : Credential(TYPE_PASSWORD_CREDENTIAL, toBundle(id, password)) {
 
     init {
         require(password.isNotEmpty()) { "password should not be empty" }
     }
+
+    /** @hide */
+    companion object {
+        // TODO: this type is officially defined in the framework. This definition should be
+        // removed when the framework type is available in jetpack.
+        /** @hide */
+        const val TYPE_PASSWORD_CREDENTIAL: String = "android.credentials.TYPE_PASSWORD_CREDENTIAL"
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
+
+        @JvmStatic
+        internal fun toBundle(id: String, password: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_ID, id)
+            bundle.putString(BUNDLE_KEY_PASSWORD, password)
+            return bundle
+        }
+    }
 }
diff --git a/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt b/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
index ccfc29c..1a87b3c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/PublicKeyCredential.kt
@@ -16,6 +16,9 @@
 
 package androidx.credentials
 
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+
 /**
  * Represents the user's passkey credential granted by the user for app sign-in.
  *
@@ -30,7 +33,10 @@
  */
 class PublicKeyCredential constructor(
     val authenticationResponseJson: String
-) : Credential() {
+) : Credential(
+    TYPE_PUBLIC_KEY_CREDENTIAL,
+    toBundle(authenticationResponseJson)
+) {
 
     init {
         require(authenticationResponseJson.isNotEmpty()) {
@@ -40,5 +46,16 @@
         /** The type value for public key credential related operations. */
         const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
             "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
+            "androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
+
+        @JvmStatic
+        internal fun toBundle(authenticationResponseJson: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson)
+            return bundle
+        }
     }
 }
\ No newline at end of file
diff --git a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
index 6b969da..cbd6d75 100644
--- a/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
+++ b/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioSerializer.kt
@@ -43,7 +43,7 @@
      *  Marshal object to a Sink.
      *
      *  @param t the data to write to output
-     *  @output the BufferedSink to serialize data to
+     *  @param sink the BufferedSink to serialize data to
      */
     public suspend fun writeTo(t: T, sink: BufferedSink)
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 4c98c732..dd40d3a 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    implementation(libs.kotlinCoroutinesCore)
     implementation("androidx.core:core-ktx:1.8.0")
     implementation project(path: ':emoji2:emoji2')
     implementation project(path: ':core:core')
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 cc5b603..f9e16bc 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,8 +19,8 @@
 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
 import org.junit.Assert.assertTrue
 import org.junit.Test
@@ -28,17 +28,17 @@
 @SmallTest
 class BundledEmojiListLoaderTest {
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val emojiCompatMetadata = EmojiPickerView.EmojiCompatMetadata(null, false)
 
     @Test
-    fun testGetCategorizedEmojiData_loaded_writeToCache() {
+    fun testGetCategorizedEmojiData_loaded_writeToCache() = runBlocking {
         // delete cache dir first
         val fileCache = FileCache.getInstance(context)
         fileCache.emojiPickerCacheDir.deleteRecursively()
         assertFalse(fileCache.emojiPickerCacheDir.exists())
 
-        BundledEmojiListLoader.load(context, emojiCompatMetadata)
-        assertTrue(BundledEmojiListLoader.categorizedEmojiData.isNotEmpty())
+        BundledEmojiListLoader.load(context)
+        val result = BundledEmojiListLoader.getCategorizedEmojiData()
+        assertTrue(result.isNotEmpty())
 
         // emoji_picker/osVersion|appVersion/ folder should be created
         val propertyFolder = fileCache.emojiPickerCacheDir.listFiles()!![0]
@@ -46,17 +46,15 @@
 
         // Number of cache files should match the size of categorizedEmojiData
         val cacheFiles = propertyFolder.listFiles()
-        assertTrue(
-            cacheFiles!!.size == BundledEmojiListLoader.categorizedEmojiData.size
-        )
+        assertTrue(cacheFiles!!.size == result.size)
     }
 
     @Test
-    fun testGetCategorizedEmojiData_loaded_readFromCache() {
+    fun testGetCategorizedEmojiData_loaded_readFromCache() = runBlocking {
         // delete cache and load again
         val fileCache = FileCache.getInstance(context)
         fileCache.emojiPickerCacheDir.deleteRecursively()
-        BundledEmojiListLoader.load(context, emojiCompatMetadata)
+        BundledEmojiListLoader.load(context)
 
         val cacheFileName = fileCache.emojiPickerCacheDir.listFiles()!![0].listFiles()!![0].name
         val emptyDefaultValue = listOf<BundledEmojiListLoader.EmojiData>()
@@ -71,21 +69,15 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 21)
-    fun testGetEmojiVariantsLookup_loaded() {
+    fun testGetEmojiVariantsLookup_loaded() = runBlocking {
         // delete cache and load again
         FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
-        BundledEmojiListLoader.load(context, emojiCompatMetadata)
+        BundledEmojiListLoader.load(context)
+        val result = BundledEmojiListLoader.getEmojiVariantsLookup()
 
         // 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
-        assertTrue(
-            BundledEmojiListLoader
-                .emojiVariantsLookup["\uD83D\uDC43"]
-            !!.contains("\uD83D\uDC43\uD83C\uDFFD")
-        )
+        assertTrue(result["\uD83D\uDC43"]!!.contains("\uD83D\uDC43\uD83C\uDFFD"))
         // 😀 has no variant
-        assertFalse(
-            BundledEmojiListLoader.emojiVariantsLookup.containsKey("\uD83D\uDE00")
-        )
+        assertFalse(result.containsKey("\uD83D\uDE00"))
     }
 }
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 8795edf..4f4d9c1 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
@@ -17,9 +17,15 @@
 package androidx.emoji2.emojipicker
 
 import android.content.Context
+import android.content.res.TypedArray
 import androidx.core.content.res.use
 import androidx.emoji2.emojipicker.utils.FileCache
 import androidx.emoji2.emojipicker.utils.UnicodeRenderableManager
+import androidx.emoji2.text.EmojiCompat
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
 
 /**
  * A data loader that loads the following objects either from file based caches or from resources.
@@ -31,79 +37,80 @@
  * emoji. This allows faster variants lookup.
  */
 internal object BundledEmojiListLoader {
-    private var _categorizedEmojiData: List<EmojiDataCategory>? = null
-    private var _emojiVariantsLookup: Map<String, List<String>>? = null
+    private var categorizedEmojiData: List<EmojiDataCategory>? = null
+    private var emojiVariantsLookup: Map<String, List<String>>? = null
 
-    internal fun load(context: Context, emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata) {
+    private var deferred: List<Deferred<EmojiDataCategory>>? = null
+
+    internal suspend fun load(context: Context) {
         val categoryNames = context.resources.getStringArray(R.array.category_names)
+        val resources = if (UnicodeRenderableManager.isEmoji12Supported())
+            R.array.emoji_by_category_raw_resources_gender_inclusive
+        else
+            R.array.emoji_by_category_raw_resources
+        val emojiFileCache = FileCache.getInstance(context)
 
-        _categorizedEmojiData = context.resources
-            .obtainTypedArray(R.array.emoji_by_category_raw_resources)
-            .use { ta ->
-                val emojiFileCache = FileCache.getInstance(context)
-                (0 until ta.length()).map {
-                    val cacheFileName = getCacheFileName(it, emojiCompatMetadata)
-                    emojiFileCache.getOrPut(cacheFileName) {
-                        loadSingleCategory(
-                            context,
-                            emojiCompatMetadata,
-                            ta.getResourceId(it, 0)
-                        )
-                    }.let { data -> EmojiDataCategory(categoryNames[it], data) }
-                }.toList()
-            }
-
-        _emojiVariantsLookup =
-            _categorizedEmojiData!!
-                .map { it.emojiDataList }
-                .flatten()
-                .filter { it.variants.isNotEmpty() }
-                .associate { it.primary to it.variants }
+        deferred = context.resources
+            .obtainTypedArray(resources)
+            .use { ta -> loadEmojiAsync(ta, categoryNames, emojiFileCache, context) }
     }
 
-    internal val categorizedEmojiData: List<EmojiDataCategory>
-        get() = _categorizedEmojiData
-            ?: throw IllegalStateException("BundledEmojiListLoader.load is not called")
+    internal suspend fun getCategorizedEmojiData() =
+        categorizedEmojiData ?: deferred?.awaitAll()?.also {
+            categorizedEmojiData = it
+        } ?: throw IllegalStateException("BundledEmojiListLoader.load is not called")
 
-    internal val emojiVariantsLookup: Map<String, List<String>>
-        get() = _emojiVariantsLookup
-            ?: throw IllegalStateException("BundledEmojiListLoader.load is not called")
+    internal suspend fun getEmojiVariantsLookup() =
+        emojiVariantsLookup ?: getCategorizedEmojiData()
+            .flatMap { it.emojiDataList }
+            .filter { it.variants.isNotEmpty() }
+            .associate { it.primary to it.variants }
+            .also { emojiVariantsLookup = it }
+
+    private suspend fun loadEmojiAsync(
+        ta: TypedArray,
+        categoryNames: Array<String>,
+        emojiFileCache: FileCache,
+        context: Context
+    ): List<Deferred<EmojiDataCategory>> = coroutineScope {
+        (0 until ta.length()).map {
+            async {
+                emojiFileCache.getOrPut(getCacheFileName(it)) {
+                    loadSingleCategory(context, ta.getResourceId(it, 0))
+                }.let { data -> EmojiDataCategory(categoryNames[it], data) }
+            }
+        }
+    }
 
     private fun loadSingleCategory(
         context: Context,
-        emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata,
         resId: Int,
     ): List<EmojiData> =
         context.resources
             .openRawResource(resId)
             .bufferedReader()
             .useLines { it.toList() }
-            .map { filterRenderableEmojis(it.split(","), emojiCompatMetadata) }
+            .map { filterRenderableEmojis(it.split(",")) }
             .filter { it.isNotEmpty() }
             .map { EmojiData(it.first(), it.drop(1)) }
 
-    private fun getCacheFileName(
-        categoryIndex: Int,
-        emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata
-    ) = StringBuilder().append("emoji.v1.")
-        .append(emojiCompatMetadata.hashCode())
-        .append(".")
-        .append(categoryIndex)
-        .append(".")
-        .append(
-            if (UnicodeRenderableManager.isEmoji12Supported(emojiCompatMetadata)) 1 else 0
-        ).toString()
+    private fun getCacheFileName(categoryIndex: Int) =
+        StringBuilder().append("emoji.v1.")
+            .append(if (EmojiCompat.isConfigured()) 1 else 0)
+            .append(".")
+            .append(categoryIndex)
+            .append(".")
+            .append(if (UnicodeRenderableManager.isEmoji12Supported()) 1 else 0)
+            .toString()
 
     /**
      * To eliminate 'Tofu' (the fallback glyph when an emoji is not renderable), check the
      * renderability of emojis and keep only when they are renderable on the current device.
      */
-    private fun filterRenderableEmojis(
-        emojiList: List<String>,
-        emojiCompatMetadata: EmojiPickerView.EmojiCompatMetadata,
-    ) = emojiList.filter {
-        UnicodeRenderableManager.isEmojiRenderable(it, emojiCompatMetadata)
-    }.toList()
+    private fun filterRenderableEmojis(emojiList: List<String>) =
+        emojiList.filter {
+            UnicodeRenderableManager.isEmojiRenderable(it)
+        }.toList()
 
     internal data class EmojiData(val primary: String, val variants: List<String>)
 
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 2dfc5ff..baaac54 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
@@ -20,6 +20,10 @@
 import android.content.res.TypedArray
 import android.util.AttributeSet
 import android.widget.FrameLayout
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -60,10 +64,6 @@
     private lateinit var bodyView: RecyclerView
 
     init {
-        initialize(context, attrs)
-    }
-
-    private fun initialize(context: Context, attrs: AttributeSet?) {
         val typedArray: TypedArray =
             context.obtainStyledAttributes(attrs, R.styleable.EmojiPickerView, 0, 0)
         emojiGridRows = typedArray.getFloat(
@@ -74,6 +74,18 @@
             R.styleable.EmojiPickerView_emojiGridColumns,
             EmojiPickerConstants.DEFAULT_BODY_COLUMNS
         )
+        typedArray.recycle()
+
+        CoroutineScope(Dispatchers.IO).launch {
+            BundledEmojiListLoader.load(context)
+            withContext(Dispatchers.Main) {
+                showEmojiPickerView(context)
+            }
+        }
+    }
+
+    private suspend fun showEmojiPickerView(context: Context) {
+        BundledEmojiListLoader.getCategorizedEmojiData()
 
         // get emoji picker
         val emojiPicker = inflate(context, R.layout.emoji_picker, this)
@@ -91,13 +103,5 @@
             false
         )
         bodyView.adapter = EmojiPickerBodyAdapter(context, emojiGridColumns, emojiGridRows)
-
-        // recycle the typed array
-        typedArray.recycle()
     }
-
-    /**
-     * MetaVersion will be null if EmojiCompat is not enabled.
-     */
-    internal data class EmojiCompatMetadata(val metaVersion: Int?, val replaceAll: Boolean)
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
index b8bd327..1d56bc7 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/UnicodeRenderableManager.kt
@@ -20,7 +20,6 @@
 import android.text.TextPaint
 import androidx.annotation.VisibleForTesting
 import androidx.core.graphics.PaintCompat
-import androidx.emoji2.emojipicker.EmojiPickerView
 import androidx.emoji2.text.EmojiCompat
 
 /**
@@ -59,19 +58,15 @@
      *
      * Note: For older API version, codepoints {@code U+0xFE0F} are removed.
      */
-    internal fun isEmojiRenderable(
-        emoji: String,
-        emojiCompatMetaData: EmojiPickerView.EmojiCompatMetadata
-    ) = emojiCompatMetaData.metaVersion?.run {
-        EmojiCompat.get().getEmojiMatch(emoji, this) > 0
-    } ?: (getClosestRenderable(emoji) != null)
+    internal fun isEmojiRenderable(emoji: String) =
+        if (EmojiCompat.isConfigured() &&
+            EmojiCompat.get().loadState == EmojiCompat.LOAD_STATE_SUCCEEDED)
+            EmojiCompat.get().getEmojiMatch(emoji, Int.MAX_VALUE) > 0
+        else getClosestRenderable(emoji) != null
 
-    internal fun isEmoji12Supported(
-        emojiCompatMetaData: EmojiPickerView.EmojiCompatMetadata
-    ) =
-        // Yawning face is added in emoji 12 which is the first version starts to support gender
-        // inclusive emojis.
-        isEmojiRenderable(YAWNING_FACE_EMOJI, emojiCompatMetaData)
+    // Yawning face is added in emoji 12 which is the first version starts to support gender
+    // inclusive emojis.
+    internal fun isEmoji12Supported() = isEmojiRenderable(YAWNING_FACE_EMOJI)
 
     @VisibleForTesting
     fun getClosestRenderable(emoji: String): String? {
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
index 1715fc6..01830cf 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
@@ -28,6 +28,20 @@
         <item>@raw/emoji_category_flags</item>
     </array>
 
+    <!-- The drawable resources to be used as emoji category icons. Order of the icons must match
+         order of the content descriptions below. -->
+    <array name="emoji_by_category_raw_resources_gender_inclusive">
+        <item>@raw/emoji_category_emotions</item>
+        <item>@raw/emoji_category_people_gender_inclusive</item>
+        <item>@raw/emoji_category_animals_nature</item>
+        <item>@raw/emoji_category_food_drink</item>
+        <item>@raw/emoji_category_travel_places</item>
+        <item>@raw/emoji_category_activity</item>
+        <item>@raw/emoji_category_objects</item>
+        <item>@raw/emoji_category_symbols</item>
+        <item>@raw/emoji_category_flags</item>
+    </array>
+
     <integer-array name="emoji_categories_icons">
         <item>@drawable/quantum_gm_ic_access_time_filled_vd_theme_24</item>
         <item>@drawable/gm_filled_emoji_emotions_vd_theme_24</item>
@@ -41,8 +55,6 @@
         <item>@drawable/gm_filled_flag_vd_theme_24</item>
     </integer-array>
 
-    <!-- The drawable resources to be used as emoji category icons. Order of the icons must match
-         order of the content descriptions below. -->
     <string-array name="category_names">
         <item>@string/emoji_category_emotions</item>
         <item>@string/emoji_category_people</item>
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
index 5ab086a..6ed9681 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
@@ -53,11 +53,256 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Static library support version of the framework's {@link android.app.DialogFragment}.
- * Used to write apps that run on platforms prior to Android 3.0.  When running
- * on Android 3.0 or above, this implementation is still used; it does not try
- * to switch to the framework's implementation.  See the framework SDK
- * documentation for a class overview.
+ * A fragment that displays a dialog window, floating in the foreground of its
+ * activity's window.  This fragment contains a Dialog object, which it
+ * displays as appropriate based on the fragment's state.  Control of
+ * the dialog (deciding when to show, hide, dismiss it) should be done through
+ * the APIs here, not with direct calls on the dialog.
+ *
+ * <p>Implementations should override this class and implement
+ * {@link #onViewCreated(View, Bundle)} to supply the
+ * content of the dialog.  Alternatively, they can override
+ * {@link #onCreateDialog(Bundle)} to create an entirely custom dialog, such
+ * as an AlertDialog, with its own content.
+ *
+ * <p>Topics covered here:
+ * <ol>
+ * <li><a href="#Lifecycle">Lifecycle</a>
+ * <li><a href="#BasicDialog">Basic Dialog</a>
+ * <li><a href="#AlertDialog">Alert Dialog</a>
+ * <li><a href="#DialogOrEmbed">Selecting Between Dialog or Embedding</a>
+ * </ol>
+ *
+ * <a name="Lifecycle"></a>
+ * <h3>Lifecycle</h3>
+ *
+ * <p>DialogFragment does various things to keep the fragment's lifecycle
+ * driving it, instead of the Dialog.  Note that dialogs are generally
+ * autonomous entities -- they are their own window, receiving their own
+ * input events, and often deciding on their own when to disappear (by
+ * receiving a back key event or the user clicking on a button).
+ *
+ * <p>DialogFragment needs to ensure that what is happening with the Fragment
+ * and Dialog states remains consistent.  To do this, it watches for dismiss
+ * events from the dialog and takes care of removing its own state when they
+ * happen.  This means you should use {@link #show(FragmentManager, String)},
+ * {@link #show(FragmentTransaction, String)}, or {@link #showNow(FragmentManager, String)}
+ * to add an instance of DialogFragment to your UI, as these keep track of
+ * how DialogFragment should remove itself when the dialog is dismissed.
+ *
+ * <a name="BasicDialog"></a>
+ * <h3>Basic Dialog</h3>
+ *
+ * <p>The simplest use of DialogFragment is as a floating container for the
+ * fragment's view hierarchy.  A simple implementation may look like this:
+ *
+ * <pre>{@code
+ * public class MyDialogFragment extends DialogFragment {
+ *     int mNum;
+ *
+ *     // Create a new instance of MyDialogFragment, providing "num" as an argument.
+ *     static MyDialogFragment newInstance(int num) {
+ *         MyDialogFragment f = new MyDialogFragment();
+ *
+ *         // Supply num input as an argument.
+ *         Bundle args = new Bundle();
+ *         args.putInt("num", num);
+ *         f.setArguments(args);
+ *
+ *         return f;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *         mNum = getArguments().getInt("num");
+ *
+ *         // Pick a style based on the num.
+ *         int style = DialogFragment.STYLE_NORMAL, theme = 0;
+ *         switch ((mNum-1)%6) {
+ *             case 1: style = DialogFragment.STYLE_NO_TITLE; break;
+ *             case 2: style = DialogFragment.STYLE_NO_FRAME; break;
+ *             case 3: style = DialogFragment.STYLE_NO_INPUT; break;
+ *             case 4: style = DialogFragment.STYLE_NORMAL; break;
+ *             case 5: style = DialogFragment.STYLE_NORMAL; break;
+ *             case 6: style = DialogFragment.STYLE_NO_TITLE; break;
+ *             case 7: style = DialogFragment.STYLE_NO_FRAME; break;
+ *             case 8: style = DialogFragment.STYLE_NORMAL; break;
+ *         }
+ *         switch ((mNum-1)%6) {
+ *             case 4: theme = android.R.style.Theme_Holo; break;
+ *             case 5: theme = android.R.style.Theme_Holo_Light_Dialog; break;
+ *             case 6: theme = android.R.style.Theme_Holo_Light; break;
+ *             case 7: theme = android.R.style.Theme_Holo_Light_Panel; break;
+ *             case 8: theme = android.R.style.Theme_Holo_Light; break;
+ *         }
+ *         setStyle(style, theme);
+ *     }
+ *
+ *     {@literal @}Override
+ *     public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ *                              Bundle savedInstanceState) {
+ *         return inflater.inflate(R.layout.fragment_dialog, container, false);
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ *         super.onViewCreated(view, savedInstanceState);
+ *
+ *         // set DialogFragment title
+ *         getDialog().setTitle("Dialog #" + mNum);
+ *     }
+ * }
+ * }</pre>
+ *
+ * <p>An example showDialog() method on the Activity could be:
+ *
+ * <pre>{@code
+ * public void showDialog() {
+ *     mStackLevel++;
+ *
+ *     // DialogFragment.show() will take care of adding the fragment
+ *     // in a transaction.  We also want to remove any currently showing
+ *     // dialog, so make our own transaction and take care of that here.
+ *     FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ *     Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog");
+ *     if (prev != null) {
+ *         ft.remove(prev);
+ *     }
+ *     ft.addToBackStack(null);
+ *
+ *     // Create and show the dialog.
+ *     DialogFragment newFragment = MyDialogFragment.newInstance(mStackLevel);
+ *     newFragment.show(ft, "dialog");
+ * }
+ * }</pre>
+ *
+ * <p>This removes any currently shown dialog, creates a new DialogFragment
+ * with an argument, and shows it as a new state on the back stack.  When the
+ * transaction is popped, the current DialogFragment and its Dialog will be
+ * destroyed, and the previous one (if any) re-shown.  Note that in this case
+ * DialogFragment will take care of popping the transaction of the Dialog that
+ * is dismissed separately from it.
+ *
+ * <a name="AlertDialog"></a>
+ * <h3>Alert Dialog</h3>
+ *
+ * <p>Instead of (or in addition to) implementing {@link #onViewCreated(View, Bundle)} to
+ * generate the view hierarchy inside of a dialog, you may implement
+ * {@link #onCreateDialog(Bundle)} to create your own custom Dialog object.
+ *
+ * <p>This is most useful for creating an AlertDialog, allowing you
+ * to display standard alerts to the user that are managed by a fragment.
+ * A simple example implementation of this is:
+ *
+ * <pre>{@code
+ * public static class MyAlertDialogFragment extends DialogFragment {
+ *
+ *     public static MyAlertDialogFragment newInstance(int title) {
+ *         MyAlertDialogFragment frag = new MyAlertDialogFragment();
+ *         Bundle args = new Bundle();
+ *         args.putInt("title", title);
+ *         frag.setArguments(args);
+ *         return frag;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ *
+ *         return new AlertDialog.Builder(getActivity())
+ *                 .setIcon(R.drawable.alert_dialog_icon)
+ *                 .setTitle(title)
+ *                 .setPositiveButton(R.string.alert_dialog_ok,
+ *                         (dialogInterface, i) -> ((MainActivity)getActivity()).doPositiveClick())
+ *                 .setNegativeButton(R.string.alert_dialog_cancel,
+ *                         (dialogInterface, i) -> ((MainActivity)getActivity()).doNegativeClick())
+ *                 .create();
+ *         return super.onCreateDialog(savedInstanceState);
+ *     }
+ * }
+ * }</pre>
+ *
+ * <p>The activity creating this fragment may have the following methods to
+ * show the dialog and receive results from it:
+ *
+ * <pre>{@code
+ * void showDialog() {
+ *     DialogFragment newFragment = MyAlertDialogFragment.newInstance(
+ *             R.string.alert_dialog_two_buttons_title);
+ *     newFragment.show(getSupportFragmentManager(), "dialog");
+ * }
+ *
+ * public void doPositiveClick() {
+ *     // Do stuff here.
+ *     Log.i("MainActivity", "Positive click!");
+ * }
+ *
+ * public void doNegativeClick() {
+ *     // Do stuff here.
+ *     Log.i("MainActivity", "Negative click!");
+ * }
+ * }</pre>
+ *
+ * <p>Note that in this case the fragment is not placed on the back stack, it
+ * is just added as an indefinitely running fragment.  Because dialogs normally
+ * are modal, this will still operate as a back stack, since the dialog will
+ * capture user input until it is dismissed.  When it is dismissed, DialogFragment
+ * will take care of removing itself from its fragment manager.
+ *
+ * <a name="DialogOrEmbed"></a>
+ * <h3>Selecting Between Dialog or Embedding</h3>
+ *
+ * <p>A DialogFragment can still optionally be used as a normal fragment, if
+ * desired.  This is useful if you have a fragment that in some cases should
+ * be shown as a dialog and others embedded in a larger UI.  This behavior
+ * will normally be automatically selected for you based on how you are using
+ * the fragment, but can be customized with {@link #setShowsDialog(boolean)}.
+ *
+ * <p>For example, here is a simple dialog fragment:
+ *
+ * <pre>{@code
+ * public static class MyDialogFragment extends DialogFragment {
+ *     static MyDialogFragment newInstance() {
+ *         return new MyDialogFragment();
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *
+ *         // this fragment will be displayed in a dialog
+ *         setShowsDialog(true);
+ *     }
+ *
+ *     {@literal @}Override
+ *     public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ *             Bundle savedInstanceState) {
+ *         View v = inflater.inflate(R.layout.hello_world, container, false);
+ *         View tv = v.findViewById(R.id.text);
+ *         ((TextView)tv).setText("This is an instance of MyDialogFragment");
+ *         return v;
+ *     }
+ * }
+ * }</pre>
+ *
+ * <p>An instance of this fragment can be created and shown as a dialog:
+ *
+ * <pre>{@code
+ * void showDialog() {
+ *     // Create the fragment and show it as a dialog.
+ *     DialogFragment newFragment = MyDialogFragment.newInstance();
+ *     newFragment.show(getSupportFragmentManager(), "dialog");
+ * }
+ * }</pre>
+ *
+ * <p>It can also be added as content in a view hierarchy:
+ *
+ * <pre>{@code
+ * FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ * DialogFragment newFragment = MyDialogFragment.newInstance();
+ * ft.add(R.id.embedded, newFragment);
+ * ft.commit();
+ * }</pre>
  */
 public class DialogFragment extends Fragment
         implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 6244413..ff192ef 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -372,7 +372,6 @@
 package androidx.glance.appwidget.unit {
 
   public final class ColorProviderKt {
-    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
   }
 
 }
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 27e2045..b163bae 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -386,7 +386,6 @@
 package androidx.glance.appwidget.unit {
 
   public final class ColorProviderKt {
-    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
   }
 
 }
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 6244413..ff192ef 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -372,7 +372,6 @@
 package androidx.glance.appwidget.unit {
 
   public final class ColorProviderKt {
-    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
   }
 
 }
diff --git a/glance/glance-appwidget/integration-tests/demos/build.gradle b/glance/glance-appwidget/integration-tests/demos/build.gradle
index 5a3147a..7f1553b 100644
--- a/glance/glance-appwidget/integration-tests/demos/build.gradle
+++ b/glance/glance-appwidget/integration-tests/demos/build.gradle
@@ -27,6 +27,8 @@
     implementation(libs.kotlinStdlib)
     implementation(project(":glance:glance"))
     implementation(project(":glance:glance-appwidget"))
+    implementation(project(":glance:glance-material"))
+    implementation(project(":glance:glance-material3"))
     implementation("androidx.activity:activity:1.4.0")
     implementation("androidx.activity:activity-compose:1.4.0")
     implementation("androidx.compose.material:material:1.1.0-beta02")
@@ -36,6 +38,7 @@
     implementation("androidx.compose.material:material:1.1.0-beta02")
     implementation("androidx.datastore:datastore-preferences-core:1.0.0")
     implementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+    implementation "androidx.compose.material3:material3:1.0.0"
 }
 
 android {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
index c9cca26..8cfbfc5 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
@@ -52,7 +52,7 @@
 import androidx.glance.appwidget.appWidgetBackground
 import androidx.glance.appwidget.cornerRadius
 import androidx.glance.appwidget.state.updateAppWidgetState
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.currentState
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Column
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
index eb0d0c2..c469cba 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/CompoundButtonAppWidget.kt
@@ -43,8 +43,8 @@
 import androidx.glance.appwidget.appWidgetBackground
 import androidx.glance.appwidget.cornerRadius
 import androidx.glance.appwidget.state.updateAppWidgetState
-import androidx.glance.appwidget.unit.ColorProvider
 import androidx.glance.background
+import androidx.glance.color.ColorProvider
 import androidx.glance.currentState
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Column
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/DefaultColorsAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/DefaultColorsAppWidget.kt
index 1decabe..56bb8f4 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/DefaultColorsAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/DefaultColorsAppWidget.kt
@@ -16,20 +16,32 @@
 
 package androidx.glance.appwidget.demos
 
+import android.content.Context
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import androidx.glance.Button
+import androidx.glance.GlanceId
 import androidx.glance.GlanceModifier
 import androidx.glance.GlanceTheme
+import androidx.glance.action.ActionParameters
 import androidx.glance.appwidget.CheckBox
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.RadioButton
 import androidx.glance.appwidget.Switch
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
 import androidx.glance.background
 import androidx.glance.layout.Column
 import androidx.glance.layout.Row
 import androidx.glance.layout.padding
+import androidx.glance.material.ColorProviders
+import androidx.glance.material3.ColorProviders
 import androidx.glance.text.Text
 import androidx.glance.text.TextStyle
 import androidx.glance.unit.ColorProvider
@@ -38,16 +50,34 @@
  * A demo showing how to construct a widget with [GlanceTheme]. It will use Material 3 colors and
  * when supported, it will use the dynamic color theme.
  */
-class DefaultColorsAppWidget : GlanceAppWidget() {
+class DefaultColorsAppWidget(private val theme: DemoColorScheme.Scheme) : GlanceAppWidget() {
 
     @Composable
     override fun Content() {
-        GlanceTheme {
+        val colors = when (theme) {
+            DemoColorScheme.Scheme.SystemM3 -> GlanceTheme.colors
+            DemoColorScheme.Scheme.CustomM3 -> ColorProviders(
+                light = DemoColorScheme.LightColors,
+                dark = DemoColorScheme.DarkColors
+            )
+
+            DemoColorScheme.Scheme.CustomM2 -> ColorProviders(
+                light = DemoColorScheme.SampleM2ColorsLight,
+                dark = DemoColorScheme.SampleM2ColorsDark
+            )
+        }
+
+        GlanceTheme(colors) {
             Column(
                 GlanceModifier
                     .padding(8.dp)
                     .background(GlanceTheme.colors.background)
             ) {
+                Button(
+                    text = "Theme: $currentScheme",
+                    onClick = actionRunCallback<ChangeThemeCallback>(),
+                    modifier = GlanceModifier.padding(2.dp)
+                )
                 Row(GlanceModifier.padding(top = 8.dp)) {
                     CheckBox(checked = false, onCheckedChange = doNothingAction, text = "Unchecked")
                     CheckBox(checked = true, onCheckedChange = doNothingAction, text = "Checked")
@@ -122,6 +152,183 @@
 
 private val doNothingAction = null
 
+class ChangeThemeCallback : ActionCallback {
+    override suspend fun onAction(
+        context: Context,
+        glanceId: GlanceId,
+        parameters: ActionParameters
+    ) {
+        colorSchemeIndex = (colorSchemeIndex + 1) % DemoColorScheme.Scheme.values().size
+        DefaultColorsAppWidget(currentScheme).update(context, glanceId)
+    }
+}
+
+private var colorSchemeIndex = 0
+private val currentScheme: DemoColorScheme.Scheme
+    get() = DemoColorScheme.Scheme.values()[colorSchemeIndex]
+
 class DefaultColorsAppWidgetReceiver : GlanceAppWidgetReceiver() {
-    override val glanceAppWidget = DefaultColorsAppWidget()
+    override val glanceAppWidget = DefaultColorsAppWidget(currentScheme)
+}
+
+/**
+ * Color scheme generated using https://m3.material.io/theme-builder#/custom
+ */
+object DemoColorScheme {
+    enum class Scheme { SystemM3, CustomM3, CustomM2 }
+
+    val md_theme_light_primary = Color(0xFF026E00)
+    val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+    val md_theme_light_primaryContainer = Color(0xFF77FF61)
+    val md_theme_light_onPrimaryContainer = Color(0xFF002200)
+    val md_theme_light_secondary = Color(0xFFA900A9)
+    val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+    val md_theme_light_secondaryContainer = Color(0xFFFFD7F5)
+    val md_theme_light_onSecondaryContainer = Color(0xFF380038)
+    val md_theme_light_tertiary = Color(0xFF006A6A)
+    val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+    val md_theme_light_tertiaryContainer = Color(0xFF00FBFB)
+    val md_theme_light_onTertiaryContainer = Color(0xFF002020)
+    val md_theme_light_error = Color(0xFFBA1A1A)
+    val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+    val md_theme_light_onError = Color(0xFFFFFFFF)
+    val md_theme_light_onErrorContainer = Color(0xFF410002)
+    val md_theme_light_background = Color(0xFFFFFBFF)
+    val md_theme_light_onBackground = Color(0xFF1E1C00)
+    val md_theme_light_surface = Color(0xFFFFFBFF)
+    val md_theme_light_onSurface = Color(0xFF1E1C00)
+    val md_theme_light_surfaceVariant = Color(0xFFDFE4D7)
+    val md_theme_light_onSurfaceVariant = Color(0xFF43483F)
+    val md_theme_light_outline = Color(0xFF73796E)
+    val md_theme_light_inverseOnSurface = Color(0xFFFFF565)
+    val md_theme_light_inverseSurface = Color(0xFF353200)
+    val md_theme_light_inversePrimary = Color(0xFF02E600)
+    val md_theme_light_shadow = Color(0xFF000000)
+    val md_theme_light_surfaceTint = Color(0xFF026E00)
+
+    val md_theme_dark_primary = Color(0xFF02E600)
+    val md_theme_dark_onPrimary = Color(0xFF013A00)
+    val md_theme_dark_primaryContainer = Color(0xFF015300)
+    val md_theme_dark_onPrimaryContainer = Color(0xFF77FF61)
+    val md_theme_dark_secondary = Color(0xFFFFABF3)
+    val md_theme_dark_onSecondary = Color(0xFF5B005B)
+    val md_theme_dark_secondaryContainer = Color(0xFF810081)
+    val md_theme_dark_onSecondaryContainer = Color(0xFFFFD7F5)
+    val md_theme_dark_tertiary = Color(0xFF00DDDD)
+    val md_theme_dark_onTertiary = Color(0xFF003737)
+    val md_theme_dark_tertiaryContainer = Color(0xFF004F4F)
+    val md_theme_dark_onTertiaryContainer = Color(0xFF00FBFB)
+    val md_theme_dark_error = Color(0xFFFFB4AB)
+    val md_theme_dark_errorContainer = Color(0xFF93000A)
+    val md_theme_dark_onError = Color(0xFF690005)
+    val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+    val md_theme_dark_background = Color(0xFF1E1C00)
+    val md_theme_dark_onBackground = Color(0xFFF2E720)
+    val md_theme_dark_surface = Color(0xFF1E1C00)
+    val md_theme_dark_onSurface = Color(0xFFF2E720)
+    val md_theme_dark_surfaceVariant = Color(0xFF43483F)
+    val md_theme_dark_onSurfaceVariant = Color(0xFFC3C8BC)
+    val md_theme_dark_outline = Color(0xFF8D9387)
+    val md_theme_dark_inverseOnSurface = Color(0xFF1E1C00)
+    val md_theme_dark_inverseSurface = Color(0xFFF2E720)
+    val md_theme_dark_inversePrimary = Color(0xFF026E00)
+    val md_theme_dark_shadow = Color(0xFF000000)
+    val md_theme_dark_surfaceTint = Color(0xFF02E600)
+
+    val seed = Color(0xFF00FF00)
+
+    val LightColors = lightColorScheme(
+        primary = md_theme_light_primary,
+        onPrimary = md_theme_light_onPrimary,
+        primaryContainer = md_theme_light_primaryContainer,
+        onPrimaryContainer = md_theme_light_onPrimaryContainer,
+        secondary = md_theme_light_secondary,
+        onSecondary = md_theme_light_onSecondary,
+        secondaryContainer = md_theme_light_secondaryContainer,
+        onSecondaryContainer = md_theme_light_onSecondaryContainer,
+        tertiary = md_theme_light_tertiary,
+        onTertiary = md_theme_light_onTertiary,
+        tertiaryContainer = md_theme_light_tertiaryContainer,
+        onTertiaryContainer = md_theme_light_onTertiaryContainer,
+        error = md_theme_light_error,
+        onError = md_theme_light_onError,
+        errorContainer = md_theme_light_errorContainer,
+        onErrorContainer = md_theme_light_onErrorContainer,
+        background = md_theme_light_background,
+        onBackground = md_theme_light_onBackground,
+        surface = md_theme_light_surface,
+        onSurface = md_theme_light_onSurface,
+        surfaceVariant = md_theme_light_surfaceVariant,
+        onSurfaceVariant = md_theme_light_onSurfaceVariant,
+        outline = md_theme_light_outline,
+        inverseSurface = md_theme_light_inverseSurface,
+        inverseOnSurface = md_theme_light_inverseOnSurface,
+        inversePrimary = md_theme_light_inversePrimary,
+        surfaceTint = md_theme_light_surfaceTint,
+    )
+
+    val DarkColors = darkColorScheme(
+        primary = md_theme_dark_primary,
+        onPrimary = md_theme_dark_onPrimary,
+        primaryContainer = md_theme_dark_primaryContainer,
+        onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+        secondary = md_theme_dark_secondary,
+        onSecondary = md_theme_dark_onSecondary,
+        secondaryContainer = md_theme_dark_secondaryContainer,
+        onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+        tertiary = md_theme_dark_tertiary,
+        onTertiary = md_theme_dark_onTertiary,
+        tertiaryContainer = md_theme_dark_tertiaryContainer,
+        onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+        error = md_theme_dark_error,
+        onError = md_theme_dark_onError,
+        errorContainer = md_theme_dark_errorContainer,
+        onErrorContainer = md_theme_dark_onErrorContainer,
+        background = md_theme_dark_background,
+        onBackground = md_theme_dark_onBackground,
+        surface = md_theme_dark_surface,
+        onSurface = md_theme_dark_onSurface,
+        surfaceVariant = md_theme_dark_surfaceVariant,
+        onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+        outline = md_theme_dark_outline,
+        inverseSurface = md_theme_dark_inverseSurface,
+        inverseOnSurface = md_theme_dark_inverseOnSurface,
+        inversePrimary = md_theme_dark_inversePrimary,
+        surfaceTint = md_theme_dark_surfaceTint,
+    )
+
+    // Palette based on Jetchat
+    private val Yellow400 = Color(0xFFF6E547)
+    private val Yellow700 = Color(0xFFF3B711)
+    private val Yellow800 = Color(0xFFF29F05)
+    private val Blue200 = Color(0xFF9DA3FA)
+    private val Blue400 = Color(0xFF4860F7)
+    private val Blue500 = Color(0xFF0540F2)
+    private val Blue800 = Color(0xFF001CCF)
+    private val Red300 = Color(0xFFEA6D7E)
+    private val Red800 = Color(0xFFD00036)
+
+    val SampleM2ColorsDark = darkColors(
+        primary = Blue200,
+        primaryVariant = Blue400,
+        onPrimary = Color.Black,
+        secondary = Yellow400,
+        onSecondary = Color.Black,
+        onSurface = Color.White,
+        onBackground = Color.White,
+        error = Red300,
+        onError = Color.Black
+    )
+    val SampleM2ColorsLight = lightColors(
+        primary = Blue500,
+        primaryVariant = Blue800,
+        onPrimary = Color.White,
+        secondary = Yellow700,
+        secondaryVariant = Yellow800,
+        onSecondary = Color.Black,
+        onSurface = Color.Black,
+        onBackground = Color.Black,
+        error = Red800,
+        onError = Color.White
+    )
 }
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ExactAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ExactAppWidget.kt
index 2857901..d257a49 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ExactAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ExactAppWidget.kt
@@ -26,7 +26,7 @@
 import androidx.glance.appwidget.SizeMode
 import androidx.glance.appwidget.background
 import androidx.glance.appwidget.cornerRadius
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.layout.Column
 import androidx.glance.layout.fillMaxSize
 import androidx.glance.layout.padding
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
index f15b3d8..f407767 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ImageAppWidget.kt
@@ -16,25 +16,21 @@
 
 package androidx.glance.appwidget.demos
 
-import android.content.Context
 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.graphics.Color
 import androidx.compose.ui.unit.dp
-import androidx.datastore.preferences.core.stringPreferencesKey
 import androidx.glance.Button
-import androidx.glance.GlanceId
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
 import androidx.glance.ImageProvider
-import androidx.glance.action.ActionParameters
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.SizeMode
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.appwidget.state.updateAppWidgetState
 import androidx.glance.background
-import androidx.glance.currentState
 import androidx.glance.layout.Column
 import androidx.glance.layout.ContentScale
 import androidx.glance.layout.Spacer
@@ -42,6 +38,7 @@
 import androidx.glance.layout.fillMaxWidth
 import androidx.glance.layout.padding
 import androidx.glance.layout.size
+import androidx.glance.session.GlanceSessionManager
 
 /**
  * Sample AppWidget that showcase the [ContentScale] options for [Image]
@@ -49,54 +46,40 @@
 class ImageAppWidget : GlanceAppWidget() {
 
     override val sizeMode: SizeMode = SizeMode.Exact
-
-    companion object {
-        internal val ImageTypeKey = stringPreferencesKey("imageType")
-    }
+    override val sessionManager = GlanceSessionManager
 
     @Composable
     override fun Content() {
-        val type = currentState(ImageTypeKey) ?: "Fit"
+        var type by remember { mutableStateOf(ContentScale.Fit) }
         Column(modifier = GlanceModifier.fillMaxSize().padding(8.dp)) {
             Button(
-                text = "Content Scale: $type",
+                text = "Content Scale: ${type.asString()}",
                 modifier = GlanceModifier.fillMaxWidth(),
-                onClick = actionRunCallback<ChangeImageAction>()
+                onClick = {
+                    type = when (type) {
+                        ContentScale.Crop -> ContentScale.FillBounds
+                        ContentScale.FillBounds -> ContentScale.Fit
+                        else -> ContentScale.Crop
+                    }
+                }
             )
             Spacer(GlanceModifier.size(4.dp))
             Image(
                 provider = ImageProvider(R.drawable.compose),
-                contentDescription = "Content Scale image sample (value: $type)",
-                contentScale = type.toContentScale(),
+                contentDescription = "Content Scale image sample (value: ${type.asString()})",
+                contentScale = type,
                 modifier = GlanceModifier.fillMaxSize().background(Color.DarkGray)
             )
         }
     }
 
-    private fun String.toContentScale() = when (this) {
-        "Fit" -> ContentScale.Fit
-        "Fill Bounds" -> ContentScale.FillBounds
-        "Crop" -> ContentScale.Crop
-        else -> throw IllegalArgumentException()
-    }
-}
-
-class ChangeImageAction : ActionCallback {
-    override suspend fun onAction(
-        context: Context,
-        glanceId: GlanceId,
-        parameters: ActionParameters
-    ) {
-        updateAppWidgetState(context, glanceId) { state ->
-            val value = when (state[ImageAppWidget.ImageTypeKey]) {
-                "Crop" -> "Fill Bounds"
-                "Fill Bounds" -> "Fit"
-                else -> "Crop"
-            }
-            state[ImageAppWidget.ImageTypeKey] = value
+    private fun ContentScale.asString(): String =
+        when (this) {
+            ContentScale.Fit -> "Fit"
+            ContentScale.FillBounds -> "Fill Bounds"
+            ContentScale.Crop -> "Crop"
+            else -> "Unknown content scale"
         }
-        ImageAppWidget().update(context, glanceId)
-    }
 }
 
 class ImageAppWidgetReceiver : GlanceAppWidgetReceiver() {
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/build.gradle b/glance/glance-appwidget/integration-tests/macrobenchmark-target/build.gradle
new file mode 100644
index 0000000..c4be7d4
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/build.gradle
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 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.
+ */
+
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(project(":glance:glance"))
+    implementation(project(":glance:glance-appwidget"))
+}
+
+android {
+    namespace "androidx.glance.appwidget.macrobenchmark.target"
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
+                    'proguard-rules.pro'
+        }
+    }
+
+}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/proguard-rules.pro b/glance/glance-appwidget/integration-tests/macrobenchmark-target/proguard-rules.pro
new file mode 100644
index 0000000..0674e77
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/proguard-rules.pro
@@ -0,0 +1 @@
+-dontobfuscate
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2791119
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="false"
+        android:label="glance-appwidget macrobenchmark target"
+        android:supportsRtl="true">
+        <!-- Profileable to enable macrobenchmark profiling -->
+        <profileable android:shell="true"/>
+
+        <receiver
+            android:name="androidx.glance.appwidget.macrobenchmark.target.BasicAppWidgetReceiver"
+            android:label="BasicAppWidget Receiver"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="androidx.glance.appwidget.action.DEBUG_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+        <receiver
+            android:name="androidx.glance.appwidget.macrobenchmark.target.BasicAppWidgetWithSessionReceiver"
+            android:label="BasicAppWidget Receiver with sessions enabled"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+                <action android:name="androidx.glance.appwidget.action.DEBUG_UPDATE" />
+                <action android:name="android.intent.action.LOCALE_CHANGED" />
+            </intent-filter>
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/default_app_widget_info" />
+        </receiver>
+    </application>
+</manifest>
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/java/androidx/glance/appwidget/macrobenchmark/target/BasicAppWidget.kt b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/java/androidx/glance/appwidget/macrobenchmark/target/BasicAppWidget.kt
new file mode 100644
index 0000000..b12aaca
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/java/androidx/glance/appwidget/macrobenchmark/target/BasicAppWidget.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 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.glance.appwidget.macrobenchmark.target
+
+import androidx.compose.runtime.Composable
+import androidx.datastore.preferences.core.Preferences
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalSize
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.Tracing
+import androidx.glance.currentState
+import androidx.glance.layout.Column
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.session.GlanceSessionManager
+import androidx.glance.text.Text
+import kotlin.math.roundToInt
+
+open class BasicAppWidget : GlanceAppWidget() {
+    init {
+        // Ensure tracing is enabled before starting updates.
+        Tracing.enabled.set(true)
+    }
+    override val sizeMode: SizeMode = SizeMode.Single
+
+    @Composable
+    override fun Content() {
+        val size = LocalSize.current
+        // Even though this widget does not use it, accessing state will ensure that this
+        // is recomposed every time state updates, which is useful for testing.
+        currentState<Preferences>()
+        Column(
+            modifier = GlanceModifier.fillMaxSize(),
+        ) {
+            Text(
+                " Current size: ${size.width.value.roundToInt()} dp x " +
+                    "${size.height.value.roundToInt()} dp"
+            )
+        }
+    }
+}
+
+class BasicAppWidgetWithSession : BasicAppWidget() {
+    override val sessionManager = GlanceSessionManager
+}
+
+class BasicAppWidgetReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = BasicAppWidget()
+}
+
+class BasicAppWidgetWithSessionReceiver : GlanceAppWidgetReceiver() {
+    override val glanceAppWidget: GlanceAppWidget = BasicAppWidgetWithSession()
+}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/layout/glance_default_loading_layout.xml b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/layout/glance_default_loading_layout.xml
new file mode 100644
index 0000000..509cb21
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/layout/glance_default_loading_layout.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+</LinearLayout>
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/xml/default_app_widget_info.xml b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/xml/default_app_widget_info.xml
new file mode 100644
index 0000000..35afa69
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark-target/src/main/res/xml/default_app_widget_info.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 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.
+  -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="100dp"
+    android:minHeight="100dp"
+    android:initialLayout="@layout/glance_default_loading_layout"
+    android:minResizeHeight="40dp"
+    android:minResizeWidth="40dp"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen" />
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle b/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle
new file mode 100644
index 0000000..8bc801c
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+android {
+    namespace "androidx.glance.appwidget.macrobenchmark"
+}
+
+android.defaultConfig {
+    minSdkVersion 23
+}
+
+dependencies {
+    implementation 'androidx.compose.ui:ui-unit:1.2.1'
+    androidTestImplementation('androidx.benchmark:benchmark-junit4:1.1.0')
+    androidTestImplementation('androidx.benchmark:benchmark-macro-junit4:1.1.0')
+    androidTestImplementation('androidx.core:core-ktx:1.7.0')
+    androidTestImplementation(project(":glance:glance"))
+    androidTestImplementation(project(":glance:glance-appwidget"))
+    androidTestImplementation(project(":internal-testutils-macrobenchmark"))
+    androidTestImplementation(libs.kotlinTest)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testUiautomator)
+}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..05a1d72
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.BIND_APPWIDGET" />
+
+    <application
+        android:supportsRtl="true">
+        <uses-library android:name="android.test.runner" />
+        <activity
+            android:name="androidx.glance.appwidget.macrobenchmark.AppWidgetHostTestActivity"
+            android:configChanges="orientation|screenLayout|screenSize"
+            android:exported="true"/>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt
new file mode 100644
index 0000000..93939ba
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostRule.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2021 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.glance.appwidget.macrobenchmark
+
+import android.Manifest
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Trace
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+@SdkSuppress(minSdkVersion = 29)
+class AppWidgetHostRule(
+    private var mPortraitSize: DpSize = DpSize(200.dp, 300.dp),
+    private var mLandscapeSize: DpSize = DpSize(300.dp, 200.dp),
+    useSession: Boolean = false,
+) : TestRule {
+    private val mInstrumentation = InstrumentationRegistry.getInstrumentation()
+    private val mUiAutomation = mInstrumentation.uiAutomation
+    private val targetComponent =
+        ComponentName(
+            "androidx.glance.appwidget.macrobenchmark.target",
+            if (useSession) {
+                "androidx.glance.appwidget.macrobenchmark.target.BasicAppWidgetWithSessionReceiver"
+            } else {
+                "androidx.glance.appwidget.macrobenchmark.target.BasicAppWidgetReceiver"
+            }
+        )
+
+    private val mActivityRule: ActivityScenarioRule<AppWidgetHostTestActivity> =
+        ActivityScenarioRule(
+            Intent()
+                .setComponent(
+                    ComponentName(
+                        ApplicationProvider.getApplicationContext(),
+                        AppWidgetHostTestActivity::class.java,
+                    )
+                )
+                .putExtra(
+                    AppWidgetHostTestActivity.EXTRA_TARGET_RECEIVER,
+                    targetComponent
+                )
+        )
+
+    private val mUiDevice = UiDevice.getInstance(mInstrumentation)
+
+    // Ensure the screen starts in portrait and restore the orientation on leaving
+    private val mOrientationRule = TestRule { base, _ ->
+        object : Statement() {
+            override fun evaluate() {
+                mUiDevice.freezeRotation()
+                mUiDevice.setOrientationNatural()
+                base.evaluate()
+                mUiDevice.unfreezeRotation()
+            }
+        }
+    }
+
+    private val mInnerRules = RuleChain.outerRule(mActivityRule).around(mOrientationRule)
+
+    private var mHostStarted = false
+    private var mMaybeHostView: TestAppWidgetHostView? = null
+    private var mAppWidgetId = 0
+    private val mContext = ApplicationProvider.getApplicationContext<Context>()
+
+    override fun apply(base: Statement, description: Description) = object : Statement() {
+        override fun evaluate() {
+            mInnerRules.apply(base, description).evaluate()
+            if (mHostStarted) {
+                mUiAutomation.dropShellPermissionIdentity()
+            }
+        }
+    }
+
+    /**
+     * Start the host and bind the app widget.
+     * Measures time from binding an app widget to receiving the first RemoteViews.
+     */
+    fun startHost() {
+        mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.BIND_APPWIDGET)
+        mHostStarted = true
+
+        Trace.beginSection("appWidgetInitialUpdate")
+        mActivityRule.scenario.onActivity { activity ->
+            mMaybeHostView = activity.bindAppWidget(mPortraitSize, mLandscapeSize)
+        }
+
+        val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
+
+        mAppWidgetId = hostView.appWidgetId
+        hostView.waitForRemoteViews()
+        Trace.endSection()
+    }
+
+    /**
+     * Measures time from sending APPWIDGET_UPDATE broadcast to receiving RemoteViews.
+     */
+    fun updateAppWidget() {
+        val intent = Intent(GlanceAppWidgetReceiver.ACTION_DEBUG_UPDATE)
+            .setPackage("androidx.glance.appwidget.macrobenchmark.target")
+            .setComponent(
+                ComponentName(
+                    "androidx.glance.appwidget.macrobenchmark.target",
+                    "androidx.glance.appwidget.macrobenchmark.target.BasicAppWidgetReceiver"
+                )
+            )
+            .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(mAppWidgetId))
+        val hostView = checkNotNull(mMaybeHostView) { "Host view not started" }
+        Trace.beginSection("appWidgetUpdate")
+        hostView.resetRemoteViewsLatch()
+        mContext.sendBroadcast(intent)
+        hostView.waitForRemoteViews()
+        Trace.endSection()
+    }
+}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt
new file mode 100644
index 0000000..41098fc
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetHostTestActivity.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2021 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.glance.appwidget.macrobenchmark
+
+import android.app.Activity
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.os.LocaleList
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.SizeF
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.RemoteViews
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
+import androidx.compose.ui.unit.min
+import java.util.Locale
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Assert.fail
+
+@RequiresApi(26)
+class AppWidgetHostTestActivity : Activity() {
+    private var mHost: AppWidgetHost? = null
+    private val mHostViews = mutableListOf<TestAppWidgetHostView>()
+
+    companion object {
+        const val EXTRA_TARGET_RECEIVER = "targetReceiver"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        setContentView(android.R.layout.list_content)
+
+        mHost = TestAppWidgetHost(this, 1025).also {
+            it.appWidgetIds.forEach(it::deleteAppWidgetId)
+            it.startListening()
+        }
+    }
+
+    override fun onDestroy() {
+        try {
+            mHost?.stopListening()
+            mHost?.deleteHost()
+        } catch (ex: Throwable) {
+            Log.w("AppWidgetHostTestActivity", "Error stopping the AppWidget Host", ex)
+        }
+        mHost = null
+        super.onDestroy()
+    }
+
+    private fun rootView() =
+        this.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as ViewGroup
+
+    fun bindAppWidget(portraitSize: DpSize, landscapeSize: DpSize): TestAppWidgetHostView? {
+        val host = mHost ?: error("App widgets can only be bound while the activity is created")
+
+        val appWidgetManager = AppWidgetManager.getInstance(this)
+        val appWidgetId = host.allocateAppWidgetId()
+
+        @Suppress("DEPRECATION")
+        val componentName = intent.getParcelableExtra<ComponentName>(EXTRA_TARGET_RECEIVER)!!
+
+        val wasBound = appWidgetManager.bindAppWidgetIdIfAllowed(
+            appWidgetId,
+            componentName,
+            optionsBundleOf(listOf(portraitSize, landscapeSize))
+        )
+        if (!wasBound) {
+            fail("Failed to bind the app widget")
+            mHost?.deleteHost()
+            mHost = null
+            return null
+        }
+
+        val info = appWidgetManager.getAppWidgetInfo(appWidgetId)
+        val locale = Locale.getDefault()
+        val config = resources.configuration
+        config.setLocales(LocaleList(locale))
+        config.setLayoutDirection(locale)
+        val context = this.createConfigurationContext(config)
+
+        val hostView = host.createView(context, appWidgetId, info) as TestAppWidgetHostView
+        hostView.setPadding(0, 0, 0, 0)
+        rootView().addView(hostView)
+        hostView.setSizes(portraitSize, landscapeSize)
+        hostView.setBackgroundColor(Color.WHITE)
+        mHostViews += hostView
+        return hostView
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        updateAllSizes(newConfig.orientation)
+        reapplyRemoteViews()
+    }
+
+    fun updateAllSizes(orientation: Int) {
+        mHostViews.forEach { it.updateSize(orientation) }
+    }
+
+    fun reapplyRemoteViews() {
+        mHostViews.forEach { it.reapplyRemoteViews() }
+    }
+}
+
+@RequiresApi(26)
+class TestAppWidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) {
+    override fun onCreateView(
+        context: Context,
+        appWidgetId: Int,
+        appWidget: AppWidgetProviderInfo?
+    ): AppWidgetHostView = TestAppWidgetHostView(context)
+}
+
+@RequiresApi(26)
+class TestAppWidgetHostView(context: Context) : AppWidgetHostView(context) {
+
+    init {
+        // Prevent asynchronous inflation of the App Widget
+        setExecutor(null)
+        layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+    }
+
+    private var mLatch: CountDownLatch? = null
+    private var mRemoteViews: RemoteViews? = null
+    private var mPortraitSize: DpSize = DpSize(0.dp, 0.dp)
+    private var mLandscapeSize: DpSize = DpSize(0.dp, 0.dp)
+
+    /**
+     * Wait for the new remote views to be received. If a remote views was already received, return
+     * immediately.
+     */
+    fun waitForRemoteViews() {
+        synchronized(this) {
+            mRemoteViews?.let { return }
+            mLatch = CountDownLatch(1)
+        }
+        val result = mLatch?.await(5, TimeUnit.SECONDS)!!
+        require(result) { "Timeout before getting RemoteViews" }
+    }
+
+    override fun updateAppWidget(remoteViews: RemoteViews?) {
+        super.updateAppWidget(remoteViews)
+        synchronized(this) {
+            mRemoteViews = remoteViews
+            if (remoteViews != null) {
+                mLatch?.countDown()
+            }
+        }
+    }
+
+    /** Reset the latch used to detect the arrival of a new RemoteViews. */
+    fun resetRemoteViewsLatch() {
+        synchronized(this) {
+            mRemoteViews = null
+            mLatch = null
+        }
+    }
+
+    fun setSizes(portraitSize: DpSize, landscapeSize: DpSize) {
+        mPortraitSize = portraitSize
+        mLandscapeSize = landscapeSize
+        updateSize(resources.configuration.orientation)
+    }
+
+    fun updateSize(orientation: Int) {
+        val size = when (orientation) {
+            Configuration.ORIENTATION_LANDSCAPE -> mLandscapeSize
+            Configuration.ORIENTATION_PORTRAIT -> mPortraitSize
+            else -> error("Unknown orientation ${context.resources.configuration.orientation}")
+        }
+        val displayMetrics = resources.displayMetrics
+        val width = size.width.toPixels(displayMetrics)
+        val height = size.height.toPixels(displayMetrics)
+        layoutParams = LayoutParams(width, height, Gravity.CENTER)
+        requestLayout()
+    }
+
+    private fun Dp.toPixels(displayMetrics: DisplayMetrics) =
+        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, displayMetrics).toInt()
+
+    fun reapplyRemoteViews() {
+        mRemoteViews?.let { super.updateAppWidget(it) }
+    }
+}
+
+fun optionsBundleOf(sizes: List<DpSize>): Bundle {
+    require(sizes.isNotEmpty()) { "There must be at least one size" }
+    val (minSize, maxSize) = sizes.fold(sizes[0] to sizes[0]) { acc, s ->
+        DpSize(min(acc.first.width, s.width), min(acc.first.height, s.height)) to
+            DpSize(max(acc.second.width, s.width), max(acc.second.height, s.height))
+    }
+    return Bundle().apply {
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minSize.width.value.toInt())
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minSize.height.value.toInt())
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxSize.width.value.toInt())
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxSize.height.value.toInt())
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            val sizeList = ArrayList<SizeF>(sizes.map { SizeF(it.width.value, it.height.value) })
+            putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizeList)
+        }
+    }
+}
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt
new file mode 100644
index 0000000..578f12f
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/java/androidx/glance/appwidget/macrobenchmark/AppWidgetUpdateBenchmark.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 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.glance.appwidget.macrobenchmark
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.ExperimentalMetricApi
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.TraceSectionMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.testutils.createStartupCompilationParams
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RequiresApi(Build.VERSION_CODES.Q)
+@RunWith(Parameterized::class)
+class AppWidgetUpdateBenchmark(
+    private val startupMode: StartupMode,
+    private val compilationMode: CompilationMode,
+    useGlanceSession: Boolean,
+) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @get:Rule
+    val appWidgetHostRule = AppWidgetHostRule(useSession = useGlanceSession)
+
+    @OptIn(ExperimentalMetricApi::class)
+    @Test
+    fun initialUpdate() = benchmarkRule.measureRepeated(
+        packageName = "androidx.glance.appwidget.macrobenchmark.target",
+        metrics = listOf(
+            TraceSectionMetric("appWidgetInitialUpdate"),
+            TraceSectionMetric("GlanceAppWidget::update"),
+        ),
+        iterations = 100,
+        compilationMode = compilationMode,
+        startupMode = startupMode,
+    ) {
+        appWidgetHostRule.startHost()
+    }
+
+    @OptIn(ExperimentalMetricApi::class)
+    @Test
+    fun appWidgetUpdate() = benchmarkRule.measureRepeated(
+        packageName = "androidx.glance.appwidget.macrobenchmark.target",
+        metrics = listOf(
+            TraceSectionMetric("appWidgetUpdate"),
+            TraceSectionMetric("GlanceAppWidget::update"),
+        ),
+        iterations = 100,
+        compilationMode = compilationMode,
+        startupMode = startupMode,
+        setupBlock = {
+            appWidgetHostRule.startHost()
+            if (startupMode == StartupMode.COLD) killProcess()
+        }
+    ) {
+        appWidgetHostRule.updateAppWidget()
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "startup={0},compilation={1},useSession={2}")
+        @JvmStatic
+        fun parameters() =
+            createStartupCompilationParams(
+                startupModes = listOf(StartupMode.COLD, StartupMode.WARM),
+                compilationModes = listOf(CompilationMode.DEFAULT)
+            ).flatMap {
+                listOf(
+                    it + true,
+                    it + false,
+                )
+            }
+    }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/res/layout/app_widget_host_activity.xml b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/res/layout/app_widget_host_activity.xml
new file mode 100644
index 0000000..aed767b
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/androidTest/res/layout/app_widget_host_activity.xml
@@ -0,0 +1,20 @@
+<?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"
+    android:id="@+id/content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#AAAAAA"/>
diff --git a/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b41847
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/macrobenchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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 />
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
index f2aa39f..64bd87c 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
@@ -42,6 +42,8 @@
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertIs
 import kotlin.test.fail
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import org.junit.rules.RuleChain
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -133,11 +135,14 @@
     /**
      * Run the [block] (usually some sort of app widget update) and wait for new RemoteViews to be
      * applied.
+     *
+     * This should not be called from the main thread, i.e. in [onHostView] or [onHostActivity].
      */
     suspend fun runAndWaitForUpdate(block: suspend () -> Unit) {
         val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
         hostView.resetRemoteViewsLatch()
-        block()
+        withContext(Dispatchers.Main) { block() }
+        // Do not wait on the main thread so that the UI handlers can run.
         runAndWaitForChildren {
             hostView.waitForRemoteViews()
         }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index 2ffe9a5..3a4ec0e 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -31,8 +31,8 @@
 import androidx.glance.LocalContext
 import androidx.glance.action.actionStartActivity
 import androidx.glance.appwidget.test.R
-import androidx.glance.appwidget.unit.ColorProvider
 import androidx.glance.background
+import androidx.glance.color.ColorProvider
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
 import androidx.glance.layout.Column
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index a3ca4e5..8e5d0a2 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -34,6 +34,8 @@
 import android.widget.RadioButton
 import android.widget.TextView
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
@@ -61,6 +63,7 @@
 import androidx.glance.appwidget.state.updateAppWidgetState
 import androidx.glance.appwidget.test.R
 import androidx.glance.background
+import androidx.glance.color.ColorProvider
 import androidx.glance.currentState
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
@@ -93,7 +96,9 @@
 import java.util.concurrent.atomic.AtomicReference
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
@@ -101,10 +106,11 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SdkSuppress(minSdkVersion = 29)
 @MediumTest
 @RunWith(Parameterized::class)
-class GlanceAppWidgetReceiverTest(useSessionManager: Boolean) {
+class GlanceAppWidgetReceiverTest(val useSessionManager: Boolean) {
     @get:Rule
     val mHostRule = AppWidgetHostRule(useSessionManager = useSessionManager)
 
@@ -553,7 +559,7 @@
     }
 
     @Test
-    fun updateAll() = runBlocking {
+    fun updateAll() = runTest {
         TestGlanceAppWidget.uiDefinition = {
             Text("text")
         }
@@ -566,7 +572,7 @@
     }
 
     @Test
-    fun updateIf() = runBlocking {
+    fun updateIf() = runTest {
         val didRun = AtomicBoolean(false)
         TestGlanceAppWidget.uiDefinition = {
             currentState<Preferences>()
@@ -797,7 +803,7 @@
                 onCheckedChange = null,
                 text = "Hello Checked Switch (day: Blue/Green, night: Red/Yellow)",
                 style = TextStyle(
-                    color = androidx.glance.appwidget.unit.ColorProvider(
+                    color = ColorProvider(
                         day = Color.Black,
                         night = Color.White
                     ),
@@ -805,11 +811,11 @@
                     fontStyle = FontStyle.Normal,
                 ),
                 colors = switchColors(
-                    checkedThumbColor = androidx.glance.appwidget.unit.ColorProvider(
+                    checkedThumbColor = ColorProvider(
                         day = Color.Blue,
                         night = Color.Red
                     ),
-                    checkedTrackColor = androidx.glance.appwidget.unit.ColorProvider(
+                    checkedTrackColor = ColorProvider(
                         day = Color.Green,
                         night = Color.Yellow
                     ),
@@ -854,7 +860,35 @@
     }
 
     @Test
-    fun unsetActionCallback() {
+    fun lambdaActionCallback() = runTest {
+        if (!useSessionManager) return@runTest
+        TestGlanceAppWidget.uiDefinition = {
+            val text = remember { mutableStateOf("initial") }
+            Button(
+                text = text.value,
+                onClick = {
+                    text.value = "clicked"
+                }
+            )
+        }
+
+        mHostRule.startHost()
+        var button: View? = null
+        mHostRule.onHostView { root ->
+            val text = checkNotNull(root.findChild<TextView> { it.text.toString() == "initial" })
+            button = text.parent as View
+        }
+        mHostRule.runAndWaitForUpdate {
+            button!!.performClick()
+        }
+
+        mHostRule.onHostView { root ->
+            checkNotNull(root.findChild<TextView> { it.text.toString() == "clicked" })
+        }
+    }
+
+    @Test
+    fun unsetActionCallback() = runTest {
         TestGlanceAppWidget.uiDefinition = {
             val enabled = currentState<Preferences>()[testBoolKey] ?: true
             Text(
@@ -879,13 +913,11 @@
             assertThat(view.hasOnClickListeners()).isTrue()
         }
 
-        runBlocking {
-            updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
-                it[testBoolKey] = false
-            }
-            mHostRule.runAndWaitForUpdate {
-                TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
-            }
+        updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
+            it[testBoolKey] = false
+        }
+        mHostRule.runAndWaitForUpdate {
+            TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
         }
 
         mHostRule.onHostView { root ->
@@ -898,7 +930,7 @@
     }
 
     @Test
-    fun unsetCompoundButtonActionCallback() {
+    fun unsetCompoundButtonActionCallback() = runTest {
         TestGlanceAppWidget.uiDefinition = {
             val enabled = currentState<Preferences>()[testBoolKey] ?: true
             CheckBox(
@@ -925,13 +957,11 @@
             "checkbox" to true
         )
 
-        runBlocking {
-            updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
-                it[testBoolKey] = false
-            }
-            mHostRule.runAndWaitForUpdate {
-                TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
-            }
+        updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
+            it[testBoolKey] = false
+        }
+        mHostRule.runAndWaitForUpdate {
+            TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
         }
 
         CompoundButtonActionTest.received.set(emptyList())
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
index 2250fbc..c3ddc3e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
@@ -33,6 +33,7 @@
 import androidx.glance.LocalContext
 import androidx.glance.LocalGlanceId
 import androidx.glance.LocalState
+import androidx.glance.action.LambdaAction
 import androidx.glance.session.Session
 import androidx.glance.session.SetContentFn
 import androidx.glance.state.ConfigManager
@@ -69,6 +70,7 @@
 
     private val glanceState = mutableStateOf(emptyPreferences(), neverEqualPolicy())
     private val options = mutableStateOf(Bundle(), neverEqualPolicy())
+    private var lambdas = mapOf<String, List<LambdaAction>>()
     @VisibleForTesting
     internal var lastRemoteViews: RemoteViews? = null
 
@@ -112,17 +114,23 @@
     ) {
         root as RemoteViewsRoot
         val layoutConfig = LayoutConfiguration.load(context, id.appWidgetId)
+        val appWidgetManager = context.appWidgetManager
         try {
+            val receiver = requireNotNull(appWidgetManager.getAppWidgetInfo(id.appWidgetId)) {
+                "No app widget info for ${id.appWidgetId}"
+            }.provider
             normalizeCompositionTree(root)
+            lambdas = root.updateLambdaActionKeys()
             val rv = translateComposition(
                 context,
                 id.appWidgetId,
                 root,
                 layoutConfig,
                 layoutConfig.addLayout(root),
-                DpSize.Unspecified
+                DpSize.Unspecified,
+                receiver
             )
-            context.appWidgetManager.updateAppWidget(id.appWidgetId, rv)
+            appWidgetManager.updateAppWidget(id.appWidgetId, rv)
             lastRemoteViews = rv
         } catch (ex: CancellationException) {
             // Nothing to do
@@ -132,10 +140,11 @@
             }
             logException(throwable)
             val rv = RemoteViews(context.packageName, widget.errorUiLayout)
-            context.appWidgetManager.updateAppWidget(id.appWidgetId, rv)
+            appWidgetManager.updateAppWidget(id.appWidgetId, rv)
             lastRemoteViews = rv
         } finally {
             layoutConfig.save()
+            Tracing.endGlanceAppWidgetUpdate()
         }
     }
 
@@ -156,6 +165,14 @@
                 }
                 options.value = event.newOptions
             }
+            is RunLambda -> {
+                Log.i(TAG, "Received RunLambda(${event.key}) action for session($key)")
+                lambdas[event.key]?.map { it.block() }
+                    ?: Log.w(
+                        TAG,
+                        "Triggering Action(${event.key}) for session($key) failed"
+                    )
+            }
             else -> {
                 throw IllegalArgumentException(
                     "Sent unrecognized event type ${event.javaClass} to AppWidgetSession"
@@ -172,12 +189,17 @@
         sendEvent(UpdateAppWidgetOptions(newOptions))
     }
 
+    suspend fun runLambda(key: String) {
+        sendEvent(RunLambda(key))
+    }
+
     // Action types that this session supports.
     @VisibleForTesting
     internal object UpdateGlanceState
-
     @VisibleForTesting
     internal class UpdateAppWidgetOptions(val newOptions: Bundle)
+    @VisibleForTesting
+    internal class RunLambda(val key: String)
 
     private val Context.appWidgetManager: AppWidgetManager
         get() = this.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt
index e9063f8a..1ae6a21 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt
@@ -18,12 +18,18 @@
 
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
+import android.os.Build
 import android.os.Bundle
+import android.os.Trace
 import android.util.DisplayMetrics
 import android.util.Log
 import android.util.SizeF
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import java.util.concurrent.atomic.AtomicBoolean
 import kotlin.math.ceil
 import kotlin.math.min
 
@@ -148,4 +154,41 @@
 
 internal fun logException(throwable: Throwable) {
     Log.e(GlanceAppWidgetTag, "Error in Glance App Widget", throwable)
+}
+
+/**
+ * [Tracing] contains methods for tracing sections of GlanceAppWidget.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object Tracing {
+    val enabled = AtomicBoolean(false)
+
+    fun beginGlanceAppWidgetUpdate() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && enabled.get()) {
+            TracingApi29Impl.beginAsyncSection("GlanceAppWidget::update", 0)
+        }
+    }
+
+    fun endGlanceAppWidgetUpdate() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && enabled.get()) {
+            TracingApi29Impl.endAsyncSection("GlanceAppWidget::update", 0)
+        }
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.Q)
+internal object TracingApi29Impl {
+    @DoNotInline
+    fun beginAsyncSection(
+        methodName: String,
+        cookie: Int,
+    ) = Trace.beginAsyncSection(methodName, cookie)
+
+    @DoNotInline
+    fun endAsyncSection(
+        methodName: String,
+        cookie: Int,
+    ) = Trace.endAsyncSection(methodName, cookie)
 }
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
index 482316b..c94a102 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
@@ -41,7 +41,7 @@
 import androidx.glance.VisibilityModifier
 import androidx.glance.action.ActionModifier
 import androidx.glance.appwidget.action.applyAction
-import androidx.glance.appwidget.unit.DayNightColorProvider
+import androidx.glance.color.DayNightColorProvider
 import androidx.glance.layout.HeightModifier
 import androidx.glance.layout.PaddingModifier
 import androidx.glance.layout.WidthModifier
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Background.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Background.kt
index e79e388..72dbe14 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Background.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/Background.kt
@@ -18,8 +18,8 @@
 
 import androidx.compose.ui.graphics.Color
 import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.unit.ColorProvider
 import androidx.glance.background
+import androidx.glance.color.ColorProvider
 
 /**
  * Apply a background color to the element this modifier is attached to. This will cause the
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 56938e9..d12e0c6 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -124,7 +124,10 @@
     /**
      * Triggers the composition of [Content] and sends the result to the [AppWidgetManager].
      */
-    suspend fun update(context: Context, glanceId: GlanceId) {
+    suspend fun update(
+        context: Context,
+        glanceId: GlanceId
+    ) {
         require(glanceId is AppWidgetId) {
             "The glanceId '$glanceId' is not a valid App Widget glance id"
         }
@@ -161,6 +164,7 @@
         appWidgetId: Int,
         options: Bundle? = null,
     ) {
+        Tracing.beginGlanceAppWidgetUpdate()
         sessionManager?.let {
             val glanceId = AppWidgetId(appWidgetId)
             if (!it.isSessionRunning(context, glanceId.toSessionKey())) {
@@ -184,6 +188,31 @@
     }
 
     /**
+     * Trigger an action to be run in the AppWidgetSession for this widget, starting the session if
+     * necessary.
+     */
+    internal suspend fun triggerAction(
+        context: Context,
+        appWidgetId: Int,
+        actionKey: String,
+        options: Bundle? = null,
+    ) {
+        sessionManager?.let { manager ->
+            val glanceId = AppWidgetId(appWidgetId)
+            val session = if (!manager.isSessionRunning(context, glanceId.toSessionKey())) {
+                AppWidgetSession(this, glanceId, options).also { session ->
+                    manager.startSession(context, session)
+                }
+            } else {
+                manager.getSession(glanceId.toSessionKey()) as AppWidgetSession
+            }
+            session.runLambda(actionKey)
+        } ?: error(
+            "GlanceAppWidget.triggerAction may only be used when a SessionManager is provided"
+        )
+    }
+
+    /**
      * Internal method called when a resize event is detected.
      */
     internal suspend fun resize(
@@ -489,6 +518,8 @@
             logException(throwable)
             val rv = RemoteViews(context.packageName, errorUiLayout)
             appWidgetManager.updateAppWidget(appWidgetId, rv)
+        } finally {
+            Tracing.endGlanceAppWidgetUpdate()
         }
     }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
index 461eace..93d8962 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt
@@ -25,6 +25,7 @@
 import android.os.Bundle
 import android.util.Log
 import androidx.annotation.CallSuper
+import androidx.glance.appwidget.action.LambdaActionBroadcasts
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.async
@@ -123,22 +124,35 @@
     }
 
     override fun onReceive(context: Context, intent: Intent) {
-        val forceUpdateAllWidgets = intent.action == Intent.ACTION_LOCALE_CHANGED ||
-            intent.action == ACTION_DEBUG_UPDATE
-
         runAndLogExceptions {
-            if (forceUpdateAllWidgets) {
-                val appWidgetManager = AppWidgetManager.getInstance(context)
-                val componentName =
-                    ComponentName(context.packageName, checkNotNull(javaClass.canonicalName))
-                onUpdate(
-                    context,
-                    appWidgetManager,
-                    appWidgetManager.getAppWidgetIds(componentName)
-                )
-                return
+            when (intent.action) {
+                Intent.ACTION_LOCALE_CHANGED, ACTION_DEBUG_UPDATE -> {
+                    val appWidgetManager = AppWidgetManager.getInstance(context)
+                    val componentName =
+                        ComponentName(context.packageName, checkNotNull(javaClass.canonicalName))
+                    val ids = if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)) {
+                        intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)!!
+                    } else {
+                        appWidgetManager.getAppWidgetIds(componentName)
+                    }
+                    onUpdate(
+                        context,
+                        appWidgetManager,
+                        ids,
+                    )
+                }
+                LambdaActionBroadcasts.ActionTriggerLambda -> {
+                    val actionKey = intent.getStringExtra(LambdaActionBroadcasts.ExtraActionKey)
+                            ?: error("Intent is missing ActionKey extra")
+                    val id = intent.getIntExtra(LambdaActionBroadcasts.ExtraAppWidgetId, -1)
+                    if (id == -1) error("Intent is missing AppWidgetId extra")
+                    goAsync {
+                        updateManager(context)
+                        glanceAppWidget.triggerAction(context, id, actionKey)
+                    }
+                }
+                else -> super.onReceive(context, intent)
             }
-            super.onReceive(context, intent)
         }
     }
 }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
index a191446..6f7a7cc 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -26,6 +26,7 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.ImageProvider
 import androidx.glance.action.ActionModifier
+import androidx.glance.action.LambdaAction
 import androidx.glance.appwidget.lazy.EmittableLazyListItem
 import androidx.glance.background
 import androidx.glance.extractModifier
@@ -117,6 +118,41 @@
     }
 }
 
+/**
+ * Walks through the Emittable tree and updates the key for all LambdaActions.
+ *
+ * This function updates the key such that the final key is equal to the original key plus a string
+ * indicating its index among its siblings. This is because sibling Composables will often have the
+ * same key due to how [androidx.compose.runtime.currentCompositeKeyHash] works. Adding the index
+ * makes sure that all of these keys are unique.
+ *
+ * Note that, because we run the same composition multiple times for different sizes in certain
+ * modes (see [ForEachSize]), action keys in one SizeBox should mirror the action keys in other
+ * SizeBoxes, so that if an action is triggered on the widget being displayed in one size, the state
+ * will be updated for the composition in all sizes. This is why there can be multiple LambdaActions
+ * for each key, even after de-duping.
+ */
+internal fun EmittableWithChildren.updateLambdaActionKeys(): Map<String, List<LambdaAction>> =
+    children.foldIndexed(
+        mutableMapOf<String, MutableList<LambdaAction>>()
+    ) { index, actions, child ->
+        val (actionMod, modifiers) = child.modifier.extractModifier<ActionModifier>()
+        if (actionMod != null && actionMod.action is LambdaAction &&
+            child !is EmittableSizeBox && child !is EmittableLazyListItem) {
+            val action = actionMod.action as LambdaAction
+            val newKey = action.key + "+$index"
+            val newAction = LambdaAction(newKey, action.block)
+            actions.getOrPut(newKey) { mutableListOf() }.add(newAction)
+            child.modifier = modifiers.then(ActionModifier(newAction))
+        }
+        if (child is EmittableWithChildren) {
+            child.updateLambdaActionKeys().forEach { (key, childActions) ->
+                actions.getOrPut(key) { mutableListOf() }.addAll(childActions)
+            }
+        }
+        actions
+    }
+
 private fun normalizeLazyListItem(view: EmittableLazyListItem) {
     if (view.children.size == 1 && view.alignment == Alignment.CenterStart) return
     val box = EmittableBox()
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index 9f0ec001..de2c0e4 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance.appwidget
 
+import android.content.ComponentName
 import android.content.Context
 import android.os.Build
 import android.util.Log
@@ -67,6 +68,8 @@
     layoutConfiguration: LayoutConfiguration?,
     rootViewIndex: Int,
     layoutSize: DpSize,
+    actionBroadcastReceiver: ComponentName? = null,
+
 ) =
     translateComposition(
         TranslationContext(
@@ -76,6 +79,7 @@
             layoutConfiguration,
             itemPosition = -1,
             layoutSize = layoutSize,
+            actionBroadcastReceiver = actionBroadcastReceiver,
         ),
         element.children,
         rootViewIndex,
@@ -156,6 +160,7 @@
     val layoutCollectionItemId: Int = -1,
     val canUseSelectableGroup: Boolean = false,
     val actionTargetId: Int? = null,
+    val actionBroadcastReceiver: ComponentName? = null
 ) {
     fun nextViewId() = lastViewId.incrementAndGet()
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
index 0905d7e..12457db 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ActionTrampoline.kt
@@ -59,12 +59,14 @@
     translationContext: TranslationContext,
     viewId: Int,
     type: ActionTrampolineType,
+    extraData: String = "",
 ): Uri = Uri.Builder().apply {
     scheme(ActionTrampolineScheme)
     path(type.name)
     appendQueryParameter("appWidgetId", translationContext.appWidgetId.toString())
     appendQueryParameter("viewId", viewId.toString())
     appendQueryParameter("viewSize", translationContext.layoutSize.toString())
+    appendQueryParameter("extraData", extraData)
     if (translationContext.isLazyCollectionDescendant) {
         appendQueryParameter(
             "lazyCollection",
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
index db61be0..0b2947c 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
@@ -28,6 +28,7 @@
 import androidx.core.os.bundleOf
 import androidx.glance.action.Action
 import androidx.glance.action.ActionParameters
+import androidx.glance.action.LambdaAction
 import androidx.glance.action.StartActivityAction
 import androidx.glance.action.StartActivityClassAction
 import androidx.glance.action.StartActivityComponentAction
@@ -141,6 +142,29 @@
                 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
             )
         }
+        is LambdaAction -> {
+            requireNotNull(translationContext.actionBroadcastReceiver) {
+                "In order to use LambdaAction, actionBroadcastReceiver must be provided"
+            }
+            return PendingIntent.getBroadcast(
+                translationContext.context,
+                0,
+                LambdaActionBroadcasts.createIntent(
+                    translationContext.actionBroadcastReceiver,
+                    action.key,
+                    translationContext.appWidgetId,
+                ).apply {
+                    data =
+                        createUniqueUri(
+                            translationContext,
+                            viewId,
+                            ActionTrampolineType.CALLBACK,
+                            action.key,
+                        )
+                },
+                PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+            )
+        }
         is CompoundButtonAction -> {
             return getPendingIntentForAction(
                 action.innerAction,
@@ -206,6 +230,20 @@
             type = ActionTrampolineType.BROADCAST,
         )
     }
+    is LambdaAction -> {
+        requireNotNull(translationContext.actionBroadcastReceiver) {
+            "In order to use LambdaAction, actionBroadcastReceiver must be provided"
+        }
+        LambdaActionBroadcasts.createIntent(
+            receiver = translationContext.actionBroadcastReceiver,
+            actionKey = action.key,
+            appWidgetId = translationContext.appWidgetId,
+        ).applyTrampolineIntent(
+            translationContext,
+            viewId = viewId,
+            type = ActionTrampolineType.BROADCAST,
+        )
+    }
     is CompoundButtonAction -> {
         getFillInIntentForAction(
             action.innerAction,
@@ -313,4 +351,4 @@
             PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
         )
     }
-}
\ No newline at end of file
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LambdaActionBroadcasts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LambdaActionBroadcasts.kt
new file mode 100644
index 0000000..ebc4cde
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LambdaActionBroadcasts.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.glance.appwidget.action
+
+import android.content.ComponentName
+import android.content.Intent
+
+/**
+ * This class contains constants that are used for broadcast intents that trigger lambda actions.
+ *
+ * When applying a lambda action, we create the intent based on the actionkey assigned to that
+ * lambda. When the action is triggered, a broadcast is sent to the GlanceAppWidgetReceiver to
+ * trigger the lambda in the corresponding session.
+ */
+internal object LambdaActionBroadcasts {
+    internal const val ActionTriggerLambda = "ACTION_TRIGGER_LAMBDA"
+    internal const val ExtraActionKey = "EXTRA_ACTION_KEY"
+    internal const val ExtraAppWidgetId = "EXTRA_APPWIDGET_ID"
+
+    internal fun createIntent(
+        receiver: ComponentName,
+        actionKey: String,
+        appWidgetId: Int,
+    ) = Intent()
+        .setComponent(receiver)
+        .setAction(ActionTriggerLambda)
+        .putExtra(ExtraActionKey, actionKey)
+        .putExtra(ExtraAppWidgetId, appWidgetId)
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
index 9b167e2..0ec0c95 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/CompoundButtonTranslator.kt
@@ -26,8 +26,8 @@
 import androidx.glance.appwidget.unit.CheckedStateSet
 import androidx.glance.appwidget.unit.CheckedUncheckedColorProvider
 import androidx.glance.appwidget.unit.ResourceCheckableColorProvider
-import androidx.glance.appwidget.unit.isNightMode
 import androidx.glance.appwidget.unit.resolveCheckedColor
+import androidx.glance.color.isNightMode
 
 internal val checkableColorProviderFallbackColor = Color.Black
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
index 7e9c958..52f643c 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
@@ -44,7 +44,7 @@
 import androidx.glance.appwidget.TranslationContext
 import androidx.glance.appwidget.applyModifiers
 import androidx.glance.appwidget.insertView
-import androidx.glance.appwidget.unit.DayNightColorProvider
+import androidx.glance.color.DayNightColorProvider
 import androidx.glance.text.EmittableText
 import androidx.glance.text.FontStyle
 import androidx.glance.text.FontWeight
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
index 0e848d0..d6599f0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/unit/ColorProvider.kt
@@ -25,28 +25,11 @@
 import androidx.core.content.ContextCompat
 import androidx.glance.appwidget.GlanceAppWidgetTag
 import androidx.glance.appwidget.R
+import androidx.glance.color.DayNightColorProvider
 import androidx.glance.unit.ColorProvider
 import androidx.glance.unit.FixedColorProvider
 import androidx.glance.unit.ResourceColorProvider
 
-/**
- * Returns a [ColorProvider] that provides [day] when night mode is off, and [night] when night
- * mode is on.
- */
-fun ColorProvider(day: Color, night: Color): ColorProvider {
-    return DayNightColorProvider(day, night)
-}
-
-internal data class DayNightColorProvider(val day: Color, val night: Color) : ColorProvider {
-    override fun getColor(context: Context) = getColor(context.isNightMode)
-
-    fun getColor(isNightMode: Boolean) = if (isNightMode) night else day
-}
-
-internal val Context.isNightMode: Boolean
-    get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
-        Configuration.UI_MODE_NIGHT_YES
-
 /** Provider of different colors depending on a checked state. */
 internal sealed interface CheckableColorProvider
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
index 616ba95..ec83e46 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
@@ -25,13 +25,18 @@
 import androidx.compose.ui.unit.dp
 import androidx.glance.Emittable
 import androidx.glance.GlanceModifier
+import androidx.glance.action.ActionModifier
+import androidx.glance.action.LambdaAction
+import androidx.glance.layout.EmittableBox
 import androidx.glance.state.GlanceStateDefinition
 import androidx.glance.state.PreferencesGlanceStateDefinition
 import androidx.glance.state.ConfigManager
 import androidx.glance.text.EmittableText
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFalse
 import kotlin.test.assertIs
+import kotlin.test.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
@@ -128,6 +133,57 @@
         assertThat(testState.getValueCalls).containsExactly(id.toSessionKey())
     }
 
+    @Test
+    fun processEvent_runLambda() = runTest {
+        var didRunFirst = false
+        var didRunSecond = false
+        session.processEmittableTree(context, RemoteViewsRoot(1).apply {
+            children += EmittableBox().apply {
+                modifier = GlanceModifier.then(ActionModifier(LambdaAction("123") {
+                    didRunFirst = true
+                }))
+            }
+            children += EmittableBox().apply {
+                modifier = GlanceModifier.then(ActionModifier(LambdaAction("123") {
+                    didRunSecond = true
+                }))
+            }
+        })
+        session.processEvent(context, AppWidgetSession.RunLambda("123+0"))
+        assertTrue(didRunFirst)
+        assertFalse(didRunSecond)
+
+        didRunFirst = false
+        session.processEvent(context, AppWidgetSession.RunLambda("123+1"))
+        assertTrue(didRunSecond)
+        assertFalse(didRunFirst)
+    }
+
+    @Test
+    fun runLambda() = runTest {
+        var didRunFirst = false
+        var didRunSecond = false
+        session.processEmittableTree(context, RemoteViewsRoot(1).apply {
+            children += EmittableBox().apply {
+                modifier = GlanceModifier.then(ActionModifier(LambdaAction("123") {
+                    didRunFirst = true
+                }))
+            }
+            children += EmittableBox().apply {
+                modifier = GlanceModifier.then(ActionModifier(LambdaAction("123") {
+                    didRunSecond = true
+                    this@runTest.launch { session.close() }
+                }))
+            }
+        })
+
+        session.runLambda("123+0")
+        session.runLambda("123+1")
+        session.receiveEvents(context) {}
+        assertTrue(didRunFirst)
+        assertTrue(didRunSecond)
+    }
+
     private class SampleGlanceAppWidget(
         val ui: @Composable () -> Unit,
     ) : GlanceAppWidget() {
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/LambdaActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/LambdaActionTest.kt
new file mode 100644
index 0000000..3c2d2ed
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/LambdaActionTest.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.glance.appwidget
+
+import androidx.glance.GlanceModifier
+import androidx.glance.action.clickable
+import androidx.glance.layout.Box
+import androidx.glance.text.Text
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class LambdaActionTest {
+    @Test
+    fun siblingActionsHaveDifferentKeys() = runTest {
+        val lambdas = runTestingComposition {
+            Box {
+                Text("hello1", modifier = GlanceModifier.clickable {})
+                Text("hello2", modifier = GlanceModifier.clickable {})
+            }
+            Text("hello3", modifier = GlanceModifier.clickable {})
+        }.updateLambdaActionKeys()
+
+        assertThat(lambdas.size).isEqualTo(3)
+    }
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
index b3073b3..734a6eb 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
@@ -30,7 +30,7 @@
 import androidx.glance.appwidget.configurationContext
 import androidx.glance.appwidget.findViewByType
 import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.unit.FixedColorProvider
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
index 8c645ee..1f249fc 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
@@ -32,7 +32,7 @@
 import androidx.glance.appwidget.findView
 import androidx.glance.appwidget.findViewByType
 import androidx.glance.appwidget.runAndTranslate
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.unit.ColorProvider
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
index 89d3739..3b70ee7 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
@@ -30,7 +30,7 @@
 import androidx.glance.appwidget.findView
 import androidx.glance.appwidget.runAndTranslate
 import androidx.glance.appwidget.switchColors
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.unit.ColorProvider
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
index 800d6c2..0d26478 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
@@ -42,7 +42,7 @@
 import androidx.glance.appwidget.runAndTranslateInRtl
 import androidx.glance.appwidget.test.R
 import androidx.glance.appwidget.toPixels
-import androidx.glance.appwidget.unit.ColorProvider
+import androidx.glance.color.ColorProvider
 import androidx.glance.layout.Column
 import androidx.glance.layout.fillMaxWidth
 import androidx.glance.text.FontStyle
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
index d52ed7e..55f242d 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/unit/ColorProviderTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import androidx.glance.appwidget.ColorSubject.Companion.assertThat
+import androidx.glance.color.ColorProvider
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
diff --git a/glance/glance-material/api/current.txt b/glance/glance-material/api/current.txt
new file mode 100644
index 0000000..25d2aeb
--- /dev/null
+++ b/glance/glance-material/api/current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material {
+
+  public final class MaterialThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors light, androidx.compose.material.Colors dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors colors);
+  }
+
+}
+
diff --git a/glance/glance-material/api/public_plus_experimental_current.txt b/glance/glance-material/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..25d2aeb
--- /dev/null
+++ b/glance/glance-material/api/public_plus_experimental_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material {
+
+  public final class MaterialThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors light, androidx.compose.material.Colors dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors colors);
+  }
+
+}
+
diff --git a/glance/glance-material/api/res-current.txt b/glance/glance-material/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glance/glance-material/api/res-current.txt
diff --git a/glance/glance-material/api/restricted_current.txt b/glance/glance-material/api/restricted_current.txt
new file mode 100644
index 0000000..25d2aeb
--- /dev/null
+++ b/glance/glance-material/api/restricted_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material {
+
+  public final class MaterialThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors light, androidx.compose.material.Colors dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material.Colors colors);
+  }
+
+}
+
diff --git a/glance/glance-material/build.gradle b/glance/glance-material/build.gradle
new file mode 100644
index 0000000..58668cf
--- /dev/null
+++ b/glance/glance-material/build.gradle
@@ -0,0 +1,36 @@
+import androidx.build.AndroidXComposePlugin
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXComposePlugin")
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+// Disable multi-platform; this will only be used on Android.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project, /* isMultiplatformEnabled= */false)
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.compose.runtime:runtime:1.1.1")
+    api(project(":glance:glance"))
+    implementation 'androidx.compose.material:material:1.3.0'
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+    namespace "androidx.glance.material"
+}
+
+androidx {
+    name = "Android Glance Material"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.GLANCE
+    inceptionYear = "2022"
+    description = "Glance Material 2 integration library." +
+            " This library provides interop APIs with Material 2."
+}
+
diff --git a/glance/glance-material/src/androidMain/kotlin/androidx/glance/material/MaterialThemes.kt b/glance/glance-material/src/androidMain/kotlin/androidx/glance/material/MaterialThemes.kt
new file mode 100644
index 0000000..8ef025e
--- /dev/null
+++ b/glance/glance-material/src/androidMain/kotlin/androidx/glance/material/MaterialThemes.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.glance.material
+
+import androidx.compose.material.Colors
+import androidx.compose.material.primarySurface
+import androidx.compose.ui.graphics.Color
+import androidx.glance.GlanceTheme
+import androidx.glance.color.ColorProvider
+import androidx.glance.color.ColorProviders
+import androidx.glance.color.colorProviders
+import androidx.glance.unit.ColorProvider
+
+/**
+ * Given  Material [Colors], creates a [ColorProviders] that can be passed to [GlanceTheme]
+ */
+fun ColorProviders(
+    light: Colors,
+    dark: Colors
+): ColorProviders {
+
+    val background = ColorProvider(light.background, dark.background)
+    val onBackground = ColorProvider(light.onBackground, dark.onBackground)
+    val primary = ColorProvider(light.primary, dark.primary)
+    val onPrimary = ColorProvider(light.onPrimary, dark.onPrimary)
+    val surface = ColorProvider(light.primarySurface, dark.primarySurface)
+    val onSurface = ColorProvider(light.onSurface, dark.onSurface)
+    val secondary = ColorProvider(light.secondary, dark.secondary)
+    val onSecondary = ColorProvider(light.onSecondary, dark.onSecondary)
+    val error = ColorProvider(light.error, dark.error)
+    val onError = ColorProvider(light.onError, dark.onError)
+
+    return colorProviders(
+        primary = primary,
+        onPrimary = onPrimary,
+        surface = surface,
+        onSurface = onSurface,
+        secondary = secondary,
+        onSecondary = onSecondary,
+        error = error,
+        onError = onError,
+        background = background,
+        onBackground = onBackground,
+        primaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onPrimaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        secondaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onSecondaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        tertiary = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onTertiary = ColorProvider(ColorNotDefined, ColorNotDefined),
+        tertiaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onTertiaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        errorContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onErrorContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        surfaceVariant = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onSurfaceVariant = ColorProvider(ColorNotDefined, ColorNotDefined),
+        outline = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inverseOnSurface = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inverseSurface = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inversePrimary = ColorProvider(ColorNotDefined, ColorNotDefined),
+    )
+}
+
+/**
+ * Given  Material [Colors], creates a [ColorProviders] that can be passed to [GlanceTheme]
+ */
+fun ColorProviders(
+    colors: Colors
+): ColorProviders {
+
+    val background = ColorProvider(colors.background)
+    val onBackground = ColorProvider(colors.onBackground)
+    val primary = ColorProvider(colors.primary)
+    val onPrimary = ColorProvider(colors.onPrimary)
+    val surface = ColorProvider(colors.primarySurface)
+    val onSurface = ColorProvider(colors.onSurface)
+    val secondary = ColorProvider(colors.secondary)
+    val onSecondary = ColorProvider(colors.onSecondary)
+    val error = ColorProvider(colors.error)
+    val onError = ColorProvider(colors.onError)
+
+    return colorProviders(
+        primary = primary,
+        onPrimary = onPrimary,
+        surface = surface,
+        onSurface = onSurface,
+        secondary = secondary,
+        onSecondary = onSecondary,
+        error = error,
+        onError = onError,
+        background = background,
+        onBackground = onBackground,
+        primaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onPrimaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        secondaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onSecondaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        tertiary = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onTertiary = ColorProvider(ColorNotDefined, ColorNotDefined),
+        tertiaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onTertiaryContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        errorContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onErrorContainer = ColorProvider(ColorNotDefined, ColorNotDefined),
+        surfaceVariant = ColorProvider(ColorNotDefined, ColorNotDefined),
+        onSurfaceVariant = ColorProvider(ColorNotDefined, ColorNotDefined),
+        outline = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inverseOnSurface = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inverseSurface = ColorProvider(ColorNotDefined, ColorNotDefined),
+        inversePrimary = ColorProvider(ColorNotDefined, ColorNotDefined),
+    )
+}
+
+private val ColorNotDefined = Color.Magenta
diff --git a/glance/glance-material3/api/current.txt b/glance/glance-material3/api/current.txt
new file mode 100644
index 0000000..8ff1aa6
--- /dev/null
+++ b/glance/glance-material3/api/current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material3 {
+
+  public final class Material3ThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme light, androidx.compose.material3.ColorScheme dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme scheme);
+  }
+
+}
+
diff --git a/glance/glance-material3/api/public_plus_experimental_current.txt b/glance/glance-material3/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8ff1aa6
--- /dev/null
+++ b/glance/glance-material3/api/public_plus_experimental_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material3 {
+
+  public final class Material3ThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme light, androidx.compose.material3.ColorScheme dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme scheme);
+  }
+
+}
+
diff --git a/glance/glance-material3/api/res-current.txt b/glance/glance-material3/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glance/glance-material3/api/res-current.txt
diff --git a/glance/glance-material3/api/restricted_current.txt b/glance/glance-material3/api/restricted_current.txt
new file mode 100644
index 0000000..8ff1aa6
--- /dev/null
+++ b/glance/glance-material3/api/restricted_current.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.glance.material3 {
+
+  public final class Material3ThemesKt {
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme light, androidx.compose.material3.ColorScheme dark);
+    method public static androidx.glance.color.ColorProviders ColorProviders(androidx.compose.material3.ColorScheme scheme);
+  }
+
+}
+
diff --git a/glance/glance-material3/build.gradle b/glance/glance-material3/build.gradle
new file mode 100644
index 0000000..393b4f2
--- /dev/null
+++ b/glance/glance-material3/build.gradle
@@ -0,0 +1,36 @@
+import androidx.build.AndroidXComposePlugin
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXComposePlugin")
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+// Disable multi-platform; this will only be used on Android.
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project, /* isMultiplatformEnabled= */false)
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.compose.runtime:runtime:1.1.1")
+    api(project(":glance:glance"))
+    implementation "androidx.compose.material3:material3:1.0.0"
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+    namespace "androidx.glance.material3"
+}
+
+androidx {
+    name = "Android Glance Material"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.GLANCE
+    inceptionYear = "2022"
+    description = "Glance Material integration library." +
+            " This library provides interop APIs with Material 3."
+}
+
diff --git a/glance/glance-material3/src/androidMain/kotlin/androidx/glance/material3/Material3Themes.kt b/glance/glance-material3/src/androidMain/kotlin/androidx/glance/material3/Material3Themes.kt
new file mode 100644
index 0000000..ad4718f
--- /dev/null
+++ b/glance/glance-material3/src/androidMain/kotlin/androidx/glance/material3/Material3Themes.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.glance.material3
+
+import androidx.compose.material3.ColorScheme
+import androidx.glance.color.ColorProvider
+import androidx.glance.color.ColorProviders
+import androidx.glance.color.colorProviders
+import androidx.glance.unit.ColorProvider
+
+/**
+ * Creates a Material 3 [ColorProviders] given a light and dark [ColorScheme]. Each color in the
+ * theme will have a day and night mode.
+ */
+fun ColorProviders(light: ColorScheme, dark: ColorScheme): ColorProviders {
+    return colorProviders(
+        primary = ColorProvider(day = light.primary, night = dark.primary),
+        onPrimary = ColorProvider(day = light.onPrimary, night = dark.onPrimary),
+        primaryContainer = ColorProvider(
+            day = light.primaryContainer,
+            night = dark.primaryContainer
+        ),
+        onPrimaryContainer = ColorProvider(
+            day = light.onPrimaryContainer,
+            night = dark.onPrimaryContainer
+        ),
+        secondary = ColorProvider(day = light.secondary, night = dark.secondary),
+        onSecondary = ColorProvider(day = light.onSecondary, night = dark.onSecondary),
+        secondaryContainer = ColorProvider(
+            day = light.secondaryContainer,
+            night = dark.secondaryContainer
+        ),
+        onSecondaryContainer = ColorProvider(
+            day = light.onSecondaryContainer,
+            night = dark.onSecondaryContainer
+        ),
+        tertiary = ColorProvider(day = light.tertiary, night = dark.tertiary),
+        onTertiary = ColorProvider(day = light.onTertiary, night = dark.onTertiary),
+        tertiaryContainer = ColorProvider(
+            day = light.tertiaryContainer,
+            night = dark.tertiaryContainer
+        ),
+        onTertiaryContainer = ColorProvider(
+            day = light.onTertiaryContainer,
+            night = dark.onTertiaryContainer
+        ),
+        error = ColorProvider(day = light.error, night = dark.error),
+        errorContainer = ColorProvider(day = light.errorContainer, night = dark.errorContainer),
+        onError = ColorProvider(day = light.onError, night = dark.onError),
+        onErrorContainer = ColorProvider(
+            day = light.onErrorContainer,
+            night = dark.onErrorContainer
+        ),
+        background = ColorProvider(day = light.background, night = dark.background),
+        onBackground = ColorProvider(day = light.onBackground, night = dark.onBackground),
+        surface = ColorProvider(day = light.surface, night = dark.surface),
+        onSurface = ColorProvider(day = light.onSurface, night = dark.onSurface),
+        surfaceVariant = ColorProvider(day = light.surfaceVariant, night = dark.surfaceVariant),
+        onSurfaceVariant = ColorProvider(
+            day = light.onSurfaceVariant,
+            night = dark.onSurfaceVariant
+        ),
+        outline = ColorProvider(day = light.outline, night = dark.outline),
+        inverseOnSurface = ColorProvider(
+            day = light.inverseOnSurface,
+            night = dark.inverseOnSurface
+        ),
+        inverseSurface = ColorProvider(day = light.inverseSurface, night = dark.inverseSurface),
+        inversePrimary = ColorProvider(day = light.inversePrimary, night = dark.inversePrimary),
+    )
+}
+
+/**
+ * Creates a Material 3 [ColorProviders] given a [ColorScheme]. This is a fixed scheme and does not
+ * have day/night modes.
+ */
+fun ColorProviders(scheme: ColorScheme): ColorProviders {
+    return colorProviders(
+        primary = ColorProvider(color = scheme.primary),
+        onPrimary = ColorProvider(scheme.onPrimary),
+        primaryContainer = ColorProvider(color = scheme.primaryContainer),
+        onPrimaryContainer = ColorProvider(color = scheme.onPrimaryContainer),
+        secondary = ColorProvider(color = scheme.secondary),
+        onSecondary = ColorProvider(color = scheme.onSecondary),
+        secondaryContainer = ColorProvider(color = scheme.secondaryContainer),
+        onSecondaryContainer = ColorProvider(color = scheme.onSecondaryContainer),
+        tertiary = ColorProvider(color = scheme.tertiary),
+        onTertiary = ColorProvider(color = scheme.onTertiary),
+        tertiaryContainer = ColorProvider(color = scheme.tertiaryContainer),
+        onTertiaryContainer = ColorProvider(color = scheme.onTertiaryContainer),
+        error = ColorProvider(color = scheme.error),
+        onError = ColorProvider(color = scheme.onError),
+        errorContainer = ColorProvider(color = scheme.errorContainer),
+        onErrorContainer = ColorProvider(color = scheme.onErrorContainer),
+        background = ColorProvider(color = scheme.background),
+        onBackground = ColorProvider(color = scheme.onBackground),
+        surface = ColorProvider(color = scheme.surface),
+        onSurface = ColorProvider(color = scheme.onSurface),
+        surfaceVariant = ColorProvider(color = scheme.surfaceVariant),
+        onSurfaceVariant = ColorProvider(color = scheme.onSurfaceVariant),
+        outline = ColorProvider(color = scheme.outline),
+        inverseOnSurface = ColorProvider(color = scheme.inverseOnSurface),
+        inverseSurface = ColorProvider(color = scheme.inverseSurface),
+        inversePrimary = ColorProvider(color = scheme.inversePrimary),
+    )
+}
\ No newline at end of file
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index cec3b08..7e0ee8d 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -34,6 +34,7 @@
 import androidx.glance.VisibilityModifier
 import androidx.glance.action.Action
 import androidx.glance.action.ActionModifier
+import androidx.glance.action.LambdaAction
 import androidx.glance.wear.tiles.action.RunCallbackAction
 import androidx.glance.action.StartActivityAction
 import androidx.glance.action.StartActivityClassAction
@@ -213,6 +214,13 @@
             builder.setOnClick(ActionBuilders.LoadAction.Builder().build())
                 .setId(callbackClass.canonicalName!!)
         }
+        is LambdaAction -> {
+            Log.e(
+                GlanceWearTileTag,
+                "Lambda actions are not currently supported on Wear Tiles. Use " +
+                    "actionRunCallback actions instead."
+            )
+        }
         else -> {
             Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
         }
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index bc9a2a2..ad6cf59 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -18,6 +18,7 @@
 
   public final class ButtonKt {
     method @androidx.compose.runtime.Composable public static void Button(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
@@ -117,6 +118,7 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
   }
 
   public abstract class ActionParameters {
@@ -145,6 +147,10 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
+  public final class LambdaActionKt {
+    method @androidx.compose.runtime.Composable public static androidx.glance.action.Action action(optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+  }
+
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -225,6 +231,10 @@
     method public static androidx.glance.color.ColorProviders colorProviders(androidx.glance.unit.ColorProvider primary, androidx.glance.unit.ColorProvider onPrimary, androidx.glance.unit.ColorProvider primaryContainer, androidx.glance.unit.ColorProvider onPrimaryContainer, androidx.glance.unit.ColorProvider secondary, androidx.glance.unit.ColorProvider onSecondary, androidx.glance.unit.ColorProvider secondaryContainer, androidx.glance.unit.ColorProvider onSecondaryContainer, androidx.glance.unit.ColorProvider tertiary, androidx.glance.unit.ColorProvider onTertiary, androidx.glance.unit.ColorProvider tertiaryContainer, androidx.glance.unit.ColorProvider onTertiaryContainer, androidx.glance.unit.ColorProvider error, androidx.glance.unit.ColorProvider errorContainer, androidx.glance.unit.ColorProvider onError, androidx.glance.unit.ColorProvider onErrorContainer, androidx.glance.unit.ColorProvider background, androidx.glance.unit.ColorProvider onBackground, androidx.glance.unit.ColorProvider surface, androidx.glance.unit.ColorProvider onSurface, androidx.glance.unit.ColorProvider surfaceVariant, androidx.glance.unit.ColorProvider onSurfaceVariant, androidx.glance.unit.ColorProvider outline, androidx.glance.unit.ColorProvider inverseOnSurface, androidx.glance.unit.ColorProvider inverseSurface, androidx.glance.unit.ColorProvider inversePrimary);
   }
 
+  public final class DayNightColorProvidersKt {
+    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index bc9a2a2..ad6cf59 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -18,6 +18,7 @@
 
   public final class ButtonKt {
     method @androidx.compose.runtime.Composable public static void Button(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
@@ -117,6 +118,7 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
   }
 
   public abstract class ActionParameters {
@@ -145,6 +147,10 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
+  public final class LambdaActionKt {
+    method @androidx.compose.runtime.Composable public static androidx.glance.action.Action action(optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+  }
+
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -225,6 +231,10 @@
     method public static androidx.glance.color.ColorProviders colorProviders(androidx.glance.unit.ColorProvider primary, androidx.glance.unit.ColorProvider onPrimary, androidx.glance.unit.ColorProvider primaryContainer, androidx.glance.unit.ColorProvider onPrimaryContainer, androidx.glance.unit.ColorProvider secondary, androidx.glance.unit.ColorProvider onSecondary, androidx.glance.unit.ColorProvider secondaryContainer, androidx.glance.unit.ColorProvider onSecondaryContainer, androidx.glance.unit.ColorProvider tertiary, androidx.glance.unit.ColorProvider onTertiary, androidx.glance.unit.ColorProvider tertiaryContainer, androidx.glance.unit.ColorProvider onTertiaryContainer, androidx.glance.unit.ColorProvider error, androidx.glance.unit.ColorProvider errorContainer, androidx.glance.unit.ColorProvider onError, androidx.glance.unit.ColorProvider onErrorContainer, androidx.glance.unit.ColorProvider background, androidx.glance.unit.ColorProvider onBackground, androidx.glance.unit.ColorProvider surface, androidx.glance.unit.ColorProvider onSurface, androidx.glance.unit.ColorProvider surfaceVariant, androidx.glance.unit.ColorProvider onSurfaceVariant, androidx.glance.unit.ColorProvider outline, androidx.glance.unit.ColorProvider inverseOnSurface, androidx.glance.unit.ColorProvider inverseSurface, androidx.glance.unit.ColorProvider inversePrimary);
   }
 
+  public final class DayNightColorProvidersKt {
+    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index bc9a2a2..ad6cf59 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -18,6 +18,7 @@
 
   public final class ButtonKt {
     method @androidx.compose.runtime.Composable public static void Button(String text, androidx.glance.action.Action onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
+    method @androidx.compose.runtime.Composable public static void Button(String text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.glance.GlanceModifier modifier, optional boolean enabled, optional androidx.glance.text.TextStyle? style, optional androidx.glance.ButtonColors colors, optional int maxLines);
   }
 
   public final class CombinedGlanceModifier implements androidx.glance.GlanceModifier {
@@ -117,6 +118,7 @@
 
   public final class ActionKt {
     method public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, androidx.glance.action.Action onClick);
+    method @androidx.compose.runtime.Composable public static androidx.glance.GlanceModifier clickable(androidx.glance.GlanceModifier, kotlin.jvm.functions.Function0<kotlin.Unit> block);
   }
 
   public abstract class ActionParameters {
@@ -145,6 +147,10 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
+  public final class LambdaActionKt {
+    method @androidx.compose.runtime.Composable public static androidx.glance.action.Action action(optional String? key, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+  }
+
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -225,6 +231,10 @@
     method public static androidx.glance.color.ColorProviders colorProviders(androidx.glance.unit.ColorProvider primary, androidx.glance.unit.ColorProvider onPrimary, androidx.glance.unit.ColorProvider primaryContainer, androidx.glance.unit.ColorProvider onPrimaryContainer, androidx.glance.unit.ColorProvider secondary, androidx.glance.unit.ColorProvider onSecondary, androidx.glance.unit.ColorProvider secondaryContainer, androidx.glance.unit.ColorProvider onSecondaryContainer, androidx.glance.unit.ColorProvider tertiary, androidx.glance.unit.ColorProvider onTertiary, androidx.glance.unit.ColorProvider tertiaryContainer, androidx.glance.unit.ColorProvider onTertiaryContainer, androidx.glance.unit.ColorProvider error, androidx.glance.unit.ColorProvider errorContainer, androidx.glance.unit.ColorProvider onError, androidx.glance.unit.ColorProvider onErrorContainer, androidx.glance.unit.ColorProvider background, androidx.glance.unit.ColorProvider onBackground, androidx.glance.unit.ColorProvider surface, androidx.glance.unit.ColorProvider onSurface, androidx.glance.unit.ColorProvider surfaceVariant, androidx.glance.unit.ColorProvider onSurfaceVariant, androidx.glance.unit.ColorProvider outline, androidx.glance.unit.ColorProvider inverseOnSurface, androidx.glance.unit.ColorProvider inverseSurface, androidx.glance.unit.ColorProvider inversePrimary);
   }
 
+  public final class DayNightColorProvidersKt {
+    method public static androidx.glance.unit.ColorProvider ColorProvider(long day, long night);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
index a41cba2..19116de 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Button.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 import androidx.glance.action.Action
+import androidx.glance.action.action
 import androidx.glance.action.clickable
 import androidx.glance.text.EmittableText
 import androidx.glance.text.TextStyle
@@ -45,6 +46,40 @@
     style: TextStyle? = null,
     colors: ButtonColors = defaultButtonColors(),
     maxLines: Int = Int.MAX_VALUE,
+) = ButtonElement(text, onClick, modifier, enabled, style, colors, maxLines)
+
+/**
+ * Adds a button view to the glance view.
+ *
+ * @param text The text that this button will show.
+ * @param onClick The action to be performed when this button is clicked.
+ * @param modifier The modifier to be applied to this button.
+ * @param enabled If false, the button will not be clickable.
+ * @param style The style to be applied to the text in this button.
+ * @param colors The colors to use for the background and content of the button.
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
+ * necessary. If the text exceeds the given number of lines, it will be truncated.
+ */
+@Composable
+fun Button(
+    text: String,
+    onClick: () -> Unit,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    style: TextStyle? = null,
+    colors: ButtonColors = defaultButtonColors(),
+    maxLines: Int = Int.MAX_VALUE,
+) = ButtonElement(text, action(block = onClick), modifier, enabled, style, colors, maxLines)
+
+@Composable
+internal fun ButtonElement(
+    text: String,
+    onClick: Action,
+    modifier: GlanceModifier = GlanceModifier,
+    enabled: Boolean = true,
+    style: TextStyle? = null,
+    colors: ButtonColors = defaultButtonColors(),
+    maxLines: Int = Int.MAX_VALUE,
 ) {
     var finalModifier = if (enabled) modifier.clickable(onClick) else modifier
     finalModifier = finalModifier.background(colors.backgroundColor)
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
index 28a806e..d301e87 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
@@ -18,6 +18,7 @@
 
 import android.app.Activity
 import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
 import androidx.glance.GlanceModifier
 
 /**
@@ -33,6 +34,13 @@
 fun GlanceModifier.clickable(onClick: Action): GlanceModifier =
     this.then(ActionModifier(onClick))
 
+/**
+ * Run [block] in response to a user click.
+ */
+@Composable
+fun GlanceModifier.clickable(block: () -> Unit): GlanceModifier =
+    this.then(ActionModifier(action(block = block)))
+
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ActionModifier(val action: Action) : GlanceModifier.Element {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LambdaAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LambdaAction.kt
new file mode 100644
index 0000000..8076cd7
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/LambdaAction.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.glance.action
+
+import androidx.annotation.RestrictTo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.currentCompositeKeyHash
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class LambdaAction(
+    val key: String,
+    val block: () -> Unit,
+) : Action {
+    override fun toString() = "LambdaAction($key, ${block.hashCode()})"
+}
+
+/**
+ * Create an [Action] that runs [block] when triggered.
+ *
+ * @param key An optional key to be used as a key for the action. If not provided we use the key
+ * that is automatically generated by the Compose runtime which is unique for every exact code
+ * location in the composition tree.
+ * @param block the function to be run when this action is triggered.
+ */
+@Composable
+fun action(
+    key: String? = null,
+    block: () -> Unit,
+): Action {
+    val finalKey = if (!key.isNullOrEmpty()) key else currentCompositeKeyHash.toString()
+    return LambdaAction(finalKey, block)
+}
\ No newline at end of file
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/color/DayNightColorProviders.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/color/DayNightColorProviders.kt
new file mode 100644
index 0000000..aa1e76c
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/color/DayNightColorProviders.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.glance.color
+
+import android.content.Context
+import android.content.res.Configuration
+import androidx.annotation.RestrictTo
+import androidx.compose.ui.graphics.Color
+import androidx.glance.unit.ColorProvider
+
+/**
+ * Returns a [ColorProvider] that provides [day] when night mode is off, and [night] when night
+ * mode is on.
+ */
+fun ColorProvider(day: Color, night: Color): ColorProvider {
+    return DayNightColorProvider(day, night)
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+data class DayNightColorProvider(val day: Color, val night: Color) : ColorProvider {
+    override fun getColor(context: Context) = getColor(isNightMode = context.isNightMode)
+
+    fun getColor(isNightMode: Boolean) = if (isNightMode) night else day
+}
+
+val Context.isNightMode: Boolean
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
+        Configuration.UI_MODE_NIGHT_YES
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 0bc238e..5ccbdf2 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -16,11 +16,13 @@
 
 package androidx.graphics.lowlatency
 
+import android.app.UiAutomation
 import android.graphics.Color
 import android.hardware.HardwareBuffer
 import android.opengl.GLES20
 import android.opengl.Matrix
 import android.os.Build
+import android.view.SurfaceHolder
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.graphics.opengl.egl.EGLManager
@@ -31,6 +33,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
@@ -638,6 +641,126 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun test180DegreeRotationBufferTransform() {
+        val initialFrontBufferLatch = CountDownLatch(1)
+        val secondFrontBufferLatch = CountDownLatch(1)
+        var bufferTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        var surfaceView: SurfaceView? = null
+        val surfaceHolderCallbacks = object : SurfaceHolder.Callback {
+            override fun surfaceCreated(p0: SurfaceHolder) {
+                // NO-OP
+            }
+
+            override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
+                bufferTransform =
+                    BufferTransformHintResolver().getBufferTransformHint(surfaceView!!)
+            }
+
+            override fun surfaceDestroyed(p0: SurfaceHolder) {
+                // NO-OP
+            }
+        }
+        var configuredBufferTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+            val mOrthoMatrix = FloatArray(16)
+            val mProjectionMatrix = FloatArray(16)
+            var mRectangle: Rectangle? = null
+
+            private fun getSquare(): Rectangle = mRectangle ?: Rectangle().also { mRectangle = it }
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                param: Any
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                params: Collection<Any>
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onFrontBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                configuredBufferTransform =
+                    transaction.mBufferTransforms[frontBufferedLayerSurfaceControl]
+                        ?: BufferTransformHintResolver.UNKNOWN_TRANSFORM
+                if (initialFrontBufferLatch.count == 0L) {
+                    secondFrontBufferLatch.countDown()
+                }
+                initialFrontBufferLatch.countDown()
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Any>? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    it.getSurfaceView().holder.addCallback(surfaceHolderCallbacks)
+                    renderer = GLFrontBufferedRenderer(it.getSurfaceView(), callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.renderFrontBufferedLayer(Any())
+            }
+
+            assertTrue(initialFrontBufferLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val automation = InstrumentationRegistry.getInstrumentation().uiAutomation
+            assertTrue(automation.setRotation(UiAutomation.ROTATION_FREEZE_180))
+            automation.waitForIdle(1000, 3000)
+
+            renderer?.renderFrontBufferedLayer(Any())
+
+            assertTrue(secondFrontBufferLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            assertEquals(
+                BufferTransformer().invertBufferTransform(bufferTransform),
+                configuredBufferTransform
+            )
+        } finally {
+            renderer.blockingRelease(10000)
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.Q)
     private fun GLFrontBufferedRenderer<*>?.blockingRelease(timeoutMillis: Long = 3000) {
         if (this != null) {
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 1001bdb..6c30b4c 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -29,8 +29,6 @@
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.surface.SurfaceControlCompat
-import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
-import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
 import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
 import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -264,11 +262,6 @@
      */
     private val mHardwareBufferUsageFlags: Long
 
-    /**
-     * Calculates the corresponding projection based on buffer transform hints
-     */
-    private val mBufferTransform = BufferTransformer()
-
     init {
         mParentRenderLayer.setParentLayerCallbacks(mParentLayerCallback)
         val renderer = if (glRenderer == null) {
@@ -304,18 +297,8 @@
                 }
                 .build()
 
-            val transformHint = mParentRenderLayer.getBufferTransformHint()
-            val bufferWidth: Int
-            val bufferHeight: Int
-            if (transformHint == BUFFER_TRANSFORM_ROTATE_90 ||
-                transformHint == BUFFER_TRANSFORM_ROTATE_270
-            ) {
-                bufferWidth = height
-                bufferHeight = width
-            } else {
-                bufferWidth = width
-                bufferHeight = height
-            }
+            val bufferWidth = mParentRenderLayer.getBufferWidth()
+            val bufferHeight = mParentRenderLayer.getBufferHeight()
 
             // Create buffer pool for the multi-buffered layer
             // The flags here are identical to those used for buffers in the front buffered layer
@@ -338,11 +321,8 @@
             val frontBufferedLayerRenderer =
                 createFrontBufferedLayerRenderer(
                     frontBufferedSurfaceControl,
-                    width,
-                    height,
                     bufferWidth,
                     bufferHeight,
-                    transformHint,
                     mHardwareBufferUsageFlags
                 )
             mFrontBufferedRenderTarget = mGLRenderer.createRenderTarget(
@@ -506,15 +486,10 @@
 
     private fun createFrontBufferedLayerRenderer(
         frontBufferedLayerSurfaceControl: SurfaceControlCompat,
-        width: Int,
-        height: Int,
         bufferWidth: Int,
         bufferHeight: Int,
-        transformHint: Int,
         usageFlags: Long
     ): FrameBufferRenderer {
-        val inverseTransform = mBufferTransform.invertBufferTransform(transformHint)
-        mBufferTransform.computeTransform(width, height, inverseTransform)
         return FrameBufferRenderer(
             object : FrameBufferRenderer.RenderCallback {
                 private fun createFrontBufferLayer(usageFlags: Long): HardwareBuffer {
@@ -547,14 +522,15 @@
                     mActiveSegment.next { param ->
                         mCallback.onDrawFrontBufferedLayer(
                             eglManager,
-                            mBufferTransform.glWidth,
-                            mBufferTransform.glHeight,
-                            mBufferTransform.transform,
+                            mParentRenderLayer.getBufferWidth(),
+                            mParentRenderLayer.getBufferHeight(),
+                            mParentRenderLayer.getTransform(),
                             param
                         )
                     }
                 }
 
+                @SuppressLint("WrongConstant")
                 @WorkerThread
                 override fun onDrawComplete(
                     frameBuffer: FrameBuffer,
@@ -569,7 +545,8 @@
                             syncFenceCompat
                         )
                         .setVisibility(frontBufferedLayerSurfaceControl, true)
-                    if (transformHint != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
+                    val inverseTransform = mParentRenderLayer.getInverseBufferTransform()
+                    if (inverseTransform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
                         transaction.setBufferTransform(
                             frontBufferedLayerSurfaceControl,
                             inverseTransform
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index b217dd9..a13a2a9 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -32,11 +32,29 @@
 internal interface ParentRenderLayer<T> {
 
     /**
-     * Obtains a pre-rotation hint to configure buffer content. This is helpful to
-     * avoid unnecessary GPU composition for the purposes of rotating buffer content to
-     * match display orientation
+     * Returns the inverse of the pre-rotation hint to configure buffer content. This is helpful
+     * to avoid unnecessary GPU composition for the purposes of rotating buffer content to
+     * match display orientation. Because this value is already inverted from the buffer transform
+     * hint, consumers can pass the result of this method directly into
+     * SurfaceControl.Transaction#setBufferTransform to handle pre-rotation
      */
-    fun getBufferTransformHint(): Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+    fun getInverseBufferTransform(): Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+
+    /**
+     * Return the suggested width used for buffers taking into account pre-rotation transformations
+     */
+    fun getBufferWidth(): Int
+
+    /**
+     * Return the suggested height used for buffers taking into account pre-rotation transformations
+     */
+    fun getBufferHeight(): Int
+
+    /**
+     * Return the 4 x 4 transformation matrix represented as a 1 dimensional
+     * float array of 16 values
+     */
+    fun getTransform(): FloatArray
 
     /**
      * Modify the provided [SurfaceControlCompat.Transaction] to reparent the provided
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index c31a83f..70ebccf 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -60,7 +60,7 @@
                 width: Int,
                 height: Int
             ) {
-                transformHint = getBufferTransformHint()
+                transformHint = mTransformResolver.getBufferTransformHint(surfaceView)
                 inverse = mBufferTransform.invertBufferTransform(transformHint)
                 mBufferTransform.computeTransform(width, height, inverse)
                 mParentSurfaceControl?.release()
@@ -74,9 +74,13 @@
         })
     }
 
-    override fun getBufferTransformHint(): Int {
-        return mTransformResolver.getBufferTransformHint(surfaceView)
-    }
+    override fun getInverseBufferTransform(): Int = inverse
+
+    override fun getBufferWidth(): Int = mBufferTransform.glWidth
+
+    override fun getBufferHeight(): Int = mBufferTransform.glHeight
+
+    override fun getTransform(): FloatArray = mBufferTransform.transform
 
     override fun buildReparentTransaction(
         child: SurfaceControlCompat,
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index 66a547e..905db04 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -189,6 +189,11 @@
      * An atomic set of changes to a set of [SurfaceControlCompat].
      */
     class Transaction : AutoCloseable {
+        /**
+         * internal mapping of buffer transforms used for testing purposes
+         */
+        internal val mBufferTransforms = HashMap<SurfaceControlCompat, Int>()
+
         private val mImpl = createImpl()
 
         /**
@@ -447,6 +452,7 @@
             surfaceControl: SurfaceControlCompat,
             @BufferTransform transformation: Int
         ): Transaction {
+            mBufferTransforms[surfaceControl] = transformation
             mImpl.setBufferTransform(surfaceControl.scImpl, transformation)
             return this
         }
@@ -457,6 +463,7 @@
          * called to release the transaction.
          */
         fun commit() {
+            mBufferTransforms.clear()
             mImpl.commit()
         }
 
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 081ecbd..bff896d 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -1120,7 +1120,7 @@
   }
 
   public final class StepsRecord implements androidx.health.connect.client.records.Record {
-    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, @IntRange(from=1L, to=1000000L) long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
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 081ecbd..bff896d 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -1120,7 +1120,7 @@
   }
 
   public final class StepsRecord implements androidx.health.connect.client.records.Record {
-    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, @IntRange(from=1L, to=1000000L) long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index a9c1a0f..0bf9f664 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -1143,7 +1143,7 @@
   }
 
   public final class StepsRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public StepsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, @IntRange(from=1L, to=1000000L) long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/StepsRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/StepsRecord.kt
index 6a0e2eb..4963871 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/StepsRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/StepsRecord.kt
@@ -15,6 +15,7 @@
  */
 package androidx.health.connect.client.records
 
+import androidx.annotation.IntRange
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
 import java.time.Instant
@@ -35,11 +36,12 @@
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
     /** Count. Required field. Valid range: 1-1000000. */
+    @IntRange(from = 1, to = 1000_000)
     public val count: Long,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : IntervalRecord {
     init {
-        requireNonNegative(value = count, name = "count")
+        count.requireNotLess(other = 1, name = "count")
         count.requireNotMore(other = 1000_000, name = "count")
         require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
     }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/StepsRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/StepsRecordTest.kt
index 79718d5..8e7d4a2 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/StepsRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/StepsRecordTest.kt
@@ -60,4 +60,30 @@
             )
         }
     }
+
+    @Test
+    fun invalidSteps_tooFewCount_throws() {
+        assertFailsWith<IllegalArgumentException> {
+            StepsRecord(
+                startTime = Instant.ofEpochMilli(1234L),
+                startZoneOffset = null,
+                endTime = Instant.ofEpochMilli(1235L),
+                endZoneOffset = null,
+                count = 0,
+            )
+        }
+    }
+
+    @Test
+    fun invalidSteps_tooLargeCount_throws() {
+        assertFailsWith<IllegalArgumentException> {
+            StepsRecord(
+                startTime = Instant.ofEpochMilli(1234L),
+                startZoneOffset = null,
+                endTime = Instant.ofEpochMilli(1235L),
+                endZoneOffset = null,
+                count = 1000_001,
+            )
+        }
+    }
 }
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index e37c668..590e802 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -19,6 +19,22 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
   }
 
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface ExerciseUpdateCallback {
     method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
     method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
@@ -55,6 +71,11 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
   }
 
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
   public interface PassiveListenerCallback {
     method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
     method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
@@ -85,6 +106,14 @@
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
   }
 
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+  }
+
 }
 
 package androidx.health.services.client.data {
diff --git a/health/health-services-client/build.gradle b/health/health-services-client/build.gradle
index 138d521..1e6d812 100644
--- a/health/health-services-client/build.gradle
+++ b/health/health-services-client/build.gradle
@@ -25,10 +25,12 @@
 
 dependencies {
     api(libs.kotlinStdlib)
+    implementation(libs.kotlinCoroutinesCore)
     api("androidx.annotation:annotation:1.1.0")
     implementation(libs.guavaListenableFuture)
     implementation(libs.guavaAndroid)
     implementation("androidx.core:core-ktx:1.7.0")
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     implementation(libs.protobufLite)
 
     testImplementation(libs.junit)
@@ -36,6 +38,9 @@
     testImplementation(libs.robolectric)
     testImplementation(libs.testCore)
     testImplementation(libs.truth)
+    testImplementation(libs.kotlinTest)
+    testImplementation(libs.kotlinCoroutinesTest)
+    testImplementation(libs.kotlinTest)
 }
 
 android {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index 59e76bd..fd572f3 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -52,7 +52,8 @@
      * aggregation will occur until the exercise is started.
      *
      * If an app is actively preparing and another app starts tracking an active exercise then the
-     * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.TERMINATED]
+     * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along
+     * with the reason [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback]
      * indicating that their session has been superseded and ended. At that point no additional
      * updates to availability or data will be sent until the app calls prepareExercise again.
      *
@@ -69,15 +70,17 @@
      *
      * Since Health Services only allows a single active exercise at a time, this will terminate any
      * active exercise currently in progress before starting the new one. If this occurs, clients
-     * can expect to receive an [ExerciseUpdate] with [ExerciseState.TERMINATED], indicating that
-     * their exercise has been superseded and that no additional updates will be sent. Clients can
-     * use [getCurrentExerciseInfoAsync] (described below) to check if they or another app has an
-     * active exercise in-progress.
+     * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+     * [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback] indicating that their
+     * exercise has been superseded and that no additional updates will be sent. Clients can use
+     * [getCurrentExerciseInfoAsync] (described below) to check if they or another app has an active
+     * exercise in-progress.
      *
      * If the client fails to maintain a live [ExerciseUpdateCallback] for at least five minutes
      * during the duration of the exercise, Health Services can decide to terminate the exercise. If
-     * this occurs, clients can expect to receive an [ExerciseUpdate] with
-     * [ExerciseState.AUTO_ENDED] indicating that their exercise has been automatically ended due to
+     * this occurs, clients can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED]
+     * along with the reason [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the
+     * [ExerciseUpdateCallback] indicating that their exercise has been automatically ended due to
      * the lack of callback.
      *
      * Clients should only request [ExerciseType]s, [DataType]s, goals, and auto-pause enabled that
@@ -193,7 +196,8 @@
      * deliver them as soon as the callback is registered again. If the client fails to maintain a
      * live [ExerciseUpdateCallback] for at least five minutes during the duration of the exercise
      * Health Services can decide to terminate the exercise automatically. If this occurs, clients
-     * can expect to receive an [ExerciseUpdate] with [ExerciseState.AUTO_ENDED] indicating that
+     * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+     * [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the [ExerciseUpdateCallback] indicating that
      * their exercise has been automatically ended due to the lack of callback.
      *
      * Calls to the callback will be executed on the main application thread. To control where to
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
new file mode 100644
index 0000000..2e54b70
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
@@ -0,0 +1,250 @@
+/*
+ * 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
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DataPoint
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.ExerciseCapabilities
+import androidx.health.services.client.data.ExerciseConfig
+import androidx.health.services.client.data.ExerciseEndReason
+import androidx.health.services.client.data.ExerciseGoal
+import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseState
+import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.WarmUpConfig
+
+/**
+ * Prepares for a new exercise.
+ *
+ * Once called, Health Services will warmup the sensors based on the [ExerciseType] and
+ * requested [DataType]s.
+ *
+ * If the calling app already has an active exercise in progress or if it does not have the
+ * required permissions, then this call throws [android.os.RemoteException]. If another app owns
+ * the active exercise then this call will succeed.
+ *
+ * Sensors available for warmup are GPS [DataType.LOCATION] and HeartRate
+ * [DataType.HEART_RATE_BPM]. Other [DataType]s requested for warmup based on exercise
+ * capabilities will be a no-op for the prepare stage.
+ *
+ * The DataType availability can be obtained through the
+ * [ExerciseUpdateCallback.onAvailabilityChanged] callback. [ExerciseUpdate]s with the supported
+ * DataType [DataPoint] will also be returned in the [ExerciseState.PREPARING] state, though no
+ * aggregation will occur until the exercise is started.
+ *
+ * If an app is actively preparing and another app starts tracking an active exercise then the
+ * preparing app should expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along with
+ * the reason [ExerciseEndReason.AUTO_END_SUPERSEDED] to the [ExerciseUpdateCallback] indicating
+ * that their session has been superseded and ended. At that point no additional updates to
+ * availability or data will be sent until the app calls prepareExercise again.
+ *
+ * @param configuration the [WarmUpConfig] containing the desired exercise and data types
+ * @throws [android.os.RemoteException] if the calling app already has an active exercise in
+ * progress or if it does not have the required permissions or if Health Service fails to process
+ * the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.prepareExercise(configuration: WarmUpConfig) =
+    prepareExerciseAsync(configuration).await()
+
+/**
+ * Starts a new exercise.
+ *
+ * Once started, Health Services will begin collecting data associated with the exercise.
+ *
+ * Since Health Services only allows a single active exercise at a time, this will terminate any
+ * active exercise currently in progress before starting the new one. If this occurs, clients
+ * can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED], indicating that
+ * their exercise has been superseded and that no additional updates will be sent. Clients can
+ * use [getCurrentExerciseInfo] (described below) to check if they or another app has an
+ * active exercise in-progress.
+ *
+ * If the client fails to maintain a live [ExerciseUpdateCallback] for at least five minutes
+ * during the duration of the exercise, Health Services can decide to terminate the exercise. If
+ * this occurs, clients can expect to receive an [ExerciseUpdate] with [ExerciseState.ENDED] along
+ * with the reason [ExerciseEndReason.AUTO_END_MISSING_LISTENER] to the [ExerciseUpdateCallback]
+ * indicating that their exercise has been automatically ended due to the lack of callback.
+ *
+ * Clients should only request [ExerciseType]s, [DataType]s, goals, and auto-pause enabled that
+ * matches the [ExerciseCapabilities] returned by [getCapabilities] since Health Services
+ * will reject requests asking for unsupported configurations.
+ *
+ * @param configuration the [ExerciseConfig] describing this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call or if it does
+ * not have the required permissions
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.startExercise(configuration: ExerciseConfig) =
+    startExerciseAsync(configuration).await()
+
+/**
+ * Pauses the current exercise, if it is currently started.
+ *
+ * Before transitioning to [ExerciseState.USER_PAUSED], Health Services will flush and return
+ * the sensor data. While the exercise is paused, active time and cumulative metrics such as
+ * distance will not accumulate. Instantaneous measurements such as speed and heart rate will
+ * continue to update if requested in the [ExerciseConfig].
+ *
+ * Note that GPS and other sensors may be stopped when the exercise is paused in order to
+ * conserve battery. This may happen immediately, or after some time. (The exact behavior is
+ * hardware dependent.) Should this happen, access will automatically resume when the exercise
+ * is resumed.
+ *
+ * If the exercise is already paused then this method has no effect. If the exercise has ended
+ * then [android.os.RemoteException] is thrown.
+ *
+ * @throws [android.os.RemoteException] if the exercise has ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.pauseExercise() = pauseExerciseAsync().await()
+
+/**
+ * Resumes the current exercise, if it is currently paused.
+ *
+ * Once resumed active time and cumulative metrics such as distance will resume accumulating.
+ *
+ * If the exercise has been started but is not currently paused this method has no effect. If
+ * the exercise has ended then [android.os.RemoteException] is thrown.
+ *
+ * @throws [android.os.RemoteException] if the exercise has ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.resumeExercise() = resumeExerciseAsync().await()
+
+/**
+ * Ends the current exercise, if it has been started.
+ *
+ * Health Services will flush and then shut down the active sensors and return an
+ * [ExerciseUpdate] with [ExerciseState.ENDED] along with the reason
+ * [ExerciseEndReason.USER_END] to the [ExerciseUpdateCallback]. If the exercise has already
+ * ended then this call fails with a [android.os.RemoteException].
+ *
+ * No additional metrics will be produced for the exercise and any on device persisted data
+ * about the exercise will be deleted after the summary has been sent back.
+ *
+ * @throws [android.os.RemoteException] if exercise has already ended or if Health Service fails to
+ * process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.endExercise() = endExerciseAsync().await()
+
+/**
+ * Flushes the sensors for the active exercise. This call should be used sparingly and will be
+ * subject to throttling by Health Services.
+ *
+ * @throws [android.os.RemoteException] if the Health Service fails to process the request
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.flush() = flushAsync().await()
+
+/**
+ * Ends the current lap, calls [ExerciseUpdateCallback.onLapSummaryReceived] with data spanning
+ * the marked lap and starts a new lap. If the exercise supports laps this method can be called
+ * at any point after an exercise has been started and before it has been ended regardless of
+ * the exercise status.
+ *
+ * The metrics in the lap summary will start from either the start time of the exercise or the
+ * last time a lap was marked to the time this method is being called.
+ *
+ * @throws [android.os.RemoteException] If there's no exercise being tracked or if Health Service
+ * fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.markLap() = markLapAsync().await()
+
+/**
+ * Returns the current [ExerciseInfo].
+ *
+ * This can be used by clients to determine if they or another app already owns an active
+ * exercise being tracked by Health Services. For example, if an app is killed and it learns it
+ * owns the active exercise it can register a new [ExerciseUpdateCallback] and pick tracking up
+ * from where it left off.
+ *
+ * @return a [ExerciseInfo] that contains information about the current exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.getCurrentExerciseInfo() = getCurrentExerciseInfoAsync().await()
+
+/**
+ * Clears the callback set using [ExerciseClient.setUpdateCallback].
+ *
+ * If this callback is not already registered then this will be a no-op.
+ *
+ * @param callback the [ExerciseUpdateCallback] to clear
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@Suppress("ExecutorRegistration")
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.clearUpdateCallback(callback: ExerciseUpdateCallback) =
+    clearUpdateCallbackAsync(callback).await()
+
+/**
+ * Adds an [ExerciseGoal] for an active exercise.
+ *
+ * Goals apply to only active exercises owned by the client, and will be invalidated once the
+ * exercise is complete.
+ *
+ * @param exerciseGoal the [ExerciseGoal] to add to this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.addGoalToActiveExercise(exerciseGoal: ExerciseGoal<*>) =
+    addGoalToActiveExerciseAsync(exerciseGoal).await()
+
+/**
+ * Removes an exercise goal for an active exercise.
+ *
+ * Takes into account equivalent milestones (i.e. milestones which are not equal but are
+ * different representation of a common milestone. e.g. milestone A for every 2kms, currently at
+ * threshold of 10kms, and milestone B for every 2kms, currently at threshold of 8kms).
+ *
+ * @param exerciseGoal the [ExerciseGoal] to remove from this exercise
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.removeGoalFromActiveExercise(
+    exerciseGoal: ExerciseGoal<*>
+) = removeGoalFromActiveExerciseAsync(exerciseGoal).await()
+
+/**
+ * Enables or disables auto pause/resume for the current exercise.
+ *
+ * @param enabled a boolean to indicate if should be enabled or disabled
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.overrideAutoPauseAndResumeForActiveExercise(
+    enabled: Boolean
+) = overrideAutoPauseAndResumeForActiveExerciseAsync(enabled).await()
+
+/**
+ * Returns the [ExerciseCapabilities] of this client for the device.
+ *
+ * This can be used to determine what [ExerciseType]s and [DataType]s this device supports.
+ * Clients should use the capabilities to inform their requests since Health Services will
+ * typically reject requests made for [DataType]s or features (such as auto-pause) which are not
+ * enabled for the rejected [ExerciseType].
+ *
+ * @return a the [ExerciseCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt
new file mode 100644
index 0000000..951ccd3
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/MeasureClientExtension.kt
@@ -0,0 +1,48 @@
+/*
+ * 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
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DeltaDataType
+import androidx.health.services.client.data.MeasureCapabilities
+
+/**
+ * Unregisters the given [MeasureCallback] for updates of the given [DeltaDataType].
+ *
+ * @param dataType the [DeltaDataType] that needs to be unregistered
+ * @param callback the [MeasureCallback] which was used in registration
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@Suppress("PairedRegistration")
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun MeasureClient.unregisterMeasureCallback(
+    dataType: DeltaDataType<*, *>,
+    callback: MeasureCallback
+) = unregisterMeasureCallbackAsync(dataType, callback).await()
+
+/**
+ * Returns the [MeasureCapabilities] of this client for the device.
+ *
+ * This can be used to determine what [DeltaDataType]s this device supports for live
+ * measurement. Clients should use the capabilities to inform their requests since Health
+ * Services will typically reject requests made for [DeltaDataType]s which are not enabled for
+ * measurement.
+ *
+ * @return a [MeasureCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun MeasureClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt
new file mode 100644
index 0000000..78973c6
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveMonitoringClientExtension.kt
@@ -0,0 +1,102 @@
+/*
+ * 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
+
+import androidx.concurrent.futures.await
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.PassiveListenerConfig
+import androidx.health.services.client.data.PassiveMonitoringCapabilities
+
+/**
+ * Subscribes for updates to be periodically delivered to the app.
+ *
+ * Data updates will be batched and delivered from the point of initial registration and will
+ * continue to be delivered until the [DataType] is unregistered, either by explicitly calling
+ * [clearPassiveListenerService] or by registering again without that [DataType]
+ * included in the request. Higher frequency updates are available through [ExerciseClient] or
+ * [MeasureClient]. Any requested goal, user activity, or health event updates will not be
+ * batched.
+ *
+ * Health Services will automatically bind to the provided [PassiveListenerService] to send the
+ * update. Clients are responsible for defining the service in their app manifest. They should
+ * also require the `com.google.android.wearable.healthservices.permission.PASSIVE_DATA_BINDING`
+ * permission in their app manifest service definition in order to ensure that Health Services
+ * is the source of the binding.
+ *
+ * This registration is unique per subscribing app. Subsequent registrations will replace the
+ * previous registration, if one had been made. The client is responsible for ensuring that
+ * their requested [PassiveListenerConfig] is supported on this device by checking the
+ * [PassiveMonitoringCapabilities]. The returned future will fail if the request is not
+ * supported on the current device or the client does not have the required permissions for the
+ * request.
+ *
+ * @param service the [PassiveListenerService] to bind to
+ * @param config the [PassiveListenerConfig] from the client
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.setPassiveListenerService(
+    service: Class<out PassiveListenerService>,
+    config: PassiveListenerConfig
+) = setPassiveListenerServiceAsync(service, config).await()
+
+/**
+ * Unregisters the subscription made by [setPassiveListenerService].
+ *
+ * Data will not be delivered after this call so if clients care about any pending batched data
+ * they should call flush before unregistering.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.clearPassiveListenerService() =
+    clearPassiveListenerServiceAsync().await()
+
+/**
+ * Unregisters the subscription made by [PassiveMonitoringClient.setPassiveListenerCallback].
+ *
+ * Data will not be delivered after this call so if clients care about any pending batched data
+ * they should call flush before unregistering.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.clearPassiveListenerCallback() =
+    clearPassiveListenerCallbackAsync().await()
+
+/**
+ * Flushes the sensors for the registered [DataType]s.
+ *
+ * If no listener has been registered by this client, this will be a no-op. This call should be
+ * used sparingly and will be subject to throttling by Health Services.
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.flush() = flushAsync().await()
+
+/**
+ * Returns the [PassiveMonitoringCapabilities] of this client for this device.
+ *
+ * This can be used to determine what [DataType]s this device supports for passive monitoring
+ * and goals. Clients should use the capabilities to inform their requests since Health Services
+ * will typically reject requests made for [DataType]s which are not supported.
+ *
+ * @return a [PassiveMonitoringCapabilities] for this device
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun PassiveMonitoringClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
index b23d8bd..c2db7c4 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/ExerciseInfoCallback.kt
@@ -38,6 +38,6 @@
 
     @Throws(RemoteException::class)
     override fun onFailure(message: String) {
-        resultFuture.setException(Exception(message))
+        resultFuture.setException(RemoteException(message))
     }
 }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
index 874eec1..0d71261 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/internal/StatusCallback.kt
@@ -39,6 +39,6 @@
     @Throws(RemoteException::class)
     @CallSuper
     override fun onFailure(msg: String) {
-        resultFuture.setException(Exception(msg))
+        resultFuture.setException(RemoteException(msg))
     }
 }
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
new file mode 100644
index 0000000..8d2e322
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
@@ -0,0 +1,881 @@
+/*
+ * 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
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.Availability
+import androidx.health.services.client.data.ComparisonType
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DataTypeAvailability
+import androidx.health.services.client.data.DataTypeCondition
+import androidx.health.services.client.data.ExerciseCapabilities
+import androidx.health.services.client.data.ExerciseConfig
+import androidx.health.services.client.data.ExerciseGoal
+import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseLapSummary
+import androidx.health.services.client.data.ExerciseTrackedStatus
+import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeCapabilities
+import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.WarmUpConfig
+import androidx.health.services.client.impl.IExerciseApiService
+import androidx.health.services.client.impl.IExerciseUpdateListener
+import androidx.health.services.client.impl.IpcConstants
+import androidx.health.services.client.impl.ServiceBackedExerciseClient
+import androidx.health.services.client.impl.event.ExerciseUpdateListenerEvent
+import androidx.health.services.client.impl.internal.IExerciseInfoCallback
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.ClientConfiguration
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.ExerciseGoalRequest
+import androidx.health.services.client.impl.request.FlushRequest
+import androidx.health.services.client.impl.request.PrepareExerciseRequest
+import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.response.AvailabilityResponse
+import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
+import androidx.health.services.client.impl.response.ExerciseInfoResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableSet
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseClientTest {
+
+    private lateinit var client: ServiceBackedExerciseClient
+    private lateinit var service: FakeServiceStub
+    private val callback = FakeExerciseUpdateCallback()
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        client =
+            ServiceBackedExerciseClient(context, ConnectionManager(context, context.mainLooper))
+        service = FakeServiceStub()
+
+        val packageName = CLIENT_CONFIGURATION.servicePackageName
+        val action = CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, CLIENT),
+            service
+        )
+    }
+
+    @After
+    fun tearDown() {
+        client.clearUpdateCallbackAsync(callback)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_prepareExerciseSynchronously() = runTest {
+        launch {
+            val warmUpConfig = WarmUpConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.prepareExercise(warmUpConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_prepareExerciseSynchronously_ThrowsException() =
+        runTest {
+            launch {
+                val warmUpConfig = WarmUpConfig(
+                    ExerciseType.WALKING,
+                    setOf(DataType.HEART_RATE_BPM),
+                )
+                var exception: Exception? = null
+                client.setUpdateCallback(callback)
+                // Mocking the calling app already has an active exercise in
+                // progress or if it does not have the required permissions
+                service.throwException = true
+
+                try {
+                    client.prepareExercise(warmUpConfig)
+                } catch (e: RemoteException) {
+                    exception = e
+                }
+
+                Truth.assertThat(exception).isNotNull()
+                Truth.assertThat(exception).isInstanceOf(RemoteException::class.java)
+            }
+            advanceMainLooperIdle()
+        }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justStatsType_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM_STATS),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical.
+                // The APK doesn't know about _STATS, so pass the sample type to mimic that
+                // behavior.
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM_STATS, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_statsAndSample_startExerciseSynchronously() = runTest {
+        launch {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM, DataType.HEART_RATE_BPM_STATS),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+                // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical.
+                // The APK doesn't know about _STATS, so pass the sample type to mimic that
+                // behavior.
+                AvailabilityResponse(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            )
+            client.setUpdateCallback(callback)
+            client.startExercise(exerciseConfig)
+
+            service.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+            Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+            // When both the sample type and stat type are requested, both should be notified
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM, DataTypeAvailability.ACQUIRING)
+            Truth.assertThat(callback.availabilities)
+                .containsEntry(DataType.HEART_RATE_BPM_STATS, DataTypeAvailability.ACQUIRING)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_pauseExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_resumeExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+        val resumeExercise = async {
+
+            client.resumeExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        resumeExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED,
+            TestExerciseStates.RESUMED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_endExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val endExercise = async {
+
+            client.endExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        endExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.ENDED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun callbackShouldMatchRequested_justSampleType_endPausedExerciseSynchronously() = runTest {
+        val statesList = mutableListOf<TestExerciseStates>()
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val pauseExercise = async {
+
+            client.pauseExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        pauseExercise.await()
+        val endExercise = async {
+
+            client.endExercise()
+            statesList += service.testExerciseStates
+        }
+        advanceMainLooperIdle()
+        endExercise.await()
+
+        Truth.assertThat(statesList).containsExactly(
+            TestExerciseStates.STARTED,
+            TestExerciseStates.PAUSED,
+            TestExerciseStates.ENDED
+        )
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun flushSynchronously() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val flushExercise = async {
+
+            client.flush()
+        }
+        advanceMainLooperIdle()
+        flushExercise.await()
+
+        Truth.assertThat(service.registerFlushRequests).hasSize(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun markLapSynchronously() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val markLap = async {
+
+            client.markLap()
+        }
+        advanceMainLooperIdle()
+        markLap.await()
+
+        Truth.assertThat(service.laps).isEqualTo(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously() = runTest {
+        lateinit var exerciseInfo: ExerciseInfo
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            exerciseInfo = client.getCurrentExerciseInfo()
+        }
+        advanceMainLooperIdle()
+        currentExerciseInfoDeferred.await()
+
+        Truth.assertThat(exerciseInfo.exerciseType).isEqualTo(ExerciseType.WALKING)
+        Truth.assertThat(exerciseInfo.exerciseTrackedStatus)
+            .isEqualTo(ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously_cancelled() = runTest {
+        var isCancellationException = false
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            client.getCurrentExerciseInfo()
+        }
+        val cancelDeferred = async {
+            currentExerciseInfoDeferred.cancel()
+        }
+        try {
+            currentExerciseInfoDeferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancelDeferred.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCurrentExerciseInfoSynchronously_exception() = runTest {
+        var isException = false
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val currentExerciseInfoDeferred = async {
+            service.throwException = true
+            try {
+                client.getCurrentExerciseInfo()
+            } catch (e: RemoteException) {
+                isException = true
+            }
+        }
+        advanceMainLooperIdle()
+        currentExerciseInfoDeferred.await()
+
+        Truth.assertThat(isException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun clearUpdateCallbackShouldBeInvoked() = runTest {
+        val statesList = mutableListOf<Boolean>()
+
+        client.setUpdateCallback(callback)
+        Shadows.shadowOf(Looper.getMainLooper()).idle()
+        statesList += (service.listener == null)
+        val deferred = async {
+
+            client.clearUpdateCallback(callback)
+            statesList += (service.listener == null)
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(statesList).containsExactly(false, true)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun addGoalToActiveExerciseShouldBeInvoked() = runTest {
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false,
+                exerciseGoals = listOf(
+                    ExerciseGoal.createOneTimeGoal(
+                        DataTypeCondition(
+                            DataType.DISTANCE_TOTAL, 50.0,
+                            ComparisonType.GREATER_THAN
+                        )
+                    ),
+                    ExerciseGoal.createOneTimeGoal(
+                        DataTypeCondition(
+                            DataType.DISTANCE_TOTAL, 150.0,
+                            ComparisonType.GREATER_THAN
+                        )
+                    ),
+                )
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val addGoalDeferred = async {
+            val proto = ExerciseGoal.createOneTimeGoal(
+                DataTypeCondition(DataType.HEART_RATE_BPM_STATS, 145.0, ComparisonType.GREATER_THAN)
+            ).proto
+            val goal = ExerciseGoal.fromProto(proto)
+
+            client.addGoalToActiveExercise(goal)
+        }
+        advanceMainLooperIdle()
+        addGoalDeferred.await()
+
+        Truth.assertThat(service.goals).hasSize(3)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun removeGoalFromActiveExerciseShouldBeInvoked() = runTest {
+        val goal1 = ExerciseGoal.createOneTimeGoal(
+            DataTypeCondition(
+                DataType.DISTANCE_TOTAL, 50.0,
+                ComparisonType.GREATER_THAN
+            )
+        )
+        val goal2 = ExerciseGoal.createOneTimeGoal(
+            DataTypeCondition(
+                DataType.DISTANCE_TOTAL, 150.0,
+                ComparisonType.GREATER_THAN
+            )
+        )
+        val startExercise = async {
+            val exerciseConfig = ExerciseConfig(
+                ExerciseType.WALKING,
+                setOf(DataType.HEART_RATE_BPM),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false,
+                exerciseGoals = listOf(
+                    goal1,
+                    goal2,
+                )
+            )
+            client.setUpdateCallback(callback)
+
+            client.startExercise(exerciseConfig)
+        }
+        advanceMainLooperIdle()
+        startExercise.await()
+        val removeGoalDeferred = async {
+            client.removeGoalFromActiveExercise(goal1)
+        }
+        advanceMainLooperIdle()
+        removeGoalDeferred.await()
+
+        Truth.assertThat(service.goals).hasSize(1)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously() = runTest {
+        lateinit var passiveMonitoringCapabilities: ExerciseCapabilities
+        val deferred = async {
+            passiveMonitoringCapabilities = client.getCapabilities()
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(service.registerGetCapabilitiesRequests).hasSize(1)
+        Truth.assertThat(passiveMonitoringCapabilities).isNotNull()
+        Truth.assertThat(service.getTestCapabilities().toString())
+            .isEqualTo(passiveMonitoringCapabilities.toString())
+    }
+
+    class FakeExerciseUpdateCallback : ExerciseUpdateCallback {
+        val availabilities = mutableMapOf<DataType<*, *>, Availability>()
+        val registrationFailureThrowables = mutableListOf<Throwable>()
+        var onRegisteredCalls = 0
+        var onRegistrationFailedCalls = 0
+        var update: ExerciseUpdate? = null
+
+        override fun onRegistered() {
+            onRegisteredCalls++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            onRegistrationFailedCalls++
+            registrationFailureThrowables.add(throwable)
+        }
+
+        override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
+            this@FakeExerciseUpdateCallback.update = update
+        }
+
+        override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {}
+
+        override fun onAvailabilityChanged(dataType: DataType<*, *>, availability: Availability) {
+            availabilities[dataType] = availability
+        }
+    }
+
+    class FakeServiceStub : IExerciseApiService.Stub() {
+
+        var listener: IExerciseUpdateListener? = null
+        val registerFlushRequests = mutableListOf<FlushRequest>()
+        var statusCallbackAction: (IStatusCallback?) -> Unit = { it!!.onSuccess() }
+        var testExerciseStates = TestExerciseStates.UNKNOWN
+        var laps = 0
+        var exerciseConfig: ExerciseConfig? = null
+        override fun getApiVersion(): Int = 12
+        val goals = mutableListOf<ExerciseGoal<*>>()
+        var throwException = false
+        val registerGetCapabilitiesRequests = mutableListOf<CapabilitiesRequest>()
+
+        override fun prepareExercise(
+            prepareExerciseRequest: PrepareExerciseRequest?,
+            statusCallback: IStatusCallback
+        ) {
+            if (throwException) {
+                statusCallback.onFailure("Remote Exception")
+            } else {
+                statusCallbackAction.invoke(statusCallback)
+            }
+        }
+
+        override fun startExercise(
+            startExerciseRequest: StartExerciseRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            exerciseConfig = startExerciseRequest?.exerciseConfig
+            exerciseConfig?.exerciseGoals?.let { goals.addAll(it) }
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.STARTED
+        }
+
+        override fun pauseExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.PAUSED
+        }
+
+        override fun resumeExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.RESUMED
+        }
+
+        override fun endExercise(packageName: String?, statusCallback: IStatusCallback?) {
+            statusCallbackAction.invoke(statusCallback)
+            testExerciseStates = TestExerciseStates.ENDED
+        }
+
+        override fun markLap(packageName: String?, statusCallback: IStatusCallback?) {
+            laps++
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun getCurrentExerciseInfo(
+            packageName: String?,
+            exerciseInfoCallback: IExerciseInfoCallback?
+        ) {
+            if (throwException) {
+                exerciseInfoCallback?.onFailure("Remote Exception")
+            }
+            if (exerciseConfig == null) {
+                exerciseInfoCallback?.onExerciseInfo(
+                    ExerciseInfoResponse(
+                        ExerciseInfo(
+                            ExerciseTrackedStatus.UNKNOWN,
+                            ExerciseType.UNKNOWN
+                        )
+                    )
+                )
+            } else {
+                exerciseInfoCallback?.onExerciseInfo(
+                    ExerciseInfoResponse(
+                        ExerciseInfo(
+                            ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS,
+                            exerciseConfig!!.exerciseType
+                        )
+                    )
+                )
+            }
+        }
+
+        override fun setUpdateListener(
+            packageName: String?,
+            listener: IExerciseUpdateListener?,
+            statusCallback: IStatusCallback?
+        ) {
+            this.listener = listener
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun clearUpdateListener(
+            packageName: String?,
+            listener: IExerciseUpdateListener?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (this.listener == listener)
+                this.listener = null
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun addGoalToActiveExercise(
+            request: ExerciseGoalRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (request != null) {
+                goals.add(request.exerciseGoal)
+            }
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun removeGoalFromActiveExercise(
+            request: ExerciseGoalRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            if (request != null) {
+                goals.remove(request.exerciseGoal)
+            }
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun overrideAutoPauseAndResumeForActiveExercise(
+            request: AutoPauseAndResumeConfigRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            throw NotImplementedError()
+        }
+
+        override fun getCapabilities(request: CapabilitiesRequest): ExerciseCapabilitiesResponse {
+            if (throwException) {
+                throw RemoteException("Remote Exception")
+            }
+            registerGetCapabilitiesRequests.add(request)
+            val capabilities = getTestCapabilities()
+            return ExerciseCapabilitiesResponse(capabilities)
+        }
+
+        fun getTestCapabilities(): ExerciseCapabilities {
+            val exerciseTypeToCapabilitiesMapping =
+                ImmutableMap.of(
+                    ExerciseType.WALKING, ExerciseTypeCapabilities( /* supportedDataTypes= */
+                        ImmutableSet.of(DataType.STEPS),
+                        ImmutableMap.of(
+                            DataType.STEPS_TOTAL,
+                            ImmutableSet.of(ComparisonType.GREATER_THAN)
+                        ),
+                        ImmutableMap.of(
+                            DataType.STEPS_TOTAL,
+                            ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+                        ), /* supportsAutoPauseAndResume= */
+                        false
+                    ),
+                    ExerciseType.RUNNING, ExerciseTypeCapabilities(
+                        ImmutableSet.of(DataType.HEART_RATE_BPM, DataType.SPEED),
+                        ImmutableMap.of(
+                            DataType.HEART_RATE_BPM_STATS,
+                            ImmutableSet.of(ComparisonType.GREATER_THAN, ComparisonType.LESS_THAN),
+                            DataType.SPEED_STATS,
+                            ImmutableSet.of(ComparisonType.LESS_THAN)
+                        ),
+                        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
+                    ),
+                    ExerciseType.SWIMMING_POOL, ExerciseTypeCapabilities( /* supportedDataTypes= */
+                        ImmutableSet.of(), /* supportedGoals= */
+                        ImmutableMap.of(), /* supportedMilestones= */
+                        ImmutableMap.of(), /* supportsAutoPauseAndResume= */
+                        true
+                    )
+                )
+
+            return ExerciseCapabilities(exerciseTypeToCapabilitiesMapping)
+        }
+
+        override fun flushExercise(request: FlushRequest, statusCallback: IStatusCallback?) {
+            registerFlushRequests += request
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        fun setException() {
+            throwException = true
+        }
+    }
+
+    enum class TestExerciseStates {
+        UNKNOWN,
+        PREPARED,
+        STARTED,
+        PAUSED,
+        RESUMED,
+        ENDED
+    }
+
+    internal companion object {
+        internal const val CLIENT = "HealthServicesExerciseClient"
+        internal val CLIENT_CONFIGURATION =
+            ClientConfiguration(
+                CLIENT,
+                IpcConstants.SERVICE_PACKAGE_NAME,
+                IpcConstants.EXERCISE_API_BIND_ACTION
+            )
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt
new file mode 100644
index 0000000..b15bbca
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/MeasureClientTest.kt
@@ -0,0 +1,286 @@
+/*
+ * 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
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.Availability
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DeltaDataType
+import androidx.health.services.client.data.MeasureCapabilities
+import androidx.health.services.client.impl.IMeasureApiService
+import androidx.health.services.client.impl.IMeasureCallback
+import androidx.health.services.client.impl.IpcConstants
+import androidx.health.services.client.impl.ServiceBackedMeasureClient
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.ClientConfiguration
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.MeasureRegistrationRequest
+import androidx.health.services.client.impl.request.MeasureUnregistrationRequest
+import androidx.health.services.client.impl.response.MeasureCapabilitiesResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class MeasureClientTest {
+
+    private lateinit var callback: FakeCallback
+    private lateinit var client: ServiceBackedMeasureClient
+    private lateinit var service: FakeServiceStub
+    private var cleanup: Boolean = false
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+    private fun CoroutineScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        callback = FakeCallback()
+        client =
+            ServiceBackedMeasureClient(context, ConnectionManager(context, context.mainLooper))
+        service = FakeServiceStub()
+
+        val packageName = CLIENT_CONFIGURATION.servicePackageName
+        val action = CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, CLIENT),
+            service
+        )
+        cleanup = true
+    }
+
+    @After
+    fun tearDown() {
+        if (!cleanup)
+            return
+        runBlocking {
+            launch { client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback) }
+            advanceMainLooperIdle()
+        }
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun unregisterCallbackReachesServiceSynchronously() = runTest {
+        val deferred = async {
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            cleanup = false // Already unregistered
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(service.unregisterEvents).hasSize(1)
+        Truth.assertThat(service.unregisterEvents[0].request.dataType)
+            .isEqualTo(DataType.HEART_RATE_BPM)
+        Truth.assertThat(service.unregisterEvents[0].request.packageName)
+            .isEqualTo("androidx.health.services.client.test")
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun unregisterCallbackSynchronously_throwsIllegalArgumentException() = runTest {
+        var isExceptionCaught = false
+
+        val deferred = async {
+            try {
+                client.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            } catch (e: IllegalArgumentException) {
+                isExceptionCaught = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+        cleanup = false // Not registered
+
+        Truth.assertThat(isExceptionCaught).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValueSynchronously() = runTest {
+        lateinit var capabilities: MeasureCapabilities
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            capabilities = client.getCapabilities()
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(capabilities.supportedDataTypesMeasure).hasSize(1)
+        Truth.assertThat(capabilities.supportedDataTypesMeasure)
+            .containsExactly(DataType.HEART_RATE_BPM)
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValue_cancelSuccessfully() = runTest {
+        var isCancellationException = false
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            client.getCapabilities()
+        }
+        val cancelCoroutine = async { deferred.cancel() }
+        try {
+            deferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancelCoroutine.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun capabilitiesReturnsCorrectValue_catchException() = runTest {
+        var isRemoteException = false
+        val deferred = async {
+            service.supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+            client.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)
+            service.setException()
+            try {
+                client.getCapabilities()
+            } catch (e: RemoteException) {
+                isRemoteException = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(isRemoteException).isTrue()
+    }
+
+    class FakeCallback : MeasureCallback {
+        data class AvailabilityChangeEvent(
+            val dataType: DataType<*, *>,
+            val availability: Availability
+        )
+
+        data class DataReceivedEvent(val data: DataPointContainer)
+
+        val availabilityChangeEvents = mutableListOf<AvailabilityChangeEvent>()
+        val dataReceivedEvents = mutableListOf<DataReceivedEvent>()
+        var onRegisteredInvocationCount = 0
+        var registrationFailureThrowables = mutableListOf<Throwable>()
+
+        override fun onRegistered() {
+            onRegisteredInvocationCount++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            registrationFailureThrowables += throwable
+        }
+
+        override fun onAvailabilityChanged(
+            dataType: DeltaDataType<*, *>,
+            availability: Availability
+        ) {
+            availabilityChangeEvents += AvailabilityChangeEvent(dataType, availability)
+        }
+
+        override fun onDataReceived(data: DataPointContainer) {
+            dataReceivedEvents += DataReceivedEvent(data)
+        }
+    }
+
+    class FakeServiceStub : IMeasureApiService.Stub() {
+        private var throwExcepotion = false
+
+        class RegisterEvent(
+            val request: MeasureRegistrationRequest,
+            val callback: IMeasureCallback,
+            val statusCallback: IStatusCallback
+        )
+
+        class UnregisterEvent(
+            val request: MeasureUnregistrationRequest,
+            val callback: IMeasureCallback,
+            val statusCallback: IStatusCallback
+        )
+
+        var statusCallbackAction: (IStatusCallback) -> Unit = { it.onSuccess() }
+        var supportedDataTypes = setOf(DataType.HEART_RATE_BPM)
+
+        val registerEvents = mutableListOf<RegisterEvent>()
+        val unregisterEvents = mutableListOf<UnregisterEvent>()
+
+        override fun getApiVersion() = 42
+
+        override fun registerCallback(
+            request: MeasureRegistrationRequest,
+            callback: IMeasureCallback,
+            statusCallback: IStatusCallback
+        ) {
+            registerEvents += RegisterEvent(request, callback, statusCallback)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterCallback(
+            request: MeasureUnregistrationRequest,
+            callback: IMeasureCallback,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterEvents += UnregisterEvent(request, callback, statusCallback)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun getCapabilities(request: CapabilitiesRequest): MeasureCapabilitiesResponse {
+            if (throwExcepotion) {
+                throw RemoteException("Remote Exception")
+            }
+            return MeasureCapabilitiesResponse(MeasureCapabilities(supportedDataTypes))
+        }
+
+        fun setException() {
+            throwExcepotion = true
+        }
+    }
+
+    internal companion object {
+        internal const val CLIENT = "HealthServicesMeasureClient"
+        internal val CLIENT_CONFIGURATION =
+            ClientConfiguration(
+                CLIENT,
+                IpcConstants.SERVICE_PACKAGE_NAME,
+                IpcConstants.MEASURE_API_BIND_ACTION
+            )
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt
new file mode 100644
index 0000000..1a4a39d
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveMonitoringClientTest.kt
@@ -0,0 +1,346 @@
+/*
+ * 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
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Looper
+import android.os.RemoteException
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataType.Companion.CALORIES_DAILY
+import androidx.health.services.client.data.DataType.Companion.CALORIES_TOTAL
+import androidx.health.services.client.data.DataType.Companion.DISTANCE
+import androidx.health.services.client.data.DataType.Companion.STEPS
+import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
+import androidx.health.services.client.data.HealthEvent
+import androidx.health.services.client.data.PassiveGoal
+import androidx.health.services.client.data.PassiveListenerConfig
+import androidx.health.services.client.data.PassiveMonitoringCapabilities
+import androidx.health.services.client.data.UserActivityInfo
+import androidx.health.services.client.data.UserActivityState
+import androidx.health.services.client.impl.IPassiveListenerCallback
+import androidx.health.services.client.impl.IPassiveMonitoringApiService
+import androidx.health.services.client.impl.ServiceBackedPassiveMonitoringClient
+import androidx.health.services.client.impl.internal.IStatusCallback
+import androidx.health.services.client.impl.ipc.internal.ConnectionManager
+import androidx.health.services.client.impl.request.CapabilitiesRequest
+import androidx.health.services.client.impl.request.FlushRequest
+import androidx.health.services.client.impl.request.PassiveListenerCallbackRegistrationRequest
+import androidx.health.services.client.impl.request.PassiveListenerServiceRegistrationRequest
+import androidx.health.services.client.impl.response.PassiveMonitoringCapabilitiesResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
+import java.util.concurrent.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class PassiveMonitoringClientTest {
+
+    private lateinit var client: PassiveMonitoringClient
+    private lateinit var service: FakeServiceStub
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    private fun TestScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    private fun CoroutineScope.advanceMainLooperIdle() =
+        launch { Shadows.shadowOf(Looper.getMainLooper()).idle() }
+
+    @Before
+    fun setUp() {
+        val context = ApplicationProvider.getApplicationContext<Application>()
+        client = ServiceBackedPassiveMonitoringClient(
+            context, ConnectionManager(context, context.mainLooper)
+        )
+        service = FakeServiceStub()
+
+        val packageName =
+            ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.servicePackageName
+        val action = ServiceBackedPassiveMonitoringClient.CLIENT_CONFIGURATION.bindAction
+        Shadows.shadowOf(context).setComponentNameAndServiceForBindServiceForIntent(
+            Intent().setPackage(packageName).setAction(action),
+            ComponentName(packageName, ServiceBackedPassiveMonitoringClient.CLIENT),
+            service
+        )
+    }
+
+    @After
+    fun tearDown() {
+        runBlocking {
+            launch { client.clearPassiveListenerCallback() }
+            advanceMainLooperIdle()
+            launch { client.clearPassiveListenerService() }
+            advanceMainLooperIdle()
+        }
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun registersPassiveListenerServiceSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+
+            client.setPassiveListenerService(FakeListenerService::class.java, config)
+            val request = service.registerServiceRequests[0]
+
+            Truth.assertThat(service.registerServiceRequests).hasSize(1)
+            Truth.assertThat(request.passiveListenerConfig.dataTypes).containsExactly(
+                STEPS_DAILY, CALORIES_DAILY
+            )
+            Truth.assertThat(request.passiveListenerConfig.shouldUserActivityInfoBeRequested)
+                .isTrue()
+            Truth.assertThat(request.packageName).isEqualTo("androidx.health.services.client.test")
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun flushSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+            val callback = FakeCallback()
+            client.setPassiveListenerCallback(config, callback)
+
+            client.flush()
+
+            Truth.assertThat(service.registerFlushRequests).hasSize(1)
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously() = runTest {
+        launch {
+            val config = PassiveListenerConfig(
+                dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+                shouldUserActivityInfoBeRequested = true,
+                dailyGoals = setOf(),
+                healthEventTypes = setOf()
+            )
+            val callback = FakeCallback()
+            client.setPassiveListenerCallback(config, callback)
+
+            val passiveMonitoringCapabilities = client.getCapabilities()
+
+            Truth.assertThat(service.registerGetCapabilitiesRequests).hasSize(1)
+            Truth.assertThat(passiveMonitoringCapabilities).isNotNull()
+            Truth.assertThat(service.getTestCapabilities().toString())
+                .isEqualTo(passiveMonitoringCapabilities.toString())
+        }
+        advanceMainLooperIdle()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously_cancelled() = runTest {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(),
+            healthEventTypes = setOf()
+        )
+        val callback = FakeCallback()
+        client.setPassiveListenerCallback(config, callback)
+        var isCancellationException = false
+
+        val deferred = async {
+            client.getCapabilities()
+        }
+        val cancellationDeferred = async {
+            deferred.cancel(CancellationException())
+        }
+        try {
+            deferred.await()
+        } catch (e: CancellationException) {
+            isCancellationException = true
+        }
+        cancellationDeferred.await()
+
+        Truth.assertThat(isCancellationException).isTrue()
+    }
+
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun getCapabilitiesSynchronously_Exception() = runTest {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(),
+            healthEventTypes = setOf()
+        )
+        val callback = FakeCallback()
+        client.setPassiveListenerCallback(config, callback)
+        var isExceptionCaught = false
+        val deferred = async {
+            service.setException()
+            try {
+
+                client.getCapabilities()
+            } catch (e: RemoteException) {
+                isExceptionCaught = true
+            }
+        }
+        advanceMainLooperIdle()
+        deferred.await()
+
+        Truth.assertThat(isExceptionCaught).isTrue()
+    }
+
+    class FakeListenerService : PassiveListenerService()
+
+    internal class FakeCallback : PassiveListenerCallback {
+        private var onRegisteredCalls = 0
+        private val onRegistrationFailedThrowables = mutableListOf<Throwable>()
+        private val dataPointsReceived = mutableListOf<DataPointContainer>()
+        private val userActivityInfosReceived = mutableListOf<UserActivityInfo>()
+        private val completedGoals = mutableListOf<PassiveGoal>()
+        private val healthEventsReceived = mutableListOf<HealthEvent>()
+        var onPermissionLostCalls = 0
+
+        override fun onRegistered() {
+            onRegisteredCalls++
+        }
+
+        override fun onRegistrationFailed(throwable: Throwable) {
+            onRegistrationFailedThrowables += throwable
+        }
+
+        override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
+            dataPointsReceived += dataPoints
+        }
+
+        override fun onUserActivityInfoReceived(info: UserActivityInfo) {
+            userActivityInfosReceived += info
+        }
+
+        override fun onGoalCompleted(goal: PassiveGoal) {
+            completedGoals += goal
+        }
+
+        override fun onHealthEventReceived(event: HealthEvent) {
+            healthEventsReceived += event
+        }
+
+        override fun onPermissionLost() {
+            onPermissionLostCalls++
+        }
+    }
+
+    internal class FakeServiceStub : IPassiveMonitoringApiService.Stub() {
+        @JvmField
+        var apiVersion = 42
+
+        private var statusCallbackAction: (IStatusCallback?) -> Unit = { it!!.onSuccess() }
+        val registerServiceRequests = mutableListOf<PassiveListenerServiceRegistrationRequest>()
+        private val registerCallbackRequests =
+            mutableListOf<PassiveListenerCallbackRegistrationRequest>()
+        val registerFlushRequests = mutableListOf<FlushRequest>()
+        val registerGetCapabilitiesRequests = mutableListOf<CapabilitiesRequest>()
+        private val registeredCallbacks = mutableListOf<IPassiveListenerCallback>()
+        private val unregisterServicePackageNames = mutableListOf<String>()
+        private val unregisterCallbackPackageNames = mutableListOf<String>()
+        private var throwExcepotion = false
+
+        override fun getApiVersion() = 42
+
+        override fun getCapabilities(
+            request: CapabilitiesRequest
+        ): PassiveMonitoringCapabilitiesResponse {
+            if (throwExcepotion) {
+                throw RemoteException("Remote Exception")
+            }
+            registerGetCapabilitiesRequests.add(request)
+            val capabilities = getTestCapabilities()
+            return PassiveMonitoringCapabilitiesResponse(capabilities)
+        }
+
+        override fun flush(request: FlushRequest, statusCallback: IStatusCallback?) {
+            registerFlushRequests.add(request)
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun registerPassiveListenerService(
+            request: PassiveListenerServiceRegistrationRequest,
+            statusCallback: IStatusCallback
+        ) {
+            registerServiceRequests += request
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun registerPassiveListenerCallback(
+            request: PassiveListenerCallbackRegistrationRequest,
+            callback: IPassiveListenerCallback,
+            statusCallback: IStatusCallback
+        ) {
+            registerCallbackRequests += request
+            registeredCallbacks += callback
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterPassiveListenerService(
+            packageName: String,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterServicePackageNames += packageName
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        override fun unregisterPassiveListenerCallback(
+            packageName: String,
+            statusCallback: IStatusCallback
+        ) {
+            unregisterCallbackPackageNames += packageName
+            statusCallbackAction.invoke(statusCallback)
+        }
+
+        fun getTestCapabilities(): PassiveMonitoringCapabilities {
+            return PassiveMonitoringCapabilities(
+                supportedDataTypesPassiveMonitoring = setOf(STEPS, DISTANCE),
+                supportedDataTypesPassiveGoals = setOf(CALORIES_TOTAL),
+                supportedHealthEventTypes = setOf(HealthEvent.Type.FALL_DETECTED),
+                supportedUserActivityStates = setOf(UserActivityState.USER_ACTIVITY_PASSIVE)
+            )
+        }
+
+        fun setException() {
+            throwExcepotion = true
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
index a86c098..f42c82c 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedMeasureClientTest.kt
@@ -76,6 +76,7 @@
     @After
     fun tearDown() {
         client.unregisterMeasureCallbackAsync(HEART_RATE_BPM, callback)
+        shadowOf(Looper.getMainLooper()).idle()
     }
 
     @Test
diff --git a/input/input-motionprediction/api/current.txt b/input/input-motionprediction/api/current.txt
index a2cdf99..6611119 100644
--- a/input/input-motionprediction/api/current.txt
+++ b/input/input-motionprediction/api/current.txt
@@ -1,11 +1,11 @@
 // Signature format: 4.0
 package androidx.input.motionprediction {
 
-  public interface MotionEventPredictor {
-    method public void dispose();
+  public interface MotionEventPredictor extends java.lang.AutoCloseable {
+    method public void close();
     method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
     method public android.view.MotionEvent? predict();
-    method public void recordMovement(android.view.MotionEvent);
+    method public void record(android.view.MotionEvent);
   }
 
 }
diff --git a/input/input-motionprediction/api/public_plus_experimental_current.txt b/input/input-motionprediction/api/public_plus_experimental_current.txt
index a2cdf99..6611119 100644
--- a/input/input-motionprediction/api/public_plus_experimental_current.txt
+++ b/input/input-motionprediction/api/public_plus_experimental_current.txt
@@ -1,11 +1,11 @@
 // Signature format: 4.0
 package androidx.input.motionprediction {
 
-  public interface MotionEventPredictor {
-    method public void dispose();
+  public interface MotionEventPredictor extends java.lang.AutoCloseable {
+    method public void close();
     method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
     method public android.view.MotionEvent? predict();
-    method public void recordMovement(android.view.MotionEvent);
+    method public void record(android.view.MotionEvent);
   }
 
 }
diff --git a/input/input-motionprediction/api/restricted_current.txt b/input/input-motionprediction/api/restricted_current.txt
index a2cdf99..6611119 100644
--- a/input/input-motionprediction/api/restricted_current.txt
+++ b/input/input-motionprediction/api/restricted_current.txt
@@ -1,11 +1,11 @@
 // Signature format: 4.0
 package androidx.input.motionprediction {
 
-  public interface MotionEventPredictor {
-    method public void dispose();
+  public interface MotionEventPredictor extends java.lang.AutoCloseable {
+    method public void close();
     method public static androidx.input.motionprediction.MotionEventPredictor newInstance(android.view.View);
     method public android.view.MotionEvent? predict();
-    method public void recordMovement(android.view.MotionEvent);
+    method public void record(android.view.MotionEvent);
   }
 
 }
diff --git a/input/input-motionprediction/build.gradle b/input/input-motionprediction/build.gradle
index 9475cdb..8d3d5e2 100644
--- a/input/input-motionprediction/build.gradle
+++ b/input/input-motionprediction/build.gradle
@@ -33,7 +33,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 16
+        minSdkVersion 19
     }
     namespace "androidx.input.motionprediction"
 }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index c27d293..31442ee 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -33,7 +33,7 @@
  * rendered on the display. Once no more predictions are needed, call {@link #dispose()} to stop it
  * and clean up resources.
  */
-public interface MotionEventPredictor {
+public interface MotionEventPredictor extends AutoCloseable {
     /**
      * Record a user's movement to the predictor. You should call this for every
      * {@link android.view.MotionEvent} that is received by the associated
@@ -41,7 +41,7 @@
      * @param event the {@link android.view.MotionEvent} the associated view received and that
      *              needs to be recorded.
      */
-    void recordMovement(@NonNull MotionEvent event);
+    void record(@NonNull MotionEvent event);
 
     /**
      * Compute a prediction
@@ -55,7 +55,8 @@
      * Notify the predictor that no more predictions are needed. Any subsequent call to
      * {@link #predict()} will return null.
      */
-    void dispose();
+    @Override
+    void close();
 
     /**
      * Create a new motion predictor associated to a specific {@link android.view.View}
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index e5abbd9..df88594 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -31,7 +31,7 @@
 @RestrictTo(LIBRARY)
 public class KalmanMotionEventPredictor implements MotionEventPredictor {
     private MultiPointerPredictor mMultiPointerPredictor;
-    private boolean mDisposed = false;
+    private boolean mClosed = false;
 
     public KalmanMotionEventPredictor() {
         mMultiPointerPredictor = new MultiPointerPredictor();
@@ -43,21 +43,21 @@
     }
 
     @Override
-    public void recordMovement(@NonNull MotionEvent event) {
+    public void record(@NonNull MotionEvent event) {
         mMultiPointerPredictor.onTouchEvent(event);
     }
 
     @Nullable
     @Override
     public MotionEvent predict() {
-        if (mDisposed) {
+        if (mClosed) {
             return null;
         }
         return mMultiPointerPredictor.predict();
     }
 
     @Override
-    public void dispose() {
-        mDisposed = true;
+    public void close() {
+        mClosed = true;
     }
 }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/InkPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
similarity index 97%
rename from input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/InkPredictor.java
rename to input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
index 98f4a44..f655aec 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/InkPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanPredictor.java
@@ -30,7 +30,7 @@
  * @hide
  */
 @RestrictTo(LIBRARY)
-public interface InkPredictor {
+public interface KalmanPredictor {
 
     /** Gets the current prediction target */
     int getPredictionTarget();
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 c97dd19..1b0b3cd 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
@@ -32,11 +32,11 @@
  * @hide
  */
 @RestrictTo(LIBRARY)
-public class MultiPointerPredictor implements InkPredictor {
+public class MultiPointerPredictor implements KalmanPredictor {
     private static final String TAG = "MultiPointerPredictor";
     private static final boolean DEBUG_PREDICTION = Log.isLoggable(TAG, Log.DEBUG);
 
-    private SparseArray<KalmanInkPredictor> mPredictorMap = new SparseArray<>();
+    private SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
     private int mPredictionTargetMs = 0;
     private int mReportRateMs = 0;
 
@@ -77,7 +77,7 @@
         int action = event.getActionMasked();
         int pointerId = event.getPointerId(event.getActionIndex());
         if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
-            KalmanInkPredictor predictor = new KalmanInkPredictor();
+            SinglePointerPredictor predictor = new SinglePointerPredictor();
             predictor.setPredictionTarget(mPredictionTargetMs);
             if (mReportRateMs > 0) {
                 predictor.setReportRate(mReportRateMs);
@@ -86,14 +86,14 @@
             predictor.onTouchEvent(event);
             mPredictorMap.put(pointerId, predictor);
         } else if (action == MotionEvent.ACTION_UP) {
-            KalmanInkPredictor predictor = mPredictorMap.get(pointerId);
+            SinglePointerPredictor predictor = mPredictorMap.get(pointerId);
             if (predictor != null) {
                 mPredictorMap.remove(pointerId);
                 predictor.onTouchEvent(event);
             }
             mPredictorMap.clear();
         } else if (action == MotionEvent.ACTION_POINTER_UP) {
-            KalmanInkPredictor predictor = mPredictorMap.get(pointerId);
+            SinglePointerPredictor predictor = mPredictorMap.get(pointerId);
             if (predictor != null) {
                 mPredictorMap.remove(pointerId);
                 predictor.onTouchEvent(event);
@@ -126,7 +126,7 @@
             return null;
         }
         if (pointerCount == 1) {
-            KalmanInkPredictor predictor = mPredictorMap.valueAt(0);
+            SinglePointerPredictor predictor = mPredictorMap.valueAt(0);
             MotionEvent predictedEv = predictor.predict();
             if (DEBUG_PREDICTION) {
                 Log.d(TAG, "predict() -> MotionEvent: " + predictedEv);
@@ -139,7 +139,7 @@
         MotionEvent[] singlePointerEvents = new MotionEvent[pointerCount];
         for (int i = 0; i < pointerCount; ++i) {
             pointerIds[i] = mPredictorMap.keyAt(i);
-            KalmanInkPredictor predictor = mPredictorMap.valueAt(i);
+            SinglePointerPredictor predictor = mPredictorMap.valueAt(i);
             singlePointerEvents[i] = predictor.predict();
             // If predictor consumer expect more sample, generate sample where position and
             // pressure are constant
@@ -161,7 +161,9 @@
 
         if (foundNullPrediction) {
             for (MotionEvent ev : singlePointerEvents) {
-                ev.recycle();
+                if (ev != null) {
+                    ev.recycle();
+                }
             }
             return null;
         }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PenKalmanFilter.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
similarity index 97%
rename from input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PenKalmanFilter.java
rename to input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
index a70b21e..6a80269 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PenKalmanFilter.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
@@ -29,7 +29,7 @@
  * @hide
  */
 @RestrictTo(LIBRARY)
-public class PenKalmanFilter {
+public class PointerKalmanFilter {
     private KalmanFilter mXKalman;
     private KalmanFilter mYKalman;
     private KalmanFilter mPKalman;
@@ -54,7 +54,7 @@
      * @param sigmaProcess lower value = more filtering
      * @param sigmaMeasurement higher value = more filtering
      */
-    public PenKalmanFilter(double sigmaProcess, double sigmaMeasurement) {
+    public PointerKalmanFilter(double sigmaProcess, double sigmaMeasurement) {
         mSigmaProcess = sigmaProcess;
         mSigmaMeasurement = sigmaMeasurement;
         mXKalman = createAxisKalmanFilter();
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanInkPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
similarity index 98%
rename from input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanInkPredictor.java
rename to input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
index 73c1f4a..0656734 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanInkPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/SinglePointerPredictor.java
@@ -34,7 +34,7 @@
  * @hide
  */
 @RestrictTo(LIBRARY)
-public class KalmanInkPredictor implements InkPredictor {
+public class SinglePointerPredictor implements KalmanPredictor {
     private static final String TAG = "KalmanInkPredictor";
 
     // Influence of jank during each prediction sample
@@ -67,7 +67,7 @@
     // The Kalman filter is tuned to smooth noise while maintaining fast reaction to direction
     // changes. The stronger the filter, the smoother the prediction result will be, at the
     // cost of possible prediction errors.
-    private final PenKalmanFilter mKalman = new PenKalmanFilter(0.01, 1.0);
+    private final PointerKalmanFilter mKalman = new PointerKalmanFilter(0.01, 1.0);
 
     private final DVector2 mLastPosition = new DVector2();
     private long mPrevEventTime;
@@ -93,7 +93,7 @@
      * achieving close-to-zero latency, prediction errors can be more visible and the target should
      * be reduced to 20ms.
      */
-    public KalmanInkPredictor() {
+    public SinglePointerPredictor() {
         mKalman.reset();
         mPrevEventTime = 0;
     }
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 2b1d466..f684502 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
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.tools.apicompiler
 
+import androidx.privacysandbox.tools.apicompiler.generator.SandboxApiVersion
 import androidx.privacysandbox.tools.apicompiler.generator.SdkCodeGenerator
 import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
 import com.google.devtools.ksp.processing.CodeGenerator
@@ -31,10 +32,10 @@
     private val logger: KSPLogger,
     private val codeGenerator: CodeGenerator,
     private val options: Map<String, String>,
-) :
-    SymbolProcessor {
+) : 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"
     }
 
     var invoked = false
@@ -51,9 +52,18 @@
             return emptyList()
         }
 
+        val target = if (options[USE_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase() == "true") {
+            SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
+        } else SandboxApiVersion.API_33
+
         val parsedApi = ApiParser(resolver, logger).parseApi()
 
-        SdkCodeGenerator(codeGenerator, parsedApi, path).generate()
+        SdkCodeGenerator(
+            codeGenerator,
+            parsedApi,
+            path,
+            target,
+        ).generate()
         return emptyList()
     }
 
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 856f8fd..25dd589 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
@@ -18,7 +18,6 @@
 
 import androidx.privacysandbox.tools.core.generator.build
 import androidx.privacysandbox.tools.core.generator.poetSpec
-import androidx.privacysandbox.tools.core.generator.stubDelegateNameSpec
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.getOnlyService
@@ -26,25 +25,14 @@
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.TypeSpec
 
-class AbstractSdkProviderGenerator(private val api: ParsedApi) {
+/** Generates an SDK provider that delegates calls to SDK defined classes. */
+internal abstract class AbstractSdkProviderGenerator(protected val api: ParsedApi) {
+
     companion object {
-        private val sandboxedSdkProviderClass =
-            ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkProviderCompat")
-        private val sandboxedSdkClass =
-            ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkCompat")
-        private val sandboxedSdkCreateMethod =
-            MemberName(
-                ClassName(
-                    sandboxedSdkClass.packageName,
-                    sandboxedSdkClass.simpleName,
-                    "Companion"
-                ), "create"
-            )
-        private val contextClass = ClassName("android.content", "Context")
-        private val bundleClass = ClassName("android.os", "Bundle")
+        val contextClass = ClassName("android.content", "Context")
+        val bundleClass = ClassName("android.os", "Bundle")
         private val viewClass = ClassName("android.view", "View")
     }
 
@@ -56,7 +44,7 @@
         val className = "AbstractSandboxedSdkProvider"
         val classSpec =
             TypeSpec.classBuilder(className)
-                .superclass(sandboxedSdkProviderClass)
+                .superclass(superclassName)
                 .addModifiers(KModifier.ABSTRACT)
                 .addFunction(generateOnLoadSdkFunction())
                 .addFunction(generateGetViewFunction())
@@ -67,21 +55,11 @@
             .build()
     }
 
-    private fun generateOnLoadSdkFunction(): FunSpec {
-        return FunSpec.builder("onLoadSdk").build {
-            addModifiers(KModifier.OVERRIDE)
-            addParameter("params", bundleClass)
-            returns(sandboxedSdkClass)
-            addStatement(
-                "val sdk = ${getCreateServiceFunctionName(api.getOnlyService())}(context!!)"
-            )
-            addStatement(
-                "return %M(%T(sdk))",
-                sandboxedSdkCreateMethod,
-                api.getOnlyService().stubDelegateNameSpec()
-            )
-        }
-    }
+    abstract val superclassName: ClassName
+    abstract fun generateOnLoadSdkFunction(): FunSpec
+
+    protected fun createServiceFunctionName(service: AnnotatedInterface) =
+        "create${service.type.simpleName}"
 
     private fun generateGetViewFunction(): FunSpec {
         return FunSpec.builder("getView").build {
@@ -96,13 +74,10 @@
     }
 
     private fun generateCreateServiceFunction(service: AnnotatedInterface): FunSpec {
-        return FunSpec.builder(getCreateServiceFunctionName(service))
+        return FunSpec.builder(createServiceFunctionName(service))
             .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
             .addParameter("context", contextClass)
             .returns(service.type.poetSpec())
             .build()
     }
-
-    private fun getCreateServiceFunctionName(service: AnnotatedInterface) =
-        "create${service.type.simpleName}"
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/Api33SdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/Api33SdkProviderGenerator.kt
new file mode 100644
index 0000000..d22bbda
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/Api33SdkProviderGenerator.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.generator
+
+import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.stubDelegateNameSpec
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.getOnlyService
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+
+/** SDK provider generator that uses Android API 33 to communicate with the Privacy Sandbox. */
+internal class Api33SdkProviderGenerator(parsedApi: ParsedApi) :
+    AbstractSdkProviderGenerator(parsedApi) {
+    companion object {
+        private val sandboxedSdkClass =
+            ClassName("android.app.sdksandbox", "SandboxedSdk")
+    }
+
+    override val superclassName =
+        ClassName("android.app.sdksandbox", "SandboxedSdkProvider")
+
+    override fun generateOnLoadSdkFunction() = FunSpec.builder("onLoadSdk").build {
+        addModifiers(KModifier.OVERRIDE)
+        addParameter("params", bundleClass)
+        returns(sandboxedSdkClass)
+        addStatement(
+            "val sdk = ${createServiceFunctionName(api.getOnlyService())}(context!!)"
+        )
+        addStatement(
+            "return %T(%T(sdk))",
+            sandboxedSdkClass,
+            api.getOnlyService().stubDelegateNameSpec()
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.kt
new file mode 100644
index 0000000..9934deb
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/CompatSdkProviderGenerator.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.privacysandbox.tools.apicompiler.generator
+
+import androidx.privacysandbox.tools.core.generator.build
+import androidx.privacysandbox.tools.core.generator.stubDelegateNameSpec
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.getOnlyService
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.MemberName
+
+/** SDK Provider generator that uses the SDK Runtime library to communicate with the sandbox. */
+internal class CompatSdkProviderGenerator(parsedApi: ParsedApi) :
+    AbstractSdkProviderGenerator(parsedApi) {
+    companion object {
+        private val sandboxedSdkCompatClass =
+            ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkCompat")
+        private val sandboxedSdkCompatCreateMethod =
+            MemberName(
+                ClassName(
+                    sandboxedSdkCompatClass.packageName,
+                    sandboxedSdkCompatClass.simpleName,
+                    "Companion"
+                ), "create"
+            )
+    }
+
+    override val superclassName =
+        ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkProviderCompat")
+
+    override fun generateOnLoadSdkFunction() = FunSpec.builder("onLoadSdk").build {
+        addModifiers(KModifier.OVERRIDE)
+        addParameter("params", bundleClass)
+        returns(sandboxedSdkCompatClass)
+        addStatement(
+            "val sdk = ${createServiceFunctionName(api.getOnlyService())}(context!!)"
+        )
+        addStatement(
+            "return %M(%T(sdk))",
+            sandboxedSdkCompatCreateMethod,
+            api.getOnlyService().stubDelegateNameSpec()
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index 0d1b157..f278f79 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -36,10 +36,19 @@
 import kotlin.io.path.extension
 import kotlin.io.path.nameWithoutExtension
 
-class SdkCodeGenerator(
+internal enum class SandboxApiVersion {
+    // Android 13 - Tiramisu Privacy Sandbox
+    API_33,
+
+    // SDK runtime backwards compatibility library.
+    SDK_RUNTIME_COMPAT_LIBRARY,
+}
+
+internal class SdkCodeGenerator(
     private val codeGenerator: CodeGenerator,
     private val api: ParsedApi,
     private val aidlCompilerPath: Path,
+    private val sandboxApiVersion: SandboxApiVersion
 ) {
     private val binderCodeConverter = ServerBinderCodeConverter(api)
 
@@ -77,7 +86,11 @@
     }
 
     private fun generateAbstractSdkProvider() {
-        AbstractSdkProviderGenerator(api).generate()?.also(::write)
+        val generator = when (sandboxApiVersion) {
+            SandboxApiVersion.API_33 -> Api33SdkProviderGenerator(api)
+            SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY -> CompatSdkProviderGenerator(api)
+        }
+        generator.generate()?.also(::write)
     }
 
     private fun generateStubDelegates() {
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
new file mode 100644
index 0000000..78f636c
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import java.io.File
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** Test the Privacy Sandbox API Compiler with an SDK that uses all available features. */
+@RunWith(JUnit4::class)
+class FullFeaturedSdkTest {
+
+    @Test
+    fun compileServiceInterface_ok() {
+        val inputTestDataDir = File("src/test/test-data/fullfeaturedsdk/input")
+        val outputTestDataDir = File("src/test/test-data/fullfeaturedsdk/output")
+        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
+        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
+
+        val result = compileWithPrivacySandboxKspCompiler(inputSources)
+        assertThat(result).succeeds()
+
+        val expectedAidlFilepath = listOf(
+            "com/mysdk/ICancellationSignal.java",
+            "com/mysdk/IMyCallback.java",
+            "com/mysdk/IMyInterface.java",
+            "com/mysdk/IMyInterfaceTransactionCallback.java",
+            "com/mysdk/IMySdk.java",
+            "com/mysdk/IMySecondInterface.java",
+            "com/mysdk/IMySecondInterfaceTransactionCallback.java",
+            "com/mysdk/IResponseTransactionCallback.java",
+            "com/mysdk/IStringTransactionCallback.java",
+            "com/mysdk/IUnitTransactionCallback.java",
+            "com/mysdk/ParcelableRequest.java",
+            "com/mysdk/ParcelableResponse.java",
+            "com/mysdk/ParcelableStackFrame.java",
+            "com/mysdk/PrivacySandboxThrowableParcel.java",
+        )
+        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
+            expectedKotlinSources,
+            expectedAidlFilepath
+        )
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 2aaaadc..77d6c2d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -18,72 +18,22 @@
 
 import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
 import androidx.privacysandbox.tools.testing.resourceOutputDir
 import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
-import androidx.room.compiler.processing.util.compiler.compile
 import com.google.common.truth.Truth.assertThat
-import java.io.File
-import java.nio.file.Files
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
 class PrivacySandboxKspCompilerTest {
-    @Test
-    fun compileServiceInterface_ok() {
-        val inputTestDataDir = File("src/test/test-data/testinterface/input")
-        val outputTestDataDir = File("src/test/test-data/testinterface/output")
-        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
-        val provider = PrivacySandboxKspCompiler.Provider()
-
-        val result = compileAll(
-            inputSources,
-            symbolProcessorProviders = listOf(provider),
-            processorOptions = getProcessorOptions(),
-        )
-        assertThat(result).succeeds()
-
-        val expectedAidlFilepath = listOf(
-            "com/mysdk/ICancellationSignal.java",
-            "com/mysdk/IMyCallback.java",
-            "com/mysdk/IMyInterface.java",
-            "com/mysdk/IMyInterfaceTransactionCallback.java",
-            "com/mysdk/IMySdk.java",
-            "com/mysdk/IMySecondInterface.java",
-            "com/mysdk/IMySecondInterfaceTransactionCallback.java",
-            "com/mysdk/IResponseTransactionCallback.java",
-            "com/mysdk/IStringTransactionCallback.java",
-            "com/mysdk/IUnitTransactionCallback.java",
-            "com/mysdk/ParcelableRequest.java",
-            "com/mysdk/ParcelableResponse.java",
-            "com/mysdk/ParcelableStackFrame.java",
-            "com/mysdk/PrivacySandboxThrowableParcel.java",
-        )
-        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
-            expectedKotlinSources,
-            expectedAidlFilepath
-        )
-    }
 
     @Test
     fun compileEmpty_ok() {
-        val provider = PrivacySandboxKspCompiler.Provider()
-        // Check that compilation is successful
-        assertThat(
-            compile(
-                Files.createTempDirectory("test").toFile(),
-                TestCompilationArguments(
-                    sources = emptyList(),
-                    symbolProcessorProviders = listOf(provider),
-                    processorOptions = getProcessorOptions(),
-                )
-            )
-        ).hasNoGeneratedSourceFiles()
+        assertThat(compileWithPrivacySandboxKspCompiler(listOf())).apply {
+            succeeds()
+            hasNoGeneratedSourceFiles()
+        }
     }
 
     @Test
@@ -100,13 +50,7 @@
                     }
                 """
             )
-        val provider = PrivacySandboxKspCompiler.Provider()
-        val compilationResult =
-            compileAll(
-                listOf(source),
-                symbolProcessorProviders = listOf(provider),
-                processorOptions = getProcessorOptions(),
-            )
+        val compilationResult = compileWithPrivacySandboxKspCompiler(listOf(source))
         assertThat(compilationResult).succeeds()
 
         val resourceMap = compilationResult.resourceOutputDir.walk()
@@ -138,20 +82,6 @@
                     }
                 """
             )
-        val provider = PrivacySandboxKspCompiler.Provider()
-        // Check that compilation fails
-        assertThat(
-            compileAll(
-                listOf(source),
-                symbolProcessorProviders = listOf(provider),
-                processorOptions = getProcessorOptions(),
-            )
-        ).fails()
+        assertThat(compileWithPrivacySandboxKspCompiler(listOf(source))).fails()
     }
-
-    private fun getProcessorOptions() =
-        mapOf(
-            "aidl_compiler_path" to (System.getProperty("aidl_compiler_path")
-                ?: throw IllegalArgumentException("aidl_compiler_path flag not set."))
-        )
 }
\ No newline at end of file
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
new file mode 100644
index 0000000..cda0661
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.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.privacysandbox.tools.apicompiler
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import java.io.File
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SdkRuntimeLibrarySdkTest {
+    @Test
+    fun compileServiceInterface_ok() {
+        val inputTestDataDir = File("src/test/test-data/sdkruntimelibrarysdk/input")
+        val outputTestDataDir = File("src/test/test-data/sdkruntimelibrarysdk/output")
+        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")
+        )
+        assertThat(result).succeeds()
+
+        val expectedAidlFilepath = listOf(
+            "com/mysdk/ICancellationSignal.java",
+            "com/mysdk/IBackwardsCompatibleSdk.java",
+            "com/mysdk/IStringTransactionCallback.java",
+            "com/mysdk/ParcelableStackFrame.java",
+            "com/mysdk/PrivacySandboxThrowableParcel.java",
+        )
+        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
+            expectedKotlinSources,
+            expectedAidlFilepath
+        )
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..5de917b
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.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.privacysandbox.tools.apicompiler
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+
+/**
+ * Compile the given sources using the PrivacySandboxKspCompiler.
+ *
+ * Default parameters will set required options like AIDL compiler path and use the latest
+ * Android platform API stubs that support the Privacy Sandbox.
+ */
+fun compileWithPrivacySandboxKspCompiler(
+    sources: List<Source>,
+    platformStubs: PlatformStubs = PlatformStubs.API_33,
+    extraProcessorOptions: Map<String, String> = mapOf(),
+): TestCompilationResult {
+    val provider = PrivacySandboxKspCompiler.Provider()
+
+    val processorOptions = buildMap {
+        val aidlPath = (System.getProperty("aidl_compiler_path")
+            ?: throw IllegalArgumentException("aidl_compiler_path flag not set."))
+        put("aidl_compiler_path", aidlPath)
+        putAll(extraProcessorOptions)
+    }
+
+    return CompilationTestHelper.compileAll(
+        sources + platformStubs.sources,
+        symbolProcessorProviders = listOf(provider),
+        processorOptions = processorOptions,
+    )
+}
+
+enum class PlatformStubs(val sources: List<Source>) {
+    API_33(syntheticApi33PrivacySandboxStubs),
+    SDK_RUNTIME_LIBRARY(syntheticSdkRuntimeLibraryStubs),
+}
+
+// SDK Runtime library is not available in AndroidX prebuilts, so while that's the case we use fake
+// stubs to run our compilation tests.
+private val syntheticSdkRuntimeLibraryStubs = listOf(
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
+        |
+        |import android.os.IBinder
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |sealed class SandboxedSdkCompat {
+        |    abstract fun getInterface(): IBinder?
+        |
+        |    companion object {
+        |        fun create(binder: IBinder): SandboxedSdkCompat = throw RuntimeException("Stub!")
+        |    }
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
+        |package androidx.privacysandbox.sdkruntime.core
+        |
+        |import android.content.Context
+        |import android.os.Bundle
+        |import android.view.View
+        |
+        |@Suppress("UNUSED_PARAMETER")
+        |abstract class SandboxedSdkProviderCompat {
+        |   var context: Context? = null
+        |       private set
+        |   fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
+        |
+        |   abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+        |
+        |   open fun beforeUnloadSdk() {}
+        |
+        |   abstract fun getView(
+        |       windowContext: Context,
+        |       params: Bundle,
+        |       width: Int,
+        |       height: Int
+        |   ): View
+        |}
+        |""".trimMargin()
+    )
+)
+
+// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
+// while that's the case we use fake stubs to run our compilation tests.
+val syntheticApi33PrivacySandboxStubs = listOf(
+    Source.java(
+        "android.app.sdksandbox.SandboxedSdk", """
+        |package android.app.sdksandbox;
+        |
+        |import android.os.IBinder;
+        |
+        |public final class SandboxedSdk {
+        |    public SandboxedSdk(IBinder sdkInterface) {}
+        |    public IBinder getInterface() { throw new RuntimeException("Stub!"); }
+        |}
+        |""".trimMargin()
+    ),
+    Source.java(
+        "android.app.sdksandbox.SandboxedSdkProvider", """
+        |package android.app.sdksandbox;
+        |
+        |import android.content.Context;
+        |import android.os.Bundle;
+        |import android.view.View;
+        |
+        |public abstract class SandboxedSdkProvider {
+        |    public final void attachContext(Context context) {
+        |        throw new RuntimeException("Stub!");
+        |    }
+        |    public final Context getContext() {
+        |        throw new RuntimeException("Stub!");
+        |    }
+        |    public abstract SandboxedSdk onLoadSdk(Bundle params)
+        |        throws LoadSdkException;
+        |
+        |    public void beforeUnloadSdk() {}
+        |
+        |    public abstract View getView(
+        |        Context windowContext, Bundle params, int width, int height);
+        |}
+        |""".trimMargin()
+    ),
+    Source.java(
+        "android.app.sdksandbox.LoadSdkException", """
+        |package android.app.sdksandbox;
+        |
+        |@SuppressWarnings("serial")
+        |public final class LoadSdkException extends Exception {}
+        |""".trimMargin()
+    ),
+    Source.java(
+        "android.app.sdksandbox.SandboxedSdkContext", """
+        |package android.app.sdksandbox;
+        |
+        |public final class SandboxedSdkContext {}
+        |""".trimMargin()
+    ),
+)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
new file mode 100644
index 0000000..43c54ac
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
@@ -0,0 +1,26 @@
+package com.mysdk
+
+import android.app.sdksandbox.SandboxedSdk
+import android.app.sdksandbox.SandboxedSdkProvider
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import kotlin.Int
+
+public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
+  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
+    val sdk = createMySdk(context!!)
+    return SandboxedSdk(MySdkStubDelegate(sdk))
+  }
+
+  public override fun getView(
+    windowContext: Context,
+    params: Bundle,
+    width: Int,
+    height: Int,
+  ): View {
+    TODO("Implement")
+  }
+
+  protected abstract fun createMySdk(context: Context): MySdk
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyCallbackClientProxy.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyCallbackClientProxy.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyInterfaceStubDelegate.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MyInterfaceStubDelegate.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySecondInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySecondInterfaceStubDelegate.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySecondInterfaceStubDelegate.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestConverter.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/ResponseConverter.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/ResponseConverter.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/TransportCancellationCallback.kt
similarity index 100%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/TransportCancellationCallback.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt
new file mode 100644
index 0000000..8424489
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/input/com/mysdk/BackwardsCompatibleSdk.kt
@@ -0,0 +1,11 @@
+package com.mysdk
+
+import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.PrivacySandboxValue
+
+@PrivacySandboxService
+interface BackwardsCompatibleSdk {
+    suspend fun doStuff(x: Int, y: Int): String
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
similarity index 75%
rename from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
rename to privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
index 6efe8a0..5a0d588 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/AbstractSandboxedSdkProvider.kt
@@ -10,8 +10,8 @@
 
 public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProviderCompat() {
   public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
-    val sdk = createMySdk(context!!)
-    return create(MySdkStubDelegate(sdk))
+    val sdk = createBackwardsCompatibleSdk(context!!)
+    return create(BackwardsCompatibleSdkStubDelegate(sdk))
   }
 
   public override fun getView(
@@ -23,5 +23,5 @@
     TODO("Implement")
   }
 
-  protected abstract fun createMySdk(context: Context): MySdk
+  protected abstract fun createBackwardsCompatibleSdk(context: Context): BackwardsCompatibleSdk
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt
new file mode 100644
index 0000000..bca930d
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/BackwardsCompatibleSdkStubDelegate.kt
@@ -0,0 +1,32 @@
+package com.mysdk
+
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
+import kotlin.Int
+import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+public class BackwardsCompatibleSdkStubDelegate internal constructor(
+  public val `delegate`: BackwardsCompatibleSdk,
+) : IBackwardsCompatibleSdk.Stub() {
+  public override fun doStuff(
+    x: Int,
+    y: Int,
+    transactionCallback: IStringTransactionCallback,
+  ): Unit {
+    @OptIn(DelicateCoroutinesApi::class)
+    val job = GlobalScope.launch(Dispatchers.Main) {
+      try {
+        val result = delegate.doStuff(x, y)
+        transactionCallback.onSuccess(result)
+      }
+      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/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
similarity index 100%
copy from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
copy to privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt
similarity index 100%
copy from privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/TransportCancellationCallback.kt
copy to privacysandbox/tools/tools-apicompiler/src/test/test-data/sdkruntimelibrarysdk/output/com/mysdk/TransportCancellationCallback.kt
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index 10a9cad..d29527c 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -132,7 +132,7 @@
             |interface Valid
         """.trimMargin()
         )
-        val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+        val sdkClasspath = compileAll(listOf(source))
             .outputClasspath.first().toPath()
 
         val metadataPath = sdkClasspath.resolve(Metadata.filePath).also {
@@ -186,7 +186,7 @@
             |interface Valid
         """.trimMargin()
         )
-        val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+        val sdkClasspath = compileAll(listOf(source))
             .outputClasspath.first().toPath()
         val descriptorPathThatAlreadyExists =
             makeTestDirectory().resolve("sdk-descriptors.jar").also {
@@ -201,7 +201,7 @@
 
     /** Compiles the given source file and returns a classpath with the results. */
     private fun compileAndReturnUnzippedPackagedClasspath(source: Source): File {
-        val result = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+        val result = compileAll(listOf(source))
         assertThat(result).succeeds()
         assertThat(result.outputClasspath).hasSize(1)
 
@@ -234,7 +234,6 @@
         return compileAll(
             listOf(source),
             extraClasspath = listOf(extraClasspath),
-            includePrivacySandboxPlatformSources = false
         )
     }
 }
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index f2a5114..0a9c958 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -39,18 +39,14 @@
     fun compileAll(
         sources: List<Source>,
         extraClasspath: List<File> = emptyList(),
-        includePrivacySandboxPlatformSources: Boolean = true,
         symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
         processorOptions: Map<String, String> = emptyMap()
     ): TestCompilationResult {
         val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
-        val targetSources = if (includePrivacySandboxPlatformSources) {
-            sources + syntheticPrivacySandboxSources
-        } else sources
         return compile(
             tempDir,
             TestCompilationArguments(
-                sources = targetSources,
+                sources = sources,
                 classpath = extraClasspath,
                 symbolProcessorProviders = symbolProcessorProviders,
                 processorOptions = processorOptions,
@@ -154,73 +150,3 @@
         }
     }
 }
-
-// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
-// while that's the case we use fake stubs to run our compilation tests.
-val syntheticPrivacySandboxSources = listOf(
-    Source.kotlin(
-        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
-        |package androidx.privacysandbox.sdkruntime.core
-        |
-        |import android.os.IBinder
-        |
-        |@Suppress("UNUSED_PARAMETER")
-        |sealed class SandboxedSdkCompat {
-        |    abstract fun getInterface(): IBinder?
-        |
-        |    companion object {
-        |        fun create(binder: IBinder): SandboxedSdkCompat = throw RuntimeException("Stub!")
-        |    }
-        |}
-        |""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt", """
-        |package androidx.privacysandbox.sdkruntime.client
-        |
-        |import android.content.Context
-        |import android.os.Bundle
-        |import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-        |
-        |@Suppress("UNUSED_PARAMETER")
-        |class SdkSandboxManagerCompat private constructor() {
-        |    suspend fun loadSdk(
-        |        sdkName: String,
-        |        params: Bundle,
-        |    ): SandboxedSdkCompat = throw RuntimeException("Stub!")
-        |
-        |    companion object {
-        |        fun obtain(context: Context): SdkSandboxManagerCompat =
-        |            throw RuntimeException("Stub!")
-        |    }
-        |}
-        |""".trimMargin()
-    ),
-    Source.kotlin(
-        "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
-        |package androidx.privacysandbox.sdkruntime.core
-        |
-        |import android.content.Context
-        |import android.os.Bundle
-        |import android.view.View
-        |
-        |@Suppress("UNUSED_PARAMETER")
-        |abstract class SandboxedSdkProviderCompat {
-        |   var context: Context? = null
-        |       private set
-        |   fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
-        |
-        |   abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
-        |
-        |   open fun beforeUnloadSdk() {}
-        |
-        |   abstract fun getView(
-        |       windowContext: Context,
-        |       params: Bundle,
-        |       width: Int,
-        |       height: Int
-        |   ): View
-        |}
-        |""".trimMargin()
-    )
-)
diff --git a/settings.gradle b/settings.gradle
index 78a99ac..94f37a7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -657,9 +657,13 @@
 includeProject(":glance:glance-appwidget-preview", [BuildType.GLANCE])
 includeProject(":glance:glance-appwidget-proto", [BuildType.GLANCE])
 includeProject(":glance:glance-appwidget:integration-tests:demos", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark", [BuildType.GLANCE])
+includeProject(":glance:glance-appwidget:integration-tests:macrobenchmark-target", [BuildType.GLANCE])
 includeProject(":glance:glance-appwidget:integration-tests:template-demos", [BuildType.GLANCE])
 includeProject(":glance:glance-appwidget:glance-layout-generator", [BuildType.GLANCE])
 includeProject(":glance:glance-preview", [BuildType.GLANCE])
+includeProject(":glance:glance-material", [BuildType.GLANCE])
+includeProject(":glance:glance-material3", [BuildType.GLANCE])
 includeProject(":glance:glance-wear-tiles:integration-tests:demos", [BuildType.GLANCE])
 includeProject(":glance:glance-wear-tiles:integration-tests:template-demos", [BuildType.GLANCE])
 includeProject(":glance:glance-wear-tiles", [BuildType.GLANCE])
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiCollection.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiCollection.java
index 40b8b2a..5322d2a 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiCollection.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiCollection.java
@@ -50,7 +50,6 @@
     @NonNull
     public UiObject getChildByDescription(@NonNull UiSelector childPattern, @NonNull String text)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text);
         if (text != null) {
             int count = getChildCount(childPattern);
             for (int x = 0; x < count; x++) {
@@ -84,7 +83,6 @@
     @NonNull
     public UiObject getChildByInstance(@NonNull UiSelector childPattern, int instance)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, instance);
         UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
                 UiSelector.patternBuilder(childPattern).instance(instance));
         return new UiObject(patternSelector);
@@ -108,7 +106,6 @@
     @NonNull
     public UiObject getChildByText(@NonNull UiSelector childPattern, @NonNull String text)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text);
         if (text != null) {
             int count = getChildCount(childPattern);
             for (int x = 0; x < count; x++) {
@@ -137,7 +134,6 @@
      * @return the number of matched childPattern under the current {@link UiCollection}
      */
     public int getChildCount(@NonNull UiSelector childPattern) {
-        Tracer.trace(childPattern);
         UiSelector patternSelector =
                 UiSelector.patternBuilder(getSelector(), UiSelector.patternBuilder(childPattern));
         return getQueryController().getPatternCount(patternSelector);
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 58e73a7..db7e3d3 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -37,7 +37,8 @@
  * be reused for different views that match the selector criteria.
  */
 public class UiObject {
-    private static final String LOG_TAG = UiObject.class.getSimpleName();
+    private static final String TAG = UiObject.class.getSimpleName();
+
     /** @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)} */
     @Deprecated
     protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000;
@@ -86,7 +87,6 @@
      */
     @NonNull
     public final UiSelector getSelector() {
-        Tracer.trace();
         if (mUiSelector == null) {
             throw new IllegalStateException("UiSelector not set");
         }
@@ -125,7 +125,6 @@
      */
     @NonNull
     public UiObject getChild(@NonNull UiSelector selector) throws UiObjectNotFoundException {
-        Tracer.trace(selector);
         return new UiObject(getSelector().childSelector(selector));
     }
 
@@ -139,7 +138,6 @@
      */
     @NonNull
     public UiObject getFromParent(@NonNull UiSelector selector) throws UiObjectNotFoundException {
-        Tracer.trace(selector);
         return new UiObject(getSelector().fromParent(selector));
     }
 
@@ -150,7 +148,6 @@
      * @throws UiObjectNotFoundException
      */
     public int getChildCount() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -200,6 +197,8 @@
     public boolean dragTo(@NonNull UiObject destObj, int steps) throws UiObjectNotFoundException {
         Rect srcRect = getVisibleBounds();
         Rect dstRect = destObj.getVisibleBounds();
+        Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.",
+                srcRect.centerX(), srcRect.centerY(), dstRect.centerX(), dstRect.centerY(), steps));
         return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(),
                 dstRect.centerX(), dstRect.centerY(), steps, true);
     }
@@ -218,6 +217,8 @@
      */
     public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException {
         Rect srcRect = getVisibleBounds();
+        Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.",
+                srcRect.centerX(), srcRect.centerY(), destX, destY, steps));
         return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY,
                 steps, true);
     }
@@ -238,10 +239,15 @@
      * @throws UiObjectNotFoundException
      */
     public boolean swipeUp(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
         Rect rect = getVisibleBounds();
-        if(rect.height() <= SWIPE_MARGIN_LIMIT * 2)
-            return false; // too small to swipe
+        if (rect.height() <= SWIPE_MARGIN_LIMIT * 2) {
+            Log.w(TAG, String.format("Cannot swipe. Object height too small (%d < %d).",
+                    rect.height(), SWIPE_MARGIN_LIMIT * 2));
+            return false;
+        }
+        Log.d(TAG, String.format("Swiping up from (%d, %d) to (%d, %d) in %d steps.",
+                rect.centerX(), rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(),
+                rect.top + SWIPE_MARGIN_LIMIT, steps));
         return getInteractionController().swipe(rect.centerX(),
                 rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT,
                 steps);
@@ -265,10 +271,15 @@
      * @throws UiObjectNotFoundException
      */
     public boolean swipeDown(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
         Rect rect = getVisibleBounds();
-        if(rect.height() <= SWIPE_MARGIN_LIMIT * 2)
-            return false; // too small to swipe
+        if (rect.height() <= SWIPE_MARGIN_LIMIT * 2) {
+            Log.w(TAG, String.format("Cannot swipe. Object height too small (%d < %d).",
+                    rect.height(), SWIPE_MARGIN_LIMIT * 2));
+            return false;
+        }
+        Log.d(TAG, String.format("Swiping down from (%d, %d) to (%d, %d) in %d steps.",
+                rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(),
+                rect.bottom - SWIPE_MARGIN_LIMIT, steps));
         return getInteractionController().swipe(rect.centerX(),
                 rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(),
                 rect.bottom - SWIPE_MARGIN_LIMIT, steps);
@@ -292,10 +303,15 @@
      * @throws UiObjectNotFoundException
      */
     public boolean swipeLeft(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
         Rect rect = getVisibleBounds();
-        if(rect.width() <= SWIPE_MARGIN_LIMIT * 2)
-            return false; // too small to swipe
+        if (rect.width() <= SWIPE_MARGIN_LIMIT * 2) {
+            Log.w(TAG, String.format("Cannot swipe. Object width too small (%d < %d).",
+                    rect.width(), SWIPE_MARGIN_LIMIT * 2));
+            return false;
+        }
+        Log.d(TAG, String.format("Swiping left from (%d, %d) to (%d, %d) in %d steps.",
+                rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT,
+                rect.centerY(), steps));
         return getInteractionController().swipe(rect.right - SWIPE_MARGIN_LIMIT,
                 rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
     }
@@ -318,10 +334,15 @@
      * @throws UiObjectNotFoundException
      */
     public boolean swipeRight(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
         Rect rect = getVisibleBounds();
-        if(rect.width() <= SWIPE_MARGIN_LIMIT * 2)
-            return false; // too small to swipe
+        if (rect.width() <= SWIPE_MARGIN_LIMIT * 2) {
+            Log.w(TAG, String.format("Cannot swipe. Object width too small (%d < %d).",
+                    rect.width(), SWIPE_MARGIN_LIMIT * 2));
+            return false;
+        }
+        Log.d(TAG, String.format("Swiping right from (%d, %d) to (%d, %d) in %d steps.",
+                rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT,
+                rect.centerY(), steps));
         return getInteractionController().swipe(rect.left + SWIPE_MARGIN_LIMIT,
                 rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
     }
@@ -386,12 +407,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean click() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Clicking on (%d, %d).", rect.centerX(), rect.centerY()));
         return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(),
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -405,7 +426,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException {
-        Tracer.trace();
         return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT);
     }
 
@@ -426,12 +446,14 @@
      * @throws UiObjectNotFoundException
      */
     public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException {
-        Tracer.trace(timeout);
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG,
+                String.format("Clicking on (%d, %d) and waiting %dms for new window.",
+                        rect.centerX(), rect.centerY(), timeout));
         return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(),
                 timeout);
     }
@@ -443,12 +465,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean clickTopLeft() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Clicking on (%d, %d).", rect.left + 5, rect.top + 5));
         return getInteractionController().clickAndSync(rect.left + 5, rect.top + 5,
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -460,12 +482,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean longClickBottomRight() throws UiObjectNotFoundException  {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.right - 5, rect.bottom - 5));
         return getInteractionController().longTapAndSync(rect.right - 5, rect.bottom - 5,
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -477,12 +499,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean clickBottomRight() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Clicking on (%d, %d).", rect.right - 5, rect.bottom - 5));
         return getInteractionController().clickAndSync(rect.right - 5, rect.bottom - 5,
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -494,12 +516,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean longClick() throws UiObjectNotFoundException  {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.centerX(), rect.centerY()));
         return getInteractionController().longTapAndSync(rect.centerX(), rect.centerY(),
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -511,12 +533,12 @@
      * @throws UiObjectNotFoundException
      */
     public boolean longClickTopLeft() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
         Rect rect = getVisibleBounds(node);
+        Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.left + 5, rect.top + 5));
         return getInteractionController().longTapAndSync(rect.left + 5, rect.top + 5,
                 mConfig.getActionAcknowledgmentTimeout());
     }
@@ -529,14 +551,11 @@
      */
     @NonNull
     public String getText() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
-        String retVal = safeStringReturn(node.getText());
-        Log.d(LOG_TAG, String.format("getText() = %s", retVal));
-        return retVal;
+        return safeStringReturn(node.getText());
     }
 
     /**
@@ -547,14 +566,11 @@
      */
     @NonNull
     public String getClassName() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
         }
-        String retVal = safeStringReturn(node.getClassName());
-        Log.d(LOG_TAG, String.format("getClassName() = %s", retVal));
-        return retVal;
+        return safeStringReturn(node.getClassName());
     }
 
     /**
@@ -565,7 +581,6 @@
      */
     @NonNull
     public String getContentDescription() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -582,12 +597,12 @@
         if (text == null) {
             text = "";
         }
-        Tracer.trace(text);
         // long click left + center
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if (node == null) {
             throw new UiObjectNotFoundException(getSelector().toString());
         }
+        Log.d(TAG, String.format("Setting text to '%s'.", text));
         Rect rect = getVisibleBounds(node);
         getInteractionController().longTapNoSync(rect.left + 20, rect.centerY());
         // check if the edit menu is open
@@ -631,7 +646,6 @@
         if (text == null) {
             text = "";
         }
-        Tracer.trace(text);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
             // ACTION_SET_TEXT is added in API 21.
             AccessibilityNodeInfo node = findAccessibilityNodeInfo(
@@ -639,11 +653,13 @@
             if (node == null) {
                 throw new UiObjectNotFoundException(getSelector().toString());
             }
+            Log.d(TAG, String.format("Setting text to '%s'.", text));
             Bundle args = new Bundle();
             args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
             return node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
         } else {
             clearTextField();
+            Log.d(TAG, String.format("Setting text to '%s'.", text));
             return getInteractionController().sendText(text);
         }
     }
@@ -659,7 +675,6 @@
      * @throws UiObjectNotFoundException
      */
     public void clearTextField() throws UiObjectNotFoundException {
-        Tracer.trace();
         // long click left + center
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
@@ -671,6 +686,7 @@
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                 setText("");
             } else {
+                Log.d(TAG, "Setting text to ''.");
                 Bundle selectionArgs = new Bundle();
                 // select all of the existing text
                 selectionArgs.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 0);
@@ -678,11 +694,11 @@
                         text.length());
                 boolean ret = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
                 if (!ret) {
-                    Log.w(LOG_TAG, "ACTION_FOCUS on text field failed.");
+                    Log.w(TAG, "ACTION_FOCUS on text field failed.");
                 }
                 ret = node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, selectionArgs);
                 if (!ret) {
-                    Log.w(LOG_TAG, "ACTION_SET_SELECTION on text field failed.");
+                    Log.w(TAG, "ACTION_SET_SELECTION on text field failed.");
                 }
                 // now delete all
                 getInteractionController().sendKey(KeyEvent.KEYCODE_DEL, 0);
@@ -696,7 +712,6 @@
      * @return true if it is else false
      */
     public boolean isChecked() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -711,7 +726,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isSelected() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -726,7 +740,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isCheckable() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -741,7 +754,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isEnabled() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -756,7 +768,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isClickable() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -771,7 +782,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isFocused() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -786,7 +796,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isFocusable() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -801,7 +810,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isScrollable() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -816,7 +824,6 @@
      * @throws UiObjectNotFoundException
      */
     public boolean isLongClickable() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -832,7 +839,6 @@
      */
     @NonNull
     public String getPackageName() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -852,7 +858,6 @@
      */
     @NonNull
     public Rect getVisibleBounds() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -868,7 +873,6 @@
      */
     @NonNull
     public Rect getBounds() throws UiObjectNotFoundException {
-        Tracer.trace();
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
         if(node == null) {
             throw new UiObjectNotFoundException(mUiSelector.toString());
@@ -890,7 +894,7 @@
      * @return true if the view is displayed, else false if timeout elapsed while waiting
      */
     public boolean waitForExists(long timeout) {
-        Tracer.trace(timeout);
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, mUiSelector));
         if(findAccessibilityNodeInfo(timeout) != null) {
             return true;
         }
@@ -915,7 +919,7 @@
      * but a matching element is still found.
      */
     public boolean waitUntilGone(long timeout) {
-        Tracer.trace(timeout);
+        Log.d(TAG, String.format("Waiting %dms for %s to be gone.", timeout, mUiSelector));
         long startMills = SystemClock.uptimeMillis();
         long currentMills = 0;
         while (currentMills <= timeout) {
@@ -939,7 +943,6 @@
      * @return true if the view represented by this UiObject does exist
      */
     public boolean exists() {
-        Tracer.trace();
         return waitForExists(0);
     }
 
@@ -1118,6 +1121,24 @@
      *         <code>false</code> otherwise
      */
     public boolean performMultiPointerGesture(@NonNull PointerCoords[]... touches) {
+        Log.d(TAG, String.format("Performing multi-point gesture %s", touchesToString(touches)));
         return getInteractionController().performMultiPointerGesture(touches);
     }
+
+    private static String touchesToString(@NonNull PointerCoords[]... touches) {
+        StringBuilder result = new StringBuilder();
+        result.append("[");
+        for (int i = 0; i < touches.length; i++) {
+            result.append("[");
+            for (int j = 0; j < touches[i].length; j++) {
+                PointerCoords point = touches[i][j];
+                result.append(String.format("(%f, %f)", point.x, point.y));
+                if (j + 1 < touches[i].length) result.append(", ");
+            }
+            result.append("]");
+            if (i + 1 < touches.length) result.append(", ");
+        }
+        result.append("]");
+        return result.toString();
+    }
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
index c60e8c0..8dcfbf5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
@@ -28,7 +28,7 @@
  * horizontally or vertically scrollable controls.
  */
 public class UiScrollable extends UiCollection {
-    private static final String LOG_TAG = UiScrollable.class.getSimpleName();
+    private static final String TAG = UiScrollable.class.getSimpleName();
 
     // More steps slows the swipe and prevents contents from being flung too far
     private static final int SCROLL_STEPS = 55;
@@ -64,7 +64,6 @@
      */
     @NonNull
     public UiScrollable setAsVerticalList() {
-        Tracer.trace();
         mIsVerticalList = true;
         return this;
     }
@@ -75,7 +74,6 @@
      */
     @NonNull
     public UiScrollable setAsHorizontalList() {
-        Tracer.trace();
         mIsVerticalList = false;
         return this;
     }
@@ -115,7 +113,6 @@
     public UiObject getChildByDescription(
             @NonNull UiSelector childPattern, @NonNull String text)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text);
         return getChildByDescription(childPattern, text, true);
     }
 
@@ -137,7 +134,6 @@
     @NonNull
     public UiObject getChildByDescription(@NonNull UiSelector childPattern, @NonNull String text,
             boolean allowScrollSearch) throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text, allowScrollSearch);
         if (text != null) {
             if (allowScrollSearch) {
                 scrollIntoView(new UiSelector().descriptionContains(text));
@@ -161,7 +157,6 @@
     @Override
     public UiObject getChildByInstance(@NonNull UiSelector childPattern, int instance)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, instance);
         UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
                 UiSelector.patternBuilder(childPattern).instance(instance));
         return new UiObject(patternSelector);
@@ -186,7 +181,6 @@
     @Override
     public UiObject getChildByText(@NonNull UiSelector childPattern, @NonNull String text)
             throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text);
         return getChildByText(childPattern, text, true);
     }
 
@@ -208,7 +202,6 @@
     public UiObject getChildByText(@NonNull UiSelector childPattern,
             @NonNull String text,
             boolean allowScrollSearch) throws UiObjectNotFoundException {
-        Tracer.trace(childPattern, text, allowScrollSearch);
         if (text != null) {
             if (allowScrollSearch) {
                 scrollIntoView(new UiSelector().text(text));
@@ -229,7 +222,6 @@
      */
     public boolean scrollDescriptionIntoView(@NonNull String text)
             throws UiObjectNotFoundException {
-        Tracer.trace(text);
         return scrollIntoView(new UiSelector().description(text));
     }
 
@@ -241,7 +233,6 @@
      * @return true if the item was found and now is in view else false
      */
     public boolean scrollIntoView(@NonNull UiObject obj) throws UiObjectNotFoundException {
-        Tracer.trace(obj.getSelector());
         return scrollIntoView(obj.getSelector());
     }
 
@@ -255,7 +246,7 @@
      * @return true if the item was found and now is in view; else, false
      */
     public boolean scrollIntoView(@NonNull UiSelector selector) throws UiObjectNotFoundException {
-        Tracer.trace(selector);
+        Log.d(TAG, String.format("Scrolling %s into view.", selector));
         // if we happen to be on top of the text we want then return here
         UiSelector childSelector = getSelector().childSelector(selector);
         if (exists(childSelector)) {
@@ -292,6 +283,7 @@
      */
     public boolean ensureFullyVisible(@NonNull UiObject childObject)
             throws UiObjectNotFoundException {
+        Log.d(TAG, String.format("Ensuring %s is fully visible.", childObject.getSelector()));
         Rect actual = childObject.getBounds();
         Rect visible = childObject.getVisibleBounds();
         if (visible.width() * visible.height() == actual.width() * actual.height()) {
@@ -332,7 +324,6 @@
      * @return true if item is found; else, false
      */
     public boolean scrollTextIntoView(@NonNull String text) throws UiObjectNotFoundException {
-        Tracer.trace(text);
         return scrollIntoView(new UiSelector().text(text));
     }
 
@@ -347,7 +338,6 @@
      */
     @NonNull
     public UiScrollable setMaxSearchSwipes(int swipes) {
-        Tracer.trace(swipes);
         mMaxSearchSwipes = swipes;
         return this;
     }
@@ -361,7 +351,6 @@
      * @return max the number of search swipes to perform until giving up
      */
     public int getMaxSearchSwipes() {
-        Tracer.trace();
         return mMaxSearchSwipes;
     }
 
@@ -376,7 +365,6 @@
      * @return true if scrolled, false if can't scroll anymore
      */
     public boolean flingForward() throws UiObjectNotFoundException {
-        Tracer.trace();
         return scrollForward(FLING_STEPS);
     }
 
@@ -391,7 +379,6 @@
      * @return true if scrolled, false if can't scroll anymore
      */
     public boolean scrollForward() throws UiObjectNotFoundException {
-        Tracer.trace();
         return scrollForward(SCROLL_STEPS);
     }
 
@@ -406,8 +393,6 @@
      * @return true if scrolled, false if can't scroll anymore
      */
     public boolean scrollForward(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
-        Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
         if(node == null) {
             throw new UiObjectNotFoundException(getSelector().toString());
@@ -438,6 +423,8 @@
             upX = rect.left + swipeAreaAdjust;
             upY = rect.centerY();
         }
+        Log.d(TAG, String.format("Scrolling forward from (%d, %d) to (%d, %d) in %d steps.", downX,
+                downY, upX, upY, steps));
         return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
     }
 
@@ -452,7 +439,6 @@
      * @return true if scrolled, and false if can't scroll anymore
      */
     public boolean flingBackward() throws UiObjectNotFoundException {
-        Tracer.trace();
         return scrollBackward(FLING_STEPS);
     }
 
@@ -467,7 +453,6 @@
      * @return true if scrolled, and false if can't scroll anymore
      */
     public boolean scrollBackward() throws UiObjectNotFoundException {
-        Tracer.trace();
         return scrollBackward(SCROLL_STEPS);
     }
 
@@ -482,8 +467,6 @@
      * @return true if scrolled, false if can't scroll anymore
      */
     public boolean scrollBackward(int steps) throws UiObjectNotFoundException {
-        Tracer.trace(steps);
-        Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
         if (node == null) {
             throw new UiObjectNotFoundException(getSelector().toString());
@@ -500,7 +483,6 @@
         // set otherwise by setAsHorizontalContainer()
         if(mIsVerticalList) {
             int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
-            Log.d(LOG_TAG, "scrollToBeginning() using vertical scroll");
             // scroll vertically: swipe up -> down
             downX = rect.centerX();
             downY = rect.top + swipeAreaAdjust;
@@ -508,7 +490,6 @@
             upY = rect.bottom - swipeAreaAdjust;
         } else {
             int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
-            Log.d(LOG_TAG, "scrollToBeginning() using hotizontal scroll");
             // scroll horizontally: swipe left -> right
             // TODO: Assuming device is not in right to left language
             downX = rect.left + swipeAreaAdjust;
@@ -516,6 +497,8 @@
             upX = rect.right - swipeAreaAdjust;
             upY = rect.centerY();
         }
+        Log.d(TAG, String.format("Scrolling backward from (%d, %d) to (%d, %d) in %d steps.", downX,
+                downY, upX, upY, steps));
         return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
     }
 
@@ -529,8 +512,6 @@
      * @return true on scrolled else false
      */
     public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes, steps);
-        Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector());
         // protect against potential hanging and return after preset attempts
         for(int x = 0; x < maxSwipes; x++) {
             if(!scrollBackward(steps)) {
@@ -550,7 +531,6 @@
      * @return true on scrolled else false
      */
     public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes);
         return scrollToBeginning(maxSwipes, SCROLL_STEPS);
     }
 
@@ -564,7 +544,6 @@
      * @return true on scrolled else false
      */
     public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes);
         return scrollToBeginning(maxSwipes, FLING_STEPS);
     }
 
@@ -578,7 +557,6 @@
      * @return true on scrolled else false
      */
     public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes, steps);
         // protect against potential hanging and return after preset attempts
         for(int x = 0; x < maxSwipes; x++) {
             if(!scrollForward(steps)) {
@@ -598,7 +576,6 @@
      * @return true on scrolled, else false
      */
     public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes);
         return scrollToEnd(maxSwipes, SCROLL_STEPS);
     }
 
@@ -612,7 +589,6 @@
      * @return true on scrolled, else false
      */
     public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException {
-        Tracer.trace(maxSwipes);
         return scrollToEnd(maxSwipes, FLING_STEPS);
     }
 
@@ -627,7 +603,6 @@
      * @return a value between 0 and 1
      */
     public double getSwipeDeadZonePercentage() {
-        Tracer.trace();
         return mSwipeDeadZonePercentage;
     }
 
@@ -645,7 +620,6 @@
      */
     @NonNull
     public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
-        Tracer.trace(swipeDeadZonePercentage);
         mSwipeDeadZonePercentage = swipeDeadZonePercentage;
         return this;
     }
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToDismissBoxSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToDismissBoxSample.kt
index 65e66eb..95c8021 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToDismissBoxSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/SwipeToDismissBoxSample.kt
@@ -153,6 +153,8 @@
 ) {
     val state = rememberSwipeToDismissBoxState()
 
+    // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
+    // modifier applies exists within a SwipeToDismissBox which shares the same state.
     SwipeToDismissBox(
         state = state,
         onDismissed = navigateBack
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 7264351..7b38d00 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -471,6 +471,9 @@
  * Currently Edge swipe, like swipe to dismiss, is only supported on the left part of the viewport
  * regardless of layout direction as content is swiped away from left to right.
  *
+ * Requires that the element to which this modifier is applied exists within a
+ * SwipeToDismissBox which is using the same [SwipeToDismissBoxState] instance.
+ *
  * Example of a modifier usage with SwipeToDismiss
  * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
  *
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
index e738dff..afda929 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/SwipeToDismissDemo.kt
@@ -101,6 +101,9 @@
     val colors = listOf(Color.Blue, Color.Red, Color.Green, Color.Cyan, Color.Magenta)
     Box(modifier = Modifier.fillMaxSize()) {
         LazyRow(
+            // When using Modifier.edgeSwipeToDismiss, it is required that the element on which the
+            // modifier applies exists within a SwipeToDismissBox which shares the same state.
+            // Here, we share the swipeToDismissBoxState used by DemoApp's SwipeToDismissBox.
             modifier = Modifier.border(4.dp, Color.DarkGray)
                 .fillMaxSize()
                 .edgeSwipeToDismiss(swipeToDismissBoxState),
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
new file mode 100644
index 0000000..7bdb9f6
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
@@ -0,0 +1,331 @@
+/*
+ * 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.wear.watchface.client.test
+
+import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import androidx.wear.watchface.ComplicationSlotBoundsType
+import androidx.wear.watchface.DrawMode
+import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.client.DeviceConfig
+import androidx.wear.watchface.client.HeadlessWatchFaceClient
+import androidx.wear.watchface.client.WatchFaceControlClient
+import androidx.wear.watchface.client.test.TestServicesHelpers.componentOf
+import androidx.wear.watchface.client.test.TestServicesHelpers.createTestComplications
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.control.WatchFaceControlService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService
+import androidx.wear.watchface.style.WatchFaceLayer
+import com.google.common.truth.Truth
+import java.time.Instant
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+abstract class HeadlessWatchFaceClientTestBase {
+    protected val context: Context = ApplicationProvider.getApplicationContext()
+    protected val service = runBlocking {
+        WatchFaceControlClient.createWatchFaceControlClientImpl(
+            context,
+            Intent(context, WatchFaceControlTestService::class.java).apply {
+                action = WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE
+            }
+        )
+    }
+
+    protected fun createHeadlessWatchFaceClient(
+        componentName: ComponentName = exampleCanvasAnalogWatchFaceComponentName
+    ): HeadlessWatchFaceClient {
+        return service.createHeadlessWatchFaceClient(
+            "id",
+            componentName,
+            deviceConfig,
+            400,
+            400
+        )!!
+    }
+
+    protected val exampleCanvasAnalogWatchFaceComponentName =
+        componentOf<ExampleCanvasAnalogWatchFaceService>()
+
+    protected val exampleOpenGLWatchFaceComponentName =
+        componentOf<ExampleOpenGLBackgroundInitWatchFaceService>()
+
+    protected val deviceConfig = DeviceConfig(
+        hasLowBitAmbient = false,
+        hasBurnInProtection = false,
+        analogPreviewReferenceTimeMillis = 0,
+        digitalPreviewReferenceTimeMillis = 0
+    )
+}
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+class HeadlessWatchFaceClientTest : HeadlessWatchFaceClientTestBase() {
+    @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType
+    @Test
+    fun headlessComplicationDetails() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        Truth.assertThat(headlessInstance.complicationSlotsState.size).isEqualTo(2)
+
+        val leftComplicationDetails = headlessInstance.complicationSlotsState[
+            EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+        ]!!
+        Truth.assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
+        Truth.assertThat(leftComplicationDetails.boundsType)
+            .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
+        Truth.assertThat(
+            leftComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback
+        ).isEqualTo(
+            SystemDataSources.DATA_SOURCE_DAY_OF_WEEK
+        )
+        Truth.assertThat(leftComplicationDetails.defaultDataSourceType).isEqualTo(
+            ComplicationType.SHORT_TEXT
+        )
+        Truth.assertThat(leftComplicationDetails.supportedTypes).containsExactly(
+            ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
+            ComplicationType.LONG_TEXT,
+            ComplicationType.SHORT_TEXT,
+            ComplicationType.MONOCHROMATIC_IMAGE,
+            ComplicationType.SMALL_IMAGE
+        )
+        Assert.assertTrue(leftComplicationDetails.isEnabled)
+
+        val rightComplicationDetails = headlessInstance.complicationSlotsState[
+            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+        ]!!
+        Truth.assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
+        Truth.assertThat(rightComplicationDetails.boundsType)
+            .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
+        Truth.assertThat(
+            rightComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback
+        ).isEqualTo(
+            SystemDataSources.DATA_SOURCE_STEP_COUNT
+        )
+        Truth.assertThat(rightComplicationDetails.defaultDataSourceType).isEqualTo(
+            ComplicationType.SHORT_TEXT
+        )
+        Truth.assertThat(rightComplicationDetails.supportedTypes).containsExactly(
+            ComplicationType.RANGED_VALUE,
+            ComplicationType.GOAL_PROGRESS,
+            ComplicationType.WEIGHTED_ELEMENTS,
+            ComplicationType.LONG_TEXT,
+            ComplicationType.SHORT_TEXT,
+            ComplicationType.MONOCHROMATIC_IMAGE,
+            ComplicationType.SMALL_IMAGE
+        )
+
+        Truth.assertThat(rightComplicationDetails.isEnabled).isTrue()
+
+        headlessInstance.close()
+    }
+
+    @Test
+    @Suppress("Deprecation") // userStyleSettings
+    fun headlessUserStyleSchema() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5)
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo(
+            "color_style_setting"
+        )
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo(
+            "draw_hour_pips_style_setting"
+        )
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo(
+            "watch_hand_length_style_setting"
+        )
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo(
+            "complications_style_setting"
+        )
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[4].id.value).isEqualTo(
+            "hours_draw_freq_style_setting"
+        )
+
+        headlessInstance.close()
+    }
+
+    @Test
+    fun headlessUserStyleFlavors() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        Truth.assertThat(headlessInstance.getUserStyleFlavors().flavors.size).isEqualTo(1)
+        val flavorA = headlessInstance.getUserStyleFlavors().flavors[0]
+        Truth.assertThat(flavorA.id).isEqualTo("exampleFlavor")
+        Truth.assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting"))
+        Truth.assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting"))
+        Truth.assertThat(flavorA.complications
+            .containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID))
+        Truth.assertThat(flavorA.complications
+            .containsKey(EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID))
+
+        headlessInstance.close()
+    }
+
+    @Test
+    @Suppress("Deprecation") // userStyleSettings
+    fun headlessToBundleAndCreateFromBundle() {
+        val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
+            service.createHeadlessWatchFaceClient(
+                "id",
+                exampleCanvasAnalogWatchFaceComponentName,
+                deviceConfig,
+                400,
+                400
+            )!!.toBundle()
+        )
+
+        Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5)
+    }
+
+    @Test
+    fun computeUserStyleSchemaDigestHash() {
+        val headlessInstance1 = createHeadlessWatchFaceClient(
+            exampleCanvasAnalogWatchFaceComponentName
+        )
+
+        val headlessInstance2 = createHeadlessWatchFaceClient(
+            exampleOpenGLWatchFaceComponentName
+        )
+
+        Truth.assertThat(headlessInstance1.getUserStyleSchemaDigestHash()).isNotEqualTo(
+            headlessInstance2.getUserStyleSchemaDigestHash()
+        )
+    }
+
+    @Test
+    fun headlessLifeCycle() {
+        val headlessInstance = createHeadlessWatchFaceClient(
+            componentOf<TestLifeCycleWatchFaceService>()
+        )
+
+        // Blocks until the headless instance has been fully constructed.
+        headlessInstance.previewReferenceInstant
+        headlessInstance.close()
+
+        Truth.assertThat(TestLifeCycleWatchFaceService.lifeCycleEvents).containsExactly(
+            "WatchFaceService.onCreate",
+            "Renderer.constructed",
+            "Renderer.onDestroy",
+            "WatchFaceService.onDestroy"
+        )
+    }
+}
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+class HeadlessWatchFaceClientScreenshotTest : HeadlessWatchFaceClientTestBase() {
+    @get:Rule
+    val screenshotRule: AndroidXScreenshotTestRule =
+        AndroidXScreenshotTestRule("wear/wear-watchface-client")
+
+    private val complications = createTestComplications(context)
+
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun headlessScreenshot() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        val bitmap = headlessInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
+        )
+
+        bitmap.assertAgainstGolden(screenshotRule, "headlessScreenshot")
+
+        headlessInstance.close()
+    }
+
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun yellowComplicationHighlights() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        val bitmap = headlessInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                RenderParameters.HighlightLayer(
+                    RenderParameters.HighlightedElement.AllComplicationSlots,
+                    Color.YELLOW,
+                    Color.argb(128, 0, 0, 0) // Darken everything else.
+                )
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
+        )
+
+        bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights")
+
+        headlessInstance.close()
+    }
+
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun highlightOnlyLayer() {
+        val headlessInstance = createHeadlessWatchFaceClient()
+
+        val bitmap = headlessInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                emptySet(),
+                RenderParameters.HighlightLayer(
+                    RenderParameters.HighlightedElement.AllComplicationSlots,
+                    Color.YELLOW,
+                    Color.argb(128, 0, 0, 0) // Darken everything else.
+                )
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
+        )
+
+        bitmap.assertAgainstGolden(screenshotRule, "highlightOnlyLayer")
+
+        headlessInstance.close()
+    }
+}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
new file mode 100644
index 0000000..0d06e96
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
@@ -0,0 +1,758 @@
+/*
+ * 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.wear.watchface.client.test
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.RectF
+import android.view.SurfaceHolder
+import androidx.wear.watchface.BoundingArc
+import androidx.wear.watchface.CanvasComplication
+import androidx.wear.watchface.CanvasType
+import androidx.wear.watchface.ComplicationSlot
+import androidx.wear.watchface.ComplicationSlotsManager
+import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.Renderer
+import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.WatchFaceService
+import androidx.wear.watchface.WatchFaceType
+import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationExperimental
+import androidx.wear.watchface.complications.data.ComplicationText
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.NoDataComplicationData
+import androidx.wear.watchface.complications.data.PlainComplicationText
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService
+import androidx.wear.watchface.samples.R
+import androidx.wear.watchface.style.CurrentUserStyleRepository
+import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.WatchFaceLayer
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.concurrent.CountDownLatch
+import kotlinx.coroutines.CompletableDeferred
+
+internal class TestLifeCycleWatchFaceService : WatchFaceService() {
+    companion object {
+        val lifeCycleEvents = ArrayList<String>()
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        lifeCycleEvents.add("WatchFaceService.onCreate")
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        lifeCycleEvents.add("WatchFaceService.onDestroy")
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.GlesRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            16
+        ) {
+            init {
+                lifeCycleEvents.add("Renderer.constructed")
+            }
+
+            override fun onDestroy() {
+                super.onDestroy()
+                lifeCycleEvents.add("Renderer.onDestroy")
+            }
+
+            override fun render(zonedDateTime: ZonedDateTime) {}
+
+            override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {}
+        }
+    )
+}
+
+internal class TestExampleCanvasAnalogWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder
+) : ExampleCanvasAnalogWatchFaceService() {
+    internal lateinit var watchFace: WatchFace
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): WatchFace {
+        watchFace = super.createWatchFace(
+            surfaceHolder,
+            watchState,
+            complicationSlotsManager,
+            currentUserStyleRepository
+        )
+        return watchFace
+    }
+}
+
+internal class TestExampleOpenGLBackgroundInitWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder
+) : ExampleOpenGLBackgroundInitWatchFaceService() {
+    internal lateinit var watchFace: WatchFace
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): WatchFace {
+        watchFace = super.createWatchFace(
+            surfaceHolder,
+            watchState,
+            complicationSlotsManager,
+            currentUserStyleRepository
+        )
+        return watchFace
+    }
+}
+
+internal open class TestCrashingWatchFaceService : WatchFaceService() {
+
+    companion object {
+        const val COMPLICATION_ID = 123
+    }
+
+    override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        return ComplicationSlotsManager(
+            listOf(
+                ComplicationSlot.createRoundRectComplicationSlotBuilder(
+                    COMPLICATION_ID,
+                    { _, _ -> throw Exception("Deliberately crashing") },
+                    listOf(ComplicationType.LONG_TEXT),
+                    DefaultComplicationDataSourcePolicy(
+                        SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
+                        ComplicationType.LONG_TEXT
+                    ),
+                    ComplicationSlotBounds(RectF(0.1f, 0.1f, 0.4f, 0.4f))
+                ).build()
+            ),
+            currentUserStyleRepository
+        )
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): WatchFace {
+        throw Exception("Deliberately crashing")
+    }
+}
+
+internal class TestWatchfaceOverlayStyleWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder,
+    private var watchFaceOverlayStyle: WatchFace.OverlayStyle
+) : WatchFaceService() {
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+                // Actually rendering something isn't required.
+            }
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+                // Actually rendering something isn't required.
+            }
+        }
+    ).setOverlayStyle(watchFaceOverlayStyle)
+}
+
+internal class TestAsyncCanvasRenderInitWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder,
+    private var initCompletableDeferred: CompletableDeferred<Unit>
+) : WatchFaceService() {
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override suspend fun init() {
+                initCompletableDeferred.await()
+            }
+
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+                // Actually rendering something isn't required.
+            }
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+                TODO("Not yet implemented")
+            }
+        }
+    )
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = 123456789L
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
+}
+
+internal class TestAsyncGlesRenderInitWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder,
+    private var onUiThreadGlSurfaceCreatedCompletableDeferred: CompletableDeferred<Unit>,
+    private var onBackgroundThreadGlContextCreatedCompletableDeferred: CompletableDeferred<Unit>
+) : WatchFaceService() {
+    internal lateinit var watchFace: WatchFace
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.GlesRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            16
+        ) {
+            override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
+                onUiThreadGlSurfaceCreatedCompletableDeferred.await()
+            }
+
+            override suspend fun onBackgroundThreadGlContextCreated() {
+                onBackgroundThreadGlContextCreatedCompletableDeferred.await()
+            }
+
+            override fun render(zonedDateTime: ZonedDateTime) {
+                // GLES rendering is complicated and not strictly necessary for our test.
+            }
+
+            override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
+                TODO("Not yet implemented")
+            }
+        }
+    )
+}
+
+internal class TestComplicationProviderDefaultsWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder
+) : WatchFaceService() {
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        return ComplicationSlotsManager(
+            listOf(
+                ComplicationSlot.createRoundRectComplicationSlotBuilder(
+                    123,
+                    { _, _ ->
+                        object : CanvasComplication {
+                            override fun render(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                zonedDateTime: ZonedDateTime,
+                                renderParameters: RenderParameters,
+                                slotId: Int
+                            ) {
+                            }
+
+                            override fun drawHighlight(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                boundsType: Int,
+                                zonedDateTime: ZonedDateTime,
+                                color: Int
+                            ) {
+                            }
+
+                            override fun getData() = NoDataComplicationData()
+
+                            override fun loadData(
+                                complicationData: ComplicationData,
+                                loadDrawablesAsynchronous: Boolean
+                            ) {
+                            }
+                        }
+                    },
+                    listOf(
+                        ComplicationType.PHOTO_IMAGE,
+                        ComplicationType.LONG_TEXT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    DefaultComplicationDataSourcePolicy(
+                        ComponentName("com.package1", "com.app1"),
+                        ComplicationType.PHOTO_IMAGE,
+                        ComponentName("com.package2", "com.app2"),
+                        ComplicationType.LONG_TEXT,
+                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    ComplicationSlotBounds(
+                        RectF(0.1f, 0.2f, 0.3f, 0.4f)
+                    )
+                )
+                    .build()
+            ),
+            currentUserStyleRepository
+        )
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+            }
+        }
+    )
+}
+
+internal class TestEdgeComplicationWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder
+) : WatchFaceService() {
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    @OptIn(ComplicationExperimental::class)
+    override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        return ComplicationSlotsManager(
+            listOf(
+                ComplicationSlot.createEdgeComplicationSlotBuilder(
+                    123,
+                    { _, _ ->
+                        object : CanvasComplication {
+                            override fun render(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                zonedDateTime: ZonedDateTime,
+                                renderParameters: RenderParameters,
+                                slotId: Int
+                            ) {
+                            }
+
+                            override fun drawHighlight(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                boundsType: Int,
+                                zonedDateTime: ZonedDateTime,
+                                color: Int
+                            ) {
+                            }
+
+                            override fun getData() = NoDataComplicationData()
+
+                            override fun loadData(
+                                complicationData: ComplicationData,
+                                loadDrawablesAsynchronous: Boolean
+                            ) {
+                            }
+                        }
+                    },
+                    listOf(
+                        ComplicationType.PHOTO_IMAGE,
+                        ComplicationType.LONG_TEXT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    DefaultComplicationDataSourcePolicy(
+                        ComponentName("com.package1", "com.app1"),
+                        ComplicationType.PHOTO_IMAGE,
+                        ComponentName("com.package2", "com.app2"),
+                        ComplicationType.LONG_TEXT,
+                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    ComplicationSlotBounds(
+                        RectF(0f, 0f, 1f, 1f)
+                    ),
+                    BoundingArc(45f, 90f, 0.1f)
+                )
+                    .build()
+            ),
+            currentUserStyleRepository
+        )
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.DIGITAL,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+            }
+        }
+    )
+}
+
+internal class TestWatchFaceServiceWithPreviewImageUpdateRequest(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder,
+) : WatchFaceService() {
+    val rendererInitializedLatch = CountDownLatch(1)
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    @Suppress("deprecation")
+    private lateinit var renderer: Renderer.CanvasRenderer
+
+    fun triggerPreviewImageUpdateRequest() {
+        renderer.sendPreviewImageNeedsUpdateRequest()
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): WatchFace {
+        @Suppress("deprecation")
+        renderer = object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override suspend fun init() {
+                rendererInitializedLatch.countDown()
+            }
+
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+            }
+        }
+        return WatchFace(WatchFaceType.DIGITAL, renderer)
+    }
+}
+
+internal class TestComplicationStyleUpdateWatchFaceService(
+    testContext: Context,
+    private var surfaceHolderOverride: SurfaceHolder
+) : WatchFaceService() {
+
+    init {
+        attachBaseContext(testContext)
+    }
+
+    @Suppress("deprecation")
+    private val complicationsStyleSetting =
+        UserStyleSetting.ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING),
+            resources,
+            R.string.watchface_complications_setting,
+            R.string.watchface_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id(NO_COMPLICATIONS),
+                    resources,
+                    R.string.watchface_complications_setting_none,
+                    null,
+                    listOf(
+                        UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
+                            123,
+                            enabled = false
+                        )
+                    )
+                ),
+                UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id(LEFT_COMPLICATION),
+                    resources,
+                    R.string.watchface_complications_setting_left,
+                    null,
+                    listOf(
+                        UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
+                            123,
+                            enabled = true,
+                            nameResourceId = R.string.left_complication_screen_name,
+                            screenReaderNameResourceId =
+                            R.string.left_complication_screen_reader_name
+                        )
+                    )
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+    override fun createUserStyleSchema(): UserStyleSchema =
+        UserStyleSchema(listOf(complicationsStyleSetting))
+
+    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+
+    override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        return ComplicationSlotsManager(
+            listOf(
+                ComplicationSlot.createRoundRectComplicationSlotBuilder(
+                    123,
+                    { _, _ ->
+                        object : CanvasComplication {
+                            override fun render(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                zonedDateTime: ZonedDateTime,
+                                renderParameters: RenderParameters,
+                                slotId: Int
+                            ) {
+                            }
+
+                            override fun drawHighlight(
+                                canvas: Canvas,
+                                bounds: Rect,
+                                boundsType: Int,
+                                zonedDateTime: ZonedDateTime,
+                                color: Int
+                            ) {
+                            }
+
+                            override fun getData() = NoDataComplicationData()
+
+                            override fun loadData(
+                                complicationData: ComplicationData,
+                                loadDrawablesAsynchronous: Boolean
+                            ) {
+                            }
+                        }
+                    },
+                    listOf(
+                        ComplicationType.PHOTO_IMAGE,
+                        ComplicationType.LONG_TEXT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    DefaultComplicationDataSourcePolicy(
+                        ComponentName("com.package1", "com.app1"),
+                        ComplicationType.PHOTO_IMAGE,
+                        ComponentName("com.package2", "com.app2"),
+                        ComplicationType.LONG_TEXT,
+                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
+                        ComplicationType.SHORT_TEXT
+                    ),
+                    ComplicationSlotBounds(
+                        RectF(0.1f, 0.2f, 0.3f, 0.4f)
+                    )
+                ).build()
+            ),
+            currentUserStyleRepository
+        )
+    }
+
+    override suspend fun createWatchFace(
+        surfaceHolder: SurfaceHolder,
+        watchState: WatchState,
+        complicationSlotsManager: ComplicationSlotsManager,
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ) = WatchFace(
+        WatchFaceType.ANALOG,
+        @Suppress("deprecation")
+        object : Renderer.CanvasRenderer(
+            surfaceHolder,
+            currentUserStyleRepository,
+            watchState,
+            CanvasType.HARDWARE,
+            16
+        ) {
+            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
+
+            override fun renderHighlightLayer(
+                canvas: Canvas,
+                bounds: Rect,
+                zonedDateTime: ZonedDateTime
+            ) {
+            }
+        }
+    )
+}
+
+internal object TestServicesHelpers {
+    fun createTestComplications(context: Context) = mapOf(
+        ExampleCanvasAnalogWatchFaceService.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+            ShortTextComplicationData.Builder(
+                PlainComplicationText.Builder("ID").build(),
+                ComplicationText.EMPTY
+            ).setTitle(PlainComplicationText.Builder("Left").build())
+                .setTapAction(
+                    PendingIntent.getActivity(context, 0, Intent("left"),
+                        PendingIntent.FLAG_IMMUTABLE
+                    )
+                )
+                .build(),
+        ExampleCanvasAnalogWatchFaceService.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
+            ShortTextComplicationData.Builder(
+                PlainComplicationText.Builder("ID").build(),
+                ComplicationText.EMPTY
+            ).setTitle(PlainComplicationText.Builder("Right").build())
+                .setTapAction(
+                    PendingIntent.getActivity(context, 0, Intent("right"),
+                        PendingIntent.FLAG_IMMUTABLE
+                    )
+                )
+                .build()
+    )
+
+    inline fun <reified T>componentOf(): ComponentName {
+        return ComponentName(
+            T::class.java.`package`?.name!!,
+            T::class.java.name
+        )
+    }
+}
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 99b7e4e..a10788a 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
@@ -18,7 +18,6 @@
 
 import android.annotation.SuppressLint
 import android.app.PendingIntent
-import android.app.PendingIntent.FLAG_IMMUTABLE
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
@@ -26,11 +25,9 @@
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Rect
-import android.graphics.RectF
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.os.Handler
-import android.os.IBinder
 import android.os.Looper
 import android.view.Surface
 import android.view.SurfaceHolder
@@ -41,21 +38,15 @@
 import androidx.test.screenshot.AndroidXScreenshotTestRule
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.CanvasComplication
-import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotBoundsType
-import androidx.wear.watchface.ComplicationSlotsManager
 import androidx.wear.watchface.ContentDescriptionLabel
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
-import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceColors
 import androidx.wear.watchface.WatchFaceExperimental
 import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.WatchFaceType
-import androidx.wear.watchface.WatchState
 import androidx.wear.watchface.client.DeviceConfig
 import androidx.wear.watchface.client.DisconnectReason
 import androidx.wear.watchface.client.DisconnectReasons
@@ -64,7 +55,8 @@
 import androidx.wear.watchface.client.WatchFaceClientExperimental
 import androidx.wear.watchface.client.WatchFaceControlClient
 import androidx.wear.watchface.client.WatchUiState
-import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.client.test.TestServicesHelpers.componentOf
+import androidx.wear.watchface.client.test.TestServicesHelpers.createTestComplications
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
 import androidx.wear.watchface.complications.SystemDataSources
 import androidx.wear.watchface.complications.data.ComplicationData
@@ -72,11 +64,8 @@
 import androidx.wear.watchface.complications.data.ComplicationText
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.LongTextComplicationData
-import androidx.wear.watchface.complications.data.NoDataComplicationData
 import androidx.wear.watchface.complications.data.PlainComplicationText
 import androidx.wear.watchface.complications.data.RangedValueComplicationData
-import androidx.wear.watchface.complications.data.ShortTextComplicationData
-import androidx.wear.watchface.control.IInteractiveWatchFace
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
@@ -88,26 +77,22 @@
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
-import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.WATCH_HAND_LENGTH_STYLE_SETTING
 import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService
 import androidx.wear.watchface.samples.R
-import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleData
-import androidx.wear.watchface.style.UserStyleSchema
-import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
 import androidx.wear.watchface.style.WatchFaceLayer
 import com.google.common.truth.Truth.assertThat
 import java.time.Instant
-import java.time.ZoneId
-import java.time.ZonedDateTime
 import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeoutException
+import java.util.function.Consumer
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -132,12 +117,10 @@
 private const val DESTROY_TIMEOUT_MILLIS = 500L
 private const val UPDATE_TIMEOUT_MILLIS = 500L
 
-@RunWith(AndroidJUnit4::class)
-@MediumTest
 @RequiresApi(Build.VERSION_CODES.O_MR1)
-class WatchFaceControlClientTest {
-    private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val service = runBlocking {
+abstract class WatchFaceControlClientTestBase {
+    protected val context: Context = ApplicationProvider.getApplicationContext()
+    protected val service = runBlocking {
         WatchFaceControlClient.createWatchFaceControlClientImpl(
             context,
             Intent(context, WatchFaceControlTestService::class.java).apply {
@@ -147,31 +130,35 @@
     }
 
     @Mock
-    private lateinit var mockBinder: IBinder
+    protected lateinit var surfaceHolder: SurfaceHolder
 
     @Mock
-    private lateinit var iInteractiveWatchFace: IInteractiveWatchFace
-
-    @Mock
-    private lateinit var surfaceHolder: SurfaceHolder
-
-    @Mock
-    private lateinit var surfaceHolder2: SurfaceHolder
+    protected lateinit var surfaceHolder2: SurfaceHolder
 
     @Mock
     private lateinit var surface: Surface
-    private lateinit var engine: WatchFaceService.EngineWrapper
-    private val handler = Handler(Looper.getMainLooper())
-    private val handlerCoroutineScope =
+
+    protected val handler = Handler(Looper.getMainLooper())
+    protected val handlerCoroutineScope =
         CoroutineScope(Handler(handler.looper).asCoroutineDispatcher())
-    private lateinit var wallpaperService: WatchFaceService
+
+    protected lateinit var engine: WatchFaceService.EngineWrapper
+
+    protected val deviceConfig = DeviceConfig(
+        hasLowBitAmbient = false,
+        hasBurnInProtection = false,
+        analogPreviewReferenceTimeMillis = 0,
+        digitalPreviewReferenceTimeMillis = 0
+    )
+
+    protected val systemState = WatchUiState(false, 0)
+
+    protected val complications = createTestComplications(context)
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         WatchFaceControlTestService.apiVersionOverride = null
-        wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
-
         Mockito.`when`(surfaceHolder.surfaceFrame)
             .thenReturn(Rect(0, 0, 400, 400))
         Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
@@ -193,59 +180,48 @@
         service.close()
     }
 
-    @get:Rule
-    val screenshotRule: AndroidXScreenshotTestRule =
-        AndroidXScreenshotTestRule("wear/wear-watchface-client")
-
-    private val exampleCanvasAnalogWatchFaceComponentName = ComponentName(
-        "androidx.wear.watchface.samples.test",
-        "androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService"
-    )
-
-    private val exampleOpenGLWatchFaceComponentName = ComponentName(
-        "androidx.wear.watchface.samples.test",
-        "androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService"
-    )
-
-    private val deviceConfig = DeviceConfig(
-        false,
-        false,
-        0,
-        0
-    )
-
-    private val systemState = WatchUiState(false, 0)
-
-    private val complications = mapOf(
-        EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
-            ShortTextComplicationData.Builder(
-                PlainComplicationText.Builder("ID").build(),
-                ComplicationText.EMPTY
-            ).setTitle(PlainComplicationText.Builder("Left").build())
-                .setTapAction(
-                    PendingIntent.getActivity(context, 0, Intent("left"), FLAG_IMMUTABLE)
+    protected fun getOrCreateTestSubject(
+        watchFaceService: WatchFaceService =
+            TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder),
+        instanceId: String = "testId",
+        userStyle: UserStyleData? = null,
+        complications: Map<Int, ComplicationData>? = this.complications,
+        previewExecutor: Executor? = null,
+        previewListener: Consumer<String>? = null
+    ): InteractiveWatchFaceClient {
+        val deferredInteractiveInstance = handlerCoroutineScope.async {
+            if (previewExecutor != null && previewListener != null) {
+                service.getOrCreateInteractiveWatchFaceClient(
+                    instanceId,
+                    deviceConfig,
+                    systemState,
+                    userStyle,
+                    complications,
+                    previewExecutor,
+                    previewListener
                 )
-                .build(),
-        EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
-            ShortTextComplicationData.Builder(
-                PlainComplicationText.Builder("ID").build(),
-                ComplicationText.EMPTY
-            ).setTitle(PlainComplicationText.Builder("Right").build())
-                .setTapAction(
-                    PendingIntent.getActivity(context, 0, Intent("right"), FLAG_IMMUTABLE)
+            } else {
+                @Suppress("deprecation")
+                service.getOrCreateInteractiveWatchFaceClient(
+                    instanceId,
+                    deviceConfig,
+                    systemState,
+                    userStyle,
+                    complications
                 )
-                .build()
-    )
-
-    private fun createEngine() {
-        // onCreateEngine must run after getOrCreateInteractiveWatchFaceClient. To ensure the
-        // ordering relationship both calls should run on the same handler.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
+            }
         }
+
+        // Create the engine which triggers construction of the interactive instance.
+        handler.post {
+            engine = watchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+        }
+
+        // Wait for the instance to be created.
+        return awaitWithTimeout(deferredInteractiveInstance)
     }
 
-    private fun <X> awaitWithTimeout(
+    protected fun <X> awaitWithTimeout(
         thing: Deferred<X>,
         timeoutMillis: Long = CONNECT_TIMEOUT_MILLIS
     ): X {
@@ -260,173 +236,14 @@
         }
         return value!!
     }
+}
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+class WatchFaceControlClientTest : WatchFaceControlClientTestBase() {
 
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun headlessScreenshot() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            DeviceConfig(
-                false,
-                false,
-                0,
-                0
-            ),
-            400,
-            400
-        )!!
-        val bitmap = headlessInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        bitmap.assertAgainstGolden(screenshotRule, "headlessScreenshot")
-
-        headlessInstance.close()
-    }
-
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun yellowComplicationHighlights() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            DeviceConfig(
-                false,
-                false,
-                0,
-                0
-            ),
-            400,
-            400
-        )!!
-        val bitmap = headlessInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                RenderParameters.HighlightLayer(
-                    RenderParameters.HighlightedElement.AllComplicationSlots,
-                    Color.YELLOW,
-                    Color.argb(128, 0, 0, 0) // Darken everything else.
-                )
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights")
-
-        headlessInstance.close()
-    }
-
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun highlightOnlyLayer() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            DeviceConfig(
-                false,
-                false,
-                0,
-                0
-            ),
-            400,
-            400
-        )!!
-        val bitmap = headlessInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                emptySet(),
-                RenderParameters.HighlightLayer(
-                    RenderParameters.HighlightedElement.AllComplicationSlots,
-                    Color.YELLOW,
-                    Color.argb(128, 0, 0, 0) // Darken everything else.
-                )
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        bitmap.assertAgainstGolden(screenshotRule, "highlightOnlyLayer")
-
-        headlessInstance.close()
-    }
-
-    @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType
-    @Test
-    fun headlessComplicationDetails() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            deviceConfig,
-            400,
-            400
-        )!!
-
-        assertThat(headlessInstance.complicationSlotsState.size).isEqualTo(2)
-
-        val leftComplicationDetails = headlessInstance.complicationSlotsState[
-            EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-        ]!!
-        assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
-        assertThat(leftComplicationDetails.boundsType)
-            .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
-        assertThat(
-            leftComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback
-        ).isEqualTo(
-            SystemDataSources.DATA_SOURCE_DAY_OF_WEEK
-        )
-        assertThat(leftComplicationDetails.defaultDataSourceType).isEqualTo(
-            ComplicationType.SHORT_TEXT
-        )
-        assertThat(leftComplicationDetails.supportedTypes).containsExactly(
-            ComplicationType.RANGED_VALUE,
-            ComplicationType.GOAL_PROGRESS,
-            ComplicationType.WEIGHTED_ELEMENTS,
-            ComplicationType.LONG_TEXT,
-            ComplicationType.SHORT_TEXT,
-            ComplicationType.MONOCHROMATIC_IMAGE,
-            ComplicationType.SMALL_IMAGE
-        )
-        assertTrue(leftComplicationDetails.isEnabled)
-
-        val rightComplicationDetails = headlessInstance.complicationSlotsState[
-            EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
-        ]!!
-        assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
-        assertThat(rightComplicationDetails.boundsType)
-            .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
-        assertThat(
-            rightComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback
-        ).isEqualTo(
-            SystemDataSources.DATA_SOURCE_STEP_COUNT
-        )
-        assertThat(rightComplicationDetails.defaultDataSourceType).isEqualTo(
-            ComplicationType.SHORT_TEXT
-        )
-        assertThat(rightComplicationDetails.supportedTypes).containsExactly(
-            ComplicationType.RANGED_VALUE,
-            ComplicationType.GOAL_PROGRESS,
-            ComplicationType.WEIGHTED_ELEMENTS,
-            ComplicationType.LONG_TEXT,
-            ComplicationType.SHORT_TEXT,
-            ComplicationType.MONOCHROMATIC_IMAGE,
-            ComplicationType.SMALL_IMAGE
-        )
-        assertTrue(rightComplicationDetails.isEnabled)
-
-        headlessInstance.close()
-    }
+    private val exampleCanvasAnalogWatchFaceComponentName =
+        componentOf<ExampleCanvasAnalogWatchFaceService>()
 
     @Test
     fun complicationProviderDefaults() {
@@ -434,23 +251,9 @@
             context,
             surfaceHolder
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers construction of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(
+            wallpaperService
+        )
 
         try {
             assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123)
@@ -480,23 +283,9 @@
             context,
             surfaceHolder
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers construction of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(
+            wallpaperService
+        )
 
         try {
             assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123)
@@ -516,23 +305,8 @@
             context,
             surfaceHolder
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers construction of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
 
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         // User style settings to be updated
         val userStyleSettings = interactiveInstance.userStyleSchema.userStyleSettings
@@ -561,176 +335,10 @@
         }
     }
 
-    @Test
-    @Suppress("Deprecation") // userStyleSettings
-    fun headlessUserStyleSchema() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            deviceConfig,
-            400,
-            400
-        )!!
-
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5)
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo(
-            "color_style_setting"
-        )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo(
-            "draw_hour_pips_style_setting"
-        )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo(
-            "watch_hand_length_style_setting"
-        )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo(
-            "complications_style_setting"
-        )
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings[4].id.value).isEqualTo(
-            "hours_draw_freq_style_setting"
-        )
-
-        headlessInstance.close()
-    }
-
-    @Test
-    fun headlessUserStyleFlavors() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            deviceConfig,
-            400,
-            400
-        )!!
-
-        assertThat(headlessInstance.getUserStyleFlavors().flavors.size).isEqualTo(1)
-        val flavorA = headlessInstance.getUserStyleFlavors().flavors[0]
-        assertThat(flavorA.id).isEqualTo("exampleFlavor")
-        assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting"))
-        assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting"))
-        assertThat(flavorA.complications.containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID))
-        assertThat(
-            flavorA.complications.containsKey(
-                EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
-            )
-        )
-
-        headlessInstance.close()
-    }
-
-    @Test
-    @Suppress("Deprecation") // userStyleSettings
-    fun headlessToBundleAndCreateFromBundle() {
-        val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
-            service.createHeadlessWatchFaceClient(
-                "id",
-                exampleCanvasAnalogWatchFaceComponentName,
-                deviceConfig,
-                400,
-                400
-            )!!.toBundle()
-        )
-
-        assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5)
-    }
-
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun getOrCreateInteractiveWatchFaceClient() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
-
-        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        try {
-            bitmap.assertAgainstGolden(screenshotRule, "interactiveScreenshot")
-        } finally {
-            interactiveInstance.close()
-        }
-    }
-
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun getOrCreateInteractiveWatchFaceClient_initialStyle() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                // An incomplete map which is OK.
-                UserStyleData(
-                    mapOf(
-                        "color_style_setting" to "green_style".encodeToByteArray(),
-                        "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value,
-                        "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value
-                    )
-                ),
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
-
-        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        try {
-            bitmap.assertAgainstGolden(screenshotRule, "initialStyle")
-        } finally {
-            interactiveInstance.close()
-        }
-    }
-
     @Suppress("DEPRECATION", "newApi") // defaultDataSourceType & ComplicationType
     @Test
     fun interactiveWatchFaceClient_ComplicationDetails() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject()
 
         assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2)
 
@@ -762,9 +370,9 @@
             ComplicationType.SHORT_TEXT
         )
         assertThat(leftComplicationDetails.nameResourceId)
-            .isEqualTo(androidx.wear.watchface.samples.R.string.left_complication_screen_name)
+            .isEqualTo(R.string.left_complication_screen_name)
         assertThat(leftComplicationDetails.screenReaderNameResourceId).isEqualTo(
-            androidx.wear.watchface.samples.R.string.left_complication_screen_reader_name
+            R.string.left_complication_screen_reader_name
         )
 
         val rightComplicationDetails = interactiveInstance.complicationSlotsState[
@@ -793,31 +401,17 @@
             ComplicationType.SHORT_TEXT
         )
         assertThat(rightComplicationDetails.nameResourceId)
-            .isEqualTo(androidx.wear.watchface.samples.R.string.right_complication_screen_name)
+            .isEqualTo(R.string.right_complication_screen_name)
         assertThat(rightComplicationDetails.screenReaderNameResourceId).isEqualTo(
-            androidx.wear.watchface.samples.R.string.right_complication_screen_reader_name
+            R.string.right_complication_screen_reader_name
         )
 
         interactiveInstance.close()
     }
 
     @Test
-    public fun updateComplicationData() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+    fun updateComplicationData() {
+        val interactiveInstance = getOrCreateTestSubject()
 
         // Under the hood updateComplicationData is a oneway aidl method so we need to perform some
         // additional synchronization to ensure it's side effects have been applied before
@@ -877,21 +471,9 @@
 
     @Test
     fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
+        val watchFaceService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
 
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        awaitWithTimeout(deferredInteractiveInstance)
+        getOrCreateTestSubject(watchFaceService)
 
         val deferredInteractiveInstance2 = handlerCoroutineScope.async {
             @Suppress("deprecation")
@@ -904,91 +486,22 @@
             )
         }
 
-        assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId).isEqualTo("testId")
-    }
-
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        awaitWithTimeout(deferredInteractiveInstance)
-
-        val deferredInteractiveInstance2 = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                UserStyleData(
-                    mapOf(
-                        "color_style_setting" to "blue_style".encodeToByteArray(),
-                        "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value,
-                        "watch_hand_length_style_setting" to DoubleRangeOption(0.25).id.value
-                    )
-                ),
-                complications
-            )
-        }
-
-        val interactiveInstance2 = awaitWithTimeout(deferredInteractiveInstance2)
-        assertThat(interactiveInstance2.instanceId).isEqualTo("testId")
-
-        val bitmap = interactiveInstance2.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        try {
-            // Note the hour hand pips and both complicationSlots should be visible in this image.
-            bitmap.assertAgainstGolden(screenshotRule, "existingOpenInstance_styleChange")
-        } finally {
-            interactiveInstance2.close()
-        }
+        assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId)
+            .isEqualTo("testId")
     }
 
     @Test
     fun getOrCreateInteractiveWatchFaceClient_existingClosedInstance() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
 
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         // Closing this interface means the subsequent
         // getOrCreateInteractiveWatchFaceClient won't immediately return
         // a resolved future.
         interactiveInstance.close()
 
+        // Connect again to the same wallpaperService instance
         val deferredExistingInstance = handlerCoroutineScope.async {
             @Suppress("deprecation")
             service.getOrCreateInteractiveWatchFaceClient(
@@ -1012,25 +525,13 @@
 
     @Test
     fun getInteractiveWatchFaceInstance() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
+        val testId = "testId"
+        // Create and wait for an interactive instance without capturing a reference to it
+        getOrCreateTestSubject(instanceId = testId)
 
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        // Wait for the instance to be created.
-        awaitWithTimeout(deferredInteractiveInstance)
-
+        // Get the instance created above
         val sysUiInterface =
-            service.getInteractiveWatchFaceClientInstance("testId")!!
+            service.getInteractiveWatchFaceClientInstance(testId)!!
 
         val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels
         assertThat(contentDescriptionLabels.size).isEqualTo(3)
@@ -1061,22 +562,9 @@
 
     @Test
     fun additionalContentDescriptionLabels() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
 
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         // We need to wait for watch face init to have completed before lateinit
         // wallpaperService.watchFace will be assigned. To do this we issue an arbitrary API
@@ -1092,7 +580,7 @@
             context, 0, Intent("Two"),
             PendingIntent.FLAG_IMMUTABLE
         )
-        (wallpaperService as TestExampleCanvasAnalogWatchFaceService)
+        (wallpaperService)
             .watchFace.renderer.additionalContentDescriptionLabels = listOf(
             Pair(
                 0,
@@ -1154,115 +642,16 @@
 
     @Test
     fun contentDescriptionLabels_after_close() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject()
 
         assertThat(interactiveInstance.contentDescriptionLabels).isNotEmpty()
         interactiveInstance.close()
         assertThat(interactiveInstance.contentDescriptionLabels).isEmpty()
     }
 
-    @SuppressLint("NewApi") // renderWatchFaceToBitmap
-    @Test
-    fun updateInstance() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                UserStyleData(
-                    mapOf(
-                        COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(),
-                        WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value,
-                        DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption.FALSE.id.value,
-                        COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray()
-                    )
-                ),
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
-
-        assertThat(interactiveInstance.instanceId).isEqualTo("testId")
-
-        // Note this map doesn't include all the categories, which is fine the others will be set
-        // to their defaults.
-        interactiveInstance.updateWatchFaceInstance(
-            "testId2",
-            UserStyleData(
-                mapOf(
-                    COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(),
-                    WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value,
-                )
-            )
-        )
-
-        assertThat(interactiveInstance.instanceId).isEqualTo("testId2")
-
-        // It should be possible to create an instance with the updated id.
-        val instance =
-            service.getInteractiveWatchFaceClientInstance("testId2")
-        assertThat(instance).isNotNull()
-        instance?.close()
-
-        // The previous instance should still be usable despite the new instance being closed.
-        interactiveInstance.updateComplicationData(complications)
-        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            complications
-        )
-
-        try {
-            // Note the hour hand pips and both complicationSlots should be visible in this image.
-            bitmap.assertAgainstGolden(screenshotRule, "setUserStyle")
-        } finally {
-            interactiveInstance.close()
-        }
-    }
-
     @Test
     fun getComplicationIdAt() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject()
 
         assertNull(interactiveInstance.getComplicationIdAt(0, 0))
         assertThat(interactiveInstance.getComplicationIdAt(85, 165)).isEqualTo(
@@ -1491,23 +880,8 @@
             onUiThreadGlSurfaceCreatedCompletableDeferred,
             onBackgroundThreadGlContextCreatedCompletableDeferred
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers creation of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
 
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         try {
             val wfReady = CompletableDeferred<Unit>()
@@ -1529,21 +903,8 @@
 
     @Test
     fun isConnectionAlive_false_after_close() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
+        val interactiveInstance = getOrCreateTestSubject()
 
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
         assertThat(interactiveInstance.isConnectionAlive()).isTrue()
 
         interactiveInstance.close()
@@ -1561,74 +922,6 @@
         assertTrue(service.hasComplicationDataCache())
     }
 
-    @Ignore // b/225230182
-    @Test
-    fun interactiveAndHeadlessOpenGlWatchFaceInstances() {
-        val surfaceTexture = SurfaceTexture(false)
-        surfaceTexture.setDefaultBufferSize(400, 400)
-        Mockito.`when`(surfaceHolder2.surface).thenReturn(Surface(surfaceTexture))
-        Mockito.`when`(surfaceHolder2.surfaceFrame)
-            .thenReturn(Rect(0, 0, 400, 400))
-
-        wallpaperService = TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2)
-
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                emptyMap()
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
-        val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
-            service.createHeadlessWatchFaceClient(
-                "id",
-                exampleOpenGLWatchFaceComponentName,
-                deviceConfig,
-                200,
-                200
-            )!!.toBundle()
-        )
-
-        // Take screenshots from both instances to confirm rendering works as expected despite the
-        // watch face using shared SharedAssets.
-        val interactiveBitmap = interactiveInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            null
-        )
-
-        interactiveBitmap.assertAgainstGolden(screenshotRule, "opengl_interactive")
-
-        val headlessBitmap = headlessInstance.renderWatchFaceToBitmap(
-            RenderParameters(
-                DrawMode.INTERACTIVE,
-                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
-                null
-            ),
-            Instant.ofEpochMilli(1234567),
-            null,
-            null
-        )
-
-        headlessBitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
-
-        headlessInstance.close()
-        interactiveInstance.close()
-    }
-
     @Test
     fun watchfaceOverlayStyle() {
         val wallpaperService = TestWatchfaceOverlayStyleWatchFaceService(
@@ -1636,24 +929,8 @@
             surfaceHolder,
             WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK))
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
 
-        // Create the engine which triggers creation of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         assertThat(interactiveInstance.overlayStyle.backgroundColor)
             .isEqualTo(Color.valueOf(Color.RED))
@@ -1670,24 +947,8 @@
             surfaceHolder,
             WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK))
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
 
-        // Create the engine which triggers creation of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         interactiveInstance.close()
 
@@ -1696,57 +957,14 @@
     }
 
     @Test
-    fun computeUserStyleSchemaDigestHash() {
-        val headlessInstance1 = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleCanvasAnalogWatchFaceComponentName,
-            DeviceConfig(
-                false,
-                false,
-                0,
-                0
-            ),
-            400,
-            400
-        )!!
-
-        val headlessInstance2 = service.createHeadlessWatchFaceClient(
-            "id",
-            exampleOpenGLWatchFaceComponentName,
-            deviceConfig,
-            400,
-            400
-        )!!
-
-        assertThat(headlessInstance1.getUserStyleSchemaDigestHash()).isNotEqualTo(
-            headlessInstance2.getUserStyleSchemaDigestHash()
-        )
-    }
-
-    @Test
     @OptIn(ComplicationExperimental::class)
     fun edgeComplication_boundingArc() {
         val wallpaperService = TestEdgeComplicationWatchFaceService(
             context,
             surfaceHolder
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers construction of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
 
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         try {
             assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123)
@@ -1760,49 +978,10 @@
         }
     }
 
-    @Test
-    fun headlessLifeCycle() {
-        val headlessInstance = service.createHeadlessWatchFaceClient(
-            "id",
-            ComponentName(
-                "androidx.wear.watchface.client.test",
-                "androidx.wear.watchface.client.test.TestLifeCycleWatchFaceService"
-            ),
-            deviceConfig,
-            400,
-            400
-        )!!
-
-        // Blocks until the headless instance has been fully constructed.
-        headlessInstance.previewReferenceInstant
-        headlessInstance.close()
-
-        assertThat(TestLifeCycleWatchFaceService.lifeCycleEvents).containsExactly(
-            "WatchFaceService.onCreate",
-            "Renderer.constructed",
-            "Renderer.onDestroy",
-            "WatchFaceService.onDestroy"
-        )
-    }
-
     @OptIn(WatchFaceClientExperimental::class, WatchFaceExperimental::class)
     @Test
     fun watchFaceColors() {
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-
-        // Create the engine which triggers creation of InteractiveWatchFaceClient.
-        createEngine()
-
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject()
 
         try {
             val watchFaceColorsLatch = CountDownLatch(1)
@@ -1869,25 +1048,12 @@
             TestWatchFaceServiceWithPreviewImageUpdateRequest(context, surfaceHolder)
         var lastPreviewImageUpdateRequestedId = ""
 
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            service.getOrCreateInteractiveWatchFaceClient(
-                "wfId-1",
-                deviceConfig,
-                systemState,
-                null,
-                complications,
-                { runnable -> runnable.run() },
-                { lastPreviewImageUpdateRequestedId = it }
-            )
-        }
-
-        // Create the engine which triggers creation of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
-
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(
+            watchFaceService = wallpaperService,
+            instanceId = "wfId-1",
+            previewExecutor = { runnable -> runnable.run() },
+            previewListener = { lastPreviewImageUpdateRequestedId = it }
+        )
 
         assertTrue(
             wallpaperService.rendererInitializedLatch.await(
@@ -1909,23 +1075,8 @@
             context,
             surfaceHolder
         )
-        val deferredInteractiveInstance = handlerCoroutineScope.async {
-            @Suppress("deprecation")
-            service.getOrCreateInteractiveWatchFaceClient(
-                "testId",
-                deviceConfig,
-                systemState,
-                null,
-                complications
-            )
-        }
-        // Create the engine which triggers construction of the interactive instance.
-        handler.post {
-            engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper
-        }
 
-        // Wait for the instance to be created.
-        val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance)
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
 
         var lastDisconnectReason = 0
         interactiveInstance.addClientDisconnectListener(
@@ -1933,9 +1084,8 @@
                 override fun onClientDisconnected(@DisconnectReason disconnectReason: Int) {
                     lastDisconnectReason = disconnectReason
                 }
-            },
-            { it.run() }
-        )
+            }
+        ) { it.run() }
 
         // Simulate detach.
         engine.onDestroy()
@@ -1944,665 +1094,232 @@
     }
 }
 
-internal class TestExampleCanvasAnalogWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder
-) : ExampleCanvasAnalogWatchFaceService() {
-    internal lateinit var watchFace: WatchFace
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+class WatchFaceControlClientScreenshotTest : WatchFaceControlClientTestBase() {
+    @get:Rule
+    val screenshotRule: AndroidXScreenshotTestRule =
+        AndroidXScreenshotTestRule("wear/wear-watchface-client")
 
-    init {
-        attachBaseContext(testContext)
-    }
+    private val exampleOpenGLWatchFaceComponentName =
+        componentOf<ExampleOpenGLBackgroundInitWatchFaceService>()
 
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun getOrCreateInteractiveWatchFaceClient() {
+        val interactiveInstance = getOrCreateTestSubject()
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): WatchFace {
-        watchFace = super.createWatchFace(
-            surfaceHolder,
-            watchState,
-            complicationSlotsManager,
-            currentUserStyleRepository
-        )
-        return watchFace
-    }
-}
-
-internal class TestExampleOpenGLBackgroundInitWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder
-) : ExampleOpenGLBackgroundInitWatchFaceService() {
-    internal lateinit var watchFace: WatchFace
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): WatchFace {
-        watchFace = super.createWatchFace(
-            surfaceHolder,
-            watchState,
-            complicationSlotsManager,
-            currentUserStyleRepository
-        )
-        return watchFace
-    }
-}
-
-internal open class TestCrashingWatchFaceService : WatchFaceService() {
-
-    companion object {
-        const val COMPLICATION_ID = 123
-    }
-
-    override fun createComplicationSlotsManager(
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): ComplicationSlotsManager {
-        return ComplicationSlotsManager(
-            listOf(
-                ComplicationSlot.createRoundRectComplicationSlotBuilder(
-                    COMPLICATION_ID,
-                    { _, _ -> throw Exception("Deliberately crashing") },
-                    listOf(ComplicationType.LONG_TEXT),
-                    DefaultComplicationDataSourcePolicy(
-                        SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
-                        ComplicationType.LONG_TEXT
-                    ),
-                    ComplicationSlotBounds(RectF(0.1f, 0.1f, 0.4f, 0.4f))
-                ).build()
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
             ),
-            currentUserStyleRepository
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
         )
-    }
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): WatchFace {
-        throw Exception("Deliberately crashing")
-    }
-}
-
-internal class TestWatchfaceOverlayStyleWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder,
-    private var watchFaceOverlayStyle: WatchFace.OverlayStyle
-) : WatchFaceService() {
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-                // Actually rendering something isn't required.
-            }
-
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-                // Actually rendering something isn't required.
-            }
+        try {
+            bitmap.assertAgainstGolden(screenshotRule, "interactiveScreenshot")
+        } finally {
+            interactiveInstance.close()
         }
-    ).setOverlayStyle(watchFaceOverlayStyle)
-}
-
-internal class TestAsyncCanvasRenderInitWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder,
-    private var initCompletableDeferred: CompletableDeferred<Unit>
-) : WatchFaceService() {
-
-    init {
-        attachBaseContext(testContext)
     }
 
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override suspend fun init() {
-                initCompletableDeferred.await()
-            }
-
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
-                // Actually rendering something isn't required.
-            }
-
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-                TODO("Not yet implemented")
-            }
-        }
-    )
-
-    override fun getSystemTimeProvider() = object : SystemTimeProvider {
-        override fun getSystemTimeMillis() = 123456789L
-
-        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
-    }
-}
-
-internal class TestAsyncGlesRenderInitWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder,
-    private var onUiThreadGlSurfaceCreatedCompletableDeferred: CompletableDeferred<Unit>,
-    private var onBackgroundThreadGlContextCreatedCompletableDeferred: CompletableDeferred<Unit>
-) : WatchFaceService() {
-    internal lateinit var watchFace: WatchFace
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.GlesRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            16
-        ) {
-            override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
-                onUiThreadGlSurfaceCreatedCompletableDeferred.await()
-            }
-
-            override suspend fun onBackgroundThreadGlContextCreated() {
-                onBackgroundThreadGlContextCreatedCompletableDeferred.await()
-            }
-
-            override fun render(zonedDateTime: ZonedDateTime) {
-                // GLES rendering is complicated and not strictly necessary for our test.
-            }
-
-            override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
-                TODO("Not yet implemented")
-            }
-        }
-    )
-}
-
-internal class TestComplicationProviderDefaultsWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder
-) : WatchFaceService() {
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override fun createComplicationSlotsManager(
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): ComplicationSlotsManager {
-        return ComplicationSlotsManager(
-            listOf(
-                ComplicationSlot.createRoundRectComplicationSlotBuilder(
-                    123,
-                    { _, _ ->
-                        object : CanvasComplication {
-                            override fun render(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                zonedDateTime: ZonedDateTime,
-                                renderParameters: RenderParameters,
-                                slotId: Int
-                            ) {
-                            }
-
-                            override fun drawHighlight(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                boundsType: Int,
-                                zonedDateTime: ZonedDateTime,
-                                color: Int
-                            ) {
-                            }
-
-                            override fun getData() = NoDataComplicationData()
-
-                            override fun loadData(
-                                complicationData: ComplicationData,
-                                loadDrawablesAsynchronous: Boolean
-                            ) {
-                            }
-                        }
-                    },
-                    listOf(
-                        ComplicationType.PHOTO_IMAGE,
-                        ComplicationType.LONG_TEXT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    DefaultComplicationDataSourcePolicy(
-                        ComponentName("com.package1", "com.app1"),
-                        ComplicationType.PHOTO_IMAGE,
-                        ComponentName("com.package2", "com.app2"),
-                        ComplicationType.LONG_TEXT,
-                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    ComplicationSlotBounds(
-                        RectF(0.1f, 0.2f, 0.3f, 0.4f)
-                    )
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun getOrCreateInteractiveWatchFaceClient_initialStyle() {
+        val interactiveInstance = getOrCreateTestSubject(
+            // An incomplete map which is OK.
+            userStyle = UserStyleData(
+                mapOf(
+                    "color_style_setting" to "green_style".encodeToByteArray(),
+                    "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value,
+                    "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value
                 )
-                    .build()
-            ),
-            currentUserStyleRepository
+            )
         )
-    }
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
-
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-            }
-        }
-    )
-}
-
-internal class TestEdgeComplicationWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder
-) : WatchFaceService() {
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    @OptIn(ComplicationExperimental::class)
-    override fun createComplicationSlotsManager(
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): ComplicationSlotsManager {
-        return ComplicationSlotsManager(
-            listOf(
-                ComplicationSlot.createEdgeComplicationSlotBuilder(
-                    123,
-                    { _, _ ->
-                        object : CanvasComplication {
-                            override fun render(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                zonedDateTime: ZonedDateTime,
-                                renderParameters: RenderParameters,
-                                slotId: Int
-                            ) {
-                            }
-
-                            override fun drawHighlight(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                boundsType: Int,
-                                zonedDateTime: ZonedDateTime,
-                                color: Int
-                            ) {
-                            }
-
-                            override fun getData() = NoDataComplicationData()
-
-                            override fun loadData(
-                                complicationData: ComplicationData,
-                                loadDrawablesAsynchronous: Boolean
-                            ) {
-                            }
-                        }
-                    },
-                    listOf(
-                        ComplicationType.PHOTO_IMAGE,
-                        ComplicationType.LONG_TEXT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    DefaultComplicationDataSourcePolicy(
-                        ComponentName("com.package1", "com.app1"),
-                        ComplicationType.PHOTO_IMAGE,
-                        ComponentName("com.package2", "com.app2"),
-                        ComplicationType.LONG_TEXT,
-                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    ComplicationSlotBounds(
-                        RectF(0f, 0f, 1f, 1f)
-                    ),
-                    BoundingArc(45f, 90f, 0.1f)
-                )
-                    .build()
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
             ),
-            currentUserStyleRepository
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
         )
-    }
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
-
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-            }
+        try {
+            bitmap.assertAgainstGolden(screenshotRule, "initialStyle")
+        } finally {
+            interactiveInstance.close()
         }
-    )
-}
-
-internal class TestLifeCycleWatchFaceService : WatchFaceService() {
-    companion object {
-        val lifeCycleEvents = ArrayList<String>()
     }
 
-    override fun onCreate() {
-        super.onCreate()
-        lifeCycleEvents.add("WatchFaceService.onCreate")
-    }
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() {
+        val watchFaceService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
 
-    override fun onDestroy() {
-        super.onDestroy()
-        lifeCycleEvents.add("WatchFaceService.onDestroy")
-    }
+        val testId = "testId"
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.DIGITAL,
-        @Suppress("deprecation")
-        object : Renderer.GlesRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            16
-        ) {
-            init {
-                lifeCycleEvents.add("Renderer.constructed")
-            }
+        getOrCreateTestSubject(watchFaceService, instanceId = testId)
 
-            override fun onDestroy() {
-                super.onDestroy()
-                lifeCycleEvents.add("Renderer.onDestroy")
-            }
-
-            override fun render(zonedDateTime: ZonedDateTime) {}
-
-            override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {}
-        }
-    )
-}
-
-internal class TestWatchFaceServiceWithPreviewImageUpdateRequest(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder,
-) : WatchFaceService() {
-    val rendererInitializedLatch = CountDownLatch(1)
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    @Suppress("deprecation")
-    private lateinit var renderer: Renderer.CanvasRenderer
-
-    fun triggerPreviewImageUpdateRequest() {
-        renderer.sendPreviewImageNeedsUpdateRequest()
-    }
-
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): WatchFace {
-        @Suppress("deprecation")
-        renderer = object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override suspend fun init() {
-                rendererInitializedLatch.countDown()
-            }
-
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
-
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-            }
-        }
-        return WatchFace(WatchFaceType.DIGITAL, renderer)
-    }
-}
-
-internal class TestComplicationStyleUpdateWatchFaceService(
-    testContext: Context,
-    private var surfaceHolderOverride: SurfaceHolder
-) : WatchFaceService() {
-
-    init {
-        attachBaseContext(testContext)
-    }
-
-    @Suppress("deprecation")
-    private val complicationsStyleSetting =
-        UserStyleSetting.ComplicationSlotsUserStyleSetting(
-            UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING),
-            resources,
-            R.string.watchface_complications_setting,
-            R.string.watchface_complications_setting_description,
-            icon = null,
-            complicationConfig = listOf(
-                UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                    UserStyleSetting.Option.Id(NO_COMPLICATIONS),
-                    resources,
-                    R.string.watchface_complications_setting_none,
-                    null,
-                    listOf(
-                        UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
-                            123,
-                            enabled = false
-                        )
+        val deferredInteractiveInstance2 = handlerCoroutineScope.async {
+            @Suppress("deprecation")
+            service.getOrCreateInteractiveWatchFaceClient(
+                testId,
+                deviceConfig,
+                systemState,
+                UserStyleData(
+                    mapOf(
+                        "color_style_setting" to "blue_style".encodeToByteArray(),
+                        "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value,
+                        "watch_hand_length_style_setting" to DoubleRangeOption(0.25).id.value
                     )
                 ),
-                UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
-                    UserStyleSetting.Option.Id(LEFT_COMPLICATION),
-                    resources,
-                    R.string.watchface_complications_setting_left,
-                    null,
-                    listOf(
-                        UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay(
-                            123,
-                            enabled = true,
-                            nameResourceId = R.string.left_complication_screen_name,
-                            screenReaderNameResourceId =
-                            R.string.left_complication_screen_reader_name
-                        )
-                    )
-                )
+                complications
+            )
+        }
+
+        val interactiveInstance2 = awaitWithTimeout(deferredInteractiveInstance2)
+        assertThat(interactiveInstance2.instanceId).isEqualTo("testId")
+
+        val bitmap = interactiveInstance2.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
             ),
-            listOf(WatchFaceLayer.COMPLICATIONS)
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
         )
 
-    override fun createUserStyleSchema(): UserStyleSchema =
-        UserStyleSchema(listOf(complicationsStyleSetting))
-
-    override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
-
-    override fun createComplicationSlotsManager(
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ): ComplicationSlotsManager {
-        return ComplicationSlotsManager(
-            listOf(
-                ComplicationSlot.createRoundRectComplicationSlotBuilder(
-                    123,
-                    { _, _ ->
-                        object : CanvasComplication {
-                            override fun render(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                zonedDateTime: ZonedDateTime,
-                                renderParameters: RenderParameters,
-                                slotId: Int
-                            ) {
-                            }
-
-                            override fun drawHighlight(
-                                canvas: Canvas,
-                                bounds: Rect,
-                                boundsType: Int,
-                                zonedDateTime: ZonedDateTime,
-                                color: Int
-                            ) {
-                            }
-
-                            override fun getData() = NoDataComplicationData()
-
-                            override fun loadData(
-                                complicationData: ComplicationData,
-                                loadDrawablesAsynchronous: Boolean
-                            ) {
-                            }
-                        }
-                    },
-                    listOf(
-                        ComplicationType.PHOTO_IMAGE,
-                        ComplicationType.LONG_TEXT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    DefaultComplicationDataSourcePolicy(
-                        ComponentName("com.package1", "com.app1"),
-                        ComplicationType.PHOTO_IMAGE,
-                        ComponentName("com.package2", "com.app2"),
-                        ComplicationType.LONG_TEXT,
-                        SystemDataSources.DATA_SOURCE_STEP_COUNT,
-                        ComplicationType.SHORT_TEXT
-                    ),
-                    ComplicationSlotBounds(
-                        RectF(0.1f, 0.2f, 0.3f, 0.4f)
-                    )
-                ).build()
-            ),
-            currentUserStyleRepository
-        )
+        try {
+            // Note the hour hand pips and both complicationSlots should be visible in this image.
+            bitmap.assertAgainstGolden(screenshotRule, "existingOpenInstance_styleChange")
+        } finally {
+            interactiveInstance2.close()
+        }
     }
 
-    override suspend fun createWatchFace(
-        surfaceHolder: SurfaceHolder,
-        watchState: WatchState,
-        complicationSlotsManager: ComplicationSlotsManager,
-        currentUserStyleRepository: CurrentUserStyleRepository
-    ) = WatchFace(
-        WatchFaceType.ANALOG,
-        @Suppress("deprecation")
-        object : Renderer.CanvasRenderer(
-            surfaceHolder,
-            currentUserStyleRepository,
-            watchState,
-            CanvasType.HARDWARE,
-            16
-        ) {
-            override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {}
+    @SuppressLint("NewApi") // renderWatchFaceToBitmap
+    @Test
+    fun updateInstance() {
+        val interactiveInstance = getOrCreateTestSubject(
+            userStyle = UserStyleData(
+                mapOf(
+                    COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(),
+                    WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value,
+                    DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption.FALSE.id.value,
+                    COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray()
+                )
+            )
+        )
 
-            override fun renderHighlightLayer(
-                canvas: Canvas,
-                bounds: Rect,
-                zonedDateTime: ZonedDateTime
-            ) {
-            }
+        assertThat(interactiveInstance.instanceId).isEqualTo("testId")
+
+        // Note this map doesn't include all the categories, which is fine the others will be set
+        // to their defaults.
+        interactiveInstance.updateWatchFaceInstance(
+            "testId2",
+            UserStyleData(
+                mapOf(
+                    COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(),
+                    WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value,
+                )
+            )
+        )
+
+        assertThat(interactiveInstance.instanceId).isEqualTo("testId2")
+
+        // It should be possible to create an instance with the updated id.
+        val instance =
+            service.getInteractiveWatchFaceClientInstance("testId2")
+        assertThat(instance).isNotNull()
+        instance?.close()
+
+        // The previous instance should still be usable despite the new instance being closed.
+        interactiveInstance.updateComplicationData(complications)
+        val bitmap = interactiveInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            complications
+        )
+
+        try {
+            // Note the hour hand pips and both complicationSlots should be visible in this image.
+            bitmap.assertAgainstGolden(screenshotRule, "setUserStyle")
+        } finally {
+            interactiveInstance.close()
         }
-    )
+    }
+
+    @Ignore // b/225230182
+    @Test
+    fun interactiveAndHeadlessOpenGlWatchFaceInstances() {
+        val surfaceTexture = SurfaceTexture(false)
+        surfaceTexture.setDefaultBufferSize(400, 400)
+        Mockito.`when`(surfaceHolder2.surface).thenReturn(Surface(surfaceTexture))
+        Mockito.`when`(surfaceHolder2.surfaceFrame)
+            .thenReturn(Rect(0, 0, 400, 400))
+
+        val wallpaperService =
+            TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2)
+
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService,
+            complications = emptyMap()
+        )
+
+        val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
+            service.createHeadlessWatchFaceClient(
+                "id",
+                exampleOpenGLWatchFaceComponentName,
+                deviceConfig,
+                200,
+                200
+            )!!.toBundle()
+        )
+
+        // Take screenshots from both instances to confirm rendering works as expected despite the
+        // watch face using shared SharedAssets.
+        val interactiveBitmap = interactiveInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            null
+        )
+
+        interactiveBitmap.assertAgainstGolden(screenshotRule, "opengl_interactive")
+
+        val headlessBitmap = headlessInstance.renderWatchFaceToBitmap(
+            RenderParameters(
+                DrawMode.INTERACTIVE,
+                WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+                null
+            ),
+            Instant.ofEpochMilli(1234567),
+            null,
+            null
+        )
+
+        headlessBitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
+
+        headlessInstance.close()
+        interactiveInstance.close()
+    }
 }