Merge "Use NoActionBar theme for default test activity" into androidx-main
diff --git a/appcompat/appcompat-benchmark/build.gradle b/appcompat/appcompat-benchmark/build.gradle
index 601c7ad..f1d3c0e 100644
--- a/appcompat/appcompat-benchmark/build.gradle
+++ b/appcompat/appcompat-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -34,3 +36,7 @@
 android {
     namespace = "androidx.appcompat.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
index afd7275..911bcf5 100644
--- a/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/1.1.0-beta01.txt
@@ -164,7 +164,7 @@
     method public android.net.Uri? getIconUri();
     method public String getPackageName();
     method public byte[] getSha256Certificate();
-    method public long getUpdatedTimestamp();
+    method public long getUpdatedTimestampMillis();
   }
 
   public static final class MobileApplication.Builder {
@@ -186,7 +186,7 @@
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
-    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
   }
 
diff --git a/appsearch/appsearch-builtin-types/api/current.txt b/appsearch/appsearch-builtin-types/api/current.txt
index afd7275..911bcf5 100644
--- a/appsearch/appsearch-builtin-types/api/current.txt
+++ b/appsearch/appsearch-builtin-types/api/current.txt
@@ -164,7 +164,7 @@
     method public android.net.Uri? getIconUri();
     method public String getPackageName();
     method public byte[] getSha256Certificate();
-    method public long getUpdatedTimestamp();
+    method public long getUpdatedTimestampMillis();
   }
 
   public static final class MobileApplication.Builder {
@@ -186,7 +186,7 @@
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
-    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
   }
 
diff --git a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
index a929d58..c61ebfd 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_1.1.0-beta01.txt
@@ -166,7 +166,7 @@
     method public android.net.Uri? getIconUri();
     method public String getPackageName();
     method public byte[] getSha256Certificate();
-    method public long getUpdatedTimestamp();
+    method public long getUpdatedTimestampMillis();
   }
 
   public static final class MobileApplication.Builder {
@@ -188,7 +188,7 @@
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
-    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
   }
 
diff --git a/appsearch/appsearch-builtin-types/api/restricted_current.txt b/appsearch/appsearch-builtin-types/api/restricted_current.txt
index a929d58..c61ebfd 100644
--- a/appsearch/appsearch-builtin-types/api/restricted_current.txt
+++ b/appsearch/appsearch-builtin-types/api/restricted_current.txt
@@ -166,7 +166,7 @@
     method public android.net.Uri? getIconUri();
     method public String getPackageName();
     method public byte[] getSha256Certificate();
-    method public long getUpdatedTimestamp();
+    method public long getUpdatedTimestampMillis();
   }
 
   public static final class MobileApplication.Builder {
@@ -188,7 +188,7 @@
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setImage(String?);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setName(String?);
     method @SuppressCompatibility @androidx.appsearch.app.ExperimentalAppSearchApi public androidx.appsearch.builtintypes.MobileApplication.Builder setPotentialActions(java.util.List<androidx.appsearch.builtintypes.PotentialAction!>?);
-    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestamp(long);
+    method public androidx.appsearch.builtintypes.MobileApplication.Builder setUpdatedTimestampMillis(long);
     method public androidx.appsearch.builtintypes.MobileApplication.Builder setUrl(String?);
   }
 
diff --git a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
index fc262f2..9195d39 100644
--- a/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
+++ b/appsearch/appsearch-builtin-types/src/androidTest/java/androidx/appsearch/builtintypes/MobileApplicationTest.java
@@ -51,7 +51,7 @@
         builder.setDisplayName("display name");
         builder.setAlternateNames(Arrays.asList("alternate name 1", "alternate name 2"));
         builder.setIconUri(Uri.parse("android.resource://com.example.app/drawable/12345"));
-        builder.setUpdatedTimestamp(1234567890L);
+        builder.setUpdatedTimestampMillis(1234567890L);
         builder.setClassName("com.example.app.MainActivity");
         MobileApplication mobileApplication = builder.build();
 
@@ -63,7 +63,7 @@
                 .containsExactly("alternate name 1", "alternate name 2");
         assertThat(mobileApplication.getIconUri())
                 .isEqualTo(Uri.parse("android.resource://com.example.app/drawable/12345"));
-        assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+        assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
         assertThat(mobileApplication.getClassName()).isEqualTo("com.example.app.MainActivity");
     }
 
@@ -83,7 +83,7 @@
                         .setDisplayName(name)
                         .setAlternateNames(alternateNames)
                         .setIconUri(iconUri)
-                        .setUpdatedTimestamp(updatedTimestamp)
+                        .setUpdatedTimestampMillis(updatedTimestamp)
                         .setClassName(className)
                         .build();
 
@@ -92,7 +92,7 @@
         assertThat(mobileApplication.getAlternateNames()).isEqualTo(alternateNames);
         assertThat(mobileApplication.getIconUri()).isEqualTo(iconUri);
         assertThat(mobileApplication.getSha256Certificate()).isEqualTo(sha256Certificate);
-        assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(updatedTimestamp);
+        assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(updatedTimestamp);
         assertThat(mobileApplication.getClassName()).isEqualTo(className);
     }
 
@@ -103,7 +103,7 @@
         builder.setDisplayName("display name");
         builder.setAlternateNames(Arrays.asList("alternate name 1", "alternate name 2"));
         builder.setIconUri(Uri.parse("android.resource://com.example.app/drawable/12345"));
-        builder.setUpdatedTimestamp(1234567890L);
+        builder.setUpdatedTimestampMillis(1234567890L);
         builder.setClassName("com.example.app.MainActivity");
         MobileApplication mobileApplication = builder.build();
 
@@ -111,7 +111,7 @@
                 .setDisplayName("new display name")
                 .setAlternateNames(Arrays.asList("new alternate name 1", "new alternate name 2"))
                 .setIconUri(Uri.parse("android.resource://com.example.app/drawable/98765"))
-                .setUpdatedTimestamp(9876543210L)
+                .setUpdatedTimestampMillis(9876543210L)
                 .setClassName("com.example.app.NewMainActivity");
 
         // assert the original hasn't changed
@@ -119,7 +119,7 @@
                 .containsExactly("alternate name 1", "alternate name 2");
         assertThat(mobileApplication.getIconUri())
                 .isEqualTo(Uri.parse("android.resource://com.example.app/drawable/12345"));
-        assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+        assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
         assertThat(mobileApplication.getClassName()).isEqualTo("com.example.app.MainActivity");
     }
 
@@ -140,7 +140,7 @@
                         .setDisplayName(name)
                         .setAlternateNames(alternateNames)
                         .setIconUri(iconUri)
-                        .setUpdatedTimestamp(updatedTimestamp)
+                        .setUpdatedTimestampMillis(updatedTimestamp)
                         .setClassName(className)
                         .build();
 
@@ -217,7 +217,7 @@
             assertThat(mobileApplication.getDisplayName()).isEqualTo("display name");
             assertThat(mobileApplication.getIconUri().toString())
                     .isEqualTo("android.resource://com.example.app/drawable/12345");
-            assertThat(mobileApplication.getUpdatedTimestamp()).isEqualTo(1234567890L);
+            assertThat(mobileApplication.getUpdatedTimestampMillis()).isEqualTo(1234567890L);
             assertThat(mobileApplication.getClassName())
                     .isEqualTo("com.example.app.MainActivity");
         } finally {
diff --git a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
index c7cc01a..625dc0b 100644
--- a/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
+++ b/appsearch/appsearch-builtin-types/src/main/java/androidx/appsearch/builtintypes/MobileApplication.java
@@ -57,8 +57,10 @@
 
     @Document.BytesProperty private final byte[] mSha256Certificate;
 
-    @Document.LongProperty(indexingType = LongPropertyConfig.INDEXING_TYPE_RANGE)
-    private final long mUpdatedTimestamp;
+    // Property name set to update to match framework
+    @Document.LongProperty(name = "updatedTimestamp",
+            indexingType = LongPropertyConfig.INDEXING_TYPE_RANGE)
+    private final long mUpdatedTimestampMillis;
 
     @Document.StringProperty private final String mClassName;
 
@@ -79,7 +81,7 @@
             @Nullable String displayName,
             @Nullable Uri iconUri,
             byte @NonNull [] sha256Certificate,
-            long updatedTimestamp,
+            long updatedTimestampMillis,
             @Nullable String className) {
         super(namespace, id, documentScore, creationTimestampMillis, documentTtlMillis, name,
                 alternateNames, description, image, url, potentialActions);
@@ -88,7 +90,7 @@
         mAlternateNames = Preconditions.checkNotNull(alternateNames);
         mIconUri = iconUri;
         mSha256Certificate = Preconditions.checkNotNull(sha256Certificate);
-        mUpdatedTimestamp = updatedTimestamp;
+        mUpdatedTimestampMillis = updatedTimestampMillis;
         mClassName = className;
     }
 
@@ -133,8 +135,8 @@
 
     /** Returns the last time the app was installed or updated on the device. */
     @CurrentTimeMillisLong
-    public long getUpdatedTimestamp() {
-        return mUpdatedTimestamp;
+    public long getUpdatedTimestampMillis() {
+        return mUpdatedTimestampMillis;
     }
 
     /**
@@ -185,7 +187,7 @@
         private String mDisplayName;
         private Uri mIconUri;
         private final byte[] mSha256Certificate;
-        private long mUpdatedTimestamp;
+        private long mUpdatedTimestampMillis;
         private String mClassName;
         private boolean mBuilt = false;
 
@@ -203,7 +205,7 @@
             mDisplayName = mobileApplication.mDisplayName;
             mIconUri = mobileApplication.mIconUri;
             mSha256Certificate = mobileApplication.mSha256Certificate;
-            mUpdatedTimestamp = mobileApplication.mUpdatedTimestamp;
+            mUpdatedTimestampMillis = mobileApplication.mUpdatedTimestampMillis;
             mClassName = mobileApplication.mClassName;
         }
 
@@ -222,9 +224,10 @@
         }
 
         /** Sets the last time the app was installed or updated on the device. */
-        public @NonNull T setUpdatedTimestamp(@CurrentTimeMillisLong long updatedTimestamp) {
+        public @NonNull T setUpdatedTimestampMillis(
+                @CurrentTimeMillisLong long updatedTimestampMillis) {
             resetIfBuilt();
-            mUpdatedTimestamp = updatedTimestamp;
+            mUpdatedTimestampMillis = updatedTimestampMillis;
             return (T) this;
         }
 
@@ -265,7 +268,7 @@
                     mDisplayName,
                     mIconUri,
                     mSha256Certificate,
-                    mUpdatedTimestamp,
+                    mUpdatedTimestampMillis,
                     mClassName);
         }
     }
diff --git a/benchmark/benchmark-darwin-core/build.gradle b/benchmark/benchmark-darwin-core/build.gradle
index b9c1d95..d54e610 100644
--- a/benchmark/benchmark-darwin-core/build.gradle
+++ b/benchmark/benchmark-darwin-core/build.gradle
@@ -6,7 +6,7 @@
  * modifying its settings.
  */
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
 
@@ -54,7 +54,7 @@
 
 androidx {
     name = "Benchmarks - Darwin Core"
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY
     inceptionYear = "2022"
     description = "AndroidX Benchmarks - Darwin Core"
-    publish = Publish.SNAPSHOT_ONLY
 }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 1566464..627198b 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -50,7 +50,7 @@
 
 androidx {
     name = "Benchmarks - Darwin Gradle Plugin"
-    type = LibraryType.GRADLE_PLUGIN
+    type = LibraryType.INTERNAL_GRADLE_PLUGIN
     inceptionYear = "2022"
     description = "AndroidX Benchmarks - Darwin Gradle Plugin"
 }
diff --git a/benchmark/benchmark-darwin/build.gradle b/benchmark/benchmark-darwin/build.gradle
index 9f57fa2..67a7e43 100644
--- a/benchmark/benchmark-darwin/build.gradle
+++ b/benchmark/benchmark-darwin/build.gradle
@@ -6,7 +6,7 @@
  * modifying its settings.
  */
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
 import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -87,5 +87,5 @@
     name = "Benchmarks - Darwin"
     inceptionYear = "2022"
     description = "AndroidX Benchmarks - Darwin"
-    publish = Publish.SNAPSHOT_ONLY
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY
 }
diff --git a/benchmark/benchmark/build.gradle b/benchmark/benchmark/build.gradle
index d3c0e4e..6857553 100644
--- a/benchmark/benchmark/build.gradle
+++ b/benchmark/benchmark/build.gradle
@@ -56,5 +56,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/benchmark/integration-tests/dry-run-benchmark/build.gradle b/benchmark/integration-tests/dry-run-benchmark/build.gradle
index 38997e5..d910c48 100644
--- a/benchmark/integration-tests/dry-run-benchmark/build.gradle
+++ b/benchmark/integration-tests/dry-run-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -32,3 +34,7 @@
 android {
     namespace = "androidx.benchmark.integration.dryrun.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/startup-benchmark/build.gradle b/benchmark/integration-tests/startup-benchmark/build.gradle
index f008e06..dca2751 100644
--- a/benchmark/integration-tests/startup-benchmark/build.gradle
+++ b/benchmark/integration-tests/startup-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -32,3 +34,7 @@
 android {
     namespace = "androidx.benchmark.integration.startup.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/bluetooth/integration-tests/testapp/build.gradle b/bluetooth/integration-tests/testapp/build.gradle
index 1344503..843b63a 100644
--- a/bluetooth/integration-tests/testapp/build.gradle
+++ b/bluetooth/integration-tests/testapp/build.gradle
@@ -5,7 +5,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
 
 /*
  * Copyright 2022 The Android Open Source Project
diff --git a/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl b/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl
new file mode 100644
index 0000000..d9236cd
--- /dev/null
+++ b/browser/browser/api/aidlRelease/current/android/support/customtabs/IAuthTabCallback.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.support.customtabs;
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface IAuthTabCallback {
+  oneway void onNavigationEvent(int navigationEvent, in android.os.Bundle extras) = 1;
+  oneway void onExtraCallback(String callbackName, in android.os.Bundle args) = 2;
+  android.os.Bundle onExtraCallbackWithResult(String callbackName, in android.os.Bundle args) = 3;
+  oneway void onWarmupCompleted(in android.os.Bundle extras) = 4;
+}
diff --git a/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl b/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
index 668a8a6..7662a12 100644
--- a/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
+++ b/browser/browser/api/aidlRelease/current/android/support/customtabs/ICustomTabsService.aidl
@@ -50,4 +50,5 @@
   boolean isEngagementSignalsApiAvailable(in android.support.customtabs.ICustomTabsCallback customTabsCallback, in android.os.Bundle extras) = 12;
   boolean setEngagementSignalsCallback(in android.support.customtabs.ICustomTabsCallback customTabsCallback, in IBinder callback, in android.os.Bundle extras) = 13;
   boolean isEphemeralBrowsingSupported(in android.os.Bundle extras) = 16;
+  boolean newAuthTabSession(in android.support.customtabs.IAuthTabCallback callback, in android.os.Bundle extras) = 17;
 }
diff --git a/browser/browser/api/api_lint.ignore b/browser/browser/api/api_lint.ignore
index 4ce666c..5882aac 100644
--- a/browser/browser/api/api_lint.ignore
+++ b/browser/browser/api/api_lint.ignore
@@ -99,6 +99,12 @@
     Invalid nullability on parameter `name` in method `onServiceDisconnected`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
 
 
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor) parameter #1:
+    Listeners should always be at end of argument list (method `newAuthTabSession`)
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor, int) parameter #1:
+    Listeners should always be at end of argument list (method `newAuthTabSession`)
+ListenerLast: androidx.browser.customtabs.CustomTabsClient#newAuthTabSession(androidx.browser.auth.AuthTabCallback, java.util.concurrent.Executor, int) parameter #2:
+    Listeners should always be at end of argument list (method `newAuthTabSession`)
 ListenerLast: androidx.browser.customtabs.CustomTabsClient#newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback, int) parameter #2:
     Listeners should always be at end of argument list (method `newPendingSession`)
 ListenerLast: androidx.browser.customtabs.CustomTabsClient#newSession(androidx.browser.customtabs.CustomTabsCallback, int) parameter #1:
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index f31423d..f229789 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.browser.auth {
 
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public interface AuthTabCallback {
+    method public void onExtraCallback(String, android.os.Bundle);
+    method public android.os.Bundle onExtraCallbackWithResult(String, android.os.Bundle);
+    method public void onNavigationEvent(int, android.os.Bundle);
+    method public void onWarmupCompleted(android.os.Bundle);
+  }
+
   public final class AuthTabColorSchemeParams {
     method @ColorInt public Integer? getNavigationBarColor();
     method @ColorInt public Integer? getNavigationBarDividerColor();
@@ -17,6 +24,8 @@
 
   @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
     method public static androidx.browser.auth.AuthTabColorSchemeParams getColorSchemeParams(android.content.Intent, @IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int);
+    method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession.PendingSession? getPendingSession();
+    method public androidx.browser.auth.AuthTabSession? getSession();
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public boolean isEphemeralBrowsingEnabled();
     method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
     method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String, String);
@@ -45,6 +54,22 @@
     method public androidx.browser.auth.AuthTabIntent.Builder setColorSchemeParams(@IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int, androidx.browser.auth.AuthTabColorSchemeParams);
     method public androidx.browser.auth.AuthTabIntent.Builder setDefaultColorSchemeParams(androidx.browser.auth.AuthTabColorSchemeParams);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public androidx.browser.auth.AuthTabIntent.Builder setEphemeralBrowsingEnabled(boolean);
+    method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabIntent.Builder setPendingSession(androidx.browser.auth.AuthTabSession.PendingSession);
+    method public androidx.browser.auth.AuthTabIntent.Builder setSession(androidx.browser.auth.AuthTabSession);
+  }
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public final class AuthTabSession {
+  }
+
+  @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static class AuthTabSession.PendingSession {
+  }
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabSessionToken {
+    method public androidx.browser.auth.AuthTabCallback? getCallback();
+    method public android.app.PendingIntent? getId();
+    method public static androidx.browser.auth.AuthTabSessionToken? getSessionTokenFromIntent(android.content.Intent);
+    method public boolean hasId();
+    method public boolean isAssociatedWith(androidx.browser.auth.AuthTabSession);
   }
 
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
@@ -150,6 +175,7 @@
   }
 
   public class CustomTabsClient {
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession? attachAuthTabSession(androidx.browser.auth.AuthTabSession.PendingSession);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsSession? attachSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
     method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
     method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
@@ -158,6 +184,9 @@
     method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
     method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
     method public static boolean isSetNetworkSupported(android.content.Context, String);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?, int);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.auth.AuthTabSession.PendingSession newPendingAuthTabSession(android.content.Context, int, java.util.concurrent.Executor?, androidx.browser.auth.AuthTabCallback?);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
     method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
     method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
@@ -313,11 +342,13 @@
 
   public abstract class CustomTabsService extends android.app.Service {
     ctor public CustomTabsService();
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean cleanUpSession(androidx.browser.auth.AuthTabSessionToken);
     method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
     method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
     method protected boolean isEngagementSignalsApiAvailable(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing protected boolean isEphemeralBrowsingSupported(android.os.Bundle);
     method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean newAuthTabSession(androidx.browser.auth.AuthTabSessionToken);
     method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
     method public android.os.IBinder onBind(android.content.Intent?);
     method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index ecc3b90..a0ad85a 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.browser.auth {
 
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public interface AuthTabCallback {
+    method public void onExtraCallback(String, android.os.Bundle);
+    method public android.os.Bundle onExtraCallbackWithResult(String, android.os.Bundle);
+    method public void onNavigationEvent(int, android.os.Bundle);
+    method public void onWarmupCompleted(android.os.Bundle);
+  }
+
   public final class AuthTabColorSchemeParams {
     method @ColorInt public Integer? getNavigationBarColor();
     method @ColorInt public Integer? getNavigationBarDividerColor();
@@ -17,6 +24,8 @@
 
   @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabIntent {
     method public static androidx.browser.auth.AuthTabColorSchemeParams getColorSchemeParams(android.content.Intent, @IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int);
+    method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession.PendingSession? getPendingSession();
+    method public androidx.browser.auth.AuthTabSession? getSession();
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public boolean isEphemeralBrowsingEnabled();
     method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String);
     method public void launch(androidx.activity.result.ActivityResultLauncher<android.content.Intent!>, android.net.Uri, String, String);
@@ -45,6 +54,22 @@
     method public androidx.browser.auth.AuthTabIntent.Builder setColorSchemeParams(@IntRange(from=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT, to=androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK) int, androidx.browser.auth.AuthTabColorSchemeParams);
     method public androidx.browser.auth.AuthTabIntent.Builder setDefaultColorSchemeParams(androidx.browser.auth.AuthTabColorSchemeParams);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing public androidx.browser.auth.AuthTabIntent.Builder setEphemeralBrowsingEnabled(boolean);
+    method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabIntent.Builder setPendingSession(androidx.browser.auth.AuthTabSession.PendingSession);
+    method public androidx.browser.auth.AuthTabIntent.Builder setSession(androidx.browser.auth.AuthTabSession);
+  }
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public final class AuthTabSession {
+  }
+
+  @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static class AuthTabSession.PendingSession {
+  }
+
+  @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public class AuthTabSessionToken {
+    method public androidx.browser.auth.AuthTabCallback? getCallback();
+    method public android.app.PendingIntent? getId();
+    method public static androidx.browser.auth.AuthTabSessionToken? getSessionTokenFromIntent(android.content.Intent);
+    method public boolean hasId();
+    method public boolean isAssociatedWith(androidx.browser.auth.AuthTabSession);
   }
 
   @SuppressCompatibility @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAuthTab {
@@ -161,6 +186,7 @@
   }
 
   public class CustomTabsClient {
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.auth.AuthTabSession? attachAuthTabSession(androidx.browser.auth.AuthTabSession.PendingSession);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public androidx.browser.customtabs.CustomTabsSession? attachSession(androidx.browser.customtabs.CustomTabsSession.PendingSession);
     method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
     method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
@@ -169,6 +195,9 @@
     method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
     method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
     method public static boolean isSetNetworkSupported(android.content.Context, String);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab public androidx.browser.auth.AuthTabSession? newAuthTabSession(androidx.browser.auth.AuthTabCallback?, java.util.concurrent.Executor?, int);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.auth.AuthTabSession.PendingSession newPendingAuthTabSession(android.content.Context, int, java.util.concurrent.Executor?, androidx.browser.auth.AuthTabCallback?);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalPendingSession public static androidx.browser.customtabs.CustomTabsSession.PendingSession newPendingSession(android.content.Context, androidx.browser.customtabs.CustomTabsCallback?, int);
     method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
     method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
@@ -324,11 +353,13 @@
 
   public abstract class CustomTabsService extends android.app.Service {
     ctor public CustomTabsService();
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean cleanUpSession(androidx.browser.auth.AuthTabSessionToken);
     method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
     method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
     method protected boolean isEngagementSignalsApiAvailable(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle);
     method @SuppressCompatibility @androidx.browser.customtabs.ExperimentalEphemeralBrowsing protected boolean isEphemeralBrowsingSupported(android.os.Bundle);
     method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method @SuppressCompatibility @androidx.browser.auth.ExperimentalAuthTab protected boolean newAuthTabSession(androidx.browser.auth.AuthTabSessionToken);
     method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
     method public android.os.IBinder onBind(android.content.Intent?);
     method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
diff --git a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
index 9b0aa7e..3831801 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/customtabs/TestCustomTabsService.java
@@ -24,6 +24,7 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.ICustomTabsService;
 
@@ -151,6 +152,12 @@
         public boolean isEphemeralBrowsingSupported(Bundle extras) throws RemoteException {
             return mMock.isEphemeralBrowsingSupported(extras);
         }
+
+        @Override
+        public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras)
+                throws RemoteException {
+            return false;
+        }
     };
 
     @Override
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java
new file mode 100644
index 0000000..633eb08
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabCallback.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 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.browser.auth;
+
+import android.os.Bundle;
+
+import androidx.browser.customtabs.CustomTabsCallback;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsService;
+
+import org.jspecify.annotations.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A callback class for Auth Tab clients to get messages regarding events in their Auth Tabs. In the
+ * implementation, all callbacks are sent to the {@link Executor} provided by the client to
+ * {@link CustomTabsClient#newAuthTabSession} or its UI thread if one is not provided.
+ */
+@ExperimentalAuthTab
+public interface AuthTabCallback {
+    /**
+     * To be called when a navigation event happens.
+     *
+     * @param navigationEvent The code corresponding to the navigation event.
+     * @param extras          Reserved for future use.
+     */
+    void onNavigationEvent(@CustomTabsCallback.NavigationEvent int navigationEvent,
+            @NonNull Bundle extras);
+
+    /**
+     * Unsupported callbacks that may be provided by the implementation.
+     *
+     * <p>
+     * <strong>Note:</strong>Clients should <strong>never</strong> rely on this callback to be
+     * called and/or to have a defined behavior, as it is entirely implementation-defined and not
+     * supported.
+     *
+     * <p> This can be used by implementations to add extra callbacks, for testing or experimental
+     * purposes.
+     *
+     * @param callbackName Name of the extra callback.
+     * @param args         Arguments for the callback
+     */
+    void onExtraCallback(@NonNull String callbackName, @NonNull Bundle args);
+
+    /**
+     * The same as {@link #onExtraCallback}, except that this method allows the Auth Tab provider to
+     * return a result.
+     *
+     * A return value of {@link Bundle#EMPTY} will be used to signify that the client does not know
+     * how to handle the callback.
+     *
+     * As optional best practices, {@link CustomTabsService#KEY_SUCCESS} could be use to identify
+     * that callback was *successfully* handled. For example, when returning a message with result:
+     * <pre><code>
+     *     Bundle result = new Bundle();
+     *     result.putString("message", message);
+     *     if (success)
+     *         result.putBoolean(CustomTabsService#KEY_SUCCESS, true);
+     *     return result;
+     * </code></pre>
+     * The caller side:
+     * <pre><code>
+     *     Bundle result = extraCallbackWithResult(callbackName, args);
+     *     if (result.getBoolean(CustomTabsService#KEY_SUCCESS)) {
+     *         // callback was successfully handled
+     *     }
+     * </code></pre>
+     */
+    @NonNull
+    Bundle onExtraCallbackWithResult(@NonNull String callbackName, @NonNull Bundle args);
+
+    /**
+     * Called when the browser process finished warming up initiated by
+     * {@link CustomTabsClient#warmup()}.
+     *
+     * @param extras Reserved for future use.
+     */
+    void onWarmupCompleted(@NonNull Bundle extras);
+}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
index 25a7667..43c0e43 100644
--- a/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabIntent.java
@@ -23,12 +23,15 @@
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME_PARAMS;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION;
+import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION_ID;
 
 import android.app.Activity;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.util.SparseArray;
 
 import androidx.activity.result.ActivityResultCallback;
@@ -40,6 +43,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.browser.customtabs.CustomTabsIntent;
 import androidx.browser.customtabs.ExperimentalEphemeralBrowsing;
+import androidx.browser.customtabs.ExperimentalPendingSession;
 import androidx.core.os.BundleCompat;
 
 import org.jspecify.annotations.NonNull;
@@ -147,6 +151,9 @@
     /** An {@link Intent} used to start the Auth Tab Activity. */
     public final @NonNull Intent intent;
 
+    private final @Nullable AuthTabSession mSession;
+    private final AuthTabSession.@Nullable PendingSession mPendingSession;
+
     /**
      * Launches an Auth Tab Activity. Must be used for flows that result in a redirect with a custom
      * scheme.
@@ -220,8 +227,21 @@
         return defaults;
     }
 
-    private AuthTabIntent(@NonNull Intent intent) {
+    private AuthTabIntent(@NonNull Intent intent, @Nullable AuthTabSession session,
+            AuthTabSession.@Nullable PendingSession pendingSession) {
         this.intent = intent;
+        mSession = session;
+        mPendingSession = pendingSession;
+    }
+
+    @Nullable
+    public AuthTabSession getSession() {
+        return mSession;
+    }
+
+    @ExperimentalPendingSession
+    public AuthTabSession.@Nullable PendingSession getPendingSession() {
+        return mPendingSession;
     }
 
     /**
@@ -233,11 +253,48 @@
                 new AuthTabColorSchemeParams.Builder();
         private @Nullable SparseArray<Bundle> mColorSchemeParamBundles;
         private @Nullable Bundle mDefaultColorSchemeBundle;
+        private @Nullable AuthTabSession mSession;
+        private AuthTabSession.@Nullable PendingSession mPendingSession;
 
         public Builder() {
         }
 
         /**
+         * Associates the {@link Intent} with the given {@link AuthTabSession}.
+         *
+         * Guarantees that the {@link Intent} will be sent to the same component as the one the
+         * session is associated with.
+         */
+        public @NonNull Builder setSession(@NonNull AuthTabSession session) {
+            mSession = session;
+            mIntent.setPackage(session.getComponentName().getPackageName());
+            setSessionParameters(session.getBinder(), session.getId());
+            return this;
+        }
+
+        /**
+         * Associates the {@link Intent} with the given {@link AuthTabSession.PendingSession}.
+         * Overrides the effect of {@link #setSession}.
+         */
+        @ExperimentalPendingSession
+        public @NonNull Builder setPendingSession(AuthTabSession.@NonNull PendingSession session) {
+            mPendingSession = session;
+            setSessionParameters(null, session.getId());
+            return this;
+        }
+
+        private void setSessionParameters(@Nullable IBinder binder,
+                @Nullable PendingIntent sessionId) {
+            Bundle bundle = new Bundle();
+            bundle.putBinder(EXTRA_SESSION, binder);
+            if (sessionId != null) {
+                bundle.putParcelable(EXTRA_SESSION_ID, sessionId);
+            }
+
+            mIntent.putExtras(bundle);
+        }
+
+        /**
          * Sets whether to enable ephemeral browsing within the Auth Tab. If ephemeral browsing is
          * enabled, and the browser supports it, the Auth Tab does not share cookies or other data
          * with the browser that handles the auth session.
@@ -363,7 +420,7 @@
                 mIntent.putExtras(bundle);
             }
 
-            return new AuthTabIntent(mIntent);
+            return new AuthTabIntent(mIntent, mSession, mPendingSession);
         }
     }
 
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java
new file mode 100644
index 0000000..a9e30e9
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSession.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 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.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.support.customtabs.IAuthTabCallback;
+
+import androidx.annotation.RestrictTo;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.ExperimentalPendingSession;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A class to be used for AuthTab related communication. Clients that want to launch Auth Tabs can
+ * use this class exclusively to handle all related communication.
+ *
+ * Use {@link CustomTabsClient#newAuthTabSession} to create an instance.
+ */
+@ExperimentalAuthTab
+public final class AuthTabSession {
+    private final IAuthTabCallback mCallback;
+    private final ComponentName mComponentName;
+
+    /**
+     * The session ID represented by {@link PendingIntent}. Other apps cannot forge
+     * {@link PendingIntent}. The {@link PendingIntent#equals(Object)} method considers two
+     * {@link PendingIntent} objects equal if their action, data, type, class and category are the
+     * same (even across a process being killed).
+     *
+     * {@see Intent#filterEquals()}
+     */
+    @Nullable
+    private final PendingIntent mId;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public AuthTabSession(@NonNull IAuthTabCallback callback, @NonNull ComponentName componentName,
+            @Nullable PendingIntent sessionId) {
+        mCallback = callback;
+        mComponentName = componentName;
+        mId = sessionId;
+    }
+
+    /* package */ IBinder getBinder() {
+        return mCallback.asBinder();
+    }
+
+    /* package */ ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    @Nullable
+        /* package */ PendingIntent getId() {
+        return mId;
+    }
+
+    /**
+     * A class to be used instead of {@link AuthTabSession} when an Auth Tab is launched before a
+     * Service connection is established.
+     *
+     * Use {@link CustomTabsClient#newPendingAuthTabSession} to create an instance.
+     * Use {@link CustomTabsClient#attachAuthTabSession(PendingSession)} to get an
+     * {@link AuthTabSession}.
+     */
+    @ExperimentalPendingSession
+    public static class PendingSession {
+        @Nullable
+        private final PendingIntent mId;
+        @Nullable
+        private final Executor mExecutor;
+        @Nullable
+        private final AuthTabCallback mCallback;
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        public PendingSession(@Nullable PendingIntent sessionId, @Nullable Executor executor,
+                @Nullable AuthTabCallback callback) {
+            mId = sessionId;
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Nullable
+        public PendingIntent getId() {
+            return mId;
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Nullable
+        public Executor getExecutor() {
+            return mExecutor;
+        }
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Nullable
+        public AuthTabCallback getCallback() {
+            return mCallback;
+        }
+    }
+}
diff --git a/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java
new file mode 100644
index 0000000..ff60fab
--- /dev/null
+++ b/browser/browser/src/main/java/androidx/browser/auth/AuthTabSessionToken.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 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.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.core.content.IntentCompat;
+
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Wrapper class that can be used as a unique identifier for a session. Also contains an accessor
+ * for the {@link AuthTabCallback} for the session if there was any.
+ */
+@ExperimentalAuthTab
+public class AuthTabSessionToken {
+    private static final String TAG = "AuthTabSessionToken";
+
+    /**
+     * Both {@link #mCallbackBinder} and {@link #mSessionId} are used as session ID.
+     * At least one of the ID should be not null. If {@link #mSessionId} is null,
+     * the session will be invalidated as soon as the client goes away.
+     * Otherwise the browser will attempt to keep the session parameters,
+     * but it might drop them to reclaim resources
+     */
+    @Nullable
+    private final IAuthTabCallback mCallbackBinder;
+    @Nullable
+    private final PendingIntent mSessionId;
+    @Nullable
+    private final AuthTabCallback mCallback;
+
+    /**
+     * Constructs a new session token.
+     *
+     * Both {@code callbackBinder} and {@code sessionId} are used as session ID. At least one of the
+     * IDs should be not null. If {@code sessionId} is null, the session will be invalidated as soon
+     * as the client goes away. Otherwise the browser will attempt to keep the session parameters,
+     * but it might drop them to reclaim resources.
+     *
+     * @param callbackBinder The {@link IAuthTabCallback} associated with this session.
+     * @param sessionId      The {@link PendingIntent} associated with this session.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public AuthTabSessionToken(@Nullable IAuthTabCallback callbackBinder,
+            @Nullable PendingIntent sessionId) {
+        if (callbackBinder == null && sessionId == null) {
+            throw new IllegalStateException(
+                    "AuthTabSessionToken must have either a session id or a callback (or both).");
+        }
+
+        mCallbackBinder = callbackBinder;
+        mSessionId = sessionId;
+
+        mCallback = mCallbackBinder == null ? null : new AuthTabCallback() {
+            @Override
+            public void onNavigationEvent(int navigationEvent, @NonNull Bundle extras) {
+                try {
+                    mCallbackBinder.onNavigationEvent(navigationEvent, extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+                }
+            }
+
+            @Override
+            public void onExtraCallback(@NonNull String callbackName, @NonNull Bundle args) {
+                try {
+                    mCallbackBinder.onExtraCallback(callbackName, args);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+                }
+            }
+
+            @NonNull
+            @Override
+            public Bundle onExtraCallbackWithResult(@NonNull String callbackName,
+                    @NonNull Bundle args) {
+                try {
+                    return mCallbackBinder.onExtraCallbackWithResult(callbackName, args);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+                    return Bundle.EMPTY;
+                }
+            }
+
+            @Override
+            public void onWarmupCompleted(@NonNull Bundle extras) {
+                try {
+                    mCallbackBinder.onWarmupCompleted(extras);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "RemoteException during IAuthTabCallback transaction");
+                }
+            }
+        };
+    }
+
+    /**
+     * Obtain an {@link AuthTabSessionToken} from an intent. See {@link AuthTabIntent.Builder}
+     * for ways to generate an intent for Auth Tabs.
+     *
+     * @param intent The intent to generate the token from. This has to include an extra for
+     *               {@link CustomTabsIntent#EXTRA_SESSION}.
+     * @return The token that was generated.
+     */
+    public static @Nullable AuthTabSessionToken getSessionTokenFromIntent(@NonNull Intent intent) {
+        Bundle b = intent.getExtras();
+        if (b == null) return null;
+        IBinder binder = b.getBinder(CustomTabsIntent.EXTRA_SESSION);
+        PendingIntent sessionId = IntentCompat.getParcelableExtra(intent,
+                CustomTabsIntent.EXTRA_SESSION_ID, PendingIntent.class);
+        if (binder == null && sessionId == null) return null;
+        IAuthTabCallback callback = binder == null ? null : IAuthTabCallback.Stub.asInterface(
+                binder);
+        return new AuthTabSessionToken(callback, sessionId);
+    }
+
+    /**
+     * @return {@link AuthTabCallback} corresponding to this session if there was any non-null
+     * callbacks passed by the client.
+     */
+    @Nullable
+    public AuthTabCallback getCallback() {
+        return mCallback;
+    }
+
+    /**
+     * @return The callback binder corresponding to this session, null if there is none.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Nullable
+    public IBinder getCallbackBinder() {
+        if (mCallbackBinder == null) return null;
+        return mCallbackBinder.asBinder();
+    }
+
+    /** @return The session id corresponding to this session, null if there is none. */
+    @Nullable
+    public PendingIntent getId() {
+        return mSessionId;
+    }
+
+    /** @return Whether this token is associated with a session id. */
+    public boolean hasId() {
+        return mSessionId != null;
+    }
+
+    @Override
+    public int hashCode() {
+        if (mSessionId != null) return mSessionId.hashCode();
+
+        return getCallbackBinderAssertNotNull().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof AuthTabSessionToken)) return false;
+        AuthTabSessionToken other = (AuthTabSessionToken) o;
+
+        PendingIntent otherSessionId = other.getId();
+        // If one object has a session id and the other one doesn't, they're not equal.
+        if ((mSessionId == null) != (otherSessionId == null)) return false;
+
+        // If both objects have an id, check that they are equal.
+        if (mSessionId != null) return mSessionId.equals(otherSessionId);
+
+        // Otherwise check for binder equality.
+        return getCallbackBinderAssertNotNull().equals(other.getCallbackBinderAssertNotNull());
+    }
+
+    private IBinder getCallbackBinderAssertNotNull() {
+        if (mCallbackBinder == null) {
+            throw new IllegalStateException(
+                    "AuthTabSessionToken must have valid binder or pending session");
+        }
+        return mCallbackBinder.asBinder();
+    }
+
+    /**
+     * @return Whether this token is associated with the given session.
+     */
+    public boolean isAssociatedWith(@NonNull AuthTabSession session) {
+        return session.getBinder().equals(mCallbackBinder);
+    }
+
+    static class MockCallback extends IAuthTabCallback.Stub {
+        @Override
+        public void onNavigationEvent(int navigationEvent, Bundle extras) throws RemoteException {
+        }
+
+        @Override
+        public void onExtraCallback(String callbackName, Bundle args) throws RemoteException {
+        }
+
+        @Override
+        public Bundle onExtraCallbackWithResult(String callbackName, Bundle args)
+                throws RemoteException {
+            return Bundle.EMPTY;
+        }
+
+        @Override
+        public void onWarmupCompleted(Bundle extras) throws RemoteException {
+        }
+    }
+}
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 dcac37a..990fe57 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsCallback.java
@@ -38,6 +38,15 @@
  */
 public class CustomTabsCallback {
     /**
+     * To be called when a navigation event happens.
+     *
+     * @param navigationEvent The code corresponding to the navigation event.
+     * @param extras Reserved for future use.
+     */
+    public void onNavigationEvent(@NavigationEvent int navigationEvent, @Nullable Bundle extras) {
+    }
+
+    /**
      * Sent when the tab has started loading a page.
      */
     public static final int NAVIGATION_STARTED = 1;
@@ -68,6 +77,14 @@
      */
     public static final int TAB_HIDDEN = 6;
 
+    /** Possible navigation event values. */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({NAVIGATION_STARTED, NAVIGATION_FINISHED, NAVIGATION_FAILED, NAVIGATION_ABORTED,
+            TAB_SHOWN, TAB_HIDDEN})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NavigationEvent {
+    }
+
     /**
      * Key for the extra included in {@link #onRelationshipValidationResult} {@code extras}
      * containing whether the verification was performed while the device was online. This may be
@@ -77,14 +94,6 @@
     public static final String ONLINE_EXTRAS_KEY = "online";
 
     /**
-     * To be called when a navigation event happens.
-     *
-     * @param navigationEvent The code corresponding to the navigation event.
-     * @param extras Reserved for future use.
-     */
-    public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {}
-
-    /**
      * Unsupported callbacks that may be provided by the implementation.
      *
      * <p>
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 2305715..2be2101 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsClient.java
@@ -26,26 +26,35 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.net.Uri;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.ICustomTabsService;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.OptIn;
+import androidx.browser.auth.AuthTabCallback;
+import androidx.browser.auth.AuthTabSession;
+import androidx.browser.auth.ExperimentalAuthTab;
+
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Class to communicate with a {@link CustomTabsService} and create
  * {@link CustomTabsSession} from it.
  */
+@OptIn(markerClass = ExperimentalAuthTab.class)
 public class CustomTabsClient {
     private static final String TAG = "CustomTabsClient";
 
@@ -285,6 +294,156 @@
         return new CustomTabsSession.PendingSession(callback, sessionId);
     }
 
+    /**
+     * Creates a new pending session with an optional callback. This session can be converted to
+     * a standard session using {@link #attachSession} after connection.
+     *
+     * {@see PendingSession}
+     */
+    @ExperimentalAuthTab
+    @ExperimentalPendingSession
+    public static AuthTabSession.@NonNull PendingSession newPendingAuthTabSession(
+            @NonNull Context context, int id, @Nullable Executor executor,
+            @Nullable AuthTabCallback callback) {
+        PendingIntent sessionId = createSessionId(context, id);
+
+        return new AuthTabSession.PendingSession(sessionId, executor, callback);
+    }
+
+    /**
+     * Creates a new session through an ICustomTabsService with the optional callback. This session
+     * can be used to associate any related communication through the service with an intent and
+     * then later with a Custom Tab. The client can then send later service calls or intents to
+     * through same session-intent-Custom Tab association.
+     *
+     * @param callback The callback through which the client will receive updates about the created
+     *                 session. Can be null.
+     * @param executor The {@link Executor} to be used to execute the callbacks. If null, the
+     *                 callbacks will be received on the UI thread.
+     * @return The session object that was created as a result of the transaction. The client can
+     * use this to relay session specific calls. Null if the service failed to respond
+     * (threw a RemoteException).
+     */
+    @ExperimentalAuthTab
+    @Nullable
+    public AuthTabSession newAuthTabSession(@Nullable AuthTabCallback callback,
+            @Nullable Executor executor) {
+        return newAuthTabSessionInternal(callback, executor, null);
+    }
+
+    /**
+     * Creates a new session through an ICustomTabsService with the optional callback. This session
+     * can be used to associate any related communication through the service with an intent and
+     * then later with a Custom Tab. The client can then send later service calls or intents to
+     * through same session-intent-Custom Tab association.
+     *
+     * @param callback The callback through which the client will receive updates about the created
+     *                 session. Can be null.
+     * @param executor The {@link Executor} to be used to execute the callbacks. If null, the
+     *                 callbacks will be received on the UI thread.
+     * @param id       The session id. If the session with the specified id already exists for
+     *                 the given client application, the new callback is supplied to that session
+     *                 and
+     *                 further attempts to launch URLs using that session will update the
+     *                 existing Auth
+     *                 Tab instead of launching a new one.
+     * @return The session object that was created as a result of the transaction. The client can
+     * use this to relay session specific calls. Null if the service failed to respond
+     * (threw a RemoteException).
+     */
+    @ExperimentalAuthTab
+    @Nullable
+    public AuthTabSession newAuthTabSession(@Nullable AuthTabCallback callback,
+            @Nullable Executor executor, int id) {
+        return newAuthTabSessionInternal(callback, executor,
+                createSessionId(mApplicationContext, id));
+    }
+
+    @Nullable
+    private AuthTabSession newAuthTabSessionInternal(@Nullable AuthTabCallback callback,
+            @Nullable Executor executor, @Nullable PendingIntent sessionId) {
+        IAuthTabCallback.Stub wrapper = createAuthTabCallbackWrapper(callback, executor);
+
+        try {
+            boolean success;
+
+            Bundle extras = new Bundle();
+            extras.putParcelable(CustomTabsIntent.EXTRA_SESSION_ID, sessionId);
+            success = mService.newAuthTabSession(wrapper, extras);
+
+            if (!success) return null;
+        } catch (RemoteException e) {
+            return null;
+        }
+        return new AuthTabSession(wrapper, mServiceComponentName, sessionId);
+    }
+
+    private IAuthTabCallback.Stub createAuthTabCallbackWrapper(@Nullable AuthTabCallback callback,
+            @Nullable Executor executor) {
+        return new IAuthTabCallback.Stub() {
+            private final Executor mExecutor = executor != null ? executor : new Handler(
+                    Looper.getMainLooper())::post;
+
+            @Override
+            public void onNavigationEvent(int navigationEvent, Bundle extras)
+                    throws RemoteException {
+                if (callback == null) return;
+                long identity = Binder.clearCallingIdentity();
+                try {
+                    mExecutor.execute(() -> callback.onNavigationEvent(navigationEvent, extras));
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+
+            @Override
+            public void onExtraCallback(String callbackName, Bundle args) throws RemoteException {
+                if (callback == null) return;
+                long identity = Binder.clearCallingIdentity();
+                try {
+                    mExecutor.execute(() -> callback.onExtraCallback(callbackName, args));
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+
+            @Override
+            public Bundle onExtraCallbackWithResult(String callbackName, Bundle args)
+                    throws RemoteException {
+                if (callback == null) return Bundle.EMPTY;
+                long identity = Binder.clearCallingIdentity();
+                try {
+                    return callback.onExtraCallbackWithResult(callbackName, args);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+
+            @Override
+            public void onWarmupCompleted(Bundle extras) throws RemoteException {
+                if (callback == null) return;
+                long identity = Binder.clearCallingIdentity();
+                try {
+                    mExecutor.execute(() -> callback.onWarmupCompleted(extras));
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        };
+    }
+
+    /**
+     * Associate {@link AuthTabSession.PendingSession} with the service and turn it into an
+     * {@link AuthTabSession}.
+     */
+    @ExperimentalAuthTab
+    @ExperimentalPendingSession
+    @Nullable
+    public AuthTabSession attachAuthTabSession(AuthTabSession.@NonNull PendingSession session) {
+        return newAuthTabSessionInternal(session.getCallback(), session.getExecutor(),
+                session.getId());
+    }
+
     private @Nullable CustomTabsSession newSessionInternal(
             final @Nullable CustomTabsCallback callback, @Nullable PendingIntent sessionId) {
         ICustomTabsCallback.Stub wrapper = createCallbackWrapper(callback);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index dd2faf2..95ef578 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -25,11 +25,14 @@
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.ICustomTabsService;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.RestrictTo;
+import androidx.browser.auth.AuthTabSessionToken;
+import androidx.browser.auth.ExperimentalAuthTab;
 import androidx.collection.SimpleArrayMap;
 
 import org.jspecify.annotations.NonNull;
@@ -333,6 +336,23 @@
                 return bundle.getParcelable(CustomTabsSession.TARGET_ORIGIN_KEY);
             }
         }
+
+        @ExperimentalAuthTab
+        @Override
+        public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras) {
+            PendingIntent sessionId = getSessionIdFromBundle(extras);
+            AuthTabSessionToken sessionToken = new AuthTabSessionToken(callback, sessionId);
+            try {
+                DeathRecipient deathRecipient = () -> cleanUpSession(sessionToken);
+                synchronized (mDeathRecipientMap) {
+                    callback.asBinder().linkToDeath(deathRecipient, 0);
+                    mDeathRecipientMap.put(callback.asBinder(), deathRecipient);
+                }
+                return CustomTabsService.this.newAuthTabSession(sessionToken);
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
     };
 
     @Override
@@ -365,6 +385,31 @@
     }
 
     /**
+     * Called when the client side {@link IBinder} for this {@link AuthTabSessionToken} is dead.
+     * Can also be used to clean up {@link DeathRecipient} instances allocated for the given token.
+     *
+     * @param sessionToken The session token for which the {@link DeathRecipient} call has been
+     *                     received.
+     * @return Whether the clean up was successful. Multiple calls with two tokens holdings the
+     * same binder will return false.
+     */
+    @ExperimentalAuthTab
+    protected boolean cleanUpSession(@NonNull AuthTabSessionToken sessionToken) {
+        try {
+            synchronized (mDeathRecipientMap) {
+                IBinder binder = sessionToken.getCallbackBinder();
+                if (binder == null) return false;
+                DeathRecipient deathRecipient = mDeathRecipientMap.get(binder);
+                binder.unlinkToDeath(deathRecipient, 0);
+                mDeathRecipientMap.remove(binder);
+            }
+        } catch (NoSuchElementException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Warms up the browser process asynchronously.
      *
      * @param flags Reserved for future use.
@@ -621,4 +666,20 @@
     protected boolean isEphemeralBrowsingSupported(@NonNull Bundle extras) {
         return false;
     }
+
+    /**
+     * Creates a new Auth Tab session through an ICustomTabsService with the optional callback. This
+     * session can be used to associate any related communication through the service with an intent
+     * and then later with an Auth Tab. The client can then send later service calls or intents
+     * through the same session-intent-Auth Tab association.
+     *
+     * @param sessionToken Session token to be used as a unique identifier. This also has access
+     *                     to the {@link AuthTabCallback} passed from the client side through
+     *                     {@link AuthTabSessionToken#getCallback()}.
+     * @return Whether a new session was successfully created.
+     */
+    @ExperimentalAuthTab
+    protected boolean newAuthTabSession(@NonNull AuthTabSessionToken sessionToken) {
+        return false;
+    }
 }
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
index 0dc92fa..c68384f 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsSession.java
@@ -26,6 +26,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.ICustomTabsService;
 import android.support.customtabs.IEngagementSignalsCallback;
@@ -702,5 +703,11 @@
         public boolean isEphemeralBrowsingSupported(Bundle extras) throws RemoteException {
             return false;
         }
+
+        @Override
+        public boolean newAuthTabSession(IAuthTabCallback callback, Bundle extras)
+                throws RemoteException {
+            return false;
+        }
     }
 }
diff --git a/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl b/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl
new file mode 100644
index 0000000..379e46f
--- /dev/null
+++ b/browser/browser/src/main/stableAidl/android/support/customtabs/IAuthTabCallback.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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 android.support.customtabs;
+
+import android.os.Bundle;
+
+/**
+ * Interface to an AuthTabCallback.
+ */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
+interface IAuthTabCallback {
+    oneway void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
+    oneway void onExtraCallback(String callbackName, in Bundle args) = 2;
+    Bundle onExtraCallbackWithResult(String callbackName, in Bundle args) = 3;
+    oneway void onWarmupCompleted(in Bundle extras) = 4;
+}
\ No newline at end of file
diff --git a/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl b/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
index 84a6a4a..67645fc 100644
--- a/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
+++ b/browser/browser/src/main/stableAidl/android/support/customtabs/ICustomTabsService.aidl
@@ -19,6 +19,7 @@
 import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Bundle;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.IEngagementSignalsCallback;
 
@@ -46,4 +47,5 @@
     boolean isEngagementSignalsApiAvailable(in ICustomTabsCallback customTabsCallback, in Bundle extras) = 12;
     boolean setEngagementSignalsCallback(in ICustomTabsCallback customTabsCallback, in IBinder callback, in Bundle extras) = 13;
     boolean isEphemeralBrowsingSupported(in Bundle extras) = 16;
+    boolean newAuthTabSession(in IAuthTabCallback callback, in Bundle extras) = 17;
 }
diff --git a/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
index 8c26499..9b1d422 100644
--- a/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
+++ b/browser/browser/src/test/java/androidx/browser/auth/AuthTabIntentTest.java
@@ -37,6 +37,7 @@
 
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.TestUtil;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -166,4 +167,48 @@
                 .intent;
         assertTrue(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false));
     }
+
+    @Test
+    public void testPutsNullSessionExtra_WhenBuiltWithDefaultConstructor() {
+        Intent intent = new AuthTabIntent.Builder().build().intent;
+        assertNullSessionInExtras(intent);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testPutsSessionBinderAndId_IfSuppliedInConstructor() {
+        AuthTabSession session = TestUtil.makeMockAuthTabSession();
+        Intent intent = new AuthTabIntent.Builder().setSession(session).build().intent;
+        assertEquals(session.getBinder(),
+                intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+        assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testPutsSessionBinderAndId_IfSuppliedInSetter() {
+        AuthTabSession session = TestUtil.makeMockAuthTabSession();
+        AuthTabIntent authTabIntent = new AuthTabIntent.Builder().setSession(session).build();
+        assertEquals(session, authTabIntent.getSession());
+        assertEquals(session.getBinder(),
+                authTabIntent.intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+        assertEquals(session.getId(),
+                authTabIntent.intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testPutsPendingSessionId() {
+        AuthTabSession.PendingSession pendingSession = TestUtil.makeMockPendingAuthTabSession();
+        AuthTabIntent authTabIntent = new AuthTabIntent.Builder().setPendingSession(
+                pendingSession).build();
+        assertEquals(pendingSession, authTabIntent.getPendingSession());
+        assertEquals(pendingSession.getId(),
+                authTabIntent.intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
+    }
+
+    private void assertNullSessionInExtras(Intent intent) {
+        assertTrue(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION));
+        assertNull(intent.getExtras().getBinder(CustomTabsIntent.EXTRA_SESSION));
+    }
 }
diff --git a/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java b/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java
new file mode 100644
index 0000000..01087e5
--- /dev/null
+++ b/browser/browser/src/test/java/androidx/browser/auth/AuthTabSessionTokenTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 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.browser.auth;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.customtabs.IAuthTabCallback;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/**
+ * Tests for {@link AuthTabSessionToken}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AuthTabSessionTokenTest {
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+    }
+
+    @Test
+    public void testEquality_withId() {
+        AuthTabSessionToken token1 = new AuthTabSessionToken(new AuthTabSessionToken.MockCallback(),
+                createSessionId(27));
+
+        AuthTabSessionToken token2 = new AuthTabSessionToken(new AuthTabSessionToken.MockCallback(),
+                createSessionId(27));
+
+        assertEquals(token1, token2);
+    }
+
+    @Test
+    public void testNonEquality_withId() {
+        // Using the same binder to ensure only the id matters.
+        IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+        AuthTabSessionToken token1 = new AuthTabSessionToken(binder, createSessionId(10));
+        AuthTabSessionToken token2 = new AuthTabSessionToken(binder, createSessionId(20));
+
+        assertNotEquals(token1, token2);
+    }
+
+    @Test
+    public void testEquality_withBinder() {
+        IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+        AuthTabSessionToken token1 = new AuthTabSessionToken(binder, null);
+        AuthTabSessionToken token2 = new AuthTabSessionToken(binder, null);
+
+        assertEquals(token1, token2);
+    }
+
+    @Test
+    public void testNonEquality_withBinder() {
+        IAuthTabCallback.Stub binder1 = new AuthTabSessionToken.MockCallback();
+        IAuthTabCallback.Stub binder2 = new AuthTabSessionToken.MockCallback();
+
+        AuthTabSessionToken token1 = new AuthTabSessionToken(binder1, null);
+        AuthTabSessionToken token2 = new AuthTabSessionToken(binder2, null);
+
+        assertNotEquals(token1, token2);
+    }
+
+    @Test
+    public void testNonEquality_mixedIdAndBinder() {
+        // Using the same binder to ensure only the id matters.
+        IAuthTabCallback.Stub binder = new AuthTabSessionToken.MockCallback();
+
+        AuthTabSessionToken token1 = new AuthTabSessionToken(binder, createSessionId(10));
+        // Tokens cannot be mixed if only one has an id even if the binder is the same.
+        AuthTabSessionToken token2 = new AuthTabSessionToken(binder, null);
+
+        assertNotEquals(token1, token2);
+    }
+
+    // This code does the same as CustomTabsClient#createSessionId but that is not necessary for the
+    // test, we just need to create a PendingIntent that uses sessionId as the requestCode.
+    private PendingIntent createSessionId(int sessionId) {
+        return PendingIntent.getActivity(mContext, sessionId, new Intent(), 0);
+    }
+
+    private void assertEquals(AuthTabSessionToken token1, AuthTabSessionToken token2) {
+        Assert.assertEquals(token1, token2);
+        Assert.assertEquals(token2, token1);
+
+        Assert.assertEquals(token1.hashCode(), token2.hashCode());
+    }
+
+    private void assertNotEquals(AuthTabSessionToken token1, AuthTabSessionToken token2) {
+        Assert.assertNotEquals(token1, token2);
+        Assert.assertNotEquals(token2, token1);
+
+        // I guess technically this could be flaky, but let's hope not...
+        Assert.assertNotEquals(token1.hashCode(), token2.hashCode());
+    }
+}
diff --git a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
index c020c0d..2e6315f 100644
--- a/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
+++ b/browser/browser/src/test/java/androidx/browser/customtabs/TestUtil.java
@@ -23,11 +23,17 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.support.customtabs.IAuthTabCallback;
 import android.support.customtabs.ICustomTabsCallback;
 import android.support.customtabs.ICustomTabsService;
 
+import androidx.browser.auth.AuthTabCallback;
+import androidx.browser.auth.AuthTabSession;
+
 import org.jspecify.annotations.NonNull;
 
+import java.util.concurrent.Executor;
+
 /**
  * Utilities for unit testing Custom Tabs.
  */
@@ -56,4 +62,17 @@
                 CustomTabsIntent.EXTRA_SESSION));
         assertEquals(session.getId(), intent.getParcelableExtra(CustomTabsIntent.EXTRA_SESSION_ID));
     }
+
+    /** Create s a mock {@link AuthTabSession} for testing. */
+    @NonNull
+    public static AuthTabSession makeMockAuthTabSession() {
+        return new AuthTabSession(mock(IAuthTabCallback.class),
+                new ComponentName("", ""), makeMockPendingIntent());
+    }
+
+    /** Creates a mock {@link AuthTabSession.PendingSession} for testing. */
+    public static AuthTabSession.@NonNull PendingSession makeMockPendingAuthTabSession() {
+        return new AuthTabSession.PendingSession(makeMockPendingIntent(), mock(Executor.class),
+                mock(AuthTabCallback.class));
+    }
 }
diff --git a/buildSrc-tests/build.gradle b/buildSrc-tests/build.gradle
index f2e88f3..d70a688 100644
--- a/buildSrc-tests/build.gradle
+++ b/buildSrc-tests/build.gradle
@@ -19,7 +19,6 @@
 
 import androidx.build.LibraryType
 import androidx.build.KotlinTarget
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -90,8 +89,7 @@
 }
 
 androidx {
-    type = LibraryType.GRADLE_PLUGIN
-    publish = Publish.NONE
+    type = LibraryType.INTERNAL_GRADLE_PLUGIN
     kotlinTarget = KotlinTarget.KOTLIN_1_9
 }
 
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index f24bb50..b7dd377 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -66,15 +66,6 @@
 
     <issue
         id="EagerGradleConfiguration"
-        message="Avoid using method get"
-        errorLine1="            val verifyOutputsTask = verifyOutputs.get()"
-        errorLine2="                                                  ~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
-    </issue>
-
-    <issue
-        id="EagerGradleConfiguration"
         message="Use configureEach instead of whenObjectAdded"
         errorLine1="    configurations.whenObjectAdded {"
         errorLine2="                   ~~~~~~~~~~~~~~~">
@@ -256,7 +247,7 @@
     <issue
         id="GradleProjectIsolation"
         message="Use isolated.rootProject instead of getRootProject"
-        errorLine1="    rootProject.layout.buildDirectory.dir(&quot;privacysandbox-files&quot;)"
+        errorLine1="    rootProject.layout.buildDirectory.dir(&quot;app-apks-files&quot;)"
         errorLine2="    ~~~~~~~~~~~">
         <location
             file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
@@ -273,6 +264,15 @@
 
     <issue
         id="GradleProjectIsolation"
+        message="Avoid using method getParent"
+        errorLine1="                    ${project.parent}."
+        errorLine2="                              ~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/checkapi/CompilationInputs.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
         message="Use isolated.rootProject instead of getRootProject"
         errorLine1="    rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
         errorLine2="    ~~~~~~~~~~~">
@@ -345,15 +345,6 @@
 
     <issue
         id="GradleProjectIsolation"
-        message="Avoid using method getParent"
-        errorLine1="                    ${project.parent}."
-        errorLine2="                              ~~~~~~">
-        <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
         message="Use isolated.rootProject instead of getRootProject"
         errorLine1="        val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
         errorLine2="                                                                      ~~~~~~~~~~~">
diff --git a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
index 92f2564..38cc6f4 100644
--- a/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
+++ b/buildSrc-tests/max-dep-versions/buildSrc-tests-max-dep-versions-dep/build.gradle
@@ -6,7 +6,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 4d328d0..90069e4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -24,7 +24,12 @@
 /**
  * Whether to enable constraints for projects in same-version groups
  *
- * This is default true.
+ * This is expected to be true during builds that publish artifacts externally This is expected to
+ * be false during most other builds because: Developers may be interested in including only a
+ * subset of projects in ANDROIDX_PROJECTS to make Studio run more quickly. If a build contains only
+ * a subset of projects, we cannot necessarily add constraints between all pairs of projects in the
+ * same group. We want most builds to have high remote cache usage, so we want constraints to be
+ * similar across most builds See go/androidx-group-constraints for more information
  */
 const val ADD_GROUP_CONSTRAINTS = "androidx.constraints"
 
@@ -177,8 +182,7 @@
  * Whether to enable constraints for projects in same-version groups See the property definition for
  * more details
  */
-fun Project.shouldAddGroupConstraints() =
-    project.providers.gradleProperty(ADD_GROUP_CONSTRAINTS).map { s -> s.toBoolean() }.orElse(true)
+fun Project.shouldAddGroupConstraints() = booleanPropertyProvider(ADD_GROUP_CONSTRAINTS)
 
 /**
  * Returns alternative project url that will be used as "url" property in publishing maven artifact
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index a016ea3..4dc9c6e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -727,6 +727,7 @@
         project.afterEvaluate {
             project.addToBuildOnServer("assembleAndroidMain")
             project.addToBuildOnServer("lint")
+            // Created to be consumed by docs-tip-of-tree
             project.configurations.create("androidIntermediates") {
                 it.isVisible = false
                 it.isCanBeResolved = false
@@ -1062,8 +1063,7 @@
             val isProbablyPublished =
                 androidXExtension.type == LibraryType.PUBLISHED_LIBRARY ||
                     androidXExtension.type ==
-                        LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS ||
-                    androidXExtension.type == LibraryType.UNSET
+                        LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
             if (mavenGroup != null && isProbablyPublished && androidXExtension.shouldPublish()) {
                 validateProjectMavenGroup(mavenGroup.group)
                 validateProjectMavenName(androidXExtension.name.get(), mavenGroup.group)
@@ -1310,7 +1310,7 @@
     ) {
         if (buildFeatures.isIsolatedProjectsEnabled()) return
         afterEvaluate {
-            if (androidXExtension.type !in listOf(LibraryType.UNSET, LibraryType.SAMPLES)) {
+            if (androidXExtension.type.requiresDependencyVerification()) {
                 val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
                 if (verifyDependencyVersionsTask != null) {
                     taskConfigurator(verifyDependencyVersionsTask)
@@ -1635,10 +1635,6 @@
             "$errorPrefix Incorrectly computed libraryType = ${parsed.libraryType} " +
                 "instead of ${androidXExtension.type}"
         }
-        check(androidXExtension.publish == parsed.publish) {
-            "$errorPrefix Incorrectly computed publish = ${parsed.publish} " +
-                "instead of ${androidXExtension.publish}"
-        }
         check(androidXExtension.shouldPublish() == parsed.shouldPublish()) {
             "$errorPrefix Incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " +
                 "instead of ${androidXExtension.shouldPublish()}"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 005f95f..044bcbb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -16,7 +16,7 @@
 
 package androidx.build
 
-import androidx.build.java.JavaCompileInputs
+import androidx.build.checkapi.CompilationInputs
 import com.android.build.api.variant.AndroidComponentsExtension
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
@@ -58,7 +58,7 @@
         makeKmpErrorProneTask(
             COMPILE_JAVA_TASK_NAME,
             jvmJarProvider,
-            JavaCompileInputs.fromKmpJvmTarget(project)
+            CompilationInputs.fromKmpJvmTarget(project)
         )
     } else {
         makeErrorProneTask(COMPILE_JAVA_TASK_NAME)
@@ -275,12 +275,12 @@
  * Note: Since ErrorProne only understands Java files which may be dependent on Kotlin source, using
  * this method to register ErrorProne task causes it to be dependent on jvmJar task.
  *
- * @param jvmCompileInputs [JavaCompileInputs] that specifies jvm source including Kotlin sources.
+ * @param jvmCompileInputs [CompilationInputs] that specifies jvm source including Kotlin sources.
  */
 private fun Project.makeKmpErrorProneTask(
     compileTaskName: String,
     jvmJarTaskProvider: TaskProvider<Jar>,
-    jvmCompileInputs: JavaCompileInputs
+    jvmCompileInputs: CompilationInputs
 ) {
     makeErrorProneTask(compileTaskName) { errorProneTask ->
         // ErrorProne doesn't understand Kotlin source, so first let kotlinCompile finish, then
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index d26533a..812d98f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -164,7 +164,10 @@
     val lintChecksProject = findLintProject(":lint-checks") ?: return
     project.dependencies.add("lintChecks", lintChecksProject)
 
-    if (extension.type == LibraryType.GRADLE_PLUGIN) {
+    if (
+        extension.type == LibraryType.GRADLE_PLUGIN ||
+            extension.type == LibraryType.INTERNAL_GRADLE_PLUGIN
+    ) {
         project.rootProject.findProject(":lint:lint-gradle")?.let {
             project.dependencies.add("lintChecks", it)
         }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
index 0cb1030..3d34b79 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt
@@ -42,36 +42,13 @@
             if (line.contains("mavenVersion =")) specifiesVersion = true
         }
         val libraryTypeEnum = libraryType?.let { LibraryType.valueOf(it) } ?: LibraryType.UNSET
-        val publishEnum = publish?.let { Publish.valueOf(it) } ?: Publish.UNSET
-        return ParsedProject(
-            libraryType = libraryTypeEnum,
-            publish = publishEnum,
-            specifiesVersion = specifiesVersion
-        )
+        return ParsedProject(libraryType = libraryTypeEnum, specifiesVersion = specifiesVersion)
     }
 
-    data class ParsedProject(
-        val libraryType: LibraryType,
-        val publish: Publish,
-        val specifiesVersion: Boolean
-    ) {
-        fun shouldPublish(): Boolean =
-            if (publish != Publish.UNSET) {
-                publish.shouldPublish()
-            } else if (libraryType != LibraryType.UNSET) {
-                libraryType.publish.shouldPublish()
-            } else {
-                false
-            }
+    data class ParsedProject(val libraryType: LibraryType, val specifiesVersion: Boolean) {
+        fun shouldPublish(): Boolean = libraryType.publish.shouldPublish()
 
-        fun shouldRelease(): Boolean =
-            if (publish != Publish.UNSET) {
-                publish.shouldRelease()
-            } else if (libraryType != LibraryType.UNSET) {
-                libraryType.publish.shouldRelease()
-            } else {
-                false
-            }
+        fun shouldRelease(): Boolean = libraryType.publish.shouldRelease()
     }
 }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
index d3b8452..bfbb70c 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/Release.kt
@@ -114,13 +114,6 @@
             )
             return
         }
-        if (!androidXExtension.isPublishConfigured()) {
-            project.logger.info(
-                "project ${project.name} isn't part of release, because" +
-                    " it does not set the \"publish\" property."
-            )
-            return
-        }
         if (!androidXExtension.shouldRelease() && !isSnapshotBuild()) {
             project.logger.info(
                 "project ${project.name} isn't part of release, because its" +
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
index 17d960d..24bc918 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
@@ -19,11 +19,9 @@
 import androidx.build.AndroidXExtension
 import androidx.build.Release
 import androidx.build.RunApiTasks
-import androidx.build.Version
 import androidx.build.binarycompatibilityvalidator.BinaryCompatibilityValidation
 import androidx.build.getSupportRootFolder
 import androidx.build.isWriteVersionedApiFilesEnabled
-import androidx.build.java.JavaCompileInputs
 import androidx.build.metalava.MetalavaTasks
 import androidx.build.multiplatformExtension
 import androidx.build.resources.ResourceTasks
@@ -61,63 +59,21 @@
         )
     }
 
-    // API behavior is default for type
-    if (type.checkApi is RunApiTasks.No && runApiTasks is RunApiTasks.No) {
-        project.logger.info("Projects of type ${type.name} do not track API.")
-        return false
-    }
-
-    when (runApiTasks) {
-        // API behavior for type must have been overridden, because previous check did not trigger
+    return when (type.checkApi) {
         is RunApiTasks.No -> {
-            project.logger.info(
-                "Project ${project.name} has explicitly disabled API tasks with " +
-                    "reason: ${(runApiTasks as RunApiTasks.No).reason}"
-            )
-            return false
+            project.logger.info("Projects of type ${type.name} do not track API.")
+            false
         }
         is RunApiTasks.Yes -> {
-            // API behavior is default for type; not overridden
-            if (type.checkApi is RunApiTasks.Yes) {
-                return true
-            }
-            // API behavior for type is overridden
-            (runApiTasks as RunApiTasks.Yes).reason?.let { reason ->
+            (type.checkApi as RunApiTasks.Yes).reason?.let { reason ->
                 project.logger.info(
                     "Project ${project.name} has explicitly enabled API tasks " +
                         "with reason: $reason"
                 )
             }
-            return true
+            true
         }
-        else -> {}
     }
-
-    if (project.version !is Version) {
-        project.logger.info("Project ${project.name} has no version set, ignoring API tasks.")
-        return false
-    }
-
-    // If the project has an "api" directory, either because they used to track APIs or they
-    // added one manually to force tracking (as recommended below), continue tracking APIs.
-    if (project.hasApiFileDirectory() && !shouldRelease()) {
-        project.logger.error(
-            "Project ${project.name} is not published, but has an existing API " +
-                "directory. Forcing API tasks enabled. Please migrate to runApiTasks=Yes."
-        )
-        return true
-    }
-
-    if (!shouldRelease()) {
-        project.logger.info(
-            "Project ${project.name} is not published, ignoring API tasks. " +
-                "If you still want to track APIs, create an \"api\" directory in your project" +
-                " root and run the updateApi task."
-        )
-        return false
-    }
-
-    return true
 }
 
 /**
@@ -162,14 +118,14 @@
                 listOf(currentApiLocation)
             }
 
-        val (javaInputs, androidManifest) =
-            configureJavaInputsAndManifest(config) ?: return@afterEvaluate
+        val (compilationInputs, androidManifest) =
+            configureCompilationInputsAndManifest(config) ?: return@afterEvaluate
         val baselinesApiLocation = ApiBaselinesLocation.fromApiLocation(currentApiLocation)
         val generateApiDependencies = createReleaseApiConfiguration()
 
         MetalavaTasks.setupProject(
             project,
-            javaInputs,
+            compilationInputs,
             generateApiDependencies,
             extension,
             androidManifest,
@@ -204,27 +160,27 @@
     }
 }
 
-internal fun Project.configureJavaInputsAndManifest(
+internal fun Project.configureCompilationInputsAndManifest(
     config: ApiTaskConfig
-): Pair<JavaCompileInputs, Provider<RegularFile>?>? {
+): Pair<CompilationInputs, Provider<RegularFile>?>? {
     return when (config) {
         is LibraryApiTaskConfig -> {
             if (config.variant.name != Release.DEFAULT_PUBLISH_CONFIG) {
                 return null
             }
-            JavaCompileInputs.fromLibraryVariant(config.variant, project) to
+            CompilationInputs.fromLibraryVariant(config.variant, project) to
                 config.variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)
         }
         is AndroidMultiplatformApiTaskConfig -> {
-            JavaCompileInputs.fromKmpAndroidTarget(project) to null
+            CompilationInputs.fromKmpAndroidTarget(project) to null
         }
         is KmpApiTaskConfig -> {
-            JavaCompileInputs.fromKmpJvmTarget(project) to null
+            CompilationInputs.fromKmpJvmTarget(project) to null
         }
         is JavaApiTaskConfig -> {
             val javaExtension = extensions.getByType<JavaPluginExtension>()
             val mainSourceSet = javaExtension.sourceSets.getByName("main")
-            JavaCompileInputs.fromSourceSet(mainSourceSet, this) to null
+            CompilationInputs.fromSourceSet(mainSourceSet, this) to null
         }
     }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt
new file mode 100644
index 0000000..ff46bb5
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2024 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.build.checkapi
+
+import androidx.build.getAndroidJar
+import androidx.build.multiplatformExtension
+import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.api.variant.LibraryVariant
+import org.gradle.api.Project
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.FileCollection
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.SourceSet
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+
+/**
+ * [CompilationInputs] contains the information required to compile Java/Kotlin code. This can be
+ * helpful for creating Metalava and Kzip tasks with the same settings.
+ *
+ * There are two implementations: [StandardCompilationInputs] for non-multiplatform projects and
+ * [MultiplatformCompilationInputs] for multiplatform projects.
+ */
+internal sealed interface CompilationInputs {
+    /** Source files to process */
+    val sourcePaths: FileCollection
+
+    /** Source files from the KMP common module of this project */
+    val commonModuleSourcePaths: FileCollection
+
+    /** Dependencies (compiled classes) of [sourcePaths]. */
+    val dependencyClasspath: FileCollection
+
+    /** Android's boot classpath. */
+    val bootClasspath: FileCollection
+
+    companion object {
+        /** Constructs a [CompilationInputs] from a library and its variant */
+        fun fromLibraryVariant(variant: LibraryVariant, project: Project): CompilationInputs {
+            // The boot classpath is common to both multiplatform and standard configurations.
+            val bootClasspath =
+                project.files(
+                    project.extensions
+                        .findByType(LibraryAndroidComponentsExtension::class.java)!!
+                        .sdkComponents
+                        .bootClasspath
+                )
+
+            // If this is a multiplatform project, set up inputs for the androidJvm compilation
+            val multiplatformExtension = project.multiplatformExtension
+            if (multiplatformExtension != null) {
+                val androidJvmTarget =
+                    multiplatformExtension.targets
+                        .requirePlatform(KotlinPlatformType.androidJvm)
+                        .findCompilation(compilationName = variant.name)
+
+                return MultiplatformCompilationInputs.fromCompilation(
+                    project = project,
+                    compilationProvider = androidJvmTarget,
+                    bootClasspath = bootClasspath,
+                )
+            }
+
+            // Not a multiplatform project, set up standard inputs
+            val kotlinCollection = project.files(variant.sources.kotlin?.all)
+            val javaCollection = project.files(variant.sources.java?.all)
+            val sourceCollection = kotlinCollection + javaCollection
+
+            return StandardCompilationInputs(
+                sourcePaths = sourceCollection,
+                commonModuleSourcePaths = project.files(),
+                dependencyClasspath = variant.compileClasspath,
+                bootClasspath = bootClasspath
+            )
+        }
+
+        /**
+         * Returns the CompilationInputs for the `jvm` target of a KMP project.
+         *
+         * @param project The project whose main jvm target inputs will be returned.
+         */
+        fun fromKmpJvmTarget(project: Project): CompilationInputs {
+            val kmpExtension =
+                checkNotNull(project.multiplatformExtension) {
+                    """
+                ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
+                jvm source sets.
+                """
+                        .trimIndent()
+                }
+            val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm)
+            val jvmCompilation =
+                jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME)
+
+            return MultiplatformCompilationInputs.fromCompilation(
+                project = project,
+                compilationProvider = jvmCompilation,
+                bootClasspath = project.getAndroidJar()
+            )
+        }
+
+        /**
+         * Returns the CompilationInputs for the `android` target of a KMP project.
+         *
+         * @param project The project whose main android target inputs will be returned.
+         */
+        fun fromKmpAndroidTarget(project: Project): CompilationInputs {
+            val kmpExtension =
+                checkNotNull(project.multiplatformExtension) {
+                    """
+                ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
+                android source sets.
+                """
+                        .trimIndent()
+                }
+            val target =
+                kmpExtension.targets
+                    .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
+                    .single()
+            val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
+
+            return MultiplatformCompilationInputs.fromCompilation(
+                project = project,
+                compilationProvider = compilation,
+                bootClasspath = project.getAndroidJar()
+            )
+        }
+
+        /** Constructs a [CompilationInputs] from a sourceset */
+        fun fromSourceSet(sourceSet: SourceSet, project: Project): CompilationInputs {
+            val sourcePaths: FileCollection =
+                project.files(project.provider { sourceSet.allSource.srcDirs })
+            val dependencyClasspath = sourceSet.compileClasspath
+            return StandardCompilationInputs(
+                sourcePaths = sourcePaths,
+                commonModuleSourcePaths = project.files(),
+                dependencyClasspath = dependencyClasspath,
+                bootClasspath = project.getAndroidJar()
+            )
+        }
+
+        /**
+         * Returns the list of Files (might be directories) that are included in the compilation of
+         * this target.
+         *
+         * @param compilationName The name of the compilation. A target might have separate
+         *   compilations (e.g. main vs test for jvm or debug vs release for Android)
+         */
+        private fun KotlinTarget.findCompilation(
+            compilationName: String
+        ): Provider<KotlinCompilation<*>> {
+            return project.provider {
+                val selectedCompilation =
+                    checkNotNull(compilations.findByName(compilationName)) {
+                        """
+                    Cannot find $compilationName compilation configuration of $name in
+                    ${project.parent}.
+                    Available compilations: ${compilations.joinToString(", ") { it.name }}
+                    """
+                            .trimIndent()
+                    }
+                selectedCompilation
+            }
+        }
+
+        /**
+         * Returns the [KotlinTarget] that targets the given platform type.
+         *
+         * This method will throw if there are no matching targets or there are more than 1 matching
+         * target.
+         */
+        private fun Collection<KotlinTarget>.requirePlatform(
+            expectedPlatformType: KotlinPlatformType
+        ): KotlinTarget {
+            return this.singleOrNull { it.platformType == expectedPlatformType }
+                ?: error(
+                    """
+                Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size.
+                Matching compilation targets:
+                    ${joinToString(",") { it.name }}
+                All compilation targets:
+                    ${[email protected](",") { it.name }}
+                """
+                        .trimIndent()
+                )
+        }
+    }
+}
+
+/** Compile inputs for a regular (non-multiplatform) project */
+internal data class StandardCompilationInputs(
+    override val sourcePaths: FileCollection,
+    override val dependencyClasspath: FileCollection,
+    override val bootClasspath: FileCollection,
+    override val commonModuleSourcePaths: FileCollection,
+) : CompilationInputs
+
+/** Compile inputs for a single source set from a multiplatform project. */
+internal data class SourceSetInputs(
+    /** Name of the source set, e.g. "androidMain" */
+    val sourceSetName: String,
+    /** Names of other source sets that this one depends on */
+    val dependsOnSourceSets: List<String>,
+    /** Source files of this source set */
+    val sourcePaths: FileCollection,
+    /** Compile dependencies for this source set */
+    val dependencyClasspath: FileCollection,
+)
+
+/** Inputs for a single compilation of a multiplatform project (just the android or jvm target) */
+internal class MultiplatformCompilationInputs(
+    project: Project,
+    /**
+     * The [SourceSetInputs] for this project's source sets. This is a [Provider] because not all
+     * relationships between source sets will be loaded at configuration time.
+     */
+    val sourceSets: Provider<List<SourceSetInputs>>,
+    override val bootClasspath: FileCollection,
+    override val commonModuleSourcePaths: FileCollection,
+) : CompilationInputs {
+    // Aggregate sources and classpath from all source sets
+    override val sourcePaths: ConfigurableFileCollection =
+        project.files(sourceSets.map { it.map { sourceSet -> sourceSet.sourcePaths } })
+    override val dependencyClasspath: ConfigurableFileCollection =
+        project.files(sourceSets.map { it.map { sourceSet -> sourceSet.dependencyClasspath } })
+
+    companion object {
+        /** Creates inputs based on one compilation of a multiplatform project. */
+        fun fromCompilation(
+            project: Project,
+            compilationProvider: Provider<KotlinCompilation<*>>,
+            bootClasspath: FileCollection,
+        ): MultiplatformCompilationInputs {
+            val compileDependencies =
+                compilationProvider.map { compilation ->
+                    // Sometimes an Android source set has the jvm platform type instead of
+                    // androidJvm
+                    val platformType =
+                        if (compilation.defaultSourceSet.name == "androidMain") {
+                            KotlinPlatformType.androidJvm
+                        } else {
+                            compilation.platformType
+                        }
+
+                    project.configurations
+                        .named(compilation.compileDependencyConfigurationName)
+                        .map { config ->
+                            // AGP adds files from many configurations to the
+                            // compileDependencyFiles,
+                            // so it needs to be filtered to avoid variant resolution errors.
+                            config.incoming
+                                .artifactView {
+                                    val artifactType =
+                                        if (platformType == KotlinPlatformType.androidJvm) {
+                                            "android-classes"
+                                        } else {
+                                            "jar"
+                                        }
+                                    it.attributes.attribute(
+                                        Attribute.of("artifactType", String::class.java),
+                                        artifactType
+                                    )
+                                }
+                                .files
+                        }
+                }
+            val sourceSets =
+                compilationProvider.map { compilation ->
+                    compilation.allKotlinSourceSets.map { sourceSet ->
+                        SourceSetInputs(
+                            sourceSet.name,
+                            sourceSet.dependsOn.map { it.name },
+                            sourceSet.kotlin.sourceDirectories,
+                            project.files(compileDependencies),
+                        )
+                    }
+                }
+            return MultiplatformCompilationInputs(
+                project,
+                sourceSets,
+                bootClasspath,
+                project.commonModuleSourcePaths(compilationProvider)
+            )
+        }
+
+        private fun Project.commonModuleSourcePaths(
+            kotlinCompilation: Provider<KotlinCompilation<*>>
+        ): ConfigurableFileCollection {
+            return project.files(
+                project.provider {
+                    kotlinCompilation
+                        .get()
+                        .allKotlinSourceSets
+                        .filter { it.dependsOn.isEmpty() }
+                        .flatMap { it.kotlin.sourceDirectories.files }
+                }
+            )
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
deleted file mode 100644
index cb3d5ae..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2018 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.build.java
-
-import androidx.build.getAndroidJar
-import androidx.build.multiplatformExtension
-import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
-import com.android.build.api.variant.LibraryAndroidComponentsExtension
-import com.android.build.api.variant.LibraryVariant
-import org.gradle.api.Project
-import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.FileCollection
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.SourceSet
-import org.gradle.kotlin.dsl.get
-import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
-
-// JavaCompileInputs contains the information required to compile Java/Kotlin code
-// This can be helpful for creating Metalava and Dokka tasks with the same settings
-data class JavaCompileInputs(
-    // Source files to process
-    val sourcePaths: FileCollection,
-
-    // Source files from the KMP common module of this project
-    val commonModuleSourcePaths: FileCollection,
-
-    // Dependencies (compiled classes) of [sourcePaths].
-    val dependencyClasspath: FileCollection,
-
-    // Android's boot classpath.
-    val bootClasspath: FileCollection
-) {
-    companion object {
-        // Constructs a JavaCompileInputs from a library and its variant
-        fun fromLibraryVariant(variant: LibraryVariant, project: Project): JavaCompileInputs {
-            val kotlinCollection = project.files(variant.sources.kotlin?.all)
-            val javaCollection = project.files(variant.sources.java?.all)
-
-            val androidJvmTarget =
-                project.multiplatformExtension
-                    ?.targets
-                    ?.requirePlatform(KotlinPlatformType.androidJvm)
-                    ?.findCompilation(compilationName = variant.name)
-
-            val sourceCollection =
-                androidJvmTarget?.let { project.files(project.sourceFiles(it)) }
-                    ?: (kotlinCollection + javaCollection)
-
-            val commonModuleSourceCollection =
-                project
-                    .files(androidJvmTarget?.let { project.commonModuleSourcePaths(it) })
-                    .builtBy(
-                        // Remove task dependency when b/332711506 is fixed, which should get us an
-                        // API to get all sources (static and generated)
-                        project.tasks.named("compileReleaseJavaWithJavac")
-                    )
-
-            val bootClasspath =
-                project.extensions
-                    .findByType(LibraryAndroidComponentsExtension::class.java)!!
-                    .sdkComponents
-                    .bootClasspath
-
-            return JavaCompileInputs(
-                sourcePaths = sourceCollection,
-                commonModuleSourcePaths = commonModuleSourceCollection,
-                dependencyClasspath = variant.compileClasspath,
-                bootClasspath = project.files(bootClasspath)
-            )
-        }
-
-        /**
-         * Returns the JavaCompileInputs for the `jvm` target of a KMP project.
-         *
-         * @param project The project whose main jvm target inputs will be returned.
-         */
-        fun fromKmpJvmTarget(project: Project): JavaCompileInputs {
-            val kmpExtension =
-                checkNotNull(project.multiplatformExtension) {
-                    """
-                ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
-                jvm source sets.
-                """
-                        .trimIndent()
-                }
-            val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm)
-            val jvmCompilation =
-                jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME)
-
-            val sourceCollection = project.sourceFiles(jvmCompilation)
-
-            val commonModuleSourcePaths = project.commonModuleSourcePaths(jvmCompilation)
-
-            return JavaCompileInputs(
-                sourcePaths = sourceCollection,
-                commonModuleSourcePaths = commonModuleSourcePaths,
-                dependencyClasspath =
-                    jvmTarget.compilations[KotlinCompilation.MAIN_COMPILATION_NAME]
-                        .compileDependencyFiles,
-                bootClasspath = project.getAndroidJar()
-            )
-        }
-
-        /**
-         * Returns the JavaCompileInputs for the `android` target of a KMP project.
-         *
-         * @param project The project whose main android target inputs will be returned.
-         */
-        fun fromKmpAndroidTarget(project: Project): JavaCompileInputs {
-            val kmpExtension =
-                checkNotNull(project.multiplatformExtension) {
-                    """
-                ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
-                android source sets.
-                """
-                        .trimIndent()
-                }
-            val target =
-                kmpExtension.targets
-                    .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
-                    .single()
-            val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
-            val sourceCollection = project.sourceFiles(compilation)
-
-            val commonModuleSourcePaths = project.commonModuleSourcePaths(compilation)
-
-            return JavaCompileInputs(
-                sourcePaths = sourceCollection,
-                commonModuleSourcePaths = commonModuleSourcePaths,
-                dependencyClasspath =
-                    target.compilations[KotlinCompilation.MAIN_COMPILATION_NAME]
-                        .compileDependencyFiles,
-                bootClasspath = project.getAndroidJar()
-            )
-        }
-
-        // Constructs a JavaCompileInputs from a sourceset
-        fun fromSourceSet(sourceSet: SourceSet, project: Project): JavaCompileInputs {
-            val sourcePaths: FileCollection =
-                project.files(project.provider { sourceSet.allSource.srcDirs })
-            val dependencyClasspath = sourceSet.compileClasspath
-            return JavaCompileInputs(
-                sourcePaths = sourcePaths,
-                commonModuleSourcePaths = project.files(),
-                dependencyClasspath = dependencyClasspath,
-                bootClasspath = project.getAndroidJar()
-            )
-        }
-
-        /**
-         * Returns the list of Files (might be directories) that are included in the compilation of
-         * this target.
-         *
-         * @param compilationName The name of the compilation. A target might have separate
-         *   compilations (e.g. main vs test for jvm or debug vs release for Android)
-         */
-        private fun KotlinTarget.findCompilation(
-            compilationName: String
-        ): Provider<KotlinCompilation<*>> {
-            return project.provider {
-                val selectedCompilation =
-                    checkNotNull(compilations.findByName(compilationName)) {
-                        """
-                    Cannot find $compilationName compilation configuration of $name in
-                    ${project.parent}.
-                    Available compilations: ${compilations.joinToString(", ") { it.name }}
-                    """
-                            .trimIndent()
-                    }
-                selectedCompilation
-            }
-        }
-
-        private fun Project.sourceFiles(
-            kotlinCompilation: Provider<KotlinCompilation<*>>
-        ): ConfigurableFileCollection {
-            return project.files(
-                project.provider {
-                    kotlinCompilation
-                        .get()
-                        .allKotlinSourceSets
-                        .flatMap { it.kotlin.sourceDirectories }
-                        .also {
-                            require(it.isNotEmpty()) {
-                                """
-                                    Didn't find any source sets for $kotlinCompilation in ${project.path}.
-                                    """
-                                    .trimIndent()
-                            }
-                        }
-                }
-            )
-        }
-
-        private fun Project.commonModuleSourcePaths(
-            kotlinCompilation: Provider<KotlinCompilation<*>>
-        ): ConfigurableFileCollection {
-            return project.files(
-                project.provider {
-                    kotlinCompilation
-                        .get()
-                        .allKotlinSourceSets
-                        .filter { it.dependsOn.isEmpty() }
-                        .flatMap { it.kotlin.sourceDirectories.files }
-                }
-            )
-        }
-
-        /**
-         * Returns the [KotlinTarget] that targets the given platform type.
-         *
-         * This method will throw if there are no matching targets or there are more than 1 matching
-         * target.
-         */
-        private fun Collection<KotlinTarget>.requirePlatform(
-            expectedPlatformType: KotlinPlatformType
-        ): KotlinTarget {
-            return this.singleOrNull { it.platformType == expectedPlatformType }
-                ?: error(
-                    """
-                Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size.
-                Matching compilation targets:
-                    ${joinToString(",") { it.name }}
-                All compilation targets:
-                    ${[email protected](",") { it.name }}
-                """
-                        .trimIndent()
-                )
-        }
-    }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
index 656e1b4..2177809 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt
@@ -16,9 +16,9 @@
 package androidx.build.kythe
 
 import androidx.build.addToBuildOnServer
+import androidx.build.checkapi.CompilationInputs
 import androidx.build.getCheckoutRoot
 import androidx.build.getPrebuiltsRoot
-import androidx.build.java.JavaCompileInputs
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
@@ -136,7 +136,7 @@
     internal companion object {
         fun setupProject(
             project: Project,
-            javaInputs: JavaCompileInputs,
+            compilationInputs: CompilationInputs,
             compiledSources: Configuration,
         ) {
             val annotationProcessorPaths =
@@ -162,10 +162,10 @@
                                 "build-tools/common/javac_extractor.jar"
                             )
                         )
-                        sourcePaths.setFrom(javaInputs.sourcePaths)
+                        sourcePaths.setFrom(compilationInputs.sourcePaths)
                         vnamesJson.set(project.getVnamesJson())
                         dependencyClasspath.setFrom(
-                            javaInputs.dependencyClasspath + javaInputs.bootClasspath
+                            compilationInputs.dependencyClasspath + compilationInputs.bootClasspath
                         )
                         this.compiledSources.setFrom(compiledSources)
                         kzipOutputFile.set(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
index 536f9cc..6a9d62a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt
@@ -19,10 +19,10 @@
 import androidx.build.KotlinTarget
 import androidx.build.OperatingSystem
 import androidx.build.addToBuildOnServer
+import androidx.build.checkapi.CompilationInputs
 import androidx.build.getCheckoutRoot
 import androidx.build.getOperatingSystem
 import androidx.build.getPrebuiltsRoot
-import androidx.build.java.JavaCompileInputs
 import androidx.build.multiplatformExtension
 import java.io.File
 import java.util.jar.JarOutputStream
@@ -188,7 +188,7 @@
     internal companion object {
         fun setupProject(
             project: Project,
-            javaInputs: JavaCompileInputs,
+            compilationInputs: CompilationInputs,
             compiledSources: Configuration,
             kotlinTarget: Property<KotlinTarget>,
             javaVersion: JavaVersion,
@@ -208,11 +208,11 @@
                                 "build-tools/${osName()}/bin/kotlinc_extractor"
                             )
                         )
-                        sourcePaths.setFrom(javaInputs.sourcePaths)
-                        commonModuleSourcePaths.from(javaInputs.commonModuleSourcePaths)
+                        sourcePaths.setFrom(compilationInputs.sourcePaths)
+                        commonModuleSourcePaths.from(compilationInputs.commonModuleSourcePaths)
                         vnamesJson.set(project.getVnamesJson())
                         dependencyClasspath.setFrom(
-                            javaInputs.dependencyClasspath + javaInputs.bootClasspath
+                            compilationInputs.dependencyClasspath + compilationInputs.bootClasspath
                         )
                         this.compiledSources.setFrom(compiledSources)
                         this.kotlinTarget.set(kotlinTarget)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
index 481acc4..56d327b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt
@@ -19,7 +19,7 @@
 import androidx.build.AndroidXExtension
 import androidx.build.ProjectLayoutType
 import androidx.build.checkapi.ApiTaskConfig
-import androidx.build.checkapi.configureJavaInputsAndManifest
+import androidx.build.checkapi.configureCompilationInputsAndManifest
 import androidx.build.checkapi.createReleaseApiConfiguration
 import androidx.build.getDefaultTargetJavaVersion
 import androidx.build.getSupportRootFolder
@@ -58,18 +58,19 @@
 
     // afterEvaluate required to read extension properties
     afterEvaluate {
-        val (javaInputs, _) = configureJavaInputsAndManifest(config) ?: return@afterEvaluate
+        val (compilationInputs, _) =
+            configureCompilationInputsAndManifest(config) ?: return@afterEvaluate
         val compiledSources = createReleaseApiConfiguration()
 
         GenerateKotlinKzipTask.setupProject(
             project,
-            javaInputs,
+            compilationInputs,
             compiledSources,
             extension.kotlinTarget,
             getDefaultTargetJavaVersion(extension.type, project.name)
         )
 
-        GenerateJavaKzipTask.setupProject(project, javaInputs, compiledSources)
+        GenerateJavaKzipTask.setupProject(project, compilationInputs, compiledSources)
     }
 }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
index 806aa75..7024071 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt
@@ -19,7 +19,7 @@
 import androidx.build.Version
 import androidx.build.checkapi.ApiBaselinesLocation
 import androidx.build.checkapi.ApiLocation
-import androidx.build.java.JavaCompileInputs
+import androidx.build.checkapi.StandardCompilationInputs
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.file.Directory
@@ -95,7 +95,7 @@
         }
 
         val inputs =
-            JavaCompileInputs(
+            StandardCompilationInputs(
                 sourcePaths = sourcePaths,
                 commonModuleSourcePaths = commonModuleSourcePaths,
                 dependencyClasspath = dependencyClasspath,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
index 82770b8..730d085 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt
@@ -18,8 +18,8 @@
 
 import androidx.build.Version
 import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
 import androidx.build.getLibraryByName
-import androidx.build.java.JavaCompileInputs
 import androidx.build.logging.TERMINAL_RED
 import androidx.build.logging.TERMINAL_RESET
 import java.io.ByteArrayOutputStream
@@ -240,9 +240,9 @@
 /**
  * Generates all of the specified api files, as well as a version history JSON for the public API.
  */
-fun generateApi(
+internal fun generateApi(
     metalavaClasspath: FileCollection,
-    files: JavaCompileInputs,
+    files: CompilationInputs,
     apiLocation: ApiLocation,
     apiLintMode: ApiLintMode,
     includeRestrictToLibraryGroupApis: Boolean,
@@ -284,7 +284,7 @@
  */
 private fun generateApi(
     metalavaClasspath: FileCollection,
-    files: JavaCompileInputs,
+    files: CompilationInputs,
     outputLocation: ApiLocation,
     generateApiMode: GenerateApiMode,
     apiLintMode: ApiLintMode,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
index f385c0f..544231d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
@@ -22,8 +22,8 @@
 import androidx.build.addToCheckTask
 import androidx.build.checkapi.ApiBaselinesLocation
 import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
 import androidx.build.checkapi.getRequiredCompatibilityApiLocation
-import androidx.build.java.JavaCompileInputs
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import androidx.build.version
 import org.gradle.api.Project
@@ -33,11 +33,11 @@
 import org.gradle.api.tasks.TaskProvider
 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
 
-object MetalavaTasks {
+internal object MetalavaTasks {
 
     fun setupProject(
         project: Project,
-        javaCompileInputs: JavaCompileInputs,
+        compilationInputs: CompilationInputs,
         generateApiDependencies: Configuration,
         extension: AndroidXExtension,
         androidManifest: Provider<RegularFile>?,
@@ -71,7 +71,7 @@
                 task.currentVersion.set(version)
 
                 androidManifest?.let { task.manifestPath.set(it) }
-                applyInputs(javaCompileInputs, task, generateApiDependencies)
+                applyInputs(compilationInputs, task, generateApiDependencies)
                 // If we will be updating the api lint baselines, then we should do that before
                 // using it to validate the generated api
                 task.mustRunAfter("updateApiLintBaseline")
@@ -92,8 +92,8 @@
                     task.baselines.set(baselinesApiLocation)
                     task.api.set(builtApiLocation)
                     task.version.set(version)
-                    task.dependencyClasspath = javaCompileInputs.dependencyClasspath
-                    task.bootClasspath = javaCompileInputs.bootClasspath
+                    task.dependencyClasspath = compilationInputs.dependencyClasspath
+                    task.bootClasspath = compilationInputs.bootClasspath
                     task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
                     task.kotlinSourceLevel.set(kotlinSourceLevel)
                     task.cacheEvenIfNoOutputs()
@@ -108,8 +108,8 @@
                     task.baselines.set(checkApiRelease!!.flatMap { it.baselines })
                     task.api.set(builtApiLocation)
                     task.version.set(version)
-                    task.dependencyClasspath = javaCompileInputs.dependencyClasspath
-                    task.bootClasspath = javaCompileInputs.bootClasspath
+                    task.dependencyClasspath = compilationInputs.dependencyClasspath
+                    task.bootClasspath = compilationInputs.bootClasspath
                     task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
                     task.kotlinSourceLevel.set(kotlinSourceLevel)
                     task.dependsOn(generateApi)
@@ -127,7 +127,7 @@
                 task.k2UastEnabled.set(extension.metalavaK2UastEnabled)
                 task.kotlinSourceLevel.set(kotlinSourceLevel)
                 androidManifest?.let { task.manifestPath.set(it) }
-                applyInputs(javaCompileInputs, task, generateApiDependencies)
+                applyInputs(compilationInputs, task, generateApiDependencies)
             }
 
         // Policy: All changes to API surfaces for which compatibility is enforced must be
@@ -208,7 +208,7 @@
     }
 
     private fun applyInputs(
-        inputs: JavaCompileInputs,
+        inputs: CompilationInputs,
         task: MetalavaTask,
         generateApiDependencies: Configuration
     ) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
index 70390ec..cbb8eda 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt
@@ -18,13 +18,14 @@
 
 import androidx.build.Version
 import androidx.build.checkapi.ApiLocation
+import androidx.build.checkapi.CompilationInputs
+import androidx.build.checkapi.StandardCompilationInputs
 import androidx.build.checkapi.getApiFileVersion
 import androidx.build.checkapi.getRequiredCompatibilityApiLocation
 import androidx.build.checkapi.getVersionedApiLocation
 import androidx.build.checkapi.isValidArtifactVersion
 import androidx.build.getAndroidJar
 import androidx.build.getCheckoutRoot
-import androidx.build.java.JavaCompileInputs
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
@@ -159,7 +160,7 @@
         outputApiLocation: ApiLocation,
     ) {
         val mavenId = "$groupId:$artifactId:$version"
-        val inputs: JavaCompileInputs?
+        val inputs: CompilationInputs?
         try {
             inputs = getFiles(runnerProject, mavenId)
         } catch (e: TypedResolveException) {
@@ -185,14 +186,13 @@
         }
     }
 
-    private fun getFiles(runnerProject: Project, mavenId: String): JavaCompileInputs {
+    private fun getFiles(runnerProject: Project, mavenId: String): CompilationInputs {
         val jars = getJars(runnerProject, mavenId)
         val sources = getSources(runnerProject, "$mavenId:sources")
 
-        return JavaCompileInputs(
+        // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for KMP projects
+        return StandardCompilationInputs(
             sourcePaths = sources,
-            // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for
-            // common sources
             commonModuleSourcePaths = project.files(),
             dependencyClasspath = jars,
             bootClasspath = project.getAndroidJar()
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 99b1d02..d3272c5 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -294,26 +294,9 @@
 
     private var extraLicenses: MutableCollection<License> = ArrayList()
 
-    // Should only be used to override LibraryType.publish, if a library isn't ready to publish yet
-    var publish: Publish = Publish.UNSET
+    fun shouldPublish(): Boolean = type.publish.shouldPublish()
 
-    fun shouldPublish(): Boolean =
-        if (publish != Publish.UNSET) {
-            publish.shouldPublish()
-        } else if (type != LibraryType.UNSET) {
-            type.publish.shouldPublish()
-        } else {
-            false
-        }
-
-    fun shouldRelease(): Boolean =
-        if (publish != Publish.UNSET) {
-            publish.shouldRelease()
-        } else if (type != LibraryType.UNSET) {
-            type.publish.shouldRelease()
-        } else {
-            false
-        }
+    fun shouldRelease(): Boolean = type.publish.shouldRelease()
 
     fun ifReleasing(action: () -> Unit) {
         project.afterEvaluate {
@@ -323,31 +306,12 @@
         }
     }
 
-    fun isPublishConfigured(): Boolean = (publish != Publish.UNSET || type.publish != Publish.UNSET)
-
     fun shouldPublishSbom(): Boolean {
         if (isIsolatedProjectsEnabled()) return false
         // IDE plugins are used by and ship inside Studio
         return shouldPublish() || type == LibraryType.IDE_PLUGIN
     }
 
-    /**
-     * Whether to run API tasks such as tracking and linting. The default value is
-     * [RunApiTasks.Auto], which automatically picks based on the project's properties.
-     */
-    // TODO: decide whether we want to support overriding runApiTasks
-    // @Deprecated("Replaced with AndroidXExtension.type: LibraryType.runApiTasks")
-    var runApiTasks: RunApiTasks = RunApiTasks.Auto
-        get() = if (field == RunApiTasks.Auto && type != LibraryType.UNSET) type.checkApi else field
-        set(value) {
-            if (value is RunApiTasks.No) {
-                throw GradleException(
-                    "runApiTasks cannot be disabled from the AndroidX extension. Ensure you're using the correct library type if you really do not need API tracking"
-                )
-            }
-            field = value
-        }
-
     var doNotDocumentReason: String? = null
 
     var type: LibraryType = LibraryType.UNSET
@@ -387,7 +351,7 @@
     }
 
     fun shouldEnforceKotlinStrictApiMode(): Boolean {
-        return !legacyDisableKotlinStrictApiMode && runApiTasks is RunApiTasks.Yes
+        return !legacyDisableKotlinStrictApiMode && type.checkApi is RunApiTasks.Yes
     }
 
     fun extraLicense(closure: Closure<Any>): License {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index b0be6fb..8347d6d 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -16,205 +16,304 @@
 
 package androidx.build
 
+import androidx.build.LibraryType.Companion.BENCHMARK
+import androidx.build.LibraryType.Companion.SAMPLES
+import androidx.build.LibraryType.Companion.TEST_APPLICATION
+import androidx.build.LibraryType.Companion.UNSET
+import kotlin.collections.contains
+
 /**
- * LibraryType represents the purpose and type of a library, whether it is a conventional library, a
- * set of samples showing how to use a conventional library, a set of lint rules for using a
- * conventional library, or any other type of published project.
+ * Represents the purpose and configuration of a library, including how it is published, whether it
+ * enforces API compatibility checks, and which environment it targets. By using [LibraryType],
+ * developers can select from predefined library configurations or create their own through
+ * [ConfigurableLibrary]. This reduces complexity by capturing a library's behavior and rationale in
+ * one place, rather than requiring manual configuration of multiple independent properties.
  *
- * LibraryType collects a set of properties together, to make the "why" more clear and to simplify
- * setting these properties for library developers, so that only a single enum inferrable from the
- * purpose of the library needs to be set, rather than a variety of more arcane options.
+ * The key properties controlled by [LibraryType] are:
+ * - [publish]: Defines how (or if) the library is published to external repositories (e.g.,
+ *   GMaven).
+ * - [checkApi]: Determines whether API compatibility tasks are run, which enforce semantic
+ *   versioning and API stability.
+ * - [compilationTarget]: Specifies whether the library runs on a host machine or an Android device.
+ * - [allowCallingVisibleForTestsApis]: Indicates whether calling `@VisibleForTesting` APIs is
+ *   allowed, useful for test libraries.
+ * - [targetsKotlinConsumersOnly]: When `true`, the library is intended for Kotlin consumers only,
+ *   allowing for more Kotlin-centric API design.
  *
- * These properties are as follows: LibraryType.publish represents how the library is published to
- * GMaven LibraryType.checkApi represents whether we enforce API compatibility of the library
- * according to our semantic versioning protocol
+ * [LibraryType] includes a variety of predefined configurations commonly used in Android libraries:
+ * - Conventional published libraries ([PUBLISHED_LIBRARY], [PUBLISHED_PROTO_LIBRARY], etc.)
+ * - Internal libraries not published externally ([INTERNAL_TEST_LIBRARY],
+ *   [INTERNAL_HOST_TEST_LIBRARY])
+ * - Test libraries that allow testing internal or unstable APIs ([PUBLISHED_TEST_LIBRARY],
+ *   [INTERNAL_TEST_LIBRARY])
+ * - Lint rule sets ([LINT], [STANDALONE_PUBLISHED_LINT]) for guiding correct usage of a library
+ * - Libraries containing samples to supplement documentation ([SAMPLES])
+ * - Host-only libraries such as Gradle plugins, annotation processors, and code generators
+ *   ([GRADLE_PLUGIN], [ANNOTATION_PROCESSOR], [OTHER_CODE_PROCESSOR])
+ * - Libraries specifically meant for IDE consumption ([IDE_PLUGIN])
+ * - Snapshot-only libraries for early access or development use cases
+ *   ([SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS], etc.)
+ * - Libraries that do not publish artifacts but still run API tasks, or vice versa
+ *   ([INTERNAL_LIBRARY_WITH_API_TASKS], [SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS])
+ * - [UNSET]: a default or transitional state indicating the library's type isn't fully determined
  *
- * The possible values of LibraryType are as follows:
- * - [PUBLISHED_LIBRARY]: a conventional library published, sourced, documented, and versioned.
- * - [PUBLISHED_TEST_LIBRARY]: [PUBLISHED_LIBRARY], but allows calling `@VisibleForTesting` API.
- *   Used for libraries that allow developers to test code that uses your library. Often provides
- *   test fakes.
- * - [INTERNAL_TEST_LIBRARY]: unpublished, untracked, undocumented. Used in internal tests. Usually
- *   contains integration tests, but is _not_ an app. Runs device tests.
- * - [INTERNAL_HOST_TEST_LIBRARY]: as [INTERNAL_TEST_LIBRARY], but runs host tests instead. Avoid
- *   mixing host tests and device tests in the same library, for performance / test-result-caching
- *   reasons.
- * - [SAMPLES]: a library containing sample code referenced in your library's documentation with
- *   `@sampled`, published as a documentation-related supplement to a conventional library.
- * - [LINT]: a library of lint rules for using a conventional library. Published through lintPublish
- *   as part of an AAR, not published standalone.
- * - [GRADLE_PLUGIN]: a library that is a gradle plugin.
- * - [ANNOTATION_PROCESSOR]: a library consisting of an annotation processor. Used only while
- *   compiling.
- * - [ANNOTATION_PROCESSOR_UTILS]: contains reference code for understanding an annotation
- *   processor. Publishes source jars, but does not track API.
- * - [OTHER_CODE_PROCESSOR]: a library that algorithmically generates and/or alters code but not
- *   through hooking into custom annotations or the kotlin compiler. For example,
- *   navigation:safe-args-generator or Jetifier.
- * - [IDE_PLUGIN]: a library that should only ever be downloaded by studio. Unfortunately, we don't
- *   yet have a good way to track API for these. b/281843422
- * - [UNSET]: a library that has not yet been migrated to using LibraryType. Should never be used.
+ * Although predefined library types cover many common scenarios, you can create new
+ * [ConfigurableLibrary] instances if your project requires a unique combination of publish
+ * settings, API checking, and compilation targeting. In doing so, you ensure the project's
+ * configuration is concise, clear, and consistently applied.
  */
 sealed class LibraryType(
+    val name: String,
     val publish: Publish = Publish.NONE,
     val checkApi: RunApiTasks = RunApiTasks.No("Unknown Library Type"),
     val compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
     val allowCallingVisibleForTestsApis: Boolean = false,
     val targetsKotlinConsumersOnly: Boolean = false
 ) {
-    val name: String
-        get() = javaClass.simpleName
-
-    companion object {
-        @JvmStatic val ANNOTATION_PROCESSOR = AnnotationProcessor()
-        @JvmStatic val ANNOTATION_PROCESSOR_UTILS = AnnotationProcessorUtils()
-        @JvmStatic val GRADLE_PLUGIN = GradlePlugin()
-        @JvmStatic val IDE_PLUGIN = IdePlugin()
-        @JvmStatic val INTERNAL_TEST_LIBRARY = InternalTestLibrary()
-        @JvmStatic val INTERNAL_HOST_TEST_LIBRARY = InternalHostTestLibrary()
-        @JvmStatic val LINT = Lint()
-        @JvmStatic val STANDALONE_PUBLISHED_LINT = StandalonePublishedLint()
-        @JvmStatic val PUBLISHED_LIBRARY = PublishedLibrary()
-        @JvmStatic
-        val PUBLISHED_PROTO_LIBRARY =
-            PublishedLibrary(
-                checkApi =
-                    RunApiTasks.No("Metalava doesn't properly parse the proto sources b/180579063")
-            )
-        @JvmStatic
-        val PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
-            PublishedLibrary(targetsKotlinConsumersOnly = true)
-        @JvmStatic val PUBLISHED_TEST_LIBRARY = PublishedTestLibrary()
-        @JvmStatic
-        val PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY =
-            PublishedTestLibrary(targetsKotlinConsumersOnly = true)
-        @JvmStatic val SAMPLES = Samples()
-        @JvmStatic val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
-        val UNSET = Unset()
-
-        private val allTypes =
-            mapOf(
-                "PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
-                "PUBLISHED_PROTO_LIBRARY" to PUBLISHED_PROTO_LIBRARY,
-                "PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS" to
-                    PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
-                "PUBLISHED_TEST_LIBRARY" to PUBLISHED_TEST_LIBRARY,
-                "PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY" to PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY,
-                "INTERNAL_TEST_LIBRARY" to INTERNAL_TEST_LIBRARY,
-                "INTERNAL_HOST_TEST_LIBRARY" to INTERNAL_HOST_TEST_LIBRARY,
-                "SAMPLES" to SAMPLES,
-                "LINT" to LINT,
-                "STANDALONE_PUBLISHED_LINT" to STANDALONE_PUBLISHED_LINT,
-                "GRADLE_PLUGIN" to GRADLE_PLUGIN,
-                "ANNOTATION_PROCESSOR" to ANNOTATION_PROCESSOR,
-                "ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
-                "OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
-                "IDE_PLUGIN" to IDE_PLUGIN,
-                "UNSET" to UNSET,
-            )
-
-        fun valueOf(name: String): LibraryType {
-            val result = allTypes[name]
-            check(result != null) { "LibraryType with name $name not found" }
-            return result
-        }
-    }
-
-    open class PublishedLibrary(
-        checkApi: RunApiTasks = RunApiTasks.Yes(),
+    class ConfigurableLibrary(
+        name: String,
+        publish: Publish = Publish.NONE,
+        checkApi: RunApiTasks = RunApiTasks.No("Unknown Library Type"),
+        compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
         allowCallingVisibleForTestsApis: Boolean = false,
         targetsKotlinConsumersOnly: Boolean = false
     ) :
         LibraryType(
-            publish = Publish.SNAPSHOT_AND_RELEASE,
-            checkApi = checkApi,
-            allowCallingVisibleForTestsApis = allowCallingVisibleForTestsApis,
-            targetsKotlinConsumersOnly = targetsKotlinConsumersOnly
+            name,
+            publish,
+            checkApi,
+            compilationTarget,
+            allowCallingVisibleForTestsApis,
+            targetsKotlinConsumersOnly
         )
 
-    open class InternalLibrary(
-        compilationTarget: CompilationTarget = CompilationTarget.DEVICE,
-        allowCallingVisibleForTestsApis: Boolean = false
-    ) :
-        LibraryType(
-            checkApi = RunApiTasks.No("Internal Library"),
-            compilationTarget = compilationTarget,
-            allowCallingVisibleForTestsApis = allowCallingVisibleForTestsApis
-        )
+    companion object {
+        // Host-only tooling libraries
+        val ANNOTATION_PROCESSOR =
+            ConfigurableLibrary(
+                name = "ANNOTATION_PROCESSOR",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Annotation Processor"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class PublishedTestLibrary(targetsKotlinConsumersOnly: Boolean = false) :
-        PublishedLibrary(
-            allowCallingVisibleForTestsApis = true,
-            targetsKotlinConsumersOnly = targetsKotlinConsumersOnly
-        )
+        val ANNOTATION_PROCESSOR_UTILS =
+            ConfigurableLibrary(
+                name = "ANNOTATION_PROCESSOR_UTILS",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Annotation Processor Helper Library"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class InternalTestLibrary() : InternalLibrary(allowCallingVisibleForTestsApis = true)
+        val GRADLE_PLUGIN =
+            ConfigurableLibrary(
+                name = "GRADLE_PLUGIN",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Gradle Plugin (Host-only)"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class InternalHostTestLibrary() : InternalLibrary(CompilationTarget.HOST)
+        val OTHER_CODE_PROCESSOR =
+            ConfigurableLibrary(
+                name = "OTHER_CODE_PROCESSOR",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Code Processor (Host-only)"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class Samples :
-        LibraryType(
-            publish = Publish.SNAPSHOT_AND_RELEASE,
-            checkApi = RunApiTasks.No("Sample Library")
-        )
+        // Lint libraries
+        val LINT =
+            ConfigurableLibrary(
+                name = "LINT",
+                checkApi = RunApiTasks.No("Lint Library"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class Lint :
-        LibraryType(
-            publish = Publish.NONE,
-            checkApi = RunApiTasks.No("Lint Library"),
-            compilationTarget = CompilationTarget.HOST
-        )
+        val STANDALONE_PUBLISHED_LINT =
+            ConfigurableLibrary(
+                name = "STANDALONE_PUBLISHED_LINT",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Lint Library"),
+                compilationTarget = CompilationTarget.HOST
+            )
 
-    class StandalonePublishedLint :
-        LibraryType(
-            publish = Publish.SNAPSHOT_AND_RELEASE,
-            checkApi = RunApiTasks.No("Lint Library"),
-            compilationTarget = CompilationTarget.HOST
-        )
+        // Published libraries
+        val PUBLISHED_LIBRARY =
+            ConfigurableLibrary(
+                name = "PUBLISHED_LIBRARY",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.Yes()
+            )
 
-    class GradlePlugin :
-        LibraryType(
-            Publish.SNAPSHOT_AND_RELEASE,
-            RunApiTasks.No("Gradle Plugin (Host-only)"),
-            CompilationTarget.HOST
-        )
+        val PUBLISHED_PROTO_LIBRARY =
+            ConfigurableLibrary(
+                name = "PUBLISHED_PROTO_LIBRARY",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi =
+                    RunApiTasks.No("Metalava doesn't properly parse the proto sources b/180579063")
+            )
 
-    class AnnotationProcessor :
-        LibraryType(
-            publish = Publish.SNAPSHOT_AND_RELEASE,
-            checkApi = RunApiTasks.No("Annotation Processor"),
-            compilationTarget = CompilationTarget.HOST
-        )
+        val PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
+            ConfigurableLibrary(
+                name = "PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.Yes(),
+                targetsKotlinConsumersOnly = true
+            )
 
-    class AnnotationProcessorUtils :
-        LibraryType(
-            publish = Publish.SNAPSHOT_AND_RELEASE,
-            checkApi = RunApiTasks.No("Annotation Processor Helper Library"),
-            compilationTarget = CompilationTarget.HOST
-        )
+        // Published test libraries
+        val PUBLISHED_TEST_LIBRARY =
+            ConfigurableLibrary(
+                name = "PUBLISHED_TEST_LIBRARY",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.Yes(),
+                allowCallingVisibleForTestsApis = true
+            )
 
-    class OtherCodeProcessor(publish: Publish = Publish.SNAPSHOT_AND_RELEASE) :
-        LibraryType(
-            publish = publish,
-            checkApi = RunApiTasks.No("Code Processor (Host-only)"),
-            compilationTarget = CompilationTarget.HOST
-        )
+        val PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY =
+            ConfigurableLibrary(
+                name = "PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.Yes(),
+                allowCallingVisibleForTestsApis = true,
+                targetsKotlinConsumersOnly = true
+            )
 
-    class IdePlugin :
-        LibraryType(
-            publish = Publish.NONE,
-            // TODO: figure out a way to make sure we don't break Studio
-            checkApi = RunApiTasks.No("IDE Plugin (consumed only by Android Studio"),
-            // This is a bit complicated. IDE plugins usually have an on-device component installed
-            // by
-            // Android Studio, rather than by a client of the library, but also a host-side
-            // component.
-            compilationTarget = CompilationTarget.DEVICE
-        )
+        // Snapshot-only libraries
+        val SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS =
+            ConfigurableLibrary(
+                name = "SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS",
+                publish = Publish.SNAPSHOT_ONLY,
+                checkApi = RunApiTasks.Yes(),
+                targetsKotlinConsumersOnly = true
+            )
 
-    class Unset : LibraryType()
+        val SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS =
+            ConfigurableLibrary(
+                name = "SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS",
+                publish = Publish.SNAPSHOT_ONLY,
+                checkApi = RunApiTasks.Yes(),
+                allowCallingVisibleForTestsApis = true
+            )
+
+        val SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS =
+            ConfigurableLibrary(
+                name = "SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS",
+                publish = Publish.SNAPSHOT_ONLY,
+                checkApi = RunApiTasks.Yes("Snapshot-only library that runs API tasks")
+            )
+
+        val SNAPSHOT_ONLY_LIBRARY =
+            ConfigurableLibrary(
+                name = "SNAPSHOT_ONLY_LIBRARY",
+                publish = Publish.SNAPSHOT_ONLY,
+                checkApi = RunApiTasks.No("Snapshot-only library that does not run API tasks")
+            )
+
+        // Samples library
+        val SAMPLES =
+            ConfigurableLibrary(
+                name = "SAMPLES",
+                publish = Publish.SNAPSHOT_AND_RELEASE,
+                checkApi = RunApiTasks.No("Sample Library")
+            )
+
+        // IDE libraries
+        val IDE_PLUGIN =
+            ConfigurableLibrary(
+                name = "IDE_PLUGIN",
+                checkApi = RunApiTasks.No("IDE Plugin (consumed only by Android Studio)"),
+                compilationTarget = CompilationTarget.DEVICE
+            )
+
+        // Internal libraries
+        val INTERNAL_GRADLE_PLUGIN =
+            ConfigurableLibrary(
+                name = "INTERNAL_GRADLE_PLUGIN",
+                checkApi = RunApiTasks.No("Internal Gradle Plugin"),
+                compilationTarget = CompilationTarget.HOST
+            )
+
+        val INTERNAL_HOST_TEST_LIBRARY =
+            ConfigurableLibrary(
+                name = "INTERNAL_HOST_TEST_LIBRARY",
+                checkApi = RunApiTasks.No("Internal Library"),
+                compilationTarget = CompilationTarget.HOST
+            )
+
+        val INTERNAL_LIBRARY_WITH_API_TASKS =
+            ConfigurableLibrary(
+                name = "INTERNAL_LIBRARY_WITH_API_TASKS",
+                checkApi = RunApiTasks.Yes("Always run API tasks even if not published")
+            )
+
+        val INTERNAL_OTHER_CODE_PROCESSOR =
+            ConfigurableLibrary(
+                name = "INTERNAL_OTHER_CODE_PROCESSOR",
+                checkApi = RunApiTasks.No("Code Processor (Host-only)"),
+                compilationTarget = CompilationTarget.HOST
+            )
+
+        val INTERNAL_TEST_LIBRARY =
+            ConfigurableLibrary(
+                name = "INTERNAL_TEST_LIBRARY",
+                checkApi = RunApiTasks.No("Internal Library"),
+                allowCallingVisibleForTestsApis = true
+            )
+
+        // Misc libraries
+        val BENCHMARK =
+            ConfigurableLibrary(
+                name = "BENCHMARK",
+                checkApi = RunApiTasks.No("Benchmark Library"),
+                allowCallingVisibleForTestsApis = true
+            )
+
+        val TEST_APPLICATION =
+            ConfigurableLibrary(name = "TEST_APPLICATION", checkApi = RunApiTasks.No("Test App"))
+
+        val UNSET = ConfigurableLibrary(name = "UNSET")
+
+        private val allTypes: Map<String, LibraryType> by lazy {
+            listOf(
+                    PUBLISHED_LIBRARY,
+                    PUBLISHED_PROTO_LIBRARY,
+                    PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
+                    PUBLISHED_TEST_LIBRARY,
+                    PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY,
+                    INTERNAL_GRADLE_PLUGIN,
+                    INTERNAL_HOST_TEST_LIBRARY,
+                    INTERNAL_LIBRARY_WITH_API_TASKS,
+                    INTERNAL_OTHER_CODE_PROCESSOR,
+                    INTERNAL_TEST_LIBRARY,
+                    SAMPLES,
+                    SNAPSHOT_ONLY_LIBRARY,
+                    SNAPSHOT_ONLY_LIBRARY_WITH_API_TASKS,
+                    SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS,
+                    SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS,
+                    TEST_APPLICATION,
+                    LINT,
+                    STANDALONE_PUBLISHED_LINT,
+                    GRADLE_PLUGIN,
+                    ANNOTATION_PROCESSOR,
+                    ANNOTATION_PROCESSOR_UTILS,
+                    BENCHMARK,
+                    OTHER_CODE_PROCESSOR,
+                    IDE_PLUGIN,
+                    UNSET
+                )
+                .associateBy { it.name }
+        }
+
+        fun valueOf(name: String): LibraryType {
+            return requireNotNull(allTypes[name]) { "LibraryType with name $name not found" }
+        }
+    }
 }
 
+fun LibraryType.requiresDependencyVerification(): Boolean =
+    this !in listOf(BENCHMARK, SAMPLES, TEST_APPLICATION, UNSET)
+
 enum class CompilationTarget {
     /** This library is meant to run on the host machine (like an annotation processor). */
     HOST,
@@ -226,17 +325,11 @@
  * Publish Enum: Publish.NONE -> Generates no artifacts; does not generate snapshot artifacts or
  * releasable maven artifacts Publish.SNAPSHOT_ONLY -> Only generates snapshot artifacts
  * Publish.SNAPSHOT_AND_RELEASE -> Generates both snapshot artifacts and releasable maven artifact
- * Publish.UNSET -> Do the default, based on LibraryType. If LibraryType.UNSET -> Publish.NONE
- *
- * TODO: should we introduce a Publish.lintPublish?
- * TODO: remove Publish.UNSET once we remove LibraryType.UNSET. It is necessary now in order to be
- *   able to override LibraryType.publish (with Publish.None)
  */
 enum class Publish {
     NONE,
     SNAPSHOT_ONLY,
-    SNAPSHOT_AND_RELEASE,
-    UNSET;
+    SNAPSHOT_AND_RELEASE;
 
     fun shouldRelease() = this == SNAPSHOT_AND_RELEASE
 
@@ -244,8 +337,6 @@
 }
 
 sealed class RunApiTasks {
-    /** Automatically determine whether API tasks should be run. */
-    object Auto : RunApiTasks()
 
     /** Always run API tasks regardless of other project properties. */
     data class Yes(val reason: String? = null) : RunApiTasks()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
index a9321e0..8fc9568 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
@@ -236,7 +236,9 @@
 
                 override fun onActive(session: CameraCaptureSessionWrapper) {}
             }
-        if (!cameraDeviceWrapper.createCaptureSession(listOf(surface), callback)) {
+        if (cameraDeviceWrapper.createCaptureSession(listOf(surface), callback)) {
+            sessionConfigured.await()
+        } else {
             Log.error {
                 "Failed to create a blank capture session! " +
                     "Surfaces may not be disconnected properly."
@@ -246,7 +248,6 @@
                 surfaceTexture.release()
             }
         }
-        sessionConfigured.await()
     }
 
     companion object {
diff --git a/camera/camera-extensions-stub/build.gradle b/camera/camera-extensions-stub/build.gradle
index 67cafd2..5386fd8 100644
--- a/camera/camera-extensions-stub/build.gradle
+++ b/camera/camera-extensions-stub/build.gradle
@@ -13,7 +13,7 @@
  */
 
 
- import androidx.build.Publish
+ import androidx.build.LibraryType
 
  plugins {
     id("AndroidXPlugin")
@@ -27,8 +27,7 @@
 
 androidx {
     name = "Camera OEM Extensions Stub"
-    publish = Publish.NONE
-
+    type = LibraryType.UNSET
     inceptionYear = "2019"
     description = "OEM Extensions stub implementation for the Jetpack Camera Library, a library providing interfaces" +
             " to integrate with OEM specific camera features."
diff --git a/camera/camera-testlib-extensions/build.gradle b/camera/camera-testlib-extensions/build.gradle
index ebec508..d0e83eeb 100644
--- a/camera/camera-testlib-extensions/build.gradle
+++ b/camera/camera-testlib-extensions/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -44,7 +44,7 @@
 
 androidx {
     name = "Camera Extensions Example"
-    publish = Publish.NONE
+    type = LibraryType.UNSET
     inceptionYear = "2019"
     description = "Example extension implementation for the Jetpack Camera Library, a library providing a " +
             "consistent and reliable camera foundation that enables great camera driven " +
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt
new file mode 100644
index 0000000..59c3bb5
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/PersistentRecordingTest.kt
@@ -0,0 +1,437 @@
+/*
+ * 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.camera.video
+
+import android.Manifest
+import android.content.Context
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.AndroidUtil.isEmulator
+import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraTaskTrackingExecutor
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.InternalTestConvenience.ignoreTestForCameraPipe
+import androidx.camera.testing.impl.SurfaceTextureProvider
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.video.AudioChecker
+import androidx.camera.testing.impl.video.RecordingSession
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class PersistentRecordingTest(
+    private val implName: String,
+    private var cameraSelector: CameraSelector,
+    private val cameraConfig: CameraXConfig,
+    private val forceEnableStreamSharing: Boolean,
+) {
+
+    @get:Rule
+    val cameraPipeConfigTestRule =
+        CameraPipeConfigTestRule(
+            active = implName.contains(CameraPipeConfig::class.simpleName!!),
+        )
+
+    @get:Rule
+    val cameraRule =
+        CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+            CameraUtil.PreTestCameraIdList(cameraConfig)
+        )
+
+    @get:Rule
+    val temporaryFolder =
+        TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data(): Collection<Array<Any>> {
+            return listOf(
+                arrayOf(
+                    "back+" + Camera2Config::class.simpleName,
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ false,
+                ),
+                arrayOf(
+                    "front+" + Camera2Config::class.simpleName,
+                    CameraSelector.DEFAULT_FRONT_CAMERA,
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ false,
+                ),
+                arrayOf(
+                    "back+" + Camera2Config::class.simpleName + "+streamSharing",
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ true,
+                ),
+                arrayOf(
+                    "back+" + CameraPipeConfig::class.simpleName,
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ false,
+                ),
+                arrayOf(
+                    "front+" + CameraPipeConfig::class.simpleName,
+                    CameraSelector.DEFAULT_FRONT_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ false,
+                ),
+                arrayOf(
+                    "back+" + CameraPipeConfig::class.simpleName + "+streamSharing",
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/ true,
+                ),
+            )
+        }
+    }
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var cameraProvider: ProcessCameraProviderWrapper
+    private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private lateinit var preview: Preview
+    private lateinit var cameraInfo: CameraInfo
+    private lateinit var videoCapabilities: VideoCapabilities
+    private lateinit var camera: Camera
+    private lateinit var videoCapture: VideoCapture<Recorder>
+    private lateinit var recordingSession: RecordingSession
+    private lateinit var cameraExecutor: CameraTaskTrackingExecutor
+
+    private val oppositeCameraSelector: CameraSelector by lazy {
+        if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
+            CameraSelector.DEFAULT_FRONT_CAMERA
+        else CameraSelector.DEFAULT_BACK_CAMERA
+    }
+
+    private val oppositeCamera: Camera by lazy {
+        lateinit var camera: Camera
+        instrumentation.runOnMainSync {
+            camera = cameraProvider.bindToLifecycle(lifecycleOwner, oppositeCameraSelector)
+        }
+        camera
+    }
+
+    private val audioStreamAvailable by lazy {
+        AudioChecker.canAudioStreamBeStarted(videoCapabilities, Recorder.DEFAULT_QUALITY_SELECTOR)
+    }
+
+    @Before
+    fun setUp() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+        skipVideoRecordingTestIfNotSupportedByEmulator()
+
+        // Skip for b/264902324
+        assumeFalse(
+            "Emulator API 30 crashes running this test.",
+            Build.VERSION.SDK_INT == 30 && isEmulator()
+        )
+
+        cameraExecutor = CameraTaskTrackingExecutor()
+        val cameraXConfig =
+            CameraXConfig.Builder.fromConfig(cameraConfig).setCameraExecutor(cameraExecutor).build()
+
+        ProcessCameraProvider.configureInstance(cameraXConfig)
+
+        cameraProvider =
+            ProcessCameraProviderWrapper(
+                ProcessCameraProvider.getInstance(context).get(),
+                forceEnableStreamSharing
+            )
+        lifecycleOwner = FakeLifecycleOwner()
+        lifecycleOwner.startAndResume()
+
+        // Add extra Preview to provide an additional surface for b/168187087.
+        preview = Preview.Builder().build()
+        videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
+
+        instrumentation.runOnMainSync {
+            // Sets surface provider to preview
+            preview.surfaceProvider = SurfaceTextureProvider.createSurfaceTextureProvider()
+
+            // Retrieves the target testing camera and camera info
+            camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+            cameraInfo = camera.cameraInfo
+            videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
+        }
+
+        recordingSession =
+            RecordingSession(
+                RecordingSession.Defaults(
+                    context = context,
+                    recorder = videoCapture.output,
+                    outputOptionsProvider = {
+                        FileOutputOptions.Builder(temporaryFolder.newFile()).build()
+                    },
+                    withAudio = audioStreamAvailable,
+                )
+            )
+    }
+
+    @After
+    fun tearDown() {
+        if (this::recordingSession.isInitialized) {
+            recordingSession.release(timeoutMs = 5000)
+        }
+        if (this::cameraProvider.isInitialized) {
+            cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+        }
+    }
+
+    @Test
+    fun persistentRecording_canContinueRecordingAfterRebind() {
+        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        assumeFalse(
+            "The test is temporarily ignored when stream sharing is enabled.",
+            forceEnableStreamSharing
+        )
+
+        checkAndBindUseCases(preview, videoCapture)
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        // Bypass stream sharing if it's enforced on the device. Like quirks in
+        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+        assumeFalse(
+            "The test is temporarily ignored when the video capture requires transformation.",
+            isStreamSharingEnabled(videoCapture)
+        )
+
+        val recording =
+            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+        checkAndBindUseCases(preview, videoCapture)
+
+        recording.clearEvents()
+        recording.verifyStatus()
+
+        recording.stopAndVerify()
+    }
+
+    @Test
+    fun persistentRecording_canContinueRecordingPausedAfterRebind() {
+        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        assumeFalse(
+            "The test is temporarily ignored when stream sharing is enabled.",
+            forceEnableStreamSharing
+        )
+
+        checkAndBindUseCases(preview, videoCapture)
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        // Bypass stream sharing if it's enforced on the device. Like quirks in
+        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+        assumeFalse(
+            "The test is temporarily ignored when the video capture requires transformation.",
+            isStreamSharingEnabled(videoCapture)
+        )
+
+        val recording =
+            recordingSession
+                .createRecording(asPersistentRecording = true)
+                .startAndVerify()
+                .pauseAndVerify()
+
+        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+        checkAndBindUseCases(preview, videoCapture)
+
+        recording.resumeAndVerify().stopAndVerify()
+    }
+
+    @Test
+    fun persistentRecording_canStopAfterUnbind() {
+        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+        // TODO(b/353113961): Enable the test for camera pipe implementation.
+        implName.ignoreTestForCameraPipe(
+            "The test is temporarily ignored for camera pipe implementation.",
+            true
+        )
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        assumeFalse(
+            "The test is temporarily ignored when stream sharing is enabled.",
+            forceEnableStreamSharing
+        )
+
+        checkAndBindUseCases(preview, videoCapture)
+
+        // TODO(b/340406044): Enable the test for stream sharing use case.
+        // Bypass stream sharing if it's enforced on the device. Like quirks in
+        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
+        assumeFalse(
+            "The test is temporarily ignored when the video capture requires transformation.",
+            isStreamSharingEnabled(videoCapture)
+        )
+
+        val recording =
+            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+
+        recording.stopAndVerify()
+    }
+
+    @Test
+    fun updateVideoUsage_whenUseCaseUnboundAndReboundForPersistentRecording(): Unit = runBlocking {
+        assumeFalse(
+            "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
+            forceEnableStreamSharing
+        )
+
+        checkAndBindUseCases(preview, videoCapture)
+        val recording =
+            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+        // Act 1 - unbind VideoCapture before recording completes, isRecording should be false.
+        instrumentation.runOnMainSync { cameraProvider.unbind(videoCapture) }
+
+        camera.cameraControl.verifyIfInVideoUsage(
+            false,
+            "VideoCapture unbound but camera still in video usage"
+        )
+
+        // Act 2 - rebind VideoCapture, isRecording should be true.
+        checkAndBindUseCases(videoCapture)
+
+        camera.cameraControl.verifyIfInVideoUsage(
+            true,
+            "VideoCapture re-bound but camera still not in video usage"
+        )
+
+        // TODO(b/382158668): Remove the check for the status events.
+        recording.clearEvents()
+        recording.verifyStatus()
+        recording.stopAndVerify()
+    }
+
+    @Test
+    fun updateVideoUsage_whenUseCaseBoundToNewCameraForPersistentRecording(): Unit = runBlocking {
+        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
+
+        assumeFalse(
+            "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
+            forceEnableStreamSharing
+        )
+
+        checkAndBindUseCases(preview, videoCapture)
+        val recording =
+            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
+
+        // Act 1 - unbind before recording completes, isRecording should be false.
+        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
+
+        camera.cameraControl.verifyIfInVideoUsage(
+            false,
+            "VideoCapture unbound but camera still in video usage"
+        )
+
+        // Act 2 - rebind VideoCapture to opposite camera, isRecording should be true.
+        checkAndBindUseCases(preview, videoCapture, useOppositeCamera = true)
+
+        oppositeCamera.cameraControl.verifyIfInVideoUsage(
+            true,
+            "VideoCapture re-bound but camera still not in video usage"
+        )
+
+        // TODO(b/382158668): Remove the check for the status events.
+        recording.clearEvents()
+        recording.verifyStatus()
+        recording.stopAndVerify()
+    }
+
+    private fun getCameraSelector(useOppositeCamera: Boolean): CameraSelector =
+        if (!useOppositeCamera) cameraSelector else oppositeCameraSelector
+
+    private fun getCamera(useOppositeCamera: Boolean): Camera =
+        if (!useOppositeCamera) camera else oppositeCamera
+
+    private fun isUseCasesCombinationSupported(
+        vararg useCases: UseCase,
+        useOppositeCamera: Boolean = false,
+    ) = getCamera(useOppositeCamera).isUseCasesCombinationSupported(*useCases)
+
+    private fun checkAndBindUseCases(
+        vararg useCases: UseCase,
+        useOppositeCamera: Boolean = false,
+    ) {
+        assumeTrue(isUseCasesCombinationSupported(*useCases, useOppositeCamera = useOppositeCamera))
+
+        instrumentation.runOnMainSync {
+            cameraProvider.bindToLifecycle(
+                lifecycleOwner,
+                getCameraSelector(useOppositeCamera),
+                *useCases
+            )
+        }
+    }
+
+    private suspend fun CameraControl.verifyIfInVideoUsage(
+        expected: Boolean,
+        message: String = ""
+    ) {
+        instrumentation.waitForIdleSync() // VideoCapture observes Recorder in main thread
+        // VideoUsage is updated in camera thread. So, we should ensure all tasks already submitted
+        // to camera thread are completed before checking isInVideoUsage
+        cameraExecutor.awaitIdle()
+        assertWithMessage(message).that((this as CameraControlInternal).isInVideoUsage).apply {
+            if (expected) {
+                isTrue()
+            } else {
+                isFalse()
+            }
+        }
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 269f4ea..a93558e 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -19,11 +19,9 @@
 import android.Manifest
 import android.content.Context
 import android.graphics.Rect
-import android.graphics.SurfaceTexture
 import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.os.Build
-import android.util.Log
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
@@ -37,12 +35,8 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.DynamicRange
-import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCase
-import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
@@ -53,27 +47,22 @@
 import androidx.camera.core.impl.utils.TransformUtils.rectToSize
 import androidx.camera.core.impl.utils.TransformUtils.rotateSize
 import androidx.camera.core.impl.utils.TransformUtils.within360
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.impl.AndroidUtil.isEmulator
 import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
 import androidx.camera.testing.impl.CameraPipeConfigTestRule
 import androidx.camera.testing.impl.CameraTaskTrackingExecutor
 import androidx.camera.testing.impl.CameraUtil
-import androidx.camera.testing.impl.InternalTestConvenience.ignoreTestForCameraPipe
-import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
 import androidx.camera.testing.impl.SurfaceTextureProvider
 import androidx.camera.testing.impl.WakelockEmptyActivityRule
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
 import androidx.camera.testing.impl.getRotatedAspectRatio
 import androidx.camera.testing.impl.getRotation
-import androidx.camera.testing.impl.mocks.MockScreenFlash
 import androidx.camera.testing.impl.useAndRelease
 import androidx.camera.testing.impl.video.AudioChecker
 import androidx.camera.testing.impl.video.Recording
 import androidx.camera.testing.impl.video.RecordingSession
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
-import androidx.lifecycle.LifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -83,7 +72,6 @@
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.common.util.concurrent.ListenableFuture
 import java.io.File
-import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.runBlocking
 import org.junit.After
@@ -226,7 +214,10 @@
         ProcessCameraProvider.configureInstance(cameraXConfig)
 
         cameraProvider =
-            ProcessCameraProviderWrapper(ProcessCameraProvider.getInstance(context).get())
+            ProcessCameraProviderWrapper(
+                ProcessCameraProvider.getInstance(context).get(),
+                forceEnableStreamSharing
+            )
         lifecycleOwner = FakeLifecycleOwner()
         lifecycleOwner.startAndResume()
 
@@ -236,7 +227,7 @@
 
         instrumentation.runOnMainSync {
             // Sets surface provider to preview
-            preview.surfaceProvider = getSurfaceProvider()
+            preview.surfaceProvider = SurfaceTextureProvider.createSurfaceTextureProvider()
 
             // Retrieves the target testing camera and camera info
             camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
@@ -438,50 +429,6 @@
     }
 
     @Test
-    fun recordingWithPreviewAndImageAnalysis() {
-        // Arrange.
-        val analysis = ImageAnalysis.Builder().build()
-        val latchForImageAnalysis = CountDownLatch(5)
-        analysis.setAnalyzer(CameraXExecutors.directExecutor()) {
-            latchForImageAnalysis.countDown()
-            it.close()
-        }
-        checkAndBindUseCases(preview, videoCapture, analysis)
-
-        // Act.
-        recordingSession.createRecording().recordAndVerify()
-
-        // Verify.
-        assertThat(latchForImageAnalysis.await(10, TimeUnit.SECONDS)).isTrue()
-    }
-
-    @Test
-    fun recordingWithPreviewAndImageCapture() {
-        // Arrange.
-        val imageCapture = ImageCapture.Builder().build()
-        checkAndBindUseCases(preview, videoCapture, imageCapture)
-
-        // Act.
-        recordingSession.createRecording().recordAndVerify()
-
-        // Verify.
-        completeImageCapture(imageCapture)
-    }
-
-    @Test
-    fun recordingWithPreviewAndFlashImageCapture() {
-        // Arrange.
-        val imageCapture = ImageCapture.Builder().build()
-        checkAndBindUseCases(preview, videoCapture, imageCapture)
-
-        // Act.
-        recordingSession.createRecording().recordAndVerify()
-
-        // Verify.
-        completeImageCapture(imageCapture, useFlash = true)
-    }
-
-    @Test
     fun recordingWithPreview_boundSeparately() {
         assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture))
 
@@ -577,26 +524,6 @@
     }
 
     @Test
-    fun boundButNotRecordingDuringCapture_withPreviewAndImageCapture() {
-        // Arrange.
-        val imageCapture = ImageCapture.Builder().build()
-        checkAndBindUseCases(preview, videoCapture, imageCapture)
-
-        // Act & verify.
-        completeImageCapture(imageCapture)
-    }
-
-    @Test
-    fun boundButNotRecordingDuringFlashCapture_withPreviewAndImageCapture() {
-        // Arrange.
-        val imageCapture = ImageCapture.Builder().build()
-        checkAndBindUseCases(preview, videoCapture, imageCapture)
-
-        // Act & verify.
-        completeImageCapture(imageCapture, useFlash = true)
-    }
-
-    @Test
     fun canRecordMultipleFilesInARow() {
         checkAndBindUseCases(preview, videoCapture)
         recordingSession.createRecording().recordAndVerify()
@@ -726,104 +653,6 @@
     }
 
     @Test
-    fun persistentRecording_canContinueRecordingAfterRebind() {
-        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        assumeFalse(
-            "The test is temporarily ignored when stream sharing is enabled.",
-            forceEnableStreamSharing
-        )
-
-        checkAndBindUseCases(preview, videoCapture)
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        // Bypass stream sharing if it's enforced on the device. Like quirks in
-        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
-        assumeFalse(
-            "The test is temporarily ignored when the video capture requires transformation.",
-            isStreamSharingEnabled(videoCapture)
-        )
-
-        val recording =
-            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
-        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-        checkAndBindUseCases(preview, videoCapture)
-
-        recording.clearEvents()
-        recording.verifyStatus()
-
-        recording.stopAndVerify()
-    }
-
-    @Test
-    fun persistentRecording_canContinueRecordingPausedAfterRebind() {
-        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        assumeFalse(
-            "The test is temporarily ignored when stream sharing is enabled.",
-            forceEnableStreamSharing
-        )
-
-        checkAndBindUseCases(preview, videoCapture)
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        // Bypass stream sharing if it's enforced on the device. Like quirks in
-        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
-        assumeFalse(
-            "The test is temporarily ignored when the video capture requires transformation.",
-            isStreamSharingEnabled(videoCapture)
-        )
-
-        val recording =
-            recordingSession
-                .createRecording(asPersistentRecording = true)
-                .startAndVerify()
-                .pauseAndVerify()
-
-        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-        checkAndBindUseCases(preview, videoCapture)
-
-        recording.resumeAndVerify().stopAndVerify()
-    }
-
-    @Test
-    fun persistentRecording_canStopAfterUnbind() {
-        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
-        // TODO(b/353113961): Enable the test for camera pipe implementation.
-        implName.ignoreTestForCameraPipe(
-            "The test is temporarily ignored for camera pipe implementation.",
-            true
-        )
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        assumeFalse(
-            "The test is temporarily ignored when stream sharing is enabled.",
-            forceEnableStreamSharing
-        )
-
-        checkAndBindUseCases(preview, videoCapture)
-
-        // TODO(b/340406044): Enable the test for stream sharing use case.
-        // Bypass stream sharing if it's enforced on the device. Like quirks in
-        // androidx.camera.core.internal.compat.workaround.StreamSharingForceEnabler.
-        assumeFalse(
-            "The test is temporarily ignored when the video capture requires transformation.",
-            isStreamSharingEnabled(videoCapture)
-        )
-
-        val recording =
-            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
-        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-
-        recording.stopAndVerify()
-    }
-
-    @Test
     fun canRecordWithCorrectTransformation() {
         // Act.
         checkAndBindUseCases(preview, videoCapture)
@@ -924,74 +753,6 @@
         )
     }
 
-    @Test
-    fun updateVideoUsage_whenUseCaseUnboundAndReboundForPersistentRecording(): Unit = runBlocking {
-        assumeFalse(
-            "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
-            forceEnableStreamSharing
-        )
-
-        checkAndBindUseCases(preview, videoCapture)
-        val recording =
-            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
-        // Act 1 - unbind VideoCapture before recording completes, isRecording should be false.
-        instrumentation.runOnMainSync { cameraProvider.unbind(videoCapture) }
-
-        camera.cameraControl.verifyIfInVideoUsage(
-            false,
-            "VideoCapture unbound but camera still in video usage"
-        )
-
-        // Act 2 - rebind VideoCapture, isRecording should be true.
-        checkAndBindUseCases(videoCapture)
-
-        camera.cameraControl.verifyIfInVideoUsage(
-            true,
-            "VideoCapture re-bound but camera still not in video usage"
-        )
-
-        // TODO(b/382158668): Remove the check for the status events.
-        recording.clearEvents()
-        recording.verifyStatus()
-        recording.stopAndVerify()
-    }
-
-    @Test
-    fun updateVideoUsage_whenUseCaseBoundToNewCameraForPersistentRecording(): Unit = runBlocking {
-        assumeStopCodecAfterSurfaceRemovalCrashMediaServerQuirk()
-
-        assumeFalse(
-            "TODO: b/340406044 - Temporarily ignored when stream sharing is enabled.",
-            forceEnableStreamSharing
-        )
-
-        checkAndBindUseCases(preview, videoCapture)
-        val recording =
-            recordingSession.createRecording(asPersistentRecording = true).startAndVerify()
-
-        // Act 1 - unbind before recording completes, isRecording should be false.
-        instrumentation.runOnMainSync { cameraProvider.unbindAll() }
-
-        camera.cameraControl.verifyIfInVideoUsage(
-            false,
-            "VideoCapture unbound but camera still in video usage"
-        )
-
-        // Act 2 - rebind VideoCapture to opposite camera, isRecording should be true.
-        checkAndBindUseCases(preview, videoCapture, useOppositeCamera = true)
-
-        oppositeCamera.cameraControl.verifyIfInVideoUsage(
-            true,
-            "VideoCapture re-bound but camera still not in video usage"
-        )
-
-        // TODO(b/382158668): Remove the check for the status events.
-        recording.clearEvents()
-        recording.verifyStatus()
-        recording.stopAndVerify()
-    }
-
     // TODO: b/341691683 - Add tests for multiple VideoCapture bound and recording concurrently
 
     private fun getCameraSelector(useOppositeCamera: Boolean): CameraSelector =
@@ -1020,35 +781,6 @@
         }
     }
 
-    private fun completeImageCapture(
-        imageCapture: ImageCapture,
-        imageFile: File = temporaryFolder.newFile(),
-        useFlash: Boolean = false
-    ) {
-        val savedCallback = ImageSavedCallback()
-
-        if (useFlash) {
-            if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_FRONT) {
-                imageCapture.screenFlash = MockScreenFlash()
-                imageCapture.flashMode = ImageCapture.FLASH_MODE_SCREEN
-            } else {
-                imageCapture.flashMode = ImageCapture.FLASH_MODE_ON
-            }
-        } else {
-            imageCapture.flashMode = ImageCapture.FLASH_MODE_OFF
-        }
-
-        imageCapture.takePicture(
-            ImageCapture.OutputFileOptions.Builder(imageFile).build(),
-            CameraXExecutors.ioExecutor(),
-            savedCallback
-        )
-        savedCallback.verifyCaptureResult()
-
-        // Just in case same imageCapture is bound to rear camera later
-        imageCapture.screenFlash = null
-    }
-
     data class ExpectedRotation(val contentRotation: Int, val metadataRotation: Int)
 
     private fun getExpectedRotation(
@@ -1106,82 +838,10 @@
         }
     }
 
-    private fun getSurfaceProvider(): Preview.SurfaceProvider {
-        return SurfaceTextureProvider.createSurfaceTextureProvider(
-            object : SurfaceTextureProvider.SurfaceTextureCallback {
-                override fun onSurfaceTextureReady(
-                    surfaceTexture: SurfaceTexture,
-                    resolution: Size
-                ) {
-                    // No-op
-                }
-
-                override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
-                    surfaceTexture.release()
-                }
-            }
-        )
-    }
-
     private fun assumeExtraCroppingQuirk() {
         assumeExtraCroppingQuirk(implName)
     }
 
-    private inner class ProcessCameraProviderWrapper(val cameraProvider: ProcessCameraProvider) {
-
-        fun bindToLifecycle(
-            lifecycleOwner: LifecycleOwner,
-            cameraSelector: CameraSelector,
-            vararg useCases: UseCase
-        ): Camera {
-            if (useCases.isEmpty()) {
-                return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
-            }
-            val useCaseGroup =
-                UseCaseGroup.Builder()
-                    .apply {
-                        useCases.forEach { useCase -> addUseCase(useCase) }
-                        if (forceEnableStreamSharing) {
-                            addEffect(StreamSharingForceEnabledEffect())
-                        }
-                    }
-                    .build()
-            return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
-        }
-
-        fun unbind(vararg useCases: UseCase) {
-            cameraProvider.unbind(*useCases)
-        }
-
-        fun unbindAll() {
-            cameraProvider.unbindAll()
-        }
-
-        fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
-    }
-
-    private class ImageSavedCallback : ImageCapture.OnImageSavedCallback {
-
-        private val latch = CountDownLatch(1)
-        val results = mutableListOf<ImageCapture.OutputFileResults>()
-        val errors = mutableListOf<ImageCaptureException>()
-
-        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
-            results.add(outputFileResults)
-            latch.countDown()
-        }
-
-        override fun onError(exception: ImageCaptureException) {
-            errors.add(exception)
-            Log.e(TAG, "OnImageSavedCallback.onError: ${exception.message}")
-            latch.countDown()
-        }
-
-        fun verifyCaptureResult() {
-            assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue()
-        }
-    }
-
     private suspend fun CameraControl.verifyIfInVideoUsage(
         expected: Boolean,
         message: String = ""
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
index 4781265..e5e9d50 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoTestingUtil.kt
@@ -26,13 +26,20 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks as PipeDeviceQuirks
 import androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk as PipeExtraCroppingQuirk
+import androidx.camera.core.Camera
 import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
 import androidx.camera.core.UseCase
+import androidx.camera.core.UseCaseGroup
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
 import androidx.camera.testing.impl.getRotatedResolution
 import androidx.camera.testing.impl.useAndRelease
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
 import androidx.camera.video.internal.compat.quirk.StopCodecAfterSurfaceRemovalCrashMediaServerQuirk
+import androidx.lifecycle.LifecycleOwner
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
 import java.io.File
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
@@ -88,3 +95,39 @@
 
 fun isSurfaceProcessingEnabled(videoCapture: VideoCapture<*>) =
     videoCapture.node != null || isStreamSharingEnabled(videoCapture)
+
+class ProcessCameraProviderWrapper(
+    private val cameraProvider: ProcessCameraProvider,
+    private val forceEnableStreamSharing: Boolean
+) {
+
+    fun bindToLifecycle(
+        lifecycleOwner: LifecycleOwner,
+        cameraSelector: CameraSelector,
+        vararg useCases: UseCase
+    ): Camera {
+        if (useCases.isEmpty()) {
+            return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
+        }
+        val useCaseGroup =
+            UseCaseGroup.Builder()
+                .apply {
+                    useCases.forEach { useCase -> addUseCase(useCase) }
+                    if (forceEnableStreamSharing) {
+                        addEffect(StreamSharingForceEnabledEffect())
+                    }
+                }
+                .build()
+        return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
+    }
+
+    fun unbind(vararg useCases: UseCase) {
+        cameraProvider.unbind(*useCases)
+    }
+
+    fun unbindAll() {
+        cameraProvider.unbindAll()
+    }
+
+    fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
index defa125..518bae0 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -38,6 +38,7 @@
 import androidx.camera.testing.impl.SurfaceTextureProvider.createAutoDrainingSurfaceTextureProvider
 import androidx.camera.testing.impl.WakelockEmptyActivityRule
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.camera.testing.impl.mocks.MockScreenFlash
 import androidx.camera.testing.impl.video.AudioChecker
 import androidx.camera.testing.impl.video.RecordingSession
 import androidx.camera.video.FileOutputOptions
@@ -95,7 +96,6 @@
     @get:Rule val wakelockEmptyActivityRule = WakelockEmptyActivityRule()
 
     companion object {
-        private const val TAG = "UseCaseCombinationTest"
 
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
@@ -317,7 +317,18 @@
     }
 
     @Test
-    fun previewCombinesVideoCaptureAndImageCapture() {
+    fun previewCombinesVideoCaptureAndImageCapture_withoutRecording() {
+        // Arrange.
+        checkAndPrepareVideoCaptureSources()
+        checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+        // Assert.
+        previewMonitor.waitForStream()
+        imageCapture.waitForCapturing()
+    }
+
+    @Test
+    fun previewCombinesVideoCaptureAndImageCapture_withRecording() {
         // Arrange.
         checkAndPrepareVideoCaptureSources()
         checkAndBindUseCases(preview, videoCapture, imageCapture)
@@ -329,6 +340,29 @@
     }
 
     @Test
+    fun previewCombinesVideoCaptureAndFlashImageCapture_withoutRecording() {
+        // Arrange.
+        checkAndPrepareVideoCaptureSources()
+        checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+        // Assert.
+        previewMonitor.waitForStream()
+        imageCapture.waitForCapturing(useFlash = true)
+    }
+
+    @Test
+    fun previewCombinesVideoCaptureAndFlashImageCapture_withRecording() {
+        // Arrange.
+        checkAndPrepareVideoCaptureSources()
+        checkAndBindUseCases(preview, videoCapture, imageCapture)
+
+        // Assert.
+        previewMonitor.waitForStream()
+        recordingSession.createRecording().recordAndVerify()
+        imageCapture.waitForCapturing(useFlash = true)
+    }
+
+    @Test
     fun previewCombinesVideoCaptureAndImageAnalysis() {
         // Arrange.
         checkAndPrepareVideoCaptureSources()
@@ -487,7 +521,7 @@
         return ImageCapture.Builder().build()
     }
 
-    private fun ImageCapture.waitForCapturing(timeMillis: Long = 5000) {
+    private fun ImageCapture.waitForCapturing(timeMillis: Long = 10000, useFlash: Boolean = false) {
         val callback =
             object : ImageCapture.OnImageCapturedCallback() {
                 val latch = CountDownLatch(1)
@@ -504,12 +538,26 @@
                 }
             }
 
+        if (useFlash) {
+            if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_FRONT) {
+                screenFlash = MockScreenFlash()
+                flashMode = ImageCapture.FLASH_MODE_SCREEN
+            } else {
+                flashMode = ImageCapture.FLASH_MODE_ON
+            }
+        } else {
+            flashMode = ImageCapture.FLASH_MODE_OFF
+        }
+
         takePicture(Dispatchers.Main.asExecutor(), callback)
 
         assertThat(
                 callback.latch.await(timeMillis, TimeUnit.MILLISECONDS) && callback.errors.isEmpty()
             )
             .isTrue()
+
+        // Just in case same imageCapture is bound to rear camera later
+        screenFlash = null
     }
 
     class PreviewMonitor {
diff --git a/car/app/app-samples/navigation/automotive/build.gradle b/car/app/app-samples/navigation/automotive/build.gradle
index c7e3a8a..c75e16c 100644
--- a/car/app/app-samples/navigation/automotive/build.gradle
+++ b/car/app/app-samples/navigation/automotive/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -52,6 +51,5 @@
 }
 
 androidx {
-    type = LibraryType.SAMPLES
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
 }
diff --git a/car/app/app-samples/navigation/common/build.gradle b/car/app/app-samples/navigation/common/build.gradle
index b3be643..5977ad6 100644
--- a/car/app/app-samples/navigation/common/build.gradle
+++ b/car/app/app-samples/navigation/common/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -46,6 +45,7 @@
 }
 
 androidx {
-    type = LibraryType.SAMPLES
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
+    // TODO: b/326456246
+    optOutJSpecify = true
 }
diff --git a/collection/collection-benchmark-kmp/build.gradle b/collection/collection-benchmark-kmp/build.gradle
index 2c5b34e..a284d14 100644
--- a/collection/collection-benchmark-kmp/build.gradle
+++ b/collection/collection-benchmark-kmp/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.konan.target.HostManager
 import org.jetbrains.kotlin.konan.target.KonanTarget
 
@@ -71,6 +71,6 @@
 }
 
 androidx {
-    publish = Publish.NONE
+    type = LibraryType.UNSET
 }
 
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 2f5bdef..59b2ca4 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -21,6 +21,8 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
+
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
 import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -110,6 +112,7 @@
 
 androidx {
     name = "Collections Benchmarks (Android / iOS)"
+    type = LibraryType.BENCHMARK
     inceptionYear = "2022"
     description = "AndroidX Collections Benchmarks (Android / iOS)"
 }
diff --git a/collection/integration-tests/testapp/build.gradle b/collection/integration-tests/testapp/build.gradle
index 06b5a37..3c90a3b 100644
--- a/collection/integration-tests/testapp/build.gradle
+++ b/collection/integration-tests/testapp/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -36,7 +36,7 @@
 
 androidx {
     name = "Collection Integration Tests"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2021"
     description = "AndroidX Collection Integration Tests"
     // TODO: b/326456246
diff --git a/compose/animation/animation-core/benchmark/build.gradle b/compose/animation/animation-core/benchmark/build.gradle
index e9ef268..dfe4a41 100644
--- a/compose/animation/animation-core/benchmark/build.gradle
+++ b/compose/animation/animation-core/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -39,3 +41,7 @@
 
     namespace = "androidx.compose.animation.core.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/benchmark-utils/benchmark/build.gradle b/compose/benchmark-utils/benchmark/build.gradle
index 0ab9324..388dd5b 100644
--- a/compose/benchmark-utils/benchmark/build.gradle
+++ b/compose/benchmark-utils/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -37,3 +39,7 @@
     compileSdk = 35
     namespace = "androidx.compose.benchmarkutils.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/foundation/foundation-layout/benchmark/build.gradle b/compose/foundation/foundation-layout/benchmark/build.gradle
index 2b41dc0..0cbdfed 100644
--- a/compose/foundation/foundation-layout/benchmark/build.gradle
+++ b/compose/foundation/foundation-layout/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -42,3 +44,7 @@
     compileSdk = 35
     namespace = "androidx.compose.foundation.layout.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/foundation/foundation/benchmark/build.gradle b/compose/foundation/foundation/benchmark/build.gradle
index 7fdab92..2e666b3 100644
--- a/compose/foundation/foundation/benchmark/build.gradle
+++ b/compose/foundation/foundation/benchmark/build.gradle
@@ -66,5 +66,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
index da1ea4fb..52fb64a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldLayoutPhaseToggleTest.kt
@@ -18,6 +18,9 @@
 
 import android.os.Build
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.matchers.assertThat
@@ -31,12 +34,17 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasSetTextAction
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.test.filters.SdkSuppress
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import org.junit.Rule
 import org.junit.Test
 
@@ -102,4 +110,45 @@
         assertThat(secondTextLayoutResult.layoutInput.style.textAlign).isEqualTo(TextAlign.Start)
         assertThat(firstBitmap).isNotEqualToBitmap(secondBitmap)
     }
+
+    @Test
+    fun constraintsMinWidthDecrease_textLayoutReflects() {
+        state = TextFieldState("abc")
+        var leftBoxSize by mutableStateOf(100.dp)
+        var textLayoutResult: TextLayoutResult? = null
+        rule.setContent {
+            Row(Modifier.size(200.dp)) {
+                Box(Modifier.size(leftBoxSize))
+                BasicTextField(
+                    state = state,
+                    textStyle = textStyle,
+                    modifier = Modifier.weight(1f),
+                    lineLimits = TextFieldLineLimits.SingleLine,
+                    onTextLayout = { textLayoutResult = it.invoke() }
+                )
+            }
+        }
+
+        with(rule.density) {
+            rule.runOnIdle {
+                val width =
+                    maxOf(textLayoutResult!!.multiParagraph.maxIntrinsicWidth, 100.dp.toPx())
+                assertThat(textLayoutResult).isNotNull()
+                assertThat(textLayoutResult?.size?.width).isEqualTo(width.roundToInt(), 1)
+                assertThat(textLayoutResult?.multiParagraph?.width).isWithin(1f).of(width)
+            }
+
+            leftBoxSize = 150.dp
+
+            rule.runOnIdle {
+                val width = maxOf(textLayoutResult!!.multiParagraph.maxIntrinsicWidth, 50.dp.toPx())
+                assertThat(textLayoutResult?.size?.width).isEqualTo(width.roundToInt(), 1)
+                assertThat(textLayoutResult?.multiParagraph?.width).isWithin(1f).of(width)
+            }
+        }
+    }
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+    isIn(Range.closed(expected - tolerance, expected + tolerance))
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
index 3bf9852..634c4a0 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
@@ -223,7 +223,6 @@
         }
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun textInputStarted_forFieldInActivity_whenFocusRequestedImmediately_fromLaunchedEffect() {
         textInputStarted_whenFocusRequestedImmediately_fromEffect(
@@ -231,7 +230,6 @@
         )
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun textInputStarted_forFieldInActivity_whenFocusRequestedImmediately_fromDisposableEffect() {
         textInputStarted_whenFocusRequestedImmediately_fromEffect(
@@ -295,7 +293,6 @@
         inputMethodInterceptor.assertSessionActive()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadLeft_DPadDevice() {
         setupAndEnableBasicTextField()
@@ -311,7 +308,6 @@
         rule.onNodeWithTag("test-button-left").assertIsFocused()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadRight_DPadDevice() {
         setupAndEnableBasicTextField()
@@ -327,7 +323,6 @@
         rule.onNodeWithTag("test-button-right").assertIsFocused()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadUp_DPadDevice() {
         setupAndEnableBasicTextField()
@@ -343,7 +338,6 @@
         rule.onNodeWithTag("test-button-top").assertIsFocused()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadDown_DPadDevice() {
         setupAndEnableBasicTextField()
@@ -375,7 +369,6 @@
     }
 
     @Ignore("339495780")
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadLeft_hardwareKeyboard() {
         setupAndEnableBasicTextField()
@@ -396,7 +389,6 @@
     }
 
     @Ignore("339495780")
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadRight_hardwareKeyboard() {
         setupAndEnableBasicTextField()
@@ -419,7 +411,6 @@
     }
 
     @Ignore("339495780")
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadUp_hardwareKeyboard() {
         setupAndEnableBasicTextField()
@@ -440,7 +431,6 @@
     }
 
     @Ignore("339495780")
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onDPadDown_hardwareKeyboard() {
         setupAndEnableBasicTextField()
@@ -462,7 +452,6 @@
         rule.onNodeWithTag("test-text-field-1").assertSelection(TextRange(2))
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onTab() {
         setupAndEnableBasicTextField(singleLine = true)
@@ -475,7 +464,6 @@
         rule.onNodeWithTag("test-button-right").assertIsFocused()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_withImeActionNext_checkFocusNavigation_onEnter() {
         setupAndEnableBasicTextField(singleLine = true)
@@ -488,7 +476,6 @@
         rule.onNodeWithTag("test-button-right").assertIsFocused()
     }
 
-    @SdkSuppress(minSdkVersion = 22) // b/266742195
     @Test
     fun basicTextField_checkFocusNavigation_onShiftTab() {
         setupAndEnableBasicTextField(singleLine = true)
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index d43b9a5..19a718c 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -65,7 +65,7 @@
 
 androidx {
     name = "Compose Documentation Snippets"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     description = "Compose Documentation Snippets on developer.android.com"
 }
 
diff --git a/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle b/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
index fc2f32b9..bcca37d 100644
--- a/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
+++ b/compose/integration-tests/hero/jetsnack/jetsnack-microbenchmark/build.gradle
@@ -62,5 +62,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index ca73ac9..fc97e89 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -22,7 +22,7 @@
  * modifying its settings.
  */
 import androidx.build.ApkCopyHelperKt
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -73,7 +73,7 @@
 
 androidx {
     name = "Compose Material Catalog app"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2021"
     description = "This is a project for the Compose Material Catalog app."
 }
diff --git a/compose/material/material-ripple/benchmark/build.gradle b/compose/material/material-ripple/benchmark/build.gradle
index 4d59f30..b2d5bb9 100644
--- a/compose/material/material-ripple/benchmark/build.gradle
+++ b/compose/material/material-ripple/benchmark/build.gradle
@@ -52,5 +52,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/compose/material/material/benchmark/build.gradle b/compose/material/material/benchmark/build.gradle
index 64803b4..97ae9aa 100644
--- a/compose/material/material/benchmark/build.gradle
+++ b/compose/material/material/benchmark/build.gradle
@@ -54,5 +54,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index 29c6f75..2db1be6 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -43,7 +43,7 @@
 
 androidx {
     name = "Compose Material Catalog"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2021"
     description = "This is a project for the Compose Material Catalog."
 }
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index 5d92707..209c329 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -5,7 +5,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -31,7 +31,7 @@
 
 androidx {
     name = "Compose Material Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2019"
     description = "This is a project for Material demos."
 }
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
index 6977a047..b69db8d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/SystemBarsDefaultInsets.android.kt
@@ -17,8 +17,10 @@
 package androidx.compose.material
 
 import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
 import androidx.compose.runtime.Composable
 
 internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
-    @Composable get() = systemBars
+    @Composable get() = systemBars.union(displayCutout)
diff --git a/compose/material3/adaptive/benchmark/build.gradle b/compose/material3/adaptive/benchmark/build.gradle
index 54b0da4..f63172f 100644
--- a/compose/material3/adaptive/benchmark/build.gradle
+++ b/compose/material3/adaptive/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -37,3 +39,7 @@
     compileSdk = 35
     namespace = "androidx.compose.material3.adaptive.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/material3/benchmark/build.gradle b/compose/material3/benchmark/build.gradle
index 618db30..c2a930a 100644
--- a/compose/material3/benchmark/build.gradle
+++ b/compose/material3/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -40,4 +42,8 @@
 android {
     compileSdk = 35
     namespace = "androidx.compose.material3.benchmark"
-}
\ No newline at end of file
+}
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/material3/material3-common/build.gradle b/compose/material3/material3-common/build.gradle
index 3eac2d7..372a968 100644
--- a/compose/material3/material3-common/build.gradle
+++ b/compose/material3/material3-common/build.gradle
@@ -25,7 +25,6 @@
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index c5a99fc..2f679dbf 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -3119,19 +3119,6 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class WideNavigationRailArrangement {
-    field public static final androidx.compose.material3.WideNavigationRailArrangement.Companion Companion;
-  }
-
-  public static final class WideNavigationRailArrangement.Companion {
-    method public int getBottom();
-    method public int getCenter();
-    method public int getTop();
-    property public final int Bottom;
-    property public final int Center;
-    property public final int Top;
-  }
-
   @androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
     ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
     method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
@@ -3148,11 +3135,11 @@
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
-    method public int getArrangement();
+    method public androidx.compose.foundation.layout.Arrangement.Vertical getArrangement();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property public final int Arrangement;
+    property public final androidx.compose.foundation.layout.Arrangement.Vertical arrangement;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape modalContainerShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -3167,8 +3154,8 @@
   }
 
   public final class WideNavigationRailKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index c5a99fc..2f679dbf 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -3119,19 +3119,6 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class WideNavigationRailArrangement {
-    field public static final androidx.compose.material3.WideNavigationRailArrangement.Companion Companion;
-  }
-
-  public static final class WideNavigationRailArrangement.Companion {
-    method public int getBottom();
-    method public int getCenter();
-    method public int getTop();
-    property public final int Bottom;
-    property public final int Center;
-    property public final int Top;
-  }
-
   @androidx.compose.runtime.Immutable public final class WideNavigationRailColors {
     ctor public WideNavigationRailColors(long containerColor, long contentColor, long modalContainerColor, long modalScrimColor);
     method public androidx.compose.material3.WideNavigationRailColors copy(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
@@ -3148,11 +3135,11 @@
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WideNavigationRailDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors();
     method @androidx.compose.runtime.Composable public androidx.compose.material3.WideNavigationRailColors colors(optional long containerColor, optional long contentColor, optional long modalContainerColor, optional long modalScrimColor);
-    method public int getArrangement();
+    method public androidx.compose.foundation.layout.Arrangement.Vertical getArrangement();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getContainerShape();
     method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getModalContainerShape();
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property public final int Arrangement;
+    property public final androidx.compose.foundation.layout.Arrangement.Vertical arrangement;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape containerShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape modalContainerShape;
     property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
@@ -3167,8 +3154,8 @@
   }
 
   public final class WideNavigationRailKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ModalWideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional boolean hideOnCollapse, optional androidx.compose.ui.graphics.Shape collapsedShape, optional androidx.compose.ui.graphics.Shape expandedShape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional float expandedHeaderTopPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, optional androidx.compose.material3.ModalWideNavigationRailProperties expandedProperties, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRail(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.WideNavigationRailState state, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.WideNavigationRailColors colors, optional kotlin.jvm.functions.Function0<kotlin.Unit>? header, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.foundation.layout.Arrangement.Vertical arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void WideNavigationRailItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean railExpanded, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
diff --git a/compose/material3/material3/integration-tests/material3-catalog/build.gradle b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
index 2dfcfc5..123bb8a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-catalog/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -49,7 +49,7 @@
 
 androidx {
     name = "Compose Material3 Catalog"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2021"
     description = "This is a project for the Compose Material You Catalog."
 }
diff --git a/compose/material3/material3/integration-tests/material3-demos/build.gradle b/compose/material3/material3/integration-tests/material3-demos/build.gradle
index 510bd2e..0890c10 100644
--- a/compose/material3/material3/integration-tests/material3-demos/build.gradle
+++ b/compose/material3/material3/integration-tests/material3-demos/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -49,7 +49,7 @@
 
 androidx {
     name = "Compose Material3 Components Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2022"
     description = "Contains the demo code for the AndroidX Compose Material 3 components."
 }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
index d221c20..3aee1b1 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationRailSamples.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.content.pm.ActivityInfo
 import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
@@ -43,7 +44,6 @@
 import androidx.compose.material3.NavigationRailItem
 import androidx.compose.material3.Text
 import androidx.compose.material3.WideNavigationRail
-import androidx.compose.material3.WideNavigationRailArrangement
 import androidx.compose.material3.WideNavigationRailItem
 import androidx.compose.material3.WideNavigationRailValue
 import androidx.compose.material3.rememberWideNavigationRailState
@@ -336,7 +336,9 @@
     val selectedIcons = listOf(Icons.Filled.Home, Icons.Filled.Favorite, Icons.Filled.Star)
     val unselectedIcons =
         listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
-    WideNavigationRail(state = rememberWideNavigationRailState()) {
+    WideNavigationRail(
+        state = rememberWideNavigationRailState(initialValue = WideNavigationRailValue.Expanded)
+    ) {
         items.forEachIndexed { index, item ->
             WideNavigationRailItem(
                 railExpanded = true,
@@ -365,7 +367,7 @@
         listOf(Icons.Outlined.Home, Icons.Outlined.FavoriteBorder, Icons.Outlined.StarBorder)
     val state = rememberWideNavigationRailState()
     val scope = rememberCoroutineScope()
-    var arrangement by remember { mutableStateOf(WideNavigationRailArrangement.Center) }
+    var arrangement: Arrangement.Vertical by remember { mutableStateOf(Arrangement.Center) }
 
     Row(Modifier.fillMaxWidth()) {
         WideNavigationRail(
@@ -419,7 +421,7 @@
             }
         }
 
-        val isArrangementCenter = arrangement == WideNavigationRailArrangement.Center
+        val isArrangementCenter = arrangement == Arrangement.Center
         val changeToString = if (isArrangementCenter) "Bottom" else "Center"
         Column(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
             Text(modifier = Modifier.padding(16.dp), text = "Change arrangement to:")
@@ -427,9 +429,9 @@
                 modifier = Modifier.padding(4.dp),
                 onClick = {
                     if (isArrangementCenter) {
-                        arrangement = WideNavigationRailArrangement.Bottom
+                        arrangement = Arrangement.Bottom
                     } else {
-                        arrangement = WideNavigationRailArrangement.Center
+                        arrangement = Arrangement.Center
                     }
                 }
             ) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
index 3ff91fa..41088c5 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/MaterialComponentsInsetSupportTest.kt
@@ -19,8 +19,10 @@
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -51,7 +53,9 @@
         var expected: WindowInsets? = null
         rule.setContent {
             expected =
-                WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
             contentPadding = TopAppBarDefaults.windowInsets
         }
 
@@ -64,9 +68,9 @@
         var expected: WindowInsets? = null
         rule.setContent {
             expected =
-                WindowInsets.systemBars.only(
-                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
-                )
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)
             contentPadding = BottomAppBarDefaults.windowInsets
         }
 
@@ -82,7 +86,9 @@
         var expected: WindowInsets? = null
         rule.setContent {
             expected =
-                WindowInsets.systemBars.only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
             contentPadding = DrawerDefaults.windowInsets
         }
 
@@ -95,9 +101,9 @@
         var expected: WindowInsets? = null
         rule.setContent {
             expected =
-                WindowInsets.systemBars.only(
-                    WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal
-                )
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
             contentPadding = NavigationBarDefaults.windowInsets
         }
 
@@ -110,7 +116,9 @@
         var expected: WindowInsets? = null
         rule.setContent {
             expected =
-                WindowInsets.systemBars.only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .only(WindowInsetsSides.Start + WindowInsetsSides.Vertical)
             contentPadding = NavigationRailDefaults.windowInsets
         }
 
@@ -124,7 +132,10 @@
         var layoutDirection: LayoutDirection? = null
         rule.setContent {
             layoutDirection = LocalLayoutDirection.current
-            expected = WindowInsets.systemBars.asPaddingValues(LocalDensity.current)
+            expected =
+                WindowInsets.systemBars
+                    .union(WindowInsets.displayCutout)
+                    .asPaddingValues(LocalDensity.current)
             Scaffold { paddingValues -> contentPadding = paddingValues }
         }
 
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
index bf6d8bd..fc8f93e 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailScreenshotTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
@@ -157,7 +158,7 @@
             DefaultWideNavigationRail(
                 interactionSource,
                 expanded = scheme.expanded,
-                arrangement = WideNavigationRailArrangement.Center
+                arrangement = Arrangement.Center
             )
         }
 
@@ -182,7 +183,7 @@
             DefaultWideNavigationRail(
                 interactionSource,
                 expanded = scheme.expanded,
-                arrangement = WideNavigationRailArrangement.Bottom
+                arrangement = Arrangement.Bottom
             )
         }
 
@@ -273,7 +274,7 @@
  * @param interactionSource the [MutableInteractionSource] for the first [WideNavigationRailItem],
  *   to control its visual state
  * @param expanded whether the rail is expanded
- * @param arrangement the [WideNavigationRailArrangement] of the rail
+ * @param arrangement the [Arrangement.Vertical] of the rail
  * @param withHeader when true, shows a [FloatingActionButton] as the header
  * @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
  */
@@ -282,7 +283,7 @@
 private fun DefaultWideNavigationRail(
     interactionSource: MutableInteractionSource,
     expanded: Boolean = false,
-    arrangement: WideNavigationRailArrangement = WideNavigationRailArrangement.Top,
+    arrangement: Arrangement.Vertical = Arrangement.Top,
     withHeader: Boolean = false,
     setUnselectedItemsAsDisabled: Boolean = false,
 ) {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
index 521309b..56c08e6 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WideNavigationRailTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.material3
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
@@ -538,7 +539,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             WideNavigationRail(
                 modifier = Modifier.testTag("rail"),
-                arrangement = WideNavigationRailArrangement.Center,
+                arrangement = Arrangement.Center,
                 header = { Box(Modifier.testTag("header").size(10.dp)) }
             ) {
                 WideNavigationRailItem(
@@ -570,7 +571,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             WideNavigationRail(
                 modifier = Modifier.testTag("rail"),
-                arrangement = WideNavigationRailArrangement.Bottom,
+                arrangement = Arrangement.Bottom,
                 header = { Box(Modifier.testTag("header").size(10.dp)) }
             ) {
                 WideNavigationRailItem(
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
index fe4e4a7..5299afb 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/SystemBarsDefaultInsets.android.kt
@@ -17,8 +17,10 @@
 package androidx.compose.material3.internal
 
 import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.union
 import androidx.compose.runtime.Composable
 
 internal actual val WindowInsets.Companion.systemBarsForVisualComponents: WindowInsets
-    @Composable get() = systemBars
+    @Composable get() = systemBars.union(displayCutout)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
index da4cab6..d6b0f7f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WideNavigationRail.kt
@@ -27,13 +27,13 @@
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.imePadding
 import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.padding
@@ -94,11 +94,9 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.offset
 import androidx.compose.ui.util.fastFirst
-import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
 import androidx.compose.ui.util.fastMap
-import androidx.compose.ui.util.fastSumBy
 import androidx.compose.ui.util.lerp
-import kotlin.jvm.JvmInline
 import kotlin.math.min
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
@@ -132,9 +130,8 @@
  *
  * For a modal variation of the wide navigation rail, see [ModalWideNavigationRail].
  *
- * Finally, the [WideNavigationRail] supports setting a [WideNavigationRailArrangement] for the
- * items, so that the items can be grouped at the top (the default), at the middle, or at the bottom
- * of the rail. The header will always be at the top.
+ * Finally, the [WideNavigationRail] supports setting an [Arrangement.Vertical] for the items, with
+ * [Arrangement.Top] being the default. The header will always be at the top.
  *
  * See [WideNavigationRailItem] for configuration specific to each item, and not the overall
  * [WideNavigationRail] component.
@@ -146,7 +143,9 @@
  *   wide navigation rail. See [WideNavigationRailDefaults.colors]
  * @param header optional header that may hold a [FloatingActionButton] or a logo
  * @param windowInsets a window insets of the wide navigation rail
- * @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
+ * @param arrangement the [Arrangement.Vertical] of this wide navigation rail for its content. Note
+ *   that if there's a header present, the items will be arranged on the remaining space below it,
+ *   except for the center arrangement which considers the entire height of the container
  * @param content the content of this wide navigation rail, typically [WideNavigationRailItem]s
  */
 @ExperimentalMaterial3ExpressiveApi
@@ -158,7 +157,7 @@
     colors: WideNavigationRailColors = WideNavigationRailDefaults.colors(),
     header: @Composable (() -> Unit)? = null,
     windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
-    arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
+    arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
     content: @Composable () -> Unit
 ) {
     WideNavigationRailLayout(
@@ -174,7 +173,6 @@
     )
 }
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun WideNavigationRailLayout(
     modifier: Modifier,
@@ -184,7 +182,7 @@
     shape: Shape,
     header: @Composable (() -> Unit)?,
     windowInsets: WindowInsets,
-    arrangement: WideNavigationRailArrangement,
+    arrangement: Arrangement.Vertical,
     content: @Composable () -> Unit
 ) {
     var currentWidth by remember { mutableIntStateOf(0) }
@@ -345,34 +343,39 @@
                         currentWidth = width
 
                         return layout(width, height) {
-                            var y = 0
-                            var headerHeight = 0
+                            val railHeight = height - WNRVerticalPadding.roundToPx()
+                            var headerOffset = 0
                             if (headerPlaceable != null && headerPlaceable.height > 0) {
-                                headerPlaceable.placeRelative(0, y)
-                                headerHeight = headerPlaceable.height
-                                if (arrangement == WideNavigationRailArrangement.Top) {
-                                    y += headerHeight + WNRHeaderPadding.roundToPx()
-                                }
+                                headerPlaceable.placeRelative(0, 0)
+                                headerOffset +=
+                                    headerPlaceable.height + WNRHeaderPadding.roundToPx()
                             }
 
-                            val itemsHeight = itemsPlaceables?.fastSumBy { it.height } ?: 0
-                            val verticalPadding = itemVerticalSpacedBy.roundToPx()
-                            if (arrangement == WideNavigationRailArrangement.Center) {
-                                y =
-                                    (height -
-                                        WNRVerticalPadding.roundToPx() -
-                                        (itemsHeight + (itemsCount - 1) * verticalPadding)) / 2
-                                y = y.coerceAtLeast(headerHeight)
-                            } else if (arrangement == WideNavigationRailArrangement.Bottom) {
-                                y =
-                                    height -
-                                        WNRVerticalPadding.roundToPx() -
-                                        (itemsHeight + (itemsCount - 1) * verticalPadding)
-                                y = y.coerceAtLeast(headerHeight)
-                            }
-                            itemsPlaceables?.fastForEach { item ->
-                                item.placeRelative(0, y)
-                                y += item.height + verticalPadding
+                            if (itemsPlaceables != null) {
+                                val layoutSize =
+                                    if (arrangement == Arrangement.Center) {
+                                        // For centered arrangement the items will be centered in
+                                        // the container, not in the remaining space below the
+                                        // header.
+                                        railHeight
+                                    } else {
+                                        railHeight - headerOffset
+                                    }
+                                val sizes = IntArray(itemsPlaceables.size)
+                                itemsPlaceables.fastForEachIndexed { index, item ->
+                                    sizes[index] = item.height
+                                    if (index < itemsPlaceables.size - 1) {
+                                        sizes[index] += itemVerticalSpacedBy.roundToPx()
+                                    }
+                                }
+                                val y = IntArray(itemsPlaceables.size)
+                                with(arrangement) { arrange(layoutSize, sizes, y) }
+
+                                val offset =
+                                    if (arrangement == Arrangement.Center) 0 else headerOffset
+                                itemsPlaceables.fastForEachIndexed { index, item ->
+                                    item.placeRelative(0, y[index] + offset)
+                                }
                             }
                         }
                     }
@@ -419,7 +422,7 @@
  * @param expandedHeaderTopPadding the padding to be applied to the top of the rail. It's usually
  *   needed in order to align the content of the rail between the collapsed and expanded animation
  * @param windowInsets a window insets of the wide navigation rail
- * @param arrangement the [WideNavigationRailArrangement] of this wide navigation rail
+ * @param arrangement the [Arrangement.Vertical] of this wide navigation rail
  * @param expandedProperties [ModalWideNavigationRailProperties] for further customization of the
  *   expanded modal wide navigation rail's window behavior
  * @param content the content of this modal wide navigation rail, usually [WideNavigationRailItem]s
@@ -436,7 +439,7 @@
     header: @Composable (() -> Unit)? = null,
     expandedHeaderTopPadding: Dp = 0.dp,
     windowInsets: WindowInsets = WideNavigationRailDefaults.windowInsets,
-    arrangement: WideNavigationRailArrangement = WideNavigationRailDefaults.Arrangement,
+    arrangement: Arrangement.Vertical = WideNavigationRailDefaults.arrangement,
     expandedProperties: ModalWideNavigationRailProperties =
         ModalWideNavigationRailDefaults.Properties,
     content: @Composable () -> Unit
@@ -654,30 +657,6 @@
     )
 }
 
-/** Class that describes the different supported item arrangements of the [WideNavigationRail]. */
-@ExperimentalMaterial3ExpressiveApi
-@JvmInline
-value class WideNavigationRailArrangement private constructor(private val value: Int) {
-    companion object {
-        /* The items are grouped at the top on the wide navigation Rail. */
-        val Top = WideNavigationRailArrangement(0)
-
-        /* The items are centered on the wide navigation Rail. */
-        val Center = WideNavigationRailArrangement(1)
-
-        /* The items are grouped at the bottom on the wide navigation Rail. */
-        val Bottom = WideNavigationRailArrangement(2)
-    }
-
-    override fun toString() =
-        when (this) {
-            Top -> "Top"
-            Center -> "Center"
-            Bottom -> "Bottom"
-            else -> "Unknown"
-        }
-}
-
 /**
  * Represents the colors of the various elements of a wide navigation rail.
  *
@@ -749,8 +728,8 @@
         @Composable get() = NavigationRailExpandedTokens.ModalContainerShape.value
 
     /** Default arrangement for a wide navigation rail. */
-    val Arrangement: WideNavigationRailArrangement
-        get() = WideNavigationRailArrangement.Top
+    val arrangement: Arrangement.Vertical
+        get() = Arrangement.Top
 
     /** Default window insets for a wide navigation rail. */
     val windowInsets: WindowInsets
@@ -932,7 +911,7 @@
     header: @Composable (() -> Unit)?,
     windowInsets: WindowInsets,
     gesturesEnabled: Boolean,
-    arrangement: WideNavigationRailArrangement,
+    arrangement: Arrangement.Vertical,
     content: @Composable () -> Unit
 ) {
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -1094,7 +1073,6 @@
 private val CollapsedRailWidth = NavigationRailCollapsedTokens.ContainerWidth
 private val ExpandedRailMinWidth = NavigationRailExpandedTokens.ContainerWidthMinimum
 private val ExpandedRailMaxWidth = NavigationRailExpandedTokens.ContainerWidthMaximum
-private val ItemMinWidth = NavigationRailCollapsedTokens.ContainerWidth
 private val TopIconItemMinHeight = NavigationRailBaselineItemTokens.ContainerHeight
 private val ItemTopIconIndicatorVerticalPadding =
     (NavigationRailVerticalItemTokens.ActiveIndicatorHeight -
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index f9f82e1..0b326a2 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -15,7 +15,6 @@
  */
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -75,9 +74,9 @@
 }
 
 androidx {
+    // This library is consumed by Kotlin CI to run Compose runtime test with the latest compiler.
     name = "Compose Internal Test Utils"
-    type = LibraryType.INTERNAL_TEST_LIBRARY
-    publish = Publish.SNAPSHOT_ONLY
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY
     inceptionYear = "2024"
     description = "Compose runtime test utils shared between runtime and compiler tests."
 }
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index ce213f7..1a6f050 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -58,3 +61,7 @@
     // See aosp/1804059
     androidTestImplementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index b8bb931..49de56d 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
index 6f9f2ec..5023dea 100644
--- a/compose/ui/ui-graphics/benchmark/build.gradle
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -37,3 +39,7 @@
     compileSdk = 35
     namespace = "androidx.compose.ui.graphics.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
index f9c09ef..f3910b4 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/junit4/WaitUntilNodeCountTest.kt
@@ -140,4 +140,10 @@
             waitUntilDoesNotExist(hasTestTag(TestTag), timeoutMillis = Timeout)
         }
     }
+
+    // Regression for b/361250553
+    @Test
+    fun waitUntil_succeedsWhen_noRoots() = runComposeUiTest {
+        waitUntilDoesNotExist(hasTestTag(TestTag), Timeout)
+    }
 }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
index 987f728a..9e931b1 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt
@@ -228,7 +228,9 @@
     timeoutMillis: Long = 1_000L
 ) {
     waitUntil("exactly $count nodes match (${matcher.description})", timeoutMillis) {
-        onAllNodes(matcher).fetchSemanticsNodes().size == count
+        // Never require the existence of compose roots. Either the current UI or the anticipated UI
+        // might not have any compose at all (i.e. View only).
+        onAllNodes(matcher).fetchSemanticsNodes(atLeastOneRootRequired = false).size == count
     }
 }
 
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index b019338..63c3346 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
@@ -60,17 +60,14 @@
         atLeastOneRootRequired: Boolean,
         errorMessageOnFail: String? = null,
         skipDeactivatedNodes: Boolean = true
-    ): SelectionResult {
-        val nodes =
-            testContext.testOwner.getAllSemanticsNodes(
-                atLeastOneRootRequired = atLeastOneRootRequired,
-                useUnmergedTree = useUnmergedTree,
-                skipDeactivatedNodes = skipDeactivatedNodes
-            )
-        return testContext.testOwner.runOnUiThread {
-            selector.map(nodes, errorMessageOnFail.orEmpty())
+    ): SelectionResult =
+        testContext.testOwner.getAllSemanticsNodes(
+            atLeastOneRootRequired = atLeastOneRootRequired,
+            useUnmergedTree = useUnmergedTree,
+            skipDeactivatedNodes = skipDeactivatedNodes
+        ) {
+            selector.map(it, errorMessageOnFail.orEmpty())
         }
-    }
 
     /**
      * Returns the semantics node captured by this object.
@@ -201,13 +198,11 @@
     /** If using the merged tree, performs the same search in the unmerged tree. */
     private fun getNodesInUnmergedTree(errorMessageOnFail: String?): List<SemanticsNode> {
         return if (!useUnmergedTree) {
-            val nodes =
-                testContext.testOwner.getAllSemanticsNodes(
-                    atLeastOneRootRequired = true,
-                    useUnmergedTree = true
-                )
-            testContext.testOwner.runOnUiThread {
-                selector.map(nodes, errorMessageOnFail.orEmpty()).selectedNodes
+            testContext.testOwner.getAllSemanticsNodes(
+                atLeastOneRootRequired = true,
+                useUnmergedTree = true
+            ) {
+                selector.map(it, errorMessageOnFail.orEmpty()).selectedNodes
             }
         } else {
             emptyList()
@@ -234,8 +229,6 @@
     internal val useUnmergedTree: Boolean,
     internal val selector: SemanticsSelector
 ) {
-    private var nodeIds: List<Int>? = null
-
     constructor(
         testContext: TestContext,
         useUnmergedTree: Boolean,
@@ -258,19 +251,9 @@
         atLeastOneRootRequired: Boolean = true,
         errorMessageOnFail: String? = null
     ): List<SemanticsNode> {
-        if (nodeIds == null) {
-            val nodes =
-                testContext.testOwner.getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree)
-
-            return testContext.testOwner
-                .runOnUiThread { selector.map(nodes, errorMessageOnFail.orEmpty()) }
-                .apply { nodeIds = selectedNodes.map { it.id }.toList() }
-                .selectedNodes
+        return testContext.testOwner.getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree) {
+            selector.map(it, errorMessageOnFail.orEmpty()).selectedNodes
         }
-
-        return testContext.testOwner
-            .getAllSemanticsNodes(atLeastOneRootRequired, useUnmergedTree)
-            .filter { it.id in nodeIds!! }
     }
 
     /**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index de45f6e..8560ecd 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -40,10 +40,10 @@
     /**
      * Collects all [RootForTest]s from all compose hierarchies.
      *
-     * This is a blocking call. Returns only after compose is idle.
-     *
-     * Can crash in case it hits time out. This is not supposed to be handled as it surfaces only in
-     * incorrect tests.
+     * This method is the choke point where all assertions and interactions must go through when
+     * testing composables and where we thus have the opportunity to automatically reach quiescence.
+     * This is done by calling [ComposeUiTest.waitForIdle] before getting and returning the
+     * registered roots.
      *
      * @param atLeastOneRootExpected Whether the caller expects that at least one compose root is
      *   present in the tested app. This affects synchronization efforts / timeouts of this API.
@@ -52,18 +52,31 @@
 }
 
 /**
- * Collects all [SemanticsNode]s from all compose hierarchies.
+ * Collects all [SemanticsNode]s from all compose hierarchies, and returns the [transform]ed
+ * results.
  *
- * This is a blocking call. Returns only after compose is idle.
+ * Set [useUnmergedTree] to `true` to search through the unmerged semantics tree.
  *
- * Can crash in case it hits time out. This is not supposed to be handled as it surfaces only in
- * incorrect tests.
+ * Set [skipDeactivatedNodes] to `false` to include
+ * [deactivated][androidx.compose.ui.node.LayoutNode.isDeactivated] nodes in the search.
+ *
+ * Use [atLeastOneRootRequired] to treat not finding any compose hierarchies at all as an error. If
+ * no hierarchies are found, we will wait 2 seconds to accommodate cases where composable content is
+ * set asynchronously. On the other hand, if you expect or know that there is no composable content,
+ * set [atLeastOneRootRequired] to `false` and no error will be thrown if there are no compose
+ * roots, and the wait for compose roots will be reduced to .5 seconds.
+ *
+ * This method will wait for quiescence before collecting all SemanticsNodes. Collection happens on
+ * the main thread and the [transform]ation of all SemanticsNodes to a result is done while on the
+ * main thread. This allows us to transform the result using methods that must be called on the main
+ * thread, without switching back and forth between the main thread and the test thread.
  */
-internal fun TestOwner.getAllSemanticsNodes(
+internal fun <R> TestOwner.getAllSemanticsNodes(
     atLeastOneRootRequired: Boolean,
     useUnmergedTree: Boolean,
-    skipDeactivatedNodes: Boolean = true
-): Iterable<SemanticsNode> {
+    skipDeactivatedNodes: Boolean = true,
+    transform: (Iterable<SemanticsNode>) -> R
+): R {
     val roots =
         getRoots(atLeastOneRootRequired).also {
             check(!atLeastOneRootRequired || it.isNotEmpty()) {
@@ -77,11 +90,13 @@
         }
 
     return runOnUiThread {
-        roots.flatMap {
-            it.semanticsOwner.getAllSemanticsNodes(
-                mergingEnabled = !useUnmergedTree,
-                skipDeactivatedNodes = skipDeactivatedNodes
-            )
-        }
+        transform.invoke(
+            roots.flatMap {
+                it.semanticsOwner.getAllSemanticsNodes(
+                    mergingEnabled = !useUnmergedTree,
+                    skipDeactivatedNodes = skipDeactivatedNodes
+                )
+            }
+        )
     }
 }
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index 19f23c8..2f33706 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -42,3 +44,7 @@
     compileSdk = 35
     namespace = "androidx.compose.ui.text.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
index cbd6e80..585ce22 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/CacheTextLayoutInputTest.kt
@@ -202,12 +202,12 @@
     }
 
     @Test
-    fun minConstraints_should_not_differ() {
+    fun minConstraints_should_differ() {
         val input1 = cacheTextLayoutInput(constraints = Constraints(minWidth = 10, minHeight = 20))
         val input2 = cacheTextLayoutInput(constraints = Constraints(minWidth = 20, minHeight = 10))
 
-        assertThat(input1.hashCode()).isEqualTo(input2.hashCode())
-        assertThat(input1).isEqualTo(input2)
+        assertThat(input1.hashCode()).isNotEqualTo(input2.hashCode())
+        assertThat(input1).isNotEqualTo(input2)
     }
 
     private fun cacheTextLayoutInput(
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
index 819b397..34c02cb 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextLayoutCacheTest.kt
@@ -175,7 +175,7 @@
     }
 
     @Test
-    fun constraintsMinChanges_shouldReturnFromCache() {
+    fun constraintsMinChanges_shouldReturnNull() {
         val textLayoutCache = TextLayoutCache(16)
         val firstInput =
             textLayoutInput(
@@ -194,7 +194,7 @@
         val textLayoutResult = layoutText(firstInput)
         textLayoutCache.put(firstInput, textLayoutResult)
 
-        Truth.assertThat(textLayoutCache.get(secondInput)).isEqualTo(textLayoutResult)
+        Truth.assertThat(textLayoutCache.get(secondInput)).isNull()
     }
 
     @Test
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index 5af082b..d278c21 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -458,6 +458,31 @@
     }
 
     @Test
+    fun decreasingMinWidth_decreasesTheCalculatedWidth() {
+        val textMeasurer = textMeasurer(cacheSize = 8)
+        val firstTextLayout =
+            layoutText(
+                textLayoutInput(
+                    constraints = Constraints(minWidth = 1000, maxWidth = Int.MAX_VALUE)
+                ),
+                textMeasurer
+            )
+
+        val secondTextLayout =
+            layoutText(
+                textLayoutInput(
+                    constraints = Constraints(minWidth = 500, maxWidth = Int.MAX_VALUE)
+                ),
+                textMeasurer
+            )
+
+        assertThat(firstTextLayout.multiParagraph)
+            .isNotSameInstanceAs(secondTextLayout.multiParagraph)
+        assertThat(firstTextLayout.size.width).isEqualTo(1000)
+        assertThat(secondTextLayout.size.width).isEqualTo(500)
+    }
+
+    @Test
     fun emptyConstraints_hugeString_dontCrash() {
         val subject = textMeasurer()
         subject.measure("A".repeat(100_000), TextStyle.Default)
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index b35f666..d146fac 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -421,8 +421,7 @@
             result = 31 * result + density.hashCode()
             result = 31 * result + layoutDirection.hashCode()
             result = 31 * result + fontFamilyResolver.hashCode()
-            result = 31 * result + constraints.maxWidth.hashCode()
-            result = 31 * result + constraints.maxHeight.hashCode()
+            result = 31 * result + constraints.hashCode()
             return result
         }
 
@@ -440,8 +439,7 @@
             if (density != other.textLayoutInput.density) return false
             if (layoutDirection != other.textLayoutInput.layoutDirection) return false
             if (fontFamilyResolver !== other.textLayoutInput.fontFamilyResolver) return false
-            if (constraints.maxWidth != other.textLayoutInput.constraints.maxWidth) return false
-            if (constraints.maxHeight != other.textLayoutInput.constraints.maxHeight) return false
+            if (constraints != other.textLayoutInput.constraints) return false
         }
 
         return true
diff --git a/compose/ui/ui/benchmark/build.gradle b/compose/ui/ui/benchmark/build.gradle
index 216aa33..cddf687 100644
--- a/compose/ui/ui/benchmark/build.gradle
+++ b/compose/ui/ui/benchmark/build.gradle
@@ -55,5 +55,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
index 465fc6b..d4ba384 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
@@ -21,9 +21,6 @@
 import android.view.autofill.AutofillValue
 import androidx.benchmark.junit4.BenchmarkRule
 import androidx.benchmark.junit4.measureRepeatedOnMainThread
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.autofill.AutofillType
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.platform.LocalAutofillTree
 import androidx.compose.ui.platform.LocalView
@@ -44,7 +41,7 @@
 
     @get:Rule val benchmarkRule = BenchmarkRule()
 
-    private lateinit var autofillTree: AutofillTree
+    private lateinit var autofillTree: androidx.compose.ui.autofill.AutofillTree
     private lateinit var composeView: View
 
     @Before
@@ -62,9 +59,10 @@
             composeTestRule.runOnUiThread {
                 // Arrange.
                 val autofillNode =
-                    AutofillNode(
+                    androidx.compose.ui.autofill.AutofillNode(
                         onFill = {},
-                        autofillTypes = listOf(AutofillType.PersonFullName),
+                        autofillTypes =
+                            listOf(androidx.compose.ui.autofill.AutofillType.PersonFullName),
                         boundingBox = Rect(0f, 0f, 0f, 0f)
                     )
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 47bd70f..dc19905 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -18,7 +18,6 @@
 
 import android.os.Build
 import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.O
 import androidx.annotation.RequiresApi
 import androidx.compose.foundation.demos.text.SoftwareKeyboardControllerDemo
 import androidx.compose.integration.demos.common.ActivityDemo
@@ -38,6 +37,7 @@
 import androidx.compose.ui.demos.autofill.BasicTextFieldAutofill
 import androidx.compose.ui.demos.autofill.ExplicitAutofillTypesDemo
 import androidx.compose.ui.demos.autofill.LegacyTextFieldAutofillDemo
+import androidx.compose.ui.demos.autofill.MixedOldNewAutofillDemo
 import androidx.compose.ui.demos.autofill.OutlinedTextFieldAutofillDemo
 import androidx.compose.ui.demos.focus.AdjacentScrollablesFocusDemo
 import androidx.compose.ui.demos.focus.CancelFocusDemo
@@ -288,7 +288,8 @@
             },
             ComposableDemo("S: TextField Autofill") { LegacyTextFieldAutofillDemo() },
             ComposableDemo("S: OutlinedTextField Autofill") { OutlinedTextFieldAutofillDemo() },
-            ComposableDemo("Navigation Sample") { AutofillNavigation() }
+            ComposableDemo("Navigation Sample") { AutofillNavigation() },
+            ComposableDemo("Old and New Autofill Mixed") { MixedOldNewAutofillDemo() }
         )
     )
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index 739374e..f9e568a 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
@@ -32,8 +32,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillNode
-import androidx.compose.ui.autofill.AutofillType
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
@@ -43,7 +41,6 @@
 import androidx.compose.ui.unit.dp
 
 @Composable
-@OptIn(ExperimentalComposeUiApi::class)
 fun ExplicitAutofillTypesDemo() {
     var name by
         rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
@@ -52,7 +49,7 @@
 
     Column {
         Autofill(
-            autofillTypes = listOf(AutofillType.PersonFullName),
+            autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.PersonFullName),
             onFill = { name = TextFieldValue(it) }
         ) {
             OutlinedTextField(
@@ -65,7 +62,7 @@
         Spacer(Modifier.height(10.dp))
 
         Autofill(
-            autofillTypes = listOf(AutofillType.EmailAddress),
+            autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.EmailAddress),
             onFill = { email = TextFieldValue(it) }
         ) {
             OutlinedTextField(
@@ -77,18 +74,20 @@
     }
 }
 
-@ExperimentalComposeUiApi
 @Composable
 private fun Autofill(
-    autofillTypes: List<AutofillType>,
+    autofillTypes: List<androidx.compose.ui.autofill.AutofillType>,
     onFill: ((String) -> Unit),
     content: @Composable BoxScope.() -> Unit
 ) {
-    val autofill = LocalAutofill.current
+    val autofill = @OptIn(ExperimentalComposeUiApi::class) LocalAutofill.current
     val autofillTree = LocalAutofillTree.current
     val autofillNode =
         remember(autofillTypes, onFill) {
-            AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
+            androidx.compose.ui.autofill.AutofillNode(
+                onFill = onFill,
+                autofillTypes = autofillTypes
+            )
         }
 
     Box(
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt
new file mode 100644
index 0000000..fd19751
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/MixedOldNewAutofillDemo.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 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.demos.autofill
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.autofill.ContentType
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalAutofill
+import androidx.compose.ui.platform.LocalAutofillManager
+import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.collections.set
+
+@RequiresApi(Build.VERSION_CODES.O)
+@SuppressLint("NullAnnotationGroup")
+@Preview
+@Composable
+fun MixedOldNewAutofillDemo() {
+    Column(modifier = Modifier.background(color = Color.Black)) {
+        Text(text = "Enter your username and password below.", color = Color.White)
+
+        // Text field using new autofill API.
+        BasicTextField(
+            state = remember { TextFieldState() },
+            modifier =
+                Modifier.fillMaxWidth().border(1.dp, Color.LightGray).semantics {
+                    contentType = ContentType.Username
+                },
+            textStyle = MaterialTheme.typography.body1.copy(color = Color.LightGray),
+            cursorBrush = SolidColor(Color.White)
+        )
+
+        // Text field using old autofill API.
+        val autofill = @OptIn(ExperimentalComposeUiApi::class) LocalAutofill.current
+        val autofillTree = LocalAutofillTree.current
+        val textState = rememberTextFieldState()
+        val autofillNode = remember {
+            androidx.compose.ui.autofill.AutofillNode(
+                onFill = { textState.edit { replace(0, length, it) } },
+                autofillTypes = listOf(androidx.compose.ui.autofill.AutofillType.Password),
+            )
+        }
+        BasicTextField(
+            state = textState,
+            modifier =
+                Modifier.fillMaxWidth()
+                    .border(1.dp, Color.LightGray)
+                    .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+                    .onFocusChanged {
+                        if (it.isFocused) {
+                            autofill?.requestAutofillForNode(autofillNode)
+                        } else {
+                            autofill?.cancelAutofillForNode(autofillNode)
+                        }
+                    },
+            textStyle = MaterialTheme.typography.body1.copy(color = Color.LightGray),
+            cursorBrush = SolidColor(Color.White)
+        )
+        DisposableEffect(autofillNode) {
+            autofillTree.children[autofillNode.id] = autofillNode
+            onDispose { autofillTree.children.remove(autofillNode.id) }
+        }
+
+        // Submit button (Only available using the new autofill APIs.
+        val autofillManager = LocalAutofillManager.current
+        Button(onClick = { autofillManager?.commit() }) { Text("Submit credentials") }
+    }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index 6f44c0a..b3bc836 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.platform.LocalAutofillTree
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -77,8 +78,6 @@
     @SdkSuppress(minSdkVersion = 26)
     @Test
     fun onProvideAutofillVirtualStructure_populatesViewStructure() {
-        // TODO(b/383201236): Ensure the old API works when the new API is enabled.
-        if (isSemanticAutofillEnabled) return
         // Arrange.
         val viewStructure: ViewStructure = FakeViewStructure()
         val autofillNode =
@@ -97,14 +96,20 @@
         assertThat(viewStructure)
             .isEqualTo(
                 FakeViewStructure().apply {
+                    if (isSemanticAutofillEnabled) {
+                        autofillId = ownerView.autofillId
+                        bounds = android.graphics.Rect(0, 0, 0, 0)
+                        packageName = currentPackageName
+                        virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+                    }
                     children.add(
                         FakeViewStructure().apply {
-                            virtualId = autofillNode.id
+                            autofillHints = mutableListOf(AUTOFILL_HINT_PERSON_NAME)
                             autofillId = ownerView.autofillId
                             autofillType = View.AUTOFILL_TYPE_TEXT
-                            autofillHints = mutableListOf(AUTOFILL_HINT_PERSON_NAME)
-                            packageName = currentPackageName
                             bounds = android.graphics.Rect(0, 0, 0, 0)
+                            packageName = currentPackageName
+                            virtualId = autofillNode.id
                         }
                     )
                 }
@@ -114,14 +119,12 @@
     @SdkSuppress(minSdkVersion = 26)
     @Test
     fun autofill_triggersOnFill() {
-        // TODO(b/383201236): Ensure the old API works when the new API is enabled.
-        if (isSemanticAutofillEnabled) return
         // Arrange.
         val expectedValue = "PersonName"
-        var autofilledValue = ""
+        var autoFilledValue = ""
         val autofillNode =
             AutofillNode(
-                onFill = { autofilledValue = it },
+                onFill = { autoFilledValue = it },
                 autofillTypes = listOf(AutofillType.PersonFullName),
                 boundingBox = Rect(0f, 0f, 0f, 0f)
             )
@@ -135,6 +138,6 @@
         ownerView.autofill(autofillValues)
 
         // Assert.
-        assertThat(autofilledValue).isEqualTo(expectedValue)
+        assertThat(autoFilledValue).isEqualTo(expectedValue)
     }
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index fa7cd95..5d4cdac 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentType
 import androidx.compose.ui.semantics.editableText
 import androidx.compose.ui.semantics.focused
@@ -196,6 +197,34 @@
 
     @Test
     @SmallTest
+    fun autofillManager_doNotCallCommit_nonAutofillRelatedNodesAddedAndDisappear() {
+        val am: PlatformAutofillManager = mock()
+        var isVisible by mutableStateOf(true)
+        var semanticsExist by mutableStateOf(false)
+
+        rule.setContent {
+            (LocalAutofillManager.current as AndroidAutofillManager).platformAutofillManager = am
+            if (isVisible) {
+                Box(
+                    modifier =
+                        Modifier.then(
+                            if (semanticsExist)
+                                Modifier.semantics { contentDescription = "contentDescription" }
+                            else Modifier.size(height, width)
+                        )
+                )
+            }
+        }
+
+        rule.runOnIdle { semanticsExist = true }
+        rule.runOnIdle { isVisible = false }
+
+        // Adding in semantics not related to autofill should not trigger commit
+        rule.runOnIdle { verify(am, never()).commit() }
+    }
+
+    @Test
+    @SmallTest
     fun autofillManager_callCommit_nodesBecomeAutofillRelatedAndDisappear() {
         val am: PlatformAutofillManager = mock()
         var isVisible by mutableStateOf(true)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
index aa3ffa8..a80a5ea 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/FakeViewStructure.kt
@@ -59,7 +59,7 @@
     @JvmField var hint: CharSequence? = null,
     @JvmField var htmlInfo: HtmlInfo? = null,
     @JvmField var inputType: Int = 0,
-    @JvmField var isEnabled: Boolean = false,
+    @JvmField var isEnabled: Boolean = true,
     @JvmField var isAccessibilityFocused: Boolean = false,
     @JvmField var isChecked: Boolean = false,
     @JvmField var isClickable: Boolean = false,
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt
new file mode 100644
index 0000000..a955f9d
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/MixedAutofillTest.kt
@@ -0,0 +1,403 @@
+/*
+ * Copyright 2024 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.autofill
+
+import android.graphics.Rect as AndroidRect
+import android.os.Build
+import android.util.SparseArray
+import android.view.View
+import android.view.View.AUTOFILL_TYPE_TEXT
+import android.view.ViewStructure
+import android.view.autofill.AutofillValue
+import androidx.annotation.RequiresApi
+import androidx.autofill.HintConstants
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalAutofillTree
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 26)
+@RequiresApi(Build.VERSION_CODES.O)
+class MixedAutofillTest {
+    @get:Rule val rule = createComposeRule()
+    private val height = 200.dp
+    private val width = 200.dp
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+    @Before
+    fun enableAutofill() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isSemanticAutofillEnabled = true
+    }
+
+    @After
+    fun disableAutofill() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+    }
+
+    @Test
+    fun populateViewStructure_empty() {
+        // Arrange.
+        lateinit var view: View
+        val viewStructure: ViewStructure = FakeViewStructure()
+        rule.setContent { view = LocalView.current }
+
+        // Act.
+        rule.runOnIdle {
+            // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+            view.onProvideAutofillVirtualStructure(viewStructure, 0)
+        }
+
+        // Assert.
+        assertThat(viewStructure.childCount).isEqualTo(0)
+    }
+
+    @Test
+    fun autofill_empty() {
+        // Arrange.
+        lateinit var view: View
+        val viewStructure: ViewStructure = FakeViewStructure()
+        rule.setContent { view = LocalView.current }
+
+        // Act.
+        rule.runOnIdle {
+            // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+            view.autofill(
+                SparseArray<AutofillValue>(2).apply {
+                    append(1, AutofillValue.forText("any"))
+                    append(2, AutofillValue.forText("any"))
+                }
+            )
+        }
+
+        // Assert.
+        assertThat(viewStructure.childCount).isEqualTo(0)
+    }
+
+    @Test
+    fun populateViewStructure_new_old_sameLayoutNode() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var autofillTree: AutofillTree
+        val viewStructure: ViewStructure = FakeViewStructure()
+        lateinit var autofillNode: AutofillNode
+        rule.setContent {
+            view = LocalView.current
+            autofillTree = LocalAutofillTree.current
+            autofillNode = remember {
+                AutofillNode(
+                    onFill = {},
+                    autofillTypes = listOf(AutofillType.Password),
+                )
+            }
+            Box(
+                Modifier.semantics {
+                        testTag = "newApi"
+                        contentType = ContentType.Username
+                    }
+                    .size(height, width)
+                    .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+            ) {
+                DisposableEffect(autofillNode) {
+                    autofillTree.children[autofillNode.id] = autofillNode
+                    onDispose { autofillTree.children.remove(autofillNode.id) }
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+            view.onProvideAutofillVirtualStructure(viewStructure, 0)
+        }
+
+        // Assert.
+        assertThat(viewStructure)
+            .isEqualTo(
+                FakeViewStructure().apply {
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+                    packageName = view.context.applicationInfo.packageName
+                    bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+                    autofillId = view.autofillId
+                    isEnabled = true
+                    children.add(
+                        FakeViewStructure().apply {
+                            virtualId = rule.onNodeWithTag("newApi").semanticsId()
+                            packageName = view.context.applicationInfo.packageName
+                            bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+                            autofillId = view.autofillId
+                            isEnabled = true
+                            autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            visibility = View.VISIBLE
+                            isLongClickable = false
+                            isFocusable = false
+                            isFocused = false
+                            isEnabled = true
+                        }
+                    )
+                    children.add(
+                        FakeViewStructure().apply {
+                            virtualId = autofillNode.id
+                            packageName = view.context.applicationInfo.packageName
+                            bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+                            autofillId = view.autofillId
+                            autofillType = AUTOFILL_TYPE_TEXT
+                            autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+                            visibility = View.VISIBLE
+                            isLongClickable = false
+                            isFocusable = false
+                            isFocused = false
+                        }
+                    )
+                }
+            )
+    }
+
+    @Test
+    fun populateViewStructure_new_old_differentLayoutNodes() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var autofillTree: AutofillTree
+        val viewStructure: ViewStructure = FakeViewStructure()
+        lateinit var autofillNode: AutofillNode
+        rule.setContent {
+            view = LocalView.current
+            autofillTree = LocalAutofillTree.current
+            autofillNode = remember {
+                AutofillNode(
+                    onFill = {},
+                    autofillTypes = listOf(AutofillType.Password),
+                )
+            }
+            Column {
+                Box(
+                    Modifier.semantics { contentType = ContentType.Username }
+                        .size(height, width)
+                        .testTag("newApi")
+                )
+                Box(
+                    Modifier.size(height, width).onGloballyPositioned {
+                        autofillNode.boundingBox = it.boundsInWindow()
+                    }
+                ) {
+                    DisposableEffect(autofillNode) {
+                        autofillTree.children[autofillNode.id] = autofillNode
+                        onDispose { autofillTree.children.remove(autofillNode.id) }
+                    }
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+            view.onProvideAutofillVirtualStructure(viewStructure, 0)
+        }
+
+        // Assert.
+        assertThat(viewStructure)
+            .isEqualTo(
+                FakeViewStructure().apply {
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+                    packageName = view.context.applicationInfo.packageName
+                    bounds = AndroidRect(0, 0, width.dpToPx(), 2 * height.dpToPx())
+                    autofillId = view.autofillId
+                    isEnabled = true
+                    children.add(
+                        FakeViewStructure().apply {
+                            virtualId = rule.onNodeWithTag("newApi").semanticsId()
+                            packageName = view.context.applicationInfo.packageName
+                            bounds = AndroidRect(0, 0, width.dpToPx(), height.dpToPx())
+                            autofillId = view.autofillId
+                            isEnabled = true
+                            autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            visibility = View.VISIBLE
+                            isLongClickable = false
+                            isFocusable = false
+                            isFocused = false
+                            isEnabled = true
+                        }
+                    )
+                    children.add(
+                        FakeViewStructure().apply {
+                            virtualId = autofillNode.id
+                            packageName = view.context.applicationInfo.packageName
+                            bounds =
+                                AndroidRect(0, height.dpToPx(), width.dpToPx(), 2 * height.dpToPx())
+                            autofillId = view.autofillId
+                            autofillType = AUTOFILL_TYPE_TEXT
+                            autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
+                            visibility = View.VISIBLE
+                            isLongClickable = false
+                            isFocusable = false
+                            isFocused = false
+                        }
+                    )
+                }
+            )
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun autofill_new_old_sameLayoutNode() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var autofillTree: AutofillTree
+        lateinit var autofillNode: AutofillNode
+        lateinit var autoFilledValueNewApi: String
+        lateinit var autoFilledValueOldApi: String
+        rule.setContent {
+            view = LocalView.current
+            autofillTree = LocalAutofillTree.current
+            autofillNode = remember {
+                AutofillNode(
+                    onFill = { autoFilledValueOldApi = it },
+                    autofillTypes = listOf(AutofillType.Password),
+                )
+            }
+            Box(
+                Modifier.semantics {
+                        testTag = "newApi"
+                        contentType = ContentType.Username
+                        onAutofillText {
+                            autoFilledValueNewApi = it.toString()
+                            true
+                        }
+                    }
+                    .onGloballyPositioned { autofillNode.boundingBox = it.boundsInWindow() }
+                    .size(height, width)
+            ) {
+                DisposableEffect(autofillNode) {
+                    autofillTree.children[autofillNode.id] = autofillNode
+                    onDispose { autofillTree.children.remove(autofillNode.id) }
+                }
+            }
+        }
+
+        // Act.
+        val newApiSemanticsId = rule.onNodeWithTag("newApi").semanticsId()
+        rule.runOnIdle {
+            view.autofill(
+                SparseArray<AutofillValue>(2).apply {
+                    append(newApiSemanticsId, AutofillValue.forText("TestUsername"))
+                    append(autofillNode.id, AutofillValue.forText("TestPassword"))
+                }
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(autoFilledValueNewApi).isEqualTo("TestUsername")
+            assertThat(autoFilledValueOldApi).isEqualTo("TestPassword")
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun autofill_new_old_differentLayoutNodes() {
+        // Arrange.
+        lateinit var view: View
+        lateinit var autofillTree: AutofillTree
+        lateinit var autofillNode: AutofillNode
+        lateinit var autoFilledValueNewApi: String
+        lateinit var autoFilledValueOldApi: String
+        rule.setContent {
+            view = LocalView.current
+            autofillTree = LocalAutofillTree.current
+            autofillNode = remember {
+                AutofillNode(
+                    onFill = { autoFilledValueOldApi = it },
+                    autofillTypes = listOf(AutofillType.Password),
+                )
+            }
+            Column {
+                Box(
+                    Modifier.semantics {
+                            contentType = ContentType.Username
+                            onAutofillText {
+                                autoFilledValueNewApi = it.toString()
+                                true
+                            }
+                        }
+                        .size(height, width)
+                        .testTag("newApi")
+                )
+                Box(
+                    Modifier.size(height, width).onGloballyPositioned {
+                        autofillNode.boundingBox = it.boundsInWindow()
+                    }
+                ) {
+                    DisposableEffect(autofillNode) {
+                        autofillTree.children[autofillNode.id] = autofillNode
+                        onDispose { autofillTree.children.remove(autofillNode.id) }
+                    }
+                }
+            }
+        }
+
+        // Act.
+        val newApiSemanticsId = rule.onNodeWithTag("newApi").semanticsId()
+        rule.runOnIdle {
+            view.autofill(
+                SparseArray<AutofillValue>(2).apply {
+                    append(newApiSemanticsId, AutofillValue.forText("TestUsername"))
+                    append(autofillNode.id, AutofillValue.forText("TestPassword"))
+                }
+            )
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(autoFilledValueNewApi).isEqualTo("TestUsername")
+            assertThat(autoFilledValueOldApi).isEqualTo("TestPassword")
+        }
+    }
+
+    private fun Dp.dpToPx() = with(rule.density) { [email protected]() }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
index c878248..4d142e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/PerformAndroidAutofillManagerTest.kt
@@ -47,6 +47,7 @@
 import androidx.compose.ui.semantics.contentDataType
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentType
+import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.hideFromAccessibility
 import androidx.compose.ui.semantics.maxTextLength
 import androidx.compose.ui.semantics.onLongClick
@@ -171,26 +172,25 @@
         assertThat(viewStructure)
             .isEqualTo(
                 FakeViewStructure().apply {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
-                    packageName = view.context.applicationInfo.packageName
-                    bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
                     autofillId = view.autofillId
-                    isEnabled = true
+                    bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
                     children.add(
                         FakeViewStructure().apply {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            packageName = view.context.applicationInfo.packageName
-                            bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
-                            autofillId = view.autofillId
-                            isEnabled = true
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
-                            visibility = View.VISIBLE
-                            isLongClickable = false
+                            autofillId = view.autofillId
+                            bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
+                            isEnabled = true
                             isFocusable = false
                             isFocused = false
-                            isEnabled = true
+                            isLongClickable = false
+                            packageName = view.context.applicationInfo.packageName
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+                            visibility = View.VISIBLE
                         }
                     )
+                    isEnabled = true
+                    packageName = view.context.applicationInfo.packageName
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -221,13 +221,13 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -258,13 +258,13 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillType = AUTOFILL_TYPE_TEXT
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -296,15 +296,15 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             isClickable = true
                             isFocusable = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -338,14 +338,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             contentDescription = contentTag
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -384,16 +384,16 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             isClickable = true
                             isFocusable = true
                             isSelected = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -432,19 +432,19 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             className = "android.widget.RadioButton"
-                            isClickable = true
-                            isFocusable = true
                             isCheckable = true
                             isChecked = true
+                            isClickable = true
+                            isFocusable = true
                             isSelected = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -483,19 +483,19 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             className = "android.widget.Spinner"
-                            isClickable = true
-                            isFocusable = true
                             isCheckable = true
                             isChecked = true
+                            isClickable = true
+                            isFocusable = true
                             isSelected = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -534,19 +534,19 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             className = "android.widget.NumberPicker"
-                            isClickable = true
-                            isFocusable = true
                             isCheckable = true
                             isChecked = true
+                            isClickable = true
+                            isFocusable = true
                             isSelected = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -581,14 +581,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -621,14 +621,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.INVISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -660,14 +660,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -703,8 +703,8 @@
                     virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.INVISIBLE
                         }
                     )
@@ -741,14 +741,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             isLongClickable = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -782,14 +782,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             isFocusable = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -822,15 +822,15 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             isFocusable = true
                             isFocused = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -845,9 +845,47 @@
         rule.setContent {
             view = LocalView.current
             Box(
+                Modifier.semantics { contentType = ContentType.Username }
+                    .size(width, height)
+                    .testTag(contentTag)
+            )
+        }
+
+        // Act.
+        rule.runOnIdle {
+            // Compose does not use the Autofill flags parameter, passing in 0 as a placeholder flag
+            view.onProvideAutofillVirtualStructure(viewStructure, 0)
+        }
+
+        // Assert.
+        assertThat(viewStructure)
+            .isEqualTo(
+                ViewStructure(view) {
+                    children.add(
+                        ViewStructure(view) {
+                            autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
+                            isEnabled = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
+                        }
+                    )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+                }
+            )
+    }
+
+    @Test
+    @SmallTest
+    @SdkSuppress(minSdkVersion = 26)
+    fun populateViewStructure_disabled() {
+        // Arrange.
+        lateinit var view: View
+        val viewStructure: ViewStructure = FakeViewStructure()
+        rule.setContent {
+            view = LocalView.current
+            Box(
                 Modifier.semantics {
                         contentType = ContentType.Username
-                        isEnabled()
+                        disabled()
                     }
                     .size(width, height)
                     .testTag(contentTag)
@@ -864,14 +902,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
-                            isEnabled = true
+                            isEnabled = false
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -906,16 +944,16 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            autofillType = AUTOFILL_TYPE_TEXT
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
-                            maxTextLength = 5
+                            autofillType = AUTOFILL_TYPE_TEXT
                             className = "android.widget.EditText"
+                            maxTextLength = 5
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -949,14 +987,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             maxTextLength = -1
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -990,15 +1028,15 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             autofillType = View.AUTOFILL_TYPE_TOGGLE
                             isCheckable = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1032,16 +1070,16 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             autofillType = View.AUTOFILL_TYPE_TOGGLE
                             isCheckable = true
                             isChecked = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1073,18 +1111,18 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             autofillType = View.AUTOFILL_TYPE_TOGGLE
                             isCheckable = true
                             isChecked = true
-                            isFocusable = true
                             isClickable = true
+                            isFocusable = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1119,11 +1157,8 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            text = ""
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             autofillType = AUTOFILL_TYPE_TEXT
                             autofillValue = AutofillValue.forText("")
@@ -1131,9 +1166,12 @@
                             isClickable = true
                             isFocusable = true
                             isLongClickable = true
+                            text = ""
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1168,11 +1206,8 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            text = ""
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             autofillType = AUTOFILL_TYPE_TEXT
                             autofillValue = AutofillValue.forText("testUsername")
@@ -1180,9 +1215,12 @@
                             isClickable = true
                             isFocusable = true
                             isLongClickable = true
+                            text = ""
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1219,24 +1257,24 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            text = ""
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
                             autofillType = AUTOFILL_TYPE_TEXT
                             autofillValue = AutofillValue.forText("")
                             className = "android.widget.EditText"
-                            isClickable = true
                             dataIsSensitive = true
                             inputType =
                                 InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+                            isClickable = true
                             isFocusable = true
                             isLongClickable = true
+                            text = ""
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1271,24 +1309,24 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
-                            text = ""
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_PASSWORD)
                             autofillType = AUTOFILL_TYPE_TEXT
                             autofillValue = AutofillValue.forText("testPassword")
                             className = "android.widget.EditText"
-                            isClickable = true
                             dataIsSensitive = true
                             inputType =
                                 InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
+                            isClickable = true
                             isFocusable = true
                             isLongClickable = true
+                            text = ""
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             visibility = View.VISIBLE
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1324,14 +1362,14 @@
         assertThat(viewStructure)
             .isEqualTo(
                 ViewStructure(view) {
-                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                     children.add(
                         ViewStructure(view) {
-                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                             autofillHints = mutableListOf(HintConstants.AUTOFILL_HINT_USERNAME)
                             dataIsSensitive = true
+                            virtualId = rule.onNodeWithTag(contentTag).semanticsId()
                         }
                     )
+                    virtualId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
                 }
             )
     }
@@ -1437,10 +1475,10 @@
         block: FakeViewStructure.() -> Unit
     ): FakeViewStructure {
         return FakeViewStructure().apply {
-            packageName = view.context.applicationInfo.packageName
-            bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
             autofillId = view.autofillId
+            bounds = Rect(0, 0, width.dpToPx(), height.dpToPx())
             isEnabled = true
+            packageName = view.context.applicationInfo.packageName
             block()
         }
     }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
index 1a3e98e..680dcbd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt
@@ -83,6 +83,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {
+    if (autofillTree.children.isEmpty()) return
 
     // Add child nodes. The function returns the index to the first item.
     var index = AutofillApi26Helper.addChildCount(root, autofillTree.children.count())
@@ -123,6 +124,8 @@
 /** Triggers onFill() in response to a request from the autofill framework. */
 @RequiresApi(Build.VERSION_CODES.O)
 internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
+    if (autofillTree.children.isEmpty()) return
+
     for (index in 0 until values.size()) {
         val itemId = values.keyAt(index)
         val value = values[itemId]
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 421f7f2..3a2f75d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -130,10 +130,10 @@
         }
 
         // Update currentlyDisplayedIDs if relevance to Autofill has changed.
-        val prevRelatedToAutofill = prevConfig?.isRelatedToAutofill()
-        val currRelatedToAutofill = config?.isRelatedToAutofill()
+        val prevRelatedToAutofill = prevConfig?.isRelatedToAutofill() ?: false
+        val currRelatedToAutofill = config?.isRelatedToAutofill() ?: false
         if (prevRelatedToAutofill != currRelatedToAutofill) {
-            if (currRelatedToAutofill == true) {
+            if (currRelatedToAutofill) {
                 currentlyDisplayedIDs.add(semanticsId)
             } else {
                 currentlyDisplayedIDs.remove(semanticsId)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
index f2f7b6f..cb92103 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.android.kt
@@ -52,42 +52,6 @@
 import androidx.autofill.HintConstants.AUTOFILL_HINT_POSTAL_CODE
 import androidx.autofill.HintConstants.AUTOFILL_HINT_SMS_OTP
 import androidx.autofill.HintConstants.AUTOFILL_HINT_USERNAME
-import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
-import androidx.compose.ui.autofill.AutofillType.AddressCountry
-import androidx.compose.ui.autofill.AutofillType.AddressLocality
-import androidx.compose.ui.autofill.AutofillType.AddressRegion
-import androidx.compose.ui.autofill.AutofillType.AddressStreet
-import androidx.compose.ui.autofill.AutofillType.BirthDateDay
-import androidx.compose.ui.autofill.AutofillType.BirthDateFull
-import androidx.compose.ui.autofill.AutofillType.BirthDateMonth
-import androidx.compose.ui.autofill.AutofillType.BirthDateYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDate
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDay
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationMonth
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardNumber
-import androidx.compose.ui.autofill.AutofillType.CreditCardSecurityCode
-import androidx.compose.ui.autofill.AutofillType.EmailAddress
-import androidx.compose.ui.autofill.AutofillType.Gender
-import androidx.compose.ui.autofill.AutofillType.NewPassword
-import androidx.compose.ui.autofill.AutofillType.NewUsername
-import androidx.compose.ui.autofill.AutofillType.Password
-import androidx.compose.ui.autofill.AutofillType.PersonFirstName
-import androidx.compose.ui.autofill.AutofillType.PersonFullName
-import androidx.compose.ui.autofill.AutofillType.PersonLastName
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleInitial
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleName
-import androidx.compose.ui.autofill.AutofillType.PersonNamePrefix
-import androidx.compose.ui.autofill.AutofillType.PersonNameSuffix
-import androidx.compose.ui.autofill.AutofillType.PhoneCountryCode
-import androidx.compose.ui.autofill.AutofillType.PhoneNumber
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberDevice
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberNational
-import androidx.compose.ui.autofill.AutofillType.PostalAddress
-import androidx.compose.ui.autofill.AutofillType.PostalCode
-import androidx.compose.ui.autofill.AutofillType.PostalCodeExtended
-import androidx.compose.ui.autofill.AutofillType.SmsOtpCode
-import androidx.compose.ui.autofill.AutofillType.Username
 
 /**
  * Gets the Android specific [AutofillHint][android.view.ViewStructure.setAutofillHints]
@@ -96,47 +60,47 @@
 internal val AutofillType.androidType: String
     get() {
         val androidAutofillType = androidAutofillTypes[this]
-        requireNotNull(androidAutofillType, { "Unsupported autofill type" })
+        requireNotNull(androidAutofillType) { "Unsupported autofill type" }
         return androidAutofillType
     }
 
 /** Maps each [AutofillType] to one of the autofill hints in [androidx.autofill.HintConstants] */
 private val androidAutofillTypes: HashMap<AutofillType, String> =
     hashMapOf(
-        EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
-        Username to AUTOFILL_HINT_USERNAME,
-        Password to AUTOFILL_HINT_PASSWORD,
-        NewUsername to AUTOFILL_HINT_NEW_USERNAME,
-        NewPassword to AUTOFILL_HINT_NEW_PASSWORD,
-        PostalAddress to AUTOFILL_HINT_POSTAL_ADDRESS,
-        PostalCode to AUTOFILL_HINT_POSTAL_CODE,
-        CreditCardNumber to AUTOFILL_HINT_CREDIT_CARD_NUMBER,
-        CreditCardSecurityCode to AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
-        CreditCardExpirationDate to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
-        CreditCardExpirationMonth to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
-        CreditCardExpirationYear to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
-        CreditCardExpirationDay to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY,
-        AddressCountry to AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY,
-        AddressRegion to AUTOFILL_HINT_POSTAL_ADDRESS_REGION,
-        AddressLocality to AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY,
-        AddressStreet to AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS,
-        AddressAuxiliaryDetails to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS,
-        PostalCodeExtended to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE,
-        PersonFullName to AUTOFILL_HINT_PERSON_NAME,
-        PersonFirstName to AUTOFILL_HINT_PERSON_NAME_GIVEN,
-        PersonLastName to AUTOFILL_HINT_PERSON_NAME_FAMILY,
-        PersonMiddleName to AUTOFILL_HINT_PERSON_NAME_MIDDLE,
-        PersonMiddleInitial to AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL,
-        PersonNamePrefix to AUTOFILL_HINT_PERSON_NAME_PREFIX,
-        PersonNameSuffix to AUTOFILL_HINT_PERSON_NAME_SUFFIX,
-        PhoneNumber to AUTOFILL_HINT_PHONE_NUMBER,
-        PhoneNumberDevice to AUTOFILL_HINT_PHONE_NUMBER_DEVICE,
-        PhoneCountryCode to AUTOFILL_HINT_PHONE_COUNTRY_CODE,
-        PhoneNumberNational to AUTOFILL_HINT_PHONE_NATIONAL,
-        Gender to AUTOFILL_HINT_GENDER,
-        BirthDateFull to AUTOFILL_HINT_BIRTH_DATE_FULL,
-        BirthDateDay to AUTOFILL_HINT_BIRTH_DATE_DAY,
-        BirthDateMonth to AUTOFILL_HINT_BIRTH_DATE_MONTH,
-        BirthDateYear to AUTOFILL_HINT_BIRTH_DATE_YEAR,
-        SmsOtpCode to AUTOFILL_HINT_SMS_OTP
+        AutofillType.EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
+        AutofillType.Username to AUTOFILL_HINT_USERNAME,
+        AutofillType.Password to AUTOFILL_HINT_PASSWORD,
+        AutofillType.NewUsername to AUTOFILL_HINT_NEW_USERNAME,
+        AutofillType.NewPassword to AUTOFILL_HINT_NEW_PASSWORD,
+        AutofillType.PostalAddress to AUTOFILL_HINT_POSTAL_ADDRESS,
+        AutofillType.PostalCode to AUTOFILL_HINT_POSTAL_CODE,
+        AutofillType.CreditCardNumber to AUTOFILL_HINT_CREDIT_CARD_NUMBER,
+        AutofillType.CreditCardSecurityCode to AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE,
+        AutofillType.CreditCardExpirationDate to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE,
+        AutofillType.CreditCardExpirationMonth to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH,
+        AutofillType.CreditCardExpirationYear to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR,
+        AutofillType.CreditCardExpirationDay to AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY,
+        AutofillType.AddressCountry to AUTOFILL_HINT_POSTAL_ADDRESS_COUNTRY,
+        AutofillType.AddressRegion to AUTOFILL_HINT_POSTAL_ADDRESS_REGION,
+        AutofillType.AddressLocality to AUTOFILL_HINT_POSTAL_ADDRESS_LOCALITY,
+        AutofillType.AddressStreet to AUTOFILL_HINT_POSTAL_ADDRESS_STREET_ADDRESS,
+        AutofillType.AddressAuxiliaryDetails to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_ADDRESS,
+        AutofillType.PostalCodeExtended to AUTOFILL_HINT_POSTAL_ADDRESS_EXTENDED_POSTAL_CODE,
+        AutofillType.PersonFullName to AUTOFILL_HINT_PERSON_NAME,
+        AutofillType.PersonFirstName to AUTOFILL_HINT_PERSON_NAME_GIVEN,
+        AutofillType.PersonLastName to AUTOFILL_HINT_PERSON_NAME_FAMILY,
+        AutofillType.PersonMiddleName to AUTOFILL_HINT_PERSON_NAME_MIDDLE,
+        AutofillType.PersonMiddleInitial to AUTOFILL_HINT_PERSON_NAME_MIDDLE_INITIAL,
+        AutofillType.PersonNamePrefix to AUTOFILL_HINT_PERSON_NAME_PREFIX,
+        AutofillType.PersonNameSuffix to AUTOFILL_HINT_PERSON_NAME_SUFFIX,
+        AutofillType.PhoneNumber to AUTOFILL_HINT_PHONE_NUMBER,
+        AutofillType.PhoneNumberDevice to AUTOFILL_HINT_PHONE_NUMBER_DEVICE,
+        AutofillType.PhoneCountryCode to AUTOFILL_HINT_PHONE_COUNTRY_CODE,
+        AutofillType.PhoneNumberNational to AUTOFILL_HINT_PHONE_NATIONAL,
+        AutofillType.Gender to AUTOFILL_HINT_GENDER,
+        AutofillType.BirthDateFull to AUTOFILL_HINT_BIRTH_DATE_FULL,
+        AutofillType.BirthDateDay to AUTOFILL_HINT_BIRTH_DATE_DAY,
+        AutofillType.BirthDateMonth to AUTOFILL_HINT_BIRTH_DATE_MONTH,
+        AutofillType.BirthDateYear to AUTOFILL_HINT_BIRTH_DATE_YEAR,
+        AutofillType.SmsOtpCode to AUTOFILL_HINT_SMS_OTP
     )
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
index db2789e..72cfae7 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/PopulateViewStructure.android.kt
@@ -62,7 +62,6 @@
     var toggleableStateProp: ToggleableState? = null
 
     // Semantics properties form merged configuration.
-    var disabledMergedProp: Boolean? = null
     var textMergedProp: List<AnnotatedString>? = null
 
     // Semantics actions.
@@ -94,7 +93,7 @@
     semanticsInfo.mergedSemanticsConfiguration()?.props?.forEach { property, value ->
         @Suppress("UNCHECKED_CAST")
         when (property) {
-            properties.Disabled -> disabledMergedProp = value as Boolean
+            properties.Disabled -> autofillApi.setEnabled(this, false)
             properties.Text -> textMergedProp = value as List<AnnotatedString>
         }
     }
@@ -124,9 +123,6 @@
         autofillApi.setDimens(this, left, top, 0, 0, right - left, bottom - top)
     }
 
-    // Enabled.
-    autofillApi.setEnabled(this, disabledMergedProp != true)
-
     // Selected.
     selectedProp?.let { autofillApi.setSelected(this, it) }
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index dbb41be..604175c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -108,7 +108,6 @@
 import androidx.compose.ui.focus.focusRect
 import androidx.compose.ui.focus.is1dFocusSearch
 import androidx.compose.ui.focus.isBetterCandidate
-import androidx.compose.ui.focus.requestFocus
 import androidx.compose.ui.focus.requestInteropFocus
 import androidx.compose.ui.focus.toAndroidFocusDirection
 import androidx.compose.ui.focus.toFocusDirection
@@ -1905,10 +1904,8 @@
         if (autofillSupported() && structure != null) {
             if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
                 _autofillManager?.populateViewStructure(structure)
-            } else {
-                // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
-                _autofill?.populateViewStructure(structure)
             }
+            _autofill?.populateViewStructure(structure)
         }
     }
 
@@ -1916,10 +1913,8 @@
         if (autofillSupported()) {
             if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
                 _autofillManager?.performAutofill(values)
-            } else {
-                // TODO(b/383201236): Remove _autofill and route requests through _autofillManager.
-                _autofill?.performAutofill(values)
             }
+            _autofill?.performAutofill(values)
         }
     }
 
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
index 0475d44..b5c8886 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
@@ -16,42 +16,6 @@
 
 package androidx.compose.ui.autofill
 
-import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
-import androidx.compose.ui.autofill.AutofillType.AddressCountry
-import androidx.compose.ui.autofill.AutofillType.AddressLocality
-import androidx.compose.ui.autofill.AutofillType.AddressRegion
-import androidx.compose.ui.autofill.AutofillType.AddressStreet
-import androidx.compose.ui.autofill.AutofillType.BirthDateDay
-import androidx.compose.ui.autofill.AutofillType.BirthDateFull
-import androidx.compose.ui.autofill.AutofillType.BirthDateMonth
-import androidx.compose.ui.autofill.AutofillType.BirthDateYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDate
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationDay
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationMonth
-import androidx.compose.ui.autofill.AutofillType.CreditCardExpirationYear
-import androidx.compose.ui.autofill.AutofillType.CreditCardNumber
-import androidx.compose.ui.autofill.AutofillType.CreditCardSecurityCode
-import androidx.compose.ui.autofill.AutofillType.EmailAddress
-import androidx.compose.ui.autofill.AutofillType.Gender
-import androidx.compose.ui.autofill.AutofillType.NewPassword
-import androidx.compose.ui.autofill.AutofillType.NewUsername
-import androidx.compose.ui.autofill.AutofillType.Password
-import androidx.compose.ui.autofill.AutofillType.PersonFirstName
-import androidx.compose.ui.autofill.AutofillType.PersonFullName
-import androidx.compose.ui.autofill.AutofillType.PersonLastName
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleInitial
-import androidx.compose.ui.autofill.AutofillType.PersonMiddleName
-import androidx.compose.ui.autofill.AutofillType.PersonNamePrefix
-import androidx.compose.ui.autofill.AutofillType.PersonNameSuffix
-import androidx.compose.ui.autofill.AutofillType.PhoneCountryCode
-import androidx.compose.ui.autofill.AutofillType.PhoneNumber
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberDevice
-import androidx.compose.ui.autofill.AutofillType.PhoneNumberNational
-import androidx.compose.ui.autofill.AutofillType.PostalAddress
-import androidx.compose.ui.autofill.AutofillType.PostalCode
-import androidx.compose.ui.autofill.AutofillType.PostalCodeExtended
-import androidx.compose.ui.autofill.AutofillType.SmsOtpCode
-import androidx.compose.ui.autofill.AutofillType.Username
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -62,181 +26,186 @@
 
     @Test
     fun emailAddress() {
-        assertThat(EmailAddress.androidType).isEqualTo("emailAddress")
+        assertThat(AutofillType.EmailAddress.androidType).isEqualTo("emailAddress")
     }
 
     @Test
     fun username() {
-        assertThat(Username.androidType).isEqualTo("username")
+        assertThat(AutofillType.Username.androidType).isEqualTo("username")
     }
 
     @Test
     fun password() {
-        assertThat(Password.androidType).isEqualTo("password")
+        assertThat(AutofillType.Password.androidType).isEqualTo("password")
     }
 
     @Test
     fun newUsername() {
-        assertThat(NewUsername.androidType).isEqualTo("newUsername")
+        assertThat(AutofillType.NewUsername.androidType).isEqualTo("newUsername")
     }
 
     @Test
     fun newPassword() {
-        assertThat(NewPassword.androidType).isEqualTo("newPassword")
+        assertThat(AutofillType.NewPassword.androidType).isEqualTo("newPassword")
     }
 
     @Test
     fun postalAddress() {
-        assertThat(PostalAddress.androidType).isEqualTo("postalAddress")
+        assertThat(AutofillType.PostalAddress.androidType).isEqualTo("postalAddress")
     }
 
     @Test
     fun postalCode() {
-        assertThat(PostalCode.androidType).isEqualTo("postalCode")
+        assertThat(AutofillType.PostalCode.androidType).isEqualTo("postalCode")
     }
 
     @Test
     fun creditCardNumber() {
-        assertThat(CreditCardNumber.androidType).isEqualTo("creditCardNumber")
+        assertThat(AutofillType.CreditCardNumber.androidType).isEqualTo("creditCardNumber")
     }
 
     @Test
     fun creditCardSecurityCode() {
-        assertThat(CreditCardSecurityCode.androidType).isEqualTo("creditCardSecurityCode")
+        assertThat(AutofillType.CreditCardSecurityCode.androidType)
+            .isEqualTo("creditCardSecurityCode")
     }
 
     @Test
     fun creditCardExpirationDate() {
-        assertThat(CreditCardExpirationDate.androidType).isEqualTo("creditCardExpirationDate")
+        assertThat(AutofillType.CreditCardExpirationDate.androidType)
+            .isEqualTo("creditCardExpirationDate")
     }
 
     @Test
     fun creditCardExpirationMonth() {
-        assertThat(CreditCardExpirationMonth.androidType).isEqualTo("creditCardExpirationMonth")
+        assertThat(AutofillType.CreditCardExpirationMonth.androidType)
+            .isEqualTo("creditCardExpirationMonth")
     }
 
     @Test
     fun creditCardExpirationYear() {
-        assertThat(CreditCardExpirationYear.androidType).isEqualTo("creditCardExpirationYear")
+        assertThat(AutofillType.CreditCardExpirationYear.androidType)
+            .isEqualTo("creditCardExpirationYear")
     }
 
     @Test
     fun creditCardExpirationDay() {
-        assertThat(CreditCardExpirationDay.androidType).isEqualTo("creditCardExpirationDay")
+        assertThat(AutofillType.CreditCardExpirationDay.androidType)
+            .isEqualTo("creditCardExpirationDay")
     }
 
     @Test
     fun addressCountry() {
-        assertThat(AddressCountry.androidType).isEqualTo("addressCountry")
+        assertThat(AutofillType.AddressCountry.androidType).isEqualTo("addressCountry")
     }
 
     @Test
     fun addressRegion() {
-        assertThat(AddressRegion.androidType).isEqualTo("addressRegion")
+        assertThat(AutofillType.AddressRegion.androidType).isEqualTo("addressRegion")
     }
 
     @Test
     fun addressLocality() {
-        assertThat(AddressLocality.androidType).isEqualTo("addressLocality")
+        assertThat(AutofillType.AddressLocality.androidType).isEqualTo("addressLocality")
     }
 
     @Test
     fun addressStreet() {
-        assertThat(AddressStreet.androidType).isEqualTo("streetAddress")
+        assertThat(AutofillType.AddressStreet.androidType).isEqualTo("streetAddress")
     }
 
     @Test
     fun addressAuxiliaryDetails() {
-        assertThat(AddressAuxiliaryDetails.androidType).isEqualTo("extendedAddress")
+        assertThat(AutofillType.AddressAuxiliaryDetails.androidType).isEqualTo("extendedAddress")
     }
 
     @Test
     fun postalCodeExtended() {
-        assertThat(PostalCodeExtended.androidType).isEqualTo("extendedPostalCode")
+        assertThat(AutofillType.PostalCodeExtended.androidType).isEqualTo("extendedPostalCode")
     }
 
     @Test
     fun personFullName() {
-        assertThat(PersonFullName.androidType).isEqualTo("personName")
+        assertThat(AutofillType.PersonFullName.androidType).isEqualTo("personName")
     }
 
     @Test
     fun personFirstName() {
-        assertThat(PersonFirstName.androidType).isEqualTo("personGivenName")
+        assertThat(AutofillType.PersonFirstName.androidType).isEqualTo("personGivenName")
     }
 
     @Test
     fun personLastName() {
-        assertThat(PersonLastName.androidType).isEqualTo("personFamilyName")
+        assertThat(AutofillType.PersonLastName.androidType).isEqualTo("personFamilyName")
     }
 
     @Test
     fun personMiddleName() {
-        assertThat(PersonMiddleName.androidType).isEqualTo("personMiddleName")
+        assertThat(AutofillType.PersonMiddleName.androidType).isEqualTo("personMiddleName")
     }
 
     @Test
     fun personMiddleInitial() {
-        assertThat(PersonMiddleInitial.androidType).isEqualTo("personMiddleInitial")
+        assertThat(AutofillType.PersonMiddleInitial.androidType).isEqualTo("personMiddleInitial")
     }
 
     @Test
     fun personNamePrefix() {
-        assertThat(PersonNamePrefix.androidType).isEqualTo("personNamePrefix")
+        assertThat(AutofillType.PersonNamePrefix.androidType).isEqualTo("personNamePrefix")
     }
 
     @Test
     fun personNameSuffix() {
-        assertThat(PersonNameSuffix.androidType).isEqualTo("personNameSuffix")
+        assertThat(AutofillType.PersonNameSuffix.androidType).isEqualTo("personNameSuffix")
     }
 
     @Test
     fun phoneNumber() {
-        assertThat(PhoneNumber.androidType).isEqualTo("phoneNumber")
+        assertThat(AutofillType.PhoneNumber.androidType).isEqualTo("phoneNumber")
     }
 
     @Test
     fun phoneNumberDevice() {
-        assertThat(PhoneNumberDevice.androidType).isEqualTo("phoneNumberDevice")
+        assertThat(AutofillType.PhoneNumberDevice.androidType).isEqualTo("phoneNumberDevice")
     }
 
     @Test
     fun phoneCountryCode() {
-        assertThat(PhoneCountryCode.androidType).isEqualTo("phoneCountryCode")
+        assertThat(AutofillType.PhoneCountryCode.androidType).isEqualTo("phoneCountryCode")
     }
 
     @Test
     fun phoneNumberNational() {
-        assertThat(PhoneNumberNational.androidType).isEqualTo("phoneNational")
+        assertThat(AutofillType.PhoneNumberNational.androidType).isEqualTo("phoneNational")
     }
 
     @Test
     fun gender() {
-        assertThat(Gender.androidType).isEqualTo("gender")
+        assertThat(AutofillType.Gender.androidType).isEqualTo("gender")
     }
 
     @Test
     fun birthDateFull() {
-        assertThat(BirthDateFull.androidType).isEqualTo("birthDateFull")
+        assertThat(AutofillType.BirthDateFull.androidType).isEqualTo("birthDateFull")
     }
 
     @Test
     fun birthDateDay() {
-        assertThat(BirthDateDay.androidType).isEqualTo("birthDateDay")
+        assertThat(AutofillType.BirthDateDay.androidType).isEqualTo("birthDateDay")
     }
 
     @Test
     fun birthDateMonth() {
-        assertThat(BirthDateMonth.androidType).isEqualTo("birthDateMonth")
+        assertThat(AutofillType.BirthDateMonth.androidType).isEqualTo("birthDateMonth")
     }
 
     @Test
     fun birthDateYear() {
-        assertThat(BirthDateYear.androidType).isEqualTo("birthDateYear")
+        assertThat(AutofillType.BirthDateYear.androidType).isEqualTo("birthDateYear")
     }
 
     @Test
     fun smsOTPCode() {
-        assertThat(SmsOtpCode.androidType).isEqualTo("smsOTPCode")
+        assertThat(AutofillType.SmsOtpCode.androidType).isEqualTo("smsOTPCode")
     }
 }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
index 8edb3d1..126bfc9 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
@@ -21,7 +21,7 @@
 import android.view.View
 import android.view.autofill.AutofillValue
 import androidx.compose.ui.geometry.Rect
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -48,10 +48,10 @@
     fun performAutofill_name() {
         // Arrange.
         val expectedValue = "Name"
-        var autofilledValue = ""
+        var autoFilledValue = ""
         val autofillNode =
             AutofillNode(
-                onFill = { autofilledValue = it },
+                onFill = { autoFilledValue = it },
                 autofillTypes = listOf(AutofillType.PersonFullName),
                 boundingBox = Rect(0f, 0f, 0f, 0f)
             )
@@ -66,17 +66,17 @@
         androidAutofill.performAutofill(autofillValues)
 
         // Assert.
-        Truth.assertThat(autofilledValue).isEqualTo(expectedValue)
+        assertThat(autoFilledValue).isEqualTo(expectedValue)
     }
 
     @Test
     fun performAutofill_email() {
         // Arrange.
         val expectedValue = "[email protected]"
-        var autofilledValue = ""
+        var autoFilledValue = ""
         val autofillNode =
             AutofillNode(
-                onFill = { autofilledValue = it },
+                onFill = { autoFilledValue = it },
                 autofillTypes = listOf(AutofillType.EmailAddress),
                 boundingBox = Rect(0f, 0f, 0f, 0f)
             )
@@ -91,6 +91,6 @@
         androidAutofill.performAutofill(autofillValues)
 
         // Assert.
-        Truth.assertThat(autofilledValue).isEqualTo(expectedValue)
+        assertThat(autoFilledValue).isEqualTo(expectedValue)
     }
 }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
index 9a71fe1..debfc97 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
@@ -120,11 +120,13 @@
     val viewEnteredStats = mutableListOf<NotifyViewEntered>()
     val viewExitedStats = mutableListOf<NotifyViewExited>()
 
+    @Suppress("Unused")
     @Implementation
     fun notifyViewEntered(view: View, virtualId: Int, rect: android.graphics.Rect) {
         viewEnteredStats += NotifyViewEntered(view, virtualId, rect.toComposeRect())
     }
 
+    @Suppress("Unused")
     @Implementation
     fun notifyViewExited(view: View, virtualId: Int) {
         viewExitedStats += NotifyViewExited(view, virtualId)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
index 9c45245..bc0ac620 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
@@ -16,15 +16,18 @@
 
 package androidx.compose.ui.autofill
 
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.platform.makeSynchronizedObject
 import androidx.compose.ui.platform.synchronized
+import androidx.compose.ui.semantics.generateSemanticsId
 
 /**
  * Autofill API.
  *
  * This interface is available to all composables via a CompositionLocal. The composable can then
- * request or cancel autofill as required. For instance, the [TextField] can call
+ * request or cancel autofill as required. For instance, the TextField can call
  * [requestAutofillForNode] when it gains focus, and [cancelAutofillForNode] when it loses focus.
  */
 interface Autofill {
@@ -32,7 +35,7 @@
     /**
      * Request autofill for the specified node.
      *
-     * @param autofillNode The node that needs to be autofilled.
+     * @param autofillNode The node that needs to be auto-filled.
      *
      * This function is usually called when an autofillable component gains focus.
      */
@@ -41,7 +44,7 @@
     /**
      * Cancel a previously supplied autofill request.
      *
-     * @param autofillNode The node that needs to be autofilled.
+     * @param autofillNode The node that needs to be auto-filled.
      *
      * This function is usually called when an autofillable component loses focus.
      */
@@ -59,7 +62,7 @@
  *   list because some fields can have multiple types. For instance, userid in a login form can
  *   either be a username or an email address. TODO(b/138731416): Check with the autofill service
  *   team if the order matters, and how duplicate types are handled.
- * @property boundingBox The screen coordinates of the composable being autofilled. This data is
+ * @property boundingBox The screen coordinates of the composable being auto-filled. This data is
  *   used by the autofill framework to decide where to show the autofill popup.
  * @property onFill The callback that is called by the autofill framework to perform autofill.
  * @property id A virtual id that is automatically generated for each node.
@@ -78,7 +81,9 @@
         private fun generateId() = synchronized(lock) { ++previousId }
     }
 
-    val id: Int = generateId()
+    val id: Int =
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (ComposeUiFlags.isSemanticAutofillEnabled) generateSemanticsId() else generateId()
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
index bbfbafe..8449d95 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
@@ -23,16 +23,16 @@
  *
  * Autofill services use the [AutofillType] to determine what value to use to autofill fields
  * associated with this type. If the [AutofillType] is not specified, the autofill services have to
- * use heuristics to determine the right value to use while autofilling the corresponding field.
+ * use heuristics to determine the right value to use while auto-filling the corresponding field.
  */
 enum class AutofillType {
-    /** Indicates that the associated component can be autofilled with an email address. */
+    /** Indicates that the associated component can be auto-filled with an email address. */
     EmailAddress,
 
-    /** Indicates that the associated component can be autofilled with a username. */
+    /** Indicates that the associated component can be auto-filled with a username. */
     Username,
 
-    /** Indicates that the associated component can be autofilled with a password. */
+    /** Indicates that the associated component can be auto-filled with a password. */
     Password,
 
     /**
@@ -47,94 +47,100 @@
      */
     NewPassword,
 
-    /** Indicates that the associated component can be autofilled with a postal address. */
+    /** Indicates that the associated component can be auto-filled with a postal address. */
     PostalAddress,
 
-    /** Indicates that the associated component can be autofilled with a postal code. */
+    /** Indicates that the associated component can be auto-filled with a postal code. */
     PostalCode,
 
-    /** Indicates that the associated component can be autofilled with a credit card number. */
+    /** Indicates that the associated component can be auto-filled with a credit card number. */
     CreditCardNumber,
 
     /**
-     * Indicates that the associated component can be autofilled with a credit card security code.
+     * Indicates that the associated component can be auto-filled with a credit card security code.
      */
     CreditCardSecurityCode,
 
     /**
-     * Indicates that the associated component can be autofilled with a credit card expiration date.
+     * Indicates that the associated component can be auto-filled with a credit card expiration
+     * date.
      */
     CreditCardExpirationDate,
 
     /**
-     * Indicates that the associated component can be autofilled with a credit card expiration
+     * Indicates that the associated component can be auto-filled with a credit card expiration
      * month.
      */
     CreditCardExpirationMonth,
 
     /**
-     * Indicates that the associated component can be autofilled with a credit card expiration year.
+     * Indicates that the associated component can be auto-filled with a credit card expiration
+     * year.
      */
     CreditCardExpirationYear,
 
     /**
-     * Indicates that the associated component can be autofilled with a credit card expiration day.
+     * Indicates that the associated component can be auto-filled with a credit card expiration day.
      */
     CreditCardExpirationDay,
 
-    /** Indicates that the associated component can be autofilled with a country name/code. */
+    /** Indicates that the associated component can be auto-filled with a country name/code. */
     AddressCountry,
 
-    /** Indicates that the associated component can be autofilled with a region/state. */
+    /** Indicates that the associated component can be auto-filled with a region/state. */
     AddressRegion,
 
     /**
-     * Indicates that the associated component can be autofilled with an address locality
+     * Indicates that the associated component can be auto-filled with an address locality
      * (city/town).
      */
     AddressLocality,
 
-    /** Indicates that the associated component can be autofilled with a street address. */
+    /** Indicates that the associated component can be auto-filled with a street address. */
     AddressStreet,
 
-    /** Indicates that the associated component can be autofilled with auxiliary address details. */
+    /**
+     * Indicates that the associated component can be auto-filled with auxiliary address details.
+     */
     AddressAuxiliaryDetails,
 
     /**
-     * Indicates that the associated component can be autofilled with an extended ZIP/POSTAL code.
+     * Indicates that the associated component can be auto-filled with an extended ZIP/POSTAL code.
      *
      * Example: In forms that split the U.S. ZIP+4 Code with nine digits 99999-9999 into two fields
      * annotate the delivery route code with this hint.
      */
     PostalCodeExtended,
 
-    /** Indicates that the associated component can be autofilled with a person's full name. */
+    /** Indicates that the associated component can be auto-filled with a person's full name. */
     PersonFullName,
 
     /**
-     * Indicates that the associated component can be autofilled with a person's first/given name.
+     * Indicates that the associated component can be auto-filled with a person's first/given name.
      */
     PersonFirstName,
 
     /**
-     * Indicates that the associated component can be autofilled with a person's last/family name.
+     * Indicates that the associated component can be auto-filled with a person's last/family name.
      */
     PersonLastName,
 
-    /** Indicates that the associated component can be autofilled with a person's middle name. */
+    /** Indicates that the associated component can be auto-filled with a person's middle name. */
     PersonMiddleName,
 
-    /** Indicates that the associated component can be autofilled with a person's middle initial. */
+    /**
+     * Indicates that the associated component can be auto-filled with a person's middle initial.
+     */
     PersonMiddleInitial,
 
-    /** Indicates that the associated component can be autofilled with a person's name prefix. */
+    /** Indicates that the associated component can be auto-filled with a person's name prefix. */
     PersonNamePrefix,
 
-    /** Indicates that the associated component can be autofilled with a person's name suffix. */
+    /** Indicates that the associated component can be auto-filled with a person's name suffix. */
     PersonNameSuffix,
 
     /**
-     * Indicates that the associated component can be autofilled with a phone number with country
+     * Indicates that the associated component can be auto-filled with a phone number with country
      * code.
      *
      * Example: +1 123-456-7890
@@ -142,39 +148,43 @@
     PhoneNumber,
 
     /**
-     * Indicates that the associated component can be autofilled with the current device's phone
+     * Indicates that the associated component can be auto-filled with the current device's phone
      * number usually for Sign Up / OTP flows.
      */
     PhoneNumberDevice,
 
     /**
-     * Indicates that the associated component can be autofilled with a phone number's country code.
+     * Indicates that the associated component can be auto-filled with a phone number's country
+     * code.
      */
     PhoneCountryCode,
 
     /**
-     * Indicates that the associated component can be autofilled with a phone number without country
-     * code.
+     * Indicates that the associated component can be auto-filled with a phone number without
+     * country code.
      */
     PhoneNumberNational,
 
-    /** Indicates that the associated component can be autofilled with a gender. */
+    /** Indicates that the associated component can be auto-filled with a gender. */
     Gender,
 
-    /** Indicates that the associated component can be autofilled with a full birth date. */
+    /** Indicates that the associated component can be auto-filled with a full birth date. */
     BirthDateFull,
 
-    /** Indicates that the associated component can be autofilled with a birth day(of the month). */
+    /**
+     * Indicates that the associated component can be auto-filled with a birth day(of the month).
+     */
     BirthDateDay,
 
-    /** Indicates that the associated component can be autofilled with a birth month. */
+    /** Indicates that the associated component can be auto-filled with a birth month. */
     BirthDateMonth,
 
-    /** Indicates that the associated component can be autofilled with a birth year. */
+    /** Indicates that the associated component can be auto-filled with a birth year. */
     BirthDateYear,
 
     /**
-     * Indicates that the associated component can be autofilled with a SMS One Time Password (OTP).
+     * Indicates that the associated component can be auto-filled with a SMS One Time Password
+     * (OTP).
      *
      * TODO(b/153386346): Support use-case where you specify the start and end index of the OTP.
      */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
index d358fbb..f4fa22c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
@@ -67,8 +67,7 @@
     staticCompositionLocalOf<AutofillTree> { noLocalProvidedFor("LocalAutofillTree") }
 
 /**
- * The CompositionLocal that can be used to trigger autofill actions. Eg.
- * [LocalAutofillManager.commit].
+ * The CompositionLocal that can be used to trigger autofill actions. Eg. [AutofillManager.commit].
  */
 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
 val LocalAutofillManager =
@@ -92,8 +91,9 @@
  * Consumers that access this Local directly and call [GraphicsContext.createGraphicsLayer] are
  * responsible for calling [GraphicsContext.releaseGraphicsLayer].
  *
- * It is recommended that consumers invoke [rememberGraphicsLayer] instead to ensure that a
- * [GraphicsLayer] is released when the corresponding composable is disposed.
+ * It is recommended that consumers invoke [rememberGraphicsLayer][import
+ * androidx.compose.ui.graphics.rememberGraphicsLayer] instead to ensure that a [GraphicsLayer] is
+ * released when the corresponding composable is disposed.
  */
 val LocalGraphicsContext =
     staticCompositionLocalOf<GraphicsContext> { noLocalProvidedFor("LocalGraphicsContext") }
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
index 9e1b39c..30ed10b 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
+++ b/constraintlayout/constraintlayout-compose/integration-tests/compose-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -38,4 +40,8 @@
 android {
     compileSdk = 35
     namespace = "androidx.constraintlayout.compose.benchmark"
+}
+
+androidx {
+    type = LibraryType.BENCHMARK
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-core/build.gradle b/constraintlayout/constraintlayout-core/build.gradle
index 4f6f7c0..a2570c0 100644
--- a/constraintlayout/constraintlayout-core/build.gradle
+++ b/constraintlayout/constraintlayout-core/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/constraintlayout/constraintlayout/build.gradle b/constraintlayout/constraintlayout/build.gradle
index fa426e9..c29662e 100644
--- a/constraintlayout/constraintlayout/build.gradle
+++ b/constraintlayout/constraintlayout/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index c73b56f..7e8f7d0 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/core/core-splashscreen/samples/build.gradle b/core/core-splashscreen/samples/build.gradle
index 2417028..2ddb138 100644
--- a/core/core-splashscreen/samples/build.gradle
+++ b/core/core-splashscreen/samples/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/datastore/datastore-benchmark/build.gradle b/datastore/datastore-benchmark/build.gradle
index f58443f..2d43429 100644
--- a/datastore/datastore-benchmark/build.gradle
+++ b/datastore/datastore-benchmark/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -48,3 +48,7 @@
 android {
     namespace = "androidx.datastore.benchmark"
 }
+
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index 9dfab0a..9a16953 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -33,15 +33,22 @@
 }
 
 androidXMultiplatform {
-    jvm() {
-        withJava()
-    }
+    jvm()
     mac()
     linux()
     ios()
     watchos()
     tvos()
-    // NOTE, if you add android target here, make sure to add the proguard file as well.
+    androidLibrary {
+        namespace = "androidx.datastore.preferences.core"
+        optimization {
+            it.consumerKeepRules.publish = true
+            it.consumerKeepRules.files.add(
+                    new File("src/jvmMain/resources/META-INF/proguard/androidx.datastore_datastore-preferences-core.pro")
+            )
+        }
+        experimentalProperties["android.lint.useK2Uast"] = false
+    }
 
     defaultPlatform(PlatformIdentifier.JVM)
 
@@ -61,6 +68,7 @@
         commonTest {
             dependencies {
                 implementation(libs.kotlinTestCommon)
+                implementation(libs.kotlinTest)
                 implementation(libs.kotlinTestAnnotationsCommon)
                 implementation(libs.kotlinCoroutinesTest)
                 implementation(project(":datastore:datastore-core"))
@@ -68,19 +76,22 @@
                 implementation(project(":internal-testutils-datastore"))
             }
         }
-        jvmMain {
+        jvmAndroidMain {
             dependsOn(commonMain)
             dependencies {
                 implementation(project(":datastore:datastore-preferences-proto"))
             }
         }
-        jvmTest {
+        jvmMain {
+            dependsOn(jvmAndroidMain)
+        }
+        androidMain {
+            dependsOn(jvmAndroidMain)
+        }
+        jvmAndroidTest {
             dependsOn(commonTest)
             dependencies {
                 implementation(libs.junit)
-                implementation(libs.kotlinTest)
-                implementation(project(":internal-testutils-datastore"))
-                implementation(project(":kruth:kruth"))
             }
         }
         nativeMain {
@@ -90,7 +101,9 @@
                 implementation(libs.kotlinSerializationProtobuf)
             }
         }
-
+        jvmTest {
+            dependsOn(jvmAndroidTest)
+        }
         nativeTest {
             dependsOn(commonTest)
             dependencies {
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/Actual.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/Actual.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
similarity index 98%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
index 5436d55..365276b 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvm.kt
+++ b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.jvmAndroid.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 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.
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializer.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt b/datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvmAndroid.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvmAndroid.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactoryTest.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFileSerializerTest.kt
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesFromJavaTest.java
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt b/datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
similarity index 100%
rename from datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
rename to datastore/datastore-preferences-core/src/jvmAndroidTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
diff --git a/datastore/datastore-preferences-proto/build.gradle b/datastore/datastore-preferences-proto/build.gradle
index 28e315f..52653b5 100644
--- a/datastore/datastore-preferences-proto/build.gradle
+++ b/datastore/datastore-preferences-proto/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 18bda36..2900002 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -240,32 +240,32 @@
     docs("androidx.media2:media2-widget:1.3.0")
     docs("androidx.media:media:1.7.0")
     // androidx.media3 is not hosted in androidx
-    docsWithoutApiSince("androidx.media3:media3-cast:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-common:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-common-ktx:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-container:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-database:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-datasource:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-decoder:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-effect:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-extractor:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-muxer:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-session:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-test-utils:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-transformer:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-ui:1.5.0")
-    docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.5.0")
+    docsWithoutApiSince("androidx.media3:media3-cast:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-common:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-common-ktx:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-container:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-database:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-datasource:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-cronet:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-okhttp:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-datasource-rtmp:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-decoder:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-effect:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-dash:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-hls:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-ima:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-rtsp:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-smoothstreaming:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-exoplayer-workmanager:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-extractor:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-muxer:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-session:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-test-utils:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-test-utils-robolectric:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-transformer:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-ui:1.6.0-alpha01")
+    docsWithoutApiSince("androidx.media3:media3-ui-leanback:1.6.0-alpha01")
     docs("androidx.mediarouter:mediarouter:1.8.0-alpha01")
     docs("androidx.mediarouter:mediarouter-testing:1.8.0-alpha01")
     docs("androidx.metrics:metrics-performance:1.0.0-beta01")
diff --git a/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java b/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
index 9692bad2..d73c2bf 100644
--- a/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
+++ b/documentfile/documentfile/src/main/java/androidx/documentfile/provider/DocumentFile.java
@@ -107,6 +107,7 @@
      * {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return
      * {@code null} when called on earlier platform versions.
      *
+     * @param context {@link Context} used to resolve resources
      * @param singleUri the {@link Intent#getData()} from a successful
      *            {@link Intent#ACTION_OPEN_DOCUMENT} or
      *            {@link Intent#ACTION_CREATE_DOCUMENT} request.
@@ -122,6 +123,7 @@
      * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, and will return
      * {@code null} when called on earlier platform versions.
      *
+     * @param context {@link Context} used to resolve resources
      * @param treeUri the {@link Intent#getData()} from a successful
      *            {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request.
      */
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index ed2ec2f..5eab826 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -64,7 +64,7 @@
 
 androidx {
     name = "Emoji2 Benchmarks"
-    publish = Publish.NONE
+    type = LibraryType.BENCHMARK
     inceptionYear = "2021"
     description = "Emoji2 Benchmarks"
 }
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 45640f2..9f91b2a 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 apply plugin: 'AndroidXPlugin'
 apply plugin: 'com.android.library'
@@ -69,7 +68,7 @@
 androidx {
     name = "libyuv"
     // Only intended to be used as snapshots, do not change to PUBLISHED.
-    publish = Publish.SNAPSHOT_ONLY
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY
     inceptionYear = "2021"
     description = "libyuv is an open source project that includes YUV scaling and conversion functionality."
 }
diff --git a/fragment/fragment-truth/build.gradle b/fragment/fragment-truth/build.gradle
index 6b158d9..a1b437d 100644
--- a/fragment/fragment-truth/build.gradle
+++ b/fragment/fragment-truth/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -43,7 +43,7 @@
 
 androidx {
     name = "Fragment Truth Extensions"
-    publish = Publish.NONE
+    type = LibraryType.UNSET
     inceptionYear = "2019"
     description = "Truth extensions for Fragments"
 }
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index d52b52e..35daeff 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -7,7 +7,6 @@
  */
 
 import androidx.build.LibraryType
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
diff --git a/glance/glance-appwidget/glance-layout-generator/build.gradle b/glance/glance-appwidget/glance-layout-generator/build.gradle
index 6d3102d..1146591 100644
--- a/glance/glance-appwidget/glance-layout-generator/build.gradle
+++ b/glance/glance-appwidget/glance-layout-generator/build.gradle
@@ -23,8 +23,6 @@
  */
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -42,8 +40,7 @@
 
 androidx {
     name = "Glance AppWidget Layout Generator"
-    type = LibraryType.OTHER_CODE_PROCESSOR
-    publish = Publish.NONE
+    type = LibraryType.INTERNAL_OTHER_CODE_PROCESSOR
     inceptionYear = "2021"
     description = "Generator module that generates the layouts Glance AppWidget needs."
     kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/glance/glance-template/build.gradle b/glance/glance-template/build.gradle
index a0f7619..b358f48 100644
--- a/glance/glance-template/build.gradle
+++ b/glance/glance-template/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 import androidx.build.AndroidXComposePlugin
 
 plugins {
diff --git a/graphics/graphics-core/build.gradle b/graphics/graphics-core/build.gradle
index efeb042..34580b6 100644
--- a/graphics/graphics-core/build.gradle
+++ b/graphics/graphics-core/build.gradle
@@ -24,7 +24,6 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/graphics/graphics-core/samples/build.gradle b/graphics/graphics-core/samples/build.gradle
index 655493e..0386700 100644
--- a/graphics/graphics-core/samples/build.gradle
+++ b/graphics/graphics-core/samples/build.gradle
@@ -15,7 +15,6 @@
  */
 
 import androidx.build.KotlinTarget
-import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 0c314fa..62b1f75 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -21,7 +21,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
diff --git a/health/health-services-client-proto/build.gradle b/health/health-services-client-proto/build.gradle
index 95300ca..60cfefe 100644
--- a/health/health-services-client-proto/build.gradle
+++ b/health/health-services-client-proto/build.gradle
@@ -23,7 +23,6 @@
  */
 
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
diff --git a/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index 0c455f0..4e90f1c 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -24,7 +24,6 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index 19bab5d..c459a13 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -22,8 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
-import androidx.build.SdkResourceGenerator
 
 plugins {
     id("AndroidXPlugin")
@@ -59,8 +57,7 @@
 
 androidx {
     name = "Inspection Gradle Plugin"
-    type = LibraryType.GRADLE_PLUGIN
-    publish = Publish.NONE
+    type = LibraryType.INTERNAL_GRADLE_PLUGIN
     inceptionYear = "2019"
     description = "Android Inspection Gradle Plugin"
 }
diff --git a/inspection/inspection-testing/build.gradle b/inspection/inspection-testing/build.gradle
index 8067271..cee9da2 100644
--- a/inspection/inspection-testing/build.gradle
+++ b/inspection/inspection-testing/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
diff --git a/inspection/inspection/build.gradle b/inspection/inspection/build.gradle
index d14ab58..af4ba30 100644
--- a/inspection/inspection/build.gradle
+++ b/inspection/inspection/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
@@ -45,9 +44,6 @@
     type = LibraryType.PUBLISHED_LIBRARY
     inceptionYear = "2019"
     description = "Experimental AndroidX Inspection Project"
-    runApiTasks =  new RunApiTasks.Yes(
-            "Interfaces provided in this artifact should be binary compatible to guarantee " +
-                    "that old inspectors are compatible with newer Android Studio versions")
     legacyDisableKotlinStrictApiMode = true
     doNotDocumentReason = "Not shipped externally"
     // TODO: b/326456246
diff --git a/kruth/kruth/build.gradle b/kruth/kruth/build.gradle
index e56160f..cd7daef 100644
--- a/kruth/kruth/build.gradle
+++ b/kruth/kruth/build.gradle
@@ -26,8 +26,6 @@
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
-import androidx.build.RunApiTasks
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 
 plugins {
@@ -158,9 +156,7 @@
 
 androidx {
     legacyDisableKotlinStrictApiMode = true // Temporarily enabled to allow API tracking
-    publish = Publish.SNAPSHOT_ONLY
-    runApiTasks = new RunApiTasks.Yes() // Used to diff against Google Truth
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.SNAPSHOT_ONLY_TEST_LIBRARY_WITH_API_TASKS // Used to diff against Google Truth
     doNotDocumentReason = "Not shipped externally"
     metalavaK2UastEnabled = false
     kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/lifecycle/lifecycle-extensions/build.gradle b/lifecycle/lifecycle-extensions/build.gradle
index 802867a..5a2b6b6 100644
--- a/lifecycle/lifecycle-extensions/build.gradle
+++ b/lifecycle/lifecycle-extensions/build.gradle
@@ -21,8 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
-import androidx.build.RunApiTasks
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -59,8 +58,7 @@
 
 androidx {
     name = "Lifecycle Extensions"
-    publish = Publish.NONE
-    runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
+    type = LibraryType.INTERNAL_LIBRARY_WITH_API_TASKS
     inceptionYear = "2017"
     description = "Android Lifecycle Extensions"
     failOnDeprecationWarnings = false
diff --git a/lifecycle/lifecycle-livedata-core-truth/build.gradle b/lifecycle/lifecycle-livedata-core-truth/build.gradle
index d17fe02..68ff222 100644
--- a/lifecycle/lifecycle-livedata-core-truth/build.gradle
+++ b/lifecycle/lifecycle-livedata-core-truth/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -41,7 +41,7 @@
 
 androidx {
     name = "LiveData Core Truth Extensions"
-    publish = Publish.NONE
+    type = LibraryType.UNSET
     inceptionYear = "2019"
     description = "Truth extensions for 'livedata-core' artifact"
 }
diff --git a/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle b/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
index ddfa56f..caa6056 100644
--- a/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/integration-tests/lifecycle-runtime-compose-demos/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -36,7 +36,7 @@
 
 androidx {
     name = "Lifecycle Runtime Compose Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2022"
     description = "This is a project for Lifecycle Runtime Compose demos."
 }
diff --git a/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle b/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
index 224f7bc..3047354 100644
--- a/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -36,7 +36,7 @@
 
 androidx {
     name = "Compose Lifecycle ViewModel Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2021"
     description = "This is a project for Lifecycle ViewModel demos."
 }
diff --git a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
index 85d6c9e..e85f6a2 100644
--- a/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-navigation3/build.gradle
@@ -24,7 +24,6 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
 import androidx.build.PlatformIdentifier
 
 plugins {
@@ -103,8 +102,7 @@
 
 androidx {
     name = "Androidx Lifecycle Navigation3 ViewModel"
-    publish = Publish.SNAPSHOT_ONLY
-    type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
     inceptionYear = "2024"
     description = "Provides the ViewModel wrapper for nav3."
     doNotDocumentReason = "Not published to maven"
diff --git a/lint/lint-gradle/build.gradle b/lint/lint-gradle/build.gradle
index 9605875..65c3f7d 100644
--- a/lint/lint-gradle/build.gradle
+++ b/lint/lint-gradle/build.gradle
@@ -24,7 +24,6 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/mediarouter/mediarouter/api/current.txt b/mediarouter/mediarouter/api/current.txt
index 0c0847b..8b9f3d9 100644
--- a/mediarouter/mediarouter/api/current.txt
+++ b/mediarouter/mediarouter/api/current.txt
@@ -274,6 +274,7 @@
     method public boolean isGroupable();
     method public boolean isTransferable();
     method public boolean isUnselectable();
+    field public static final int NOT_IN_GROUP = 4; // 0x4
     field public static final int SELECTED = 3; // 0x3
     field public static final int SELECTING = 2; // 0x2
     field public static final int UNSELECTED = 1; // 0x1
@@ -379,7 +380,7 @@
     method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
     method @Deprecated @MainThread public void addRemoteControlClient(Object);
     method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
-    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getConnectedRoutes();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.GroupRouteInfo!> getConnectedGroupRoutes();
     method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
     method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
     method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
@@ -443,6 +444,33 @@
     method public void onResult(android.os.Bundle?);
   }
 
+  public static class MediaRouter.GroupRouteInfo extends androidx.mediarouter.media.MediaRouter.RouteInfo {
+    method @MainThread public int addRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
+    method public int getSelectionState(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public boolean isConnected();
+    method public boolean isGroupable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public boolean isTransferable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public boolean isUnselectable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public int removeRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public int updateRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+    field public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3; // 0x3
+    field public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5; // 0x5
+    field public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2; // 0x2
+    field public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4; // 0x4
+    field public static final int ADD_ROUTE_SUCCESSFUL = 1; // 0x1
+    field public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4; // 0x4
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6; // 0x6
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3; // 0x3
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2; // 0x2
+    field public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5; // 0x5
+    field public static final int REMOVE_ROUTE_SUCCESSFUL = 1; // 0x1
+    field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4; // 0x4
+    field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2; // 0x2
+    field public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3; // 0x3
+    field public static final int UPDATE_ROUTES_SUCCESSFUL = 1; // 0x1
+  }
+
   public static interface MediaRouter.OnPrepareTransferListener {
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
   }
@@ -471,13 +499,11 @@
     method public int getPlaybackType();
     method @MainThread public android.view.Display? getPresentationDisplay();
     method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
-    method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
     method public android.content.IntentSender? getSettingsIntent();
     method public int getVolume();
     method public int getVolumeHandling();
     method public int getVolumeMax();
     method @MainThread public boolean isBluetooth();
-    method @MainThread public boolean isConnected();
     method @Deprecated public boolean isConnecting();
     method @MainThread public boolean isDefault();
     method public boolean isDeviceSpeaker();
diff --git a/mediarouter/mediarouter/api/restricted_current.txt b/mediarouter/mediarouter/api/restricted_current.txt
index 0c0847b..8b9f3d9 100644
--- a/mediarouter/mediarouter/api/restricted_current.txt
+++ b/mediarouter/mediarouter/api/restricted_current.txt
@@ -274,6 +274,7 @@
     method public boolean isGroupable();
     method public boolean isTransferable();
     method public boolean isUnselectable();
+    field public static final int NOT_IN_GROUP = 4; // 0x4
     field public static final int SELECTED = 3; // 0x3
     field public static final int SELECTING = 2; // 0x2
     field public static final int UNSELECTED = 1; // 0x1
@@ -379,7 +380,7 @@
     method @MainThread public void addProvider(androidx.mediarouter.media.MediaRouteProvider);
     method @Deprecated @MainThread public void addRemoteControlClient(Object);
     method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo? getBluetoothRoute();
-    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getConnectedRoutes();
+    method @MainThread public java.util.List<androidx.mediarouter.media.MediaRouter.GroupRouteInfo!> getConnectedGroupRoutes();
     method @MainThread public androidx.mediarouter.media.MediaRouter.RouteInfo getDefaultRoute();
     method @MainThread public static androidx.mediarouter.media.MediaRouter getInstance(android.content.Context);
     method public android.support.v4.media.session.MediaSessionCompat.Token? getMediaSessionToken();
@@ -443,6 +444,33 @@
     method public void onResult(android.os.Bundle?);
   }
 
+  public static class MediaRouter.GroupRouteInfo extends androidx.mediarouter.media.MediaRouter.RouteInfo {
+    method @MainThread public int addRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
+    method public int getSelectionState(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public boolean isConnected();
+    method public boolean isGroupable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public boolean isTransferable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method public boolean isUnselectable(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public int removeRoute(androidx.mediarouter.media.MediaRouter.RouteInfo);
+    method @MainThread public int updateRoutes(java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!>);
+    field public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3; // 0x3
+    field public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5; // 0x5
+    field public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2; // 0x2
+    field public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4; // 0x4
+    field public static final int ADD_ROUTE_SUCCESSFUL = 1; // 0x1
+    field public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4; // 0x4
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6; // 0x6
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3; // 0x3
+    field public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2; // 0x2
+    field public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5; // 0x5
+    field public static final int REMOVE_ROUTE_SUCCESSFUL = 1; // 0x1
+    field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4; // 0x4
+    field public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2; // 0x2
+    field public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3; // 0x3
+    field public static final int UPDATE_ROUTES_SUCCESSFUL = 1; // 0x1
+  }
+
   public static interface MediaRouter.OnPrepareTransferListener {
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!>? onPrepareTransfer(androidx.mediarouter.media.MediaRouter.RouteInfo, androidx.mediarouter.media.MediaRouter.RouteInfo);
   }
@@ -471,13 +499,11 @@
     method public int getPlaybackType();
     method @MainThread public android.view.Display? getPresentationDisplay();
     method public androidx.mediarouter.media.MediaRouter.ProviderInfo getProvider();
-    method public java.util.List<androidx.mediarouter.media.MediaRouter.RouteInfo!> getRoutesInGroup();
     method public android.content.IntentSender? getSettingsIntent();
     method public int getVolume();
     method public int getVolumeHandling();
     method public int getVolumeMax();
     method @MainThread public boolean isBluetooth();
-    method @MainThread public boolean isConnected();
     method @Deprecated public boolean isConnecting();
     method @MainThread public boolean isDefault();
     method public boolean isDeviceSpeaker();
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
index 3de8cc9..5722db5 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouterDynamicProviderTest.java
@@ -16,11 +16,24 @@
 
 package androidx.mediarouter.media;
 
+import static androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.SELECTED;
+import static androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor.UNSELECTED;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_SUCCESSFUL;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_1;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_GROUPABLE_3;
 import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_1;
 import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_3;
 import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_ID_GROUP;
 import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_1;
 import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_NAME_3;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_1;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_2;
+import static androidx.mediarouter.media.StubDynamicMediaRouteProviderService.ROUTE_TRANSFERABLE_3;
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -31,7 +44,9 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
+import android.content.Intent;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.Looper;
 
@@ -45,6 +60,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -52,6 +68,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Test for {@link MediaRouter} functionality around routes from a provider that supports {@link
@@ -67,13 +84,19 @@
         STATE_DISCONNECTED
     }
 
+    private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_ADDED =
+            List.of(ROUTE_ID_1, ROUTE_ID_2);
+    private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_REMOVED = List.of(ROUTE_ID_1);
+    private static final List<String> EXPECTED_ROUTE_IDS_AFTER_ROUTE_UPDATED = List.of(ROUTE_ID_3);
     private Context mContext;
     private MediaRouter mRouter;
     private MediaRouteSelector mSelector;
     private MediaRouterCallbackImpl mCallback;
     private MediaRouter.RouteInfo mRoute1;
     private MediaRouter.RouteInfo mRoute2;
+    private MediaRouter.RouteInfo mRoute3;
     private RouteConnectionState mRouteConnectionState;
+    private MediaRouter.RouteInfo mChangedRoute;
     private MediaRouter.RouteInfo mConnectedRoute;
     private MediaRouter.RouteInfo mDisconnectedRoute;
     private MediaRouter.RouteInfo mRequestedRoute;
@@ -113,6 +136,13 @@
         Objects.requireNonNull(mediaRouteDescriptor2);
         assertEquals(ROUTE_ID_2, mediaRouteDescriptor2.getId());
         assertEquals(ROUTE_NAME_2, mediaRouteDescriptor2.getName());
+
+        mRoute3 = routeSnapshot.get(ROUTE_ID_3);
+        Objects.requireNonNull(mRoute3);
+        MediaRouteDescriptor mediaRouteDescriptor3 = mRoute3.getMediaRouteDescriptor();
+        Objects.requireNonNull(mediaRouteDescriptor3);
+        assertEquals(ROUTE_ID_3, mediaRouteDescriptor3.getId());
+        assertEquals(ROUTE_NAME_3, mediaRouteDescriptor3.getName());
     }
 
     @After
@@ -134,41 +164,37 @@
     @Test()
     public void connectDynamicRoute_shouldNotifyRouteConnected() {
         assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
-        List<MediaRouter.RouteInfo> connectedRoutes =
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
                 mCallback.connectAndWaitForOnConnected(mRoute2);
 
         assertNotNull(mConnectedRoute);
         assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
-        assertEquals(1, connectedRoutes.size());
-        MediaRouter.RouteInfo connectedRoute = connectedRoutes.get(0);
-        assertEquals(ROUTE_ID_GROUP, connectedRoute.getDescriptorId());
-        assertTrue(runBlockingOnMainThreadWithResult(connectedRoute::isConnected));
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
 
         assertNotNull(mRequestedRoute);
         assertEquals(ROUTE_ID_2, mRequestedRoute.getDescriptorId());
-        assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
-        assertFalse(runBlockingOnMainThreadWithResult(mRoute2::isConnected));
         assertEquals(RouteConnectionState.STATE_CONNECTED, mRouteConnectionState);
     }
 
     @Test()
     public void disconnectDynamicRoute_shouldNotifyRouteDisconnected() {
         assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
-        List<MediaRouter.RouteInfo> connectedRoutes =
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
                 mCallback.connectAndWaitForOnConnected(mRoute2);
         assertEquals(RouteConnectionState.STATE_CONNECTED, mRouteConnectionState);
-        assertEquals(1, connectedRoutes.size());
+        assertEquals(1, connectedGroupRoutes.size());
 
-        connectedRoutes = mCallback.disconnectAndWaitForOnDisconnected(mRoute2);
+        connectedGroupRoutes = mCallback.disconnectAndWaitForOnDisconnected(mRoute2);
 
         assertNotNull(mConnectedRoute);
         assertNotNull(mDisconnectedRoute);
-        assertEquals(0, connectedRoutes.size());
+        assertEquals(0, connectedGroupRoutes.size());
 
         assertNotNull(mRequestedRoute);
         assertEquals(ROUTE_ID_2, mRequestedRoute.getDescriptorId());
-        assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
-        assertFalse(runBlockingOnMainThreadWithResult(mRoute2::isConnected));
         assertEquals(RouteConnectionState.STATE_DISCONNECTED, mRouteConnectionState);
         assertEquals(MediaRouter.REASON_DISCONNECTED, mRouteDisconnectedReason);
     }
@@ -181,20 +207,247 @@
         assertFalse(runBlockingOnMainThreadWithResult(mRoute1::isSelected));
         assertTrue(runBlockingOnMainThreadWithResult(selectedRoute::isSelected));
 
-        List<MediaRouter.RouteInfo> connectedRoutes =
-                mCallback.connectAndWaitForOnConnected(selectedRoute);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnDisconnected(selectedRoute);
 
         assertNull(mConnectedRoute);
         assertNull(mDisconnectedRoute);
-        assertEquals(0, connectedRoutes.size());
+        assertEquals(0, connectedGroupRoutes.size());
 
         assertNotNull(mRequestedRoute);
         assertEquals(ROUTE_ID_GROUP, mRequestedRoute.getDescriptorId());
-        assertFalse(runBlockingOnMainThreadWithResult(mRequestedRoute::isConnected));
-        assertFalse(runBlockingOnMainThreadWithResult(selectedRoute::isConnected));
         assertEquals(RouteConnectionState.STATE_DISCONNECTED, mRouteConnectionState);
     }
 
+    @Test()
+    public void addRouteToGroupAndRemoveRouteFromGroup_shouldChangeGroup() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute2);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+        assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+        assertEquals(
+                ROUTE_ID_2, mConnectedRoute.getSelectedRoutesInGroup().get(0).getDescriptorId());
+        assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+        verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ false);
+        verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ true);
+        verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+        List<MediaRouter.RouteInfo> memberRoutes =
+                mCallback.addRouteToGroupAndWaitForOnChanged(groupRouteInfo, mRoute1);
+        assertEquals(2, mConnectedRoute.getSelectedRoutesInGroup().size());
+        assertEquals(2, memberRoutes.size());
+        assertNotNull(mChangedRoute);
+        assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+        assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+        verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+        verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ true);
+        verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+        mChangedRoute = null;
+        memberRoutes = mCallback.removeRouteFromGroupAndWaitForOnChanged(groupRouteInfo, mRoute2);
+        assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+        assertEquals(1, memberRoutes.size());
+        assertEquals(ROUTE_ID_1, memberRoutes.get(0).getDescriptorId());
+        assertNotNull(mChangedRoute);
+        assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+        assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+        verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+        verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+        verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+    }
+
+    @Test()
+    public void updateRoutesForGroup_shouldChangeGroup() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute1);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(ROUTE_ID_GROUP, mConnectedRoute.getDescriptorId());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertEquals(ROUTE_ID_GROUP, connectedGroupRoute.getDescriptorId());
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+        assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+        assertEquals(
+                ROUTE_ID_1, mConnectedRoute.getSelectedRoutesInGroup().get(0).getDescriptorId());
+        assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+        verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ true);
+        verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+        verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ false);
+
+        List<MediaRouter.RouteInfo> memberRoutes =
+                mCallback.updateRoutesForGroupAndWaitForOnChanged(groupRouteInfo, List.of(mRoute3));
+        assertEquals(1, mConnectedRoute.getSelectedRoutesInGroup().size());
+        assertEquals(1, memberRoutes.size());
+        assertNotNull(mChangedRoute);
+        assertEquals(ROUTE_ID_GROUP, mChangedRoute.getDescriptorId());
+        assertEquals(3, groupRouteInfo.getRoutesInGroup().size());
+        verifyMemberRouteState(groupRouteInfo, mRoute1, /* isSelected= */ false);
+        verifyMemberRouteState(groupRouteInfo, mRoute2, /* isSelected= */ false);
+        verifyMemberRouteState(groupRouteInfo, mRoute3, /* isSelected= */ true);
+    }
+
+    @Test()
+    public void setGroupVolume_shouldSetGroupVolume() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute1);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                groupRouteInfo.getVolume());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                connectedGroupRoute.getVolume());
+
+        final int expectedVolume = 6;
+        int updatedVolume =
+                mCallback.setGroupVolumeAndWaitForOnVolumeSet(groupRouteInfo, expectedVolume);
+        assertEquals(expectedVolume, updatedVolume);
+        assertEquals(expectedVolume, mConnectedRoute.getVolume());
+    }
+
+    @Test()
+    public void updateGroupVolume_shouldUpdateGroupVolume() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute2);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                groupRouteInfo.getVolume());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                connectedGroupRoute.getVolume());
+
+        final int volumeDelta = -3;
+        final int expectedVolume =
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE + volumeDelta;
+        int updatedVolume =
+                mCallback.updateGroupVolumeAndWaitForOnVolumeUpdated(groupRouteInfo, volumeDelta);
+        assertEquals(expectedVolume, updatedVolume);
+        assertEquals(expectedVolume, mConnectedRoute.getVolume());
+    }
+
+    @Test()
+    public void setMemberVolume_shouldSetMemberVolume() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute2);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                groupRouteInfo.getVolume());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                connectedGroupRoute.getVolume());
+
+        final int expectedVolume = 14;
+        getInstrumentation().runOnMainSync(() -> mRoute2.requestSetVolume(expectedVolume));
+        MediaRouter.RouteInfo memberRoute =
+                mCallback.waitForRouteVolume(ROUTE_ID_2, expectedVolume);
+        assertEquals(expectedVolume, memberRoute.getVolume());
+    }
+
+    @Test()
+    public void updateMemberVolume_shouldUpdateMemberVolume() {
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute1);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                groupRouteInfo.getVolume());
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+        assertEquals(
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE,
+                connectedGroupRoute.getVolume());
+
+        final int volumeDelta = 4;
+        final int expectedVolume =
+                StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE + volumeDelta;
+        getInstrumentation().runOnMainSync(() -> mRoute1.requestUpdateVolume(volumeDelta));
+        MediaRouter.RouteInfo memberRoute =
+                mCallback.waitForRouteVolume(ROUTE_ID_1, expectedVolume);
+        assertEquals(expectedVolume, memberRoute.getVolume());
+    }
+
+    @Test()
+    public void sendControlRequest_shouldSendControlRequestToGroupRouteController() {
+        final ConditionVariable sendControlRequestConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        assertEquals(RouteConnectionState.STATE_UNKNOWN, mRouteConnectionState);
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes =
+                mCallback.connectAndWaitForOnConnected(mRoute2);
+
+        assertNotNull(mConnectedRoute);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mConnectedRoute.asGroup();
+        assertNotNull(groupRouteInfo);
+        assertEquals(1, connectedGroupRoutes.size());
+        MediaRouter.GroupRouteInfo connectedGroupRoute = connectedGroupRoutes.get(0);
+        assertTrue(runBlockingOnMainThreadWithResult(connectedGroupRoute::isConnected));
+
+        final Bundle[] sendControlRequestResults = new Bundle[1];
+        MediaRouter.ControlRequestCallback callback =
+                new MediaRouter.ControlRequestCallback() {
+                    @Override
+                    public void onResult(@Nullable Bundle data) {
+                        sendControlRequestResults[0] = data;
+                        sendControlRequestConditionVariable.open();
+                    }
+                };
+        sendControlRequestConditionVariable.close();
+        getInstrumentation()
+                .runOnMainSync(() -> groupRouteInfo.sendControlRequest(new Intent(), callback));
+        sendControlRequestConditionVariable.block();
+
+        Bundle sendControlRequestResult = sendControlRequestResults[0];
+        assertEquals(
+                StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_RESULT,
+                sendControlRequestResult);
+        assertEquals(
+                StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_VALUE,
+                sendControlRequestResult.getString(
+                        StubDynamicMediaRouteProviderService.SEND_CONTROL_REQUEST_KEY));
+    }
+
     // Internal methods.
 
     private Map<String, MediaRouter.RouteInfo> getCurrentRoutesAsMap() {
@@ -220,6 +473,30 @@
         }
     }
 
+    private void verifyMemberRouteState(
+            MediaRouter.GroupRouteInfo groupRoute,
+            MediaRouter.RouteInfo route,
+            boolean isSelected) {
+        assertEquals(isSelected ? SELECTED : UNSELECTED, groupRoute.getSelectionState(route));
+        assertEquals(isSelected, groupRoute.isUnselectable(route));
+        switch (route.getDescriptorId()) {
+            case ROUTE_ID_1:
+                assertEquals(ROUTE_GROUPABLE_1, groupRoute.isGroupable(route));
+                assertEquals(ROUTE_TRANSFERABLE_1, groupRoute.isTransferable(route));
+                break;
+            case ROUTE_ID_2:
+                assertEquals(ROUTE_GROUPABLE_2, groupRoute.isGroupable(route));
+                assertEquals(ROUTE_TRANSFERABLE_2, groupRoute.isTransferable(route));
+                break;
+            case ROUTE_ID_3:
+                assertEquals(ROUTE_GROUPABLE_3, groupRoute.isGroupable(route));
+                assertEquals(ROUTE_TRANSFERABLE_3, groupRoute.isTransferable(route));
+                break;
+            default:
+                // Ignore.
+        }
+    }
+
     // Internal classes and interfaces.
 
     // Equivalent to java.util.function.Supplier, except it's available before API 24.
@@ -230,11 +507,24 @@
 
     private class MediaRouterCallbackImpl extends MediaRouter.Callback {
 
-        private final ConditionVariable mPendingRoutesConditionVariable = new ConditionVariable();
         private final Set<String> mRouteIdsPending = new HashSet<>();
-        private final ConditionVariable mSelectedRouteChangeConditionVariable =
+        private final Map<String, Integer> mRouteVolumePending = new HashMap<>();
+        private final ConditionVariable mPendingRoutesConditionVariable = new ConditionVariable();
+        private final ConditionVariable mPendingRouteVolumeConditionVariable =
+                new ConditionVariable();
+        private final ConditionVariable mRouteSelectedConditionVariable =
                 new ConditionVariable(/* state= */ true);
-        private final ConditionVariable mRouteConnectionConditionVariable =
+        private final ConditionVariable mRouteConnectedConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        private final ConditionVariable mRouteDisconnectedConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        private final ConditionVariable mMemberRouteAddedConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        private final ConditionVariable mMemberRouteRemovedConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        private final ConditionVariable mMemberRoutesUpdatedConditionVariable =
+                new ConditionVariable(/* state= */ true);
+        private final ConditionVariable mGroupVolumeChangedConditionVariable =
                 new ConditionVariable(/* state= */ true);
 
         private Map<String, MediaRouter.RouteInfo> waitForRoutes(String... routeIds) {
@@ -257,26 +547,103 @@
 
         public MediaRouter.RouteInfo selectAndWaitForOnSelected(
                 MediaRouter.RouteInfo routeToSelect) {
-            mSelectedRouteChangeConditionVariable.close();
+            mRouteSelectedConditionVariable.close();
             getInstrumentation().runOnMainSync(routeToSelect::select);
-            mSelectedRouteChangeConditionVariable.block();
+            mRouteSelectedConditionVariable.block();
             return runBlockingOnMainThreadWithResult(() -> mRouter.getSelectedRoute());
         }
 
-        public List<MediaRouter.RouteInfo> connectAndWaitForOnConnected(
+        public List<MediaRouter.GroupRouteInfo> connectAndWaitForOnConnected(
                 MediaRouter.RouteInfo routeToConnect) {
-            mRouteConnectionConditionVariable.close();
+            mRouteConnectedConditionVariable.close();
             getInstrumentation().runOnMainSync(routeToConnect::connect);
-            mRouteConnectionConditionVariable.block();
-            return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedRoutes());
+            mRouteConnectedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
         }
 
-        public List<MediaRouter.RouteInfo> disconnectAndWaitForOnDisconnected(
+        public List<MediaRouter.GroupRouteInfo> connectAndWaitForOnDisconnected(
+                MediaRouter.RouteInfo routeToConnect) {
+            mRouteDisconnectedConditionVariable.close();
+            getInstrumentation().runOnMainSync(routeToConnect::connect);
+            mRouteDisconnectedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
+        }
+
+        public List<MediaRouter.GroupRouteInfo> disconnectAndWaitForOnDisconnected(
                 MediaRouter.RouteInfo routeToDisconnect) {
-            mRouteConnectionConditionVariable.close();
+            mRouteDisconnectedConditionVariable.close();
             getInstrumentation().runOnMainSync(routeToDisconnect::disconnect);
-            mRouteConnectionConditionVariable.block();
-            return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedRoutes());
+            mRouteDisconnectedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(() -> mRouter.getConnectedGroupRoutes());
+        }
+
+        public List<MediaRouter.RouteInfo> addRouteToGroupAndWaitForOnChanged(
+                MediaRouter.GroupRouteInfo groupRoute, MediaRouter.RouteInfo memberRoute) {
+            mMemberRouteAddedConditionVariable.close();
+            AtomicInteger addMemberStatus = new AtomicInteger();
+            getInstrumentation()
+                    .runOnMainSync(() -> addMemberStatus.set(groupRoute.addRoute(memberRoute)));
+            assertEquals(ADD_ROUTE_SUCCESSFUL, addMemberStatus.get());
+            mMemberRouteAddedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+        }
+
+        public List<MediaRouter.RouteInfo> removeRouteFromGroupAndWaitForOnChanged(
+                MediaRouter.GroupRouteInfo groupRoute, MediaRouter.RouteInfo memberRoute) {
+            mMemberRouteRemovedConditionVariable.close();
+            AtomicInteger removeMemberStatus = new AtomicInteger();
+            getInstrumentation()
+                    .runOnMainSync(
+                            () -> removeMemberStatus.set(groupRoute.removeRoute(memberRoute)));
+            assertEquals(REMOVE_ROUTE_SUCCESSFUL, removeMemberStatus.get());
+            mMemberRouteRemovedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+        }
+
+        public List<MediaRouter.RouteInfo> updateRoutesForGroupAndWaitForOnChanged(
+                MediaRouter.GroupRouteInfo groupRoute, List<MediaRouter.RouteInfo> memberRoutes) {
+            mMemberRoutesUpdatedConditionVariable.close();
+            AtomicInteger updateMembersStatus = new AtomicInteger();
+            getInstrumentation()
+                    .runOnMainSync(
+                            () -> updateMembersStatus.set(groupRoute.updateRoutes(memberRoutes)));
+            assertEquals(UPDATE_ROUTES_SUCCESSFUL, updateMembersStatus.get());
+            mMemberRoutesUpdatedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(groupRoute::getSelectedRoutesInGroup);
+        }
+
+        public int setGroupVolumeAndWaitForOnVolumeSet(
+                MediaRouter.GroupRouteInfo groupRoute, int volume) {
+            mGroupVolumeChangedConditionVariable.close();
+            getInstrumentation().runOnMainSync(() -> groupRoute.requestSetVolume(volume));
+            mGroupVolumeChangedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(groupRoute::getVolume);
+        }
+
+        public int updateGroupVolumeAndWaitForOnVolumeUpdated(
+                MediaRouter.GroupRouteInfo groupRoute, int delta) {
+            mGroupVolumeChangedConditionVariable.close();
+            getInstrumentation().runOnMainSync(() -> groupRoute.requestUpdateVolume(delta));
+            mGroupVolumeChangedConditionVariable.block();
+            return runBlockingOnMainThreadWithResult(groupRoute::getVolume);
+        }
+
+        private MediaRouter.RouteInfo waitForRouteVolume(String routeId, int expectedVolume) {
+            getInstrumentation()
+                    .runOnMainSync(
+                            () -> {
+                                Map<String, MediaRouter.RouteInfo> routes = getCurrentRoutesAsMap();
+                                if (!routes.containsKey(routeId)
+                                        || routes.get(routeId).getVolume() != expectedVolume) {
+                                    mPendingRouteVolumeConditionVariable.close();
+                                    mRouteVolumePending.clear();
+                                    mRouteVolumePending.put(routeId, expectedVolume);
+                                } else {
+                                    mPendingRouteVolumeConditionVariable.open();
+                                }
+                            });
+            mPendingRouteVolumeConditionVariable.block();
+            return getCurrentRoutesAsMap().get(routeId);
         }
 
         @Override
@@ -285,7 +652,7 @@
                 @NonNull MediaRouter.RouteInfo selectedRoute,
                 int reason,
                 @NonNull MediaRouter.RouteInfo requestedRoute) {
-            mSelectedRouteChangeConditionVariable.open();
+            mRouteSelectedConditionVariable.open();
         }
 
         @Override
@@ -294,6 +661,43 @@
             if (getCurrentRoutesAsMap().keySet().containsAll(mRouteIdsPending)) {
                 mPendingRoutesConditionVariable.open();
             }
+            checkPendingRouteVolume(route);
+        }
+
+        @Override
+        public void onRouteChanged(
+                @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
+            mChangedRoute = route;
+            MediaRouter.GroupRouteInfo groupRoute = route.asGroup();
+            if (groupRoute != null) {
+                List<String> selectedRouteIds = new ArrayList<>();
+                for (MediaRouter.RouteInfo selectedRoute : route.getSelectedRoutesInGroup()) {
+                    selectedRouteIds.add(selectedRoute.getDescriptorId());
+                }
+                if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_ADDED)) {
+                    mMemberRouteAddedConditionVariable.open();
+                } else if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_REMOVED)) {
+                    mMemberRouteRemovedConditionVariable.open();
+                } else if (selectedRouteIds.containsAll(EXPECTED_ROUTE_IDS_AFTER_ROUTE_UPDATED)) {
+                    mMemberRoutesUpdatedConditionVariable.open();
+                }
+                boolean isVolumeChanged = false;
+                if (route.getVolume()
+                        != StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE) {
+                    isVolumeChanged = true;
+                } else {
+                    for (MediaRouter.RouteInfo memberRoute : route.getSelectedRoutesInGroup()) {
+                        if (memberRoute.getVolume()
+                                != StubDynamicMediaRouteProviderService.VOLUME_INITIAL_VALUE) {
+                            isVolumeChanged = true;
+                        }
+                    }
+                }
+                if (isVolumeChanged) {
+                    mGroupVolumeChangedConditionVariable.open();
+                }
+            }
+            checkPendingRouteVolume(route);
         }
 
         @Override
@@ -304,7 +708,7 @@
             mRouteConnectionState = RouteConnectionState.STATE_CONNECTED;
             mConnectedRoute = connectedRoute;
             mRequestedRoute = requestedRoute;
-            mRouteConnectionConditionVariable.open();
+            mRouteConnectedConditionVariable.open();
         }
 
         @Override
@@ -317,7 +721,17 @@
             mDisconnectedRoute = disconnectedRoute;
             mRequestedRoute = requestedRoute;
             mRouteDisconnectedReason = reason;
-            mRouteConnectionConditionVariable.open();
+            mRouteDisconnectedConditionVariable.open();
+        }
+
+        private void checkPendingRouteVolume(MediaRouter.RouteInfo route) {
+            String routeDescriptorId = route.getDescriptorId();
+            if (routeDescriptorId != null) {
+                Integer expectedVolume = mRouteVolumePending.get(routeDescriptorId);
+                if (expectedVolume != null && route.getVolume() == expectedVolume) {
+                    mPendingRouteVolumeConditionVariable.open();
+                }
+            }
         }
     }
 }
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
index 1cebcb0..7a6c809 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubDynamicMediaRouteProviderService.java
@@ -16,7 +16,9 @@
 package androidx.mediarouter.media;
 
 import android.content.Context;
+import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Bundle;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -37,18 +39,33 @@
     public static final String CATEGORY_DYNAMIC_PROVIDER_TEST =
             "androidx.mediarouter.media.CATEGORY_DYNAMIC_PROVIDER_TEST";
 
+    public static final int VOLUME_INITIAL_VALUE = 8;
+    public static final int VOLUME_MAX = 20;
     public static final String ROUTE_ID_GROUP = "route_id_group";
     public static final String ROUTE_NAME_GROUP = "Group route name";
     public static final String ROUTE_ID_1 = "route_id1";
     public static final String ROUTE_NAME_1 = "Sample Route 1";
+    public static final boolean ROUTE_GROUPABLE_1 = true;
+    public static final boolean ROUTE_TRANSFERABLE_1 = false;
     public static final String ROUTE_ID_2 = "route_id2";
     public static final String ROUTE_NAME_2 = "Sample Route 2";
+    public static final boolean ROUTE_GROUPABLE_2 = true;
+    public static final boolean ROUTE_TRANSFERABLE_2 = false;
+    public static final String ROUTE_ID_3 = "route_id3";
+    public static final String ROUTE_NAME_3 = "Sample Route 3";
+    public static final boolean ROUTE_GROUPABLE_3 = false;
+    public static final boolean ROUTE_TRANSFERABLE_3 = true;
     public static final List<IntentFilter> CONTROL_FILTERS_TEST = new ArrayList<>();
+    public static final String SEND_CONTROL_REQUEST_KEY = "send_control_request_key";
+    public static final String SEND_CONTROL_REQUEST_VALUE = "send_control_request_value";
+    public static final Bundle SEND_CONTROL_REQUEST_RESULT = new Bundle();
 
     static {
         IntentFilter filter = new IntentFilter();
         filter.addCategory(CATEGORY_DYNAMIC_PROVIDER_TEST);
         CONTROL_FILTERS_TEST.add(filter);
+
+        SEND_CONTROL_REQUEST_RESULT.putString(SEND_CONTROL_REQUEST_KEY, SEND_CONTROL_REQUEST_VALUE);
     }
 
     @Override
@@ -59,7 +76,7 @@
     private static final class Provider extends MediaRouteProvider {
         private final Map<String, MediaRouteDescriptor> mRoutes = new ArrayMap<>();
         private final Map<String, StubRouteController> mControllers = new ArrayMap<>();
-        private final MediaRouteDescriptor mGroupDescriptor;
+        private MediaRouteDescriptor mGroupDescriptor;
         private final Set<String> mCurrentSelectedRouteIds = new HashSet<>();
         private boolean mCurrentlyScanning = false;
         @Nullable private DynamicGroupRouteController mGroupController;
@@ -69,17 +86,30 @@
             mGroupDescriptor =
                     new MediaRouteDescriptor.Builder(ROUTE_ID_GROUP, ROUTE_NAME_GROUP)
                             .addControlFilters(CONTROL_FILTERS_TEST)
+                            .setVolumeMax(VOLUME_MAX)
+                            .setVolume(VOLUME_INITIAL_VALUE)
                             .build();
             MediaRouteDescriptor route1 =
                     new MediaRouteDescriptor.Builder(ROUTE_ID_1, ROUTE_NAME_1)
                             .addControlFilters(CONTROL_FILTERS_TEST)
+                            .setVolumeMax(VOLUME_MAX)
+                            .setVolume(VOLUME_INITIAL_VALUE)
                             .build();
             MediaRouteDescriptor route2 =
                     new MediaRouteDescriptor.Builder(ROUTE_ID_2, ROUTE_NAME_2)
                             .addControlFilters(CONTROL_FILTERS_TEST)
+                            .setVolumeMax(VOLUME_MAX)
+                            .setVolume(VOLUME_INITIAL_VALUE)
+                            .build();
+            MediaRouteDescriptor route3 =
+                    new MediaRouteDescriptor.Builder(ROUTE_ID_3, ROUTE_NAME_3)
+                            .addControlFilters(CONTROL_FILTERS_TEST)
+                            .setVolumeMax(VOLUME_MAX)
+                            .setVolume(VOLUME_INITIAL_VALUE)
                             .build();
             mRoutes.put(route1.getId(), route1);
             mRoutes.put(route2.getId(), route2);
+            mRoutes.put(route3.getId(), route3);
         }
 
         // MediaRouteProvider implementation.
@@ -127,6 +157,7 @@
                     "onCreateDynamicGroupRouteController with initialMemberRouteId = "
                             + initialMemberRouteId);
             mGroupController = new StubDynamicRouteController();
+            mCurrentSelectedRouteIds.add(initialMemberRouteId);
             return mGroupController;
         }
 
@@ -150,12 +181,41 @@
                                         mCurrentSelectedRouteIds.contains(route.getId())
                                                 ? DynamicRouteDescriptor.SELECTED
                                                 : DynamicRouteDescriptor.UNSELECTED)
+                                .setIsGroupable(getIsGroupable(route.getId()))
+                                .setIsTransferable(getIsTransferable(route.getId()))
+                                .setIsUnselectable(mCurrentSelectedRouteIds.contains(route.getId()))
                                 .build();
                 result.add(dynamicDescriptor);
             }
             return result;
         }
 
+        private boolean getIsGroupable(String routeId) {
+            switch (routeId) {
+                case ROUTE_ID_1:
+                    return ROUTE_GROUPABLE_1;
+                case ROUTE_ID_2:
+                    return ROUTE_GROUPABLE_2;
+                case ROUTE_ID_3:
+                    return ROUTE_GROUPABLE_3;
+                default:
+                    return false;
+            }
+        }
+
+        private boolean getIsTransferable(String routeId) {
+            switch (routeId) {
+                case ROUTE_ID_1:
+                    return ROUTE_TRANSFERABLE_1;
+                case ROUTE_ID_2:
+                    return ROUTE_TRANSFERABLE_2;
+                case ROUTE_ID_3:
+                    return ROUTE_TRANSFERABLE_3;
+                default:
+                    return false;
+            }
+        }
+
         // Internal classes.
 
         private class StubRouteController extends RouteController {
@@ -188,6 +248,33 @@
                                 .build());
                 publishProviderState();
             }
+
+            @Override
+            public void onSetVolume(int volume) {
+                MediaRouteDescriptor route = mRoutes.get(mRouteId);
+                if (route == null) {
+                    return;
+                }
+                mRoutes.put(
+                        mRouteId,
+                        new MediaRouteDescriptor.Builder(route).setVolume(volume).build());
+                publishProviderState();
+            }
+
+            @Override
+            public void onUpdateVolume(int delta) {
+                MediaRouteDescriptor route = mRoutes.get(mRouteId);
+                if (route == null) {
+                    return;
+                }
+                int currentVolume = route.getVolume();
+                mRoutes.put(
+                        mRouteId,
+                        new MediaRouteDescriptor.Builder(route)
+                                .setVolume(currentVolume + delta)
+                                .build());
+                publishProviderState();
+            }
         }
 
         private class StubDynamicRouteController extends DynamicGroupRouteController {
@@ -200,6 +287,9 @@
 
             @Override
             public void onUpdateMemberRoutes(@Nullable List<String> routeIds) {
+                if (routeIds == null) {
+                    return;
+                }
                 Log.i(TAG, "StubDynamicRouteController.onUpdateMemberRoutes()");
                 mCurrentSelectedRouteIds.clear();
                 mCurrentSelectedRouteIds.addAll(routeIds);
@@ -222,9 +312,41 @@
                 publishState();
             }
 
+            @Override
+            public boolean onControlRequest(
+                    @NonNull Intent intent, @Nullable MediaRouter.ControlRequestCallback callback) {
+                if (callback != null) {
+                    callback.onResult(SEND_CONTROL_REQUEST_RESULT);
+                }
+                return true;
+            }
+
+            @Override
+            public void onSetVolume(int volume) {
+                mGroupDescriptor =
+                        new MediaRouteDescriptor.Builder(mGroupDescriptor)
+                                .setVolume(volume)
+                                .build();
+                publishState();
+            }
+
+            @Override
+            public void onUpdateVolume(int delta) {
+                int currentVolume = mGroupDescriptor.getVolume();
+                mGroupDescriptor =
+                        new MediaRouteDescriptor.Builder(mGroupDescriptor)
+                                .setVolume(currentVolume + delta)
+                                .build();
+                publishState();
+            }
+
             private void publishState() {
-                Log.i(TAG, "StubDynamicRouteController.publishState()");
-                notifyDynamicRoutesChanged(mGroupDescriptor, buildDynamicRouteDescriptors());
+                Collection<DynamicRouteDescriptor> dynamicRoutes = buildDynamicRouteDescriptors();
+                Log.i(
+                        TAG,
+                        "StubDynamicRouteController.publishState() with dynamicRoutes.size() = "
+                                + dynamicRoutes.size());
+                notifyDynamicRoutesChanged(mGroupDescriptor, dynamicRoutes);
             }
         }
     }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
index aa1bf82..30172fb 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteControllerDialog.java
@@ -232,7 +232,7 @@
     }
 
     private boolean isGroup() {
-        return mRoute.isGroup() && mRoute.getRoutesInGroup().size() > 1;
+        return mRoute.isGroup() && mRoute.getSelectedRoutesInGroup().size() > 1;
     }
 
     /**
@@ -625,7 +625,9 @@
         int volumeGroupListCount = mGroupMemberRoutes.size();
         // Scale down volume group list items in landscape mode.
         int expandedGroupListHeight =
-                isGroup() ? mVolumeGroupListItemHeight * mRoute.getRoutesInGroup().size() : 0;
+                isGroup()
+                        ? mVolumeGroupListItemHeight * mRoute.getSelectedRoutesInGroup().size()
+                        : 0;
         if (volumeGroupListCount > 0) {
             expandedGroupListHeight += mVolumeGroupListPaddingTop;
         }
@@ -745,7 +747,7 @@
     }
 
     private void rebuildVolumeGroupList(boolean animate) {
-        List<MediaRouter.RouteInfo> routes = mRoute.getRoutesInGroup();
+        List<MediaRouter.RouteInfo> routes = mRoute.getSelectedRoutesInGroup();
         if (routes.isEmpty()) {
             mGroupMemberRoutes.clear();
             mVolumeGroupAdapter.notifyDataSetChanged();
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
index 46260d7..697f70f 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteDynamicControllerDialog.java
@@ -574,11 +574,12 @@
     @SuppressWarnings("WeakerAccess") /* synthetic access */
     List<MediaRouter.RouteInfo> getCurrentGroupableRoutes() {
         List<MediaRouter.RouteInfo> groupableRoutes = new ArrayList<>();
-        for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
-            MediaRouter.RouteInfo.DynamicGroupState state =
-                    mSelectedRoute.getDynamicGroupState(route);
-            if (state != null && state.isGroupable()) {
-                groupableRoutes.add(route);
+        MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+        if (groupRouteInfo != null) {
+            for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
+                if (groupRouteInfo.isGroupable(route)) {
+                    groupableRoutes.add(route);
+                }
             }
         }
         return groupableRoutes;
@@ -622,17 +623,16 @@
         mGroupableRoutes.clear();
         mTransferableRoutes.clear();
 
-        mMemberRoutes.addAll(mSelectedRoute.getRoutesInGroup());
-        for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
-            MediaRouter.RouteInfo.DynamicGroupState state =
-                    mSelectedRoute.getDynamicGroupState(route);
-            if (state == null) continue;
-
-            if (state.isGroupable()) {
-                mGroupableRoutes.add(route);
-            }
-            if (state.isTransferable()) {
-                mTransferableRoutes.add(route);
+        mMemberRoutes.addAll(mSelectedRoute.getSelectedRoutesInGroup());
+        MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+        if (groupRouteInfo != null) {
+            for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
+                if (groupRouteInfo.isGroupable(route)) {
+                    mGroupableRoutes.add(route);
+                }
+                if (groupRouteInfo.isTransferable(route)) {
+                    mTransferableRoutes.add(route);
+                }
             }
         }
 
@@ -784,7 +784,7 @@
         }
 
         boolean isGroupVolumeNeeded() {
-            return mEnableGroupVolumeUX && mSelectedRoute.getRoutesInGroup().size() > 1;
+            return mEnableGroupVolumeUX && mSelectedRoute.getSelectedRoutesInGroup().size() > 1;
         }
 
         void animateLayoutHeight(final View view, int targetHeight) {
@@ -821,12 +821,12 @@
         }
 
         void mayUpdateGroupVolume(MediaRouter.RouteInfo route, boolean selected) {
-            List<MediaRouter.RouteInfo> members = mSelectedRoute.getRoutesInGroup();
+            List<MediaRouter.RouteInfo> members = mSelectedRoute.getSelectedRoutesInGroup();
             // Assume we have at least one member route(itself)
             int memberCount = Math.max(1, members.size());
 
             if (route.isGroup()) {
-                for (MediaRouter.RouteInfo changedRoute : route.getRoutesInGroup()) {
+                for (MediaRouter.RouteInfo changedRoute : route.getSelectedRoutesInGroup()) {
                     if (members.contains(changedRoute) != selected) {
                         memberCount += selected ? 1 : -1;
                     }
@@ -1126,15 +1126,16 @@
                             boolean isGroup = mRoute.isGroup();
 
                             if (selected) {
-                                mRouter.addMemberToDynamicGroup(mRoute);
+                                mRouter.addRouteToSelectedGroup(mRoute);
                             } else {
-                                mRouter.removeMemberFromDynamicGroup(mRoute);
+                                mRouter.removeRouteFromSelectedGroup(mRoute);
                             }
                             showSelectingProgress(selected, !isGroup);
                             if (isGroup) {
                                 List<MediaRouter.RouteInfo> selectedRoutes =
-                                        mSelectedRoute.getRoutesInGroup();
-                                for (MediaRouter.RouteInfo route : mRoute.getRoutesInGroup()) {
+                                        mSelectedRoute.getSelectedRoutesInGroup();
+                                for (MediaRouter.RouteInfo route :
+                                        mRoute.getSelectedRoutesInGroup()) {
                                     if (selectedRoutes.contains(route) != selected) {
                                         MediaRouteVolumeSliderHolder volumeSliderHolder =
                                                 mVolumeSliderHolderMap.get(route.getId());
@@ -1177,11 +1178,11 @@
                 if (route.isSelected()) {
                     return true;
                 }
-                MediaRouter.RouteInfo.DynamicGroupState state =
-                        mSelectedRoute.getDynamicGroupState(route);
-                return state != null && state.getSelectionState()
-                        == MediaRouteProvider.DynamicGroupRouteController
-                        .DynamicRouteDescriptor.SELECTED;
+                MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+                return groupRouteInfo != null
+                        && groupRouteInfo.getSelectionState(route)
+                                == MediaRouteProvider.DynamicGroupRouteController
+                                        .DynamicRouteDescriptor.SELECTED;
             }
 
             private boolean isEnabled(MediaRouter.RouteInfo route) {
@@ -1190,14 +1191,13 @@
                     return false;
                 }
                 // The last member route can not be removed.
-                if (isSelected(route) && mSelectedRoute.getRoutesInGroup().size() < 2) {
+                if (isSelected(route) && mSelectedRoute.getSelectedRoutesInGroup().size() < 2) {
                     return false;
                 }
                 // Selected route that can't be unselected has to be disabled.
                 if (isSelected(route)) {
-                    MediaRouter.RouteInfo.DynamicGroupState state =
-                            mSelectedRoute.getDynamicGroupState(route);
-                    return state != null && state.isUnselectable();
+                    MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+                    return groupRouteInfo != null && groupRouteInfo.isUnselectable(route);
                 }
                 return true;
             }
@@ -1206,8 +1206,8 @@
                 MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
 
                 // This is required to sync volume and the name of the route
-                if (route == mSelectedRoute && route.getRoutesInGroup().size() > 0) {
-                    for (MediaRouter.RouteInfo memberRoute : route.getRoutesInGroup()) {
+                if (route == mSelectedRoute && route.getSelectedRoutesInGroup().size() > 0) {
+                    for (MediaRouter.RouteInfo memberRoute : route.getSelectedRoutesInGroup()) {
                         if (!mGroupableRoutes.contains(memberRoute)) {
                             route = memberRoute;
                             break;
@@ -1283,7 +1283,8 @@
             }
 
             private boolean isEnabled(MediaRouter.RouteInfo route) {
-                List<MediaRouter.RouteInfo> currentMemberRoutes = mSelectedRoute.getRoutesInGroup();
+                List<MediaRouter.RouteInfo> currentMemberRoutes =
+                        mSelectedRoute.getSelectedRoutesInGroup();
                 // Disable individual route if the only member of dynamic group is that route.
                 if (currentMemberRoutes.size() == 1 && currentMemberRoutes.get(0) == route) {
                     return false;
@@ -1357,14 +1358,15 @@
             boolean shouldRefreshRoute = false;
             if (route == mSelectedRoute && route.getDynamicGroupController() != null) {
                 for (MediaRouter.RouteInfo memberRoute : route.getProvider().getRoutes()) {
-                    if (mSelectedRoute.getRoutesInGroup().contains(memberRoute)) {
+                    if (mSelectedRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
                         continue;
                     }
-                    MediaRouter.RouteInfo.DynamicGroupState state =
-                            mSelectedRoute.getDynamicGroupState(memberRoute);
-
+                    MediaRouter.GroupRouteInfo groupRouteInfo = mSelectedRoute.asGroup();
+                    if (groupRouteInfo == null) {
+                        continue;
+                    }
                     // Refresh items only when a new groupable route is found.
-                    if (state != null && state.isGroupable()
+                    if (groupRouteInfo.isGroupable(memberRoute)
                             && !mGroupableRoutes.contains(memberRoute)) {
                         shouldRefreshRoute = true;
                         break;
@@ -1381,8 +1383,8 @@
         }
 
         @Override
-        public void onRouteVolumeChanged(@NonNull MediaRouter router,
-                @NonNull MediaRouter.RouteInfo route) {
+        public void onRouteVolumeChanged(
+                @NonNull MediaRouter router, @NonNull MediaRouter.RouteInfo route) {
             int volume = route.getVolume();
             if (DEBUG) {
                 Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 9463a7a..c7ea94e 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -21,6 +21,21 @@
 import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_FORCE_DISCOVERY;
 import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN;
 import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.ADD_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.REMOVE_ROUTE_SUCCESSFUL;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+import static androidx.mediarouter.media.MediaRouter.GroupRouteInfo.UPDATE_ROUTES_SUCCESSFUL;
 import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_DISCONNECTED;
 import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_ROUTE_CHANGED;
 import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_STOPPED;
@@ -218,10 +233,9 @@
             MediaRouter.RouteInfo route,
             Intent intent,
             MediaRouter.ControlRequestCallback callback) {
-        if (route == mSelectedRoute && mSelectedRouteController != null) {
-            if (mSelectedRouteController.onControlRequest(intent, callback)) {
-                return;
-            }
+        MediaRouteProvider.RouteController controller = getRouteController(route);
+        if (controller != null && controller.onControlRequest(intent, callback)) {
+            return;
         }
         if (mTransferNotifier != null
                 && route == mTransferNotifier.mToRoute
@@ -236,27 +250,42 @@
     }
 
     /* package */ void requestSetVolume(MediaRouter.RouteInfo route, int volume) {
-        if (route == mSelectedRoute && mSelectedRouteController != null) {
-            mSelectedRouteController.onSetVolume(volume);
-        } else {
-            MediaRouteProvider.RouteController controller =
-                    mRouteControllerMap.get(route.mUniqueId);
-            if (controller != null) {
-                controller.onSetVolume(volume);
-            }
+        MediaRouteProvider.RouteController controller = getRouteController(route);
+        if (controller != null) {
+            controller.onSetVolume(volume);
         }
     }
 
     /* package */ void requestUpdateVolume(MediaRouter.RouteInfo route, int delta) {
+        MediaRouteProvider.RouteController controller = getRouteController(route);
+        if (controller != null) {
+            controller.onUpdateVolume(delta);
+        }
+    }
+
+    @Nullable
+    private MediaRouteProvider.RouteController getRouteController(MediaRouter.RouteInfo route) {
         if (route == mSelectedRoute && mSelectedRouteController != null) {
-            mSelectedRouteController.onUpdateVolume(delta);
-        } else {
-            MediaRouteProvider.RouteController controller =
-                    mRouteControllerMap.get(route.mUniqueId);
-            if (controller != null) {
-                controller.onUpdateVolume(delta);
+            return mSelectedRouteController;
+        }
+        if (route instanceof MediaRouter.GroupRouteInfo) {
+            MediaRouter.GroupRouteInfo groupRoute = (MediaRouter.GroupRouteInfo) route;
+            if (groupRoute.isConnected()) {
+                RouteConnection routeConnection = getRouteConnection(groupRoute);
+                return (routeConnection != null) ? routeConnection.mController : null;
             }
         }
+        MediaRouteProvider.RouteController controller = mRouteControllerMap.get(route.mUniqueId);
+        if (controller != null) {
+            return controller;
+        }
+        for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
+            controller = routeConnection.mRouteIdToMemberControllerMap.get(route.mUniqueId);
+            if (controller != null) {
+                break;
+            }
+        }
+        return controller;
     }
 
     /* package */ MediaRouter.RouteInfo getRoute(String uniqueId) {
@@ -360,67 +389,183 @@
     }
 
     @NonNull
-    /* package */ List<MediaRouter.RouteInfo> getConnectedRoutes() {
-        List<MediaRouter.RouteInfo> connectedRoutes = new ArrayList<>();
+    /* package */ List<MediaRouter.GroupRouteInfo> getConnectedGroupRoutes() {
+        List<MediaRouter.GroupRouteInfo> connectedGroupRoutes = new ArrayList<>();
         for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
             if (routeConnection.mGroupRoute != null) {
-                connectedRoutes.add(routeConnection.mGroupRoute);
+                connectedGroupRoutes.add(routeConnection.mGroupRoute);
             }
         }
-        return connectedRoutes;
+        return connectedGroupRoutes;
     }
 
     @Nullable
-        /* package */ MediaRouter.RouteInfo.DynamicGroupState getDynamicGroupState(
-            MediaRouter.RouteInfo route) {
-        return mSelectedRoute.getDynamicGroupState(route);
+    private RouteConnection getRouteConnection(@NonNull MediaRouter.GroupRouteInfo groupRoute) {
+        for (RouteConnection routeConnection : mRouteIdToRouteConnectionMap.values()) {
+            if (routeConnection.mGroupRoute == groupRoute) {
+                return routeConnection;
+            }
+        }
+        return null;
     }
 
-    /* package */ void addMemberToDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
-        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
-            throw new IllegalStateException("There is no currently selected dynamic group route.");
-        }
-        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
-        if (mSelectedRoute.getRoutesInGroup().contains(route)
-                || state == null
-                || !state.isGroupable()) {
-            Log.w(TAG, "Ignoring attempt to add a non-groupable route to dynamic group : " + route);
+    /* package */ void addRouteToSelectedGroup(@NonNull MediaRouter.RouteInfo route) {
+        MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+        if (selectedGroupRoute == null) {
+            Log.w(TAG, "Ignoring attempt to add a member route to a selected non-group route");
             return;
         }
-        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
-                .onAddMemberRoute(route.getDescriptorId());
+        addRouteToGroup(selectedGroupRoute, route);
     }
 
-    /* package */ void removeMemberFromDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
-        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
-            throw new IllegalStateException("There is no currently selected dynamic group route.");
-        }
-        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
-        if (!mSelectedRoute.getRoutesInGroup().contains(route)
-                || state == null
-                || !state.isUnselectable()) {
-            Log.w(TAG, "Ignoring attempt to remove a non-unselectable member route : " + route);
+    /* package */ void removeRouteFromSelectedGroup(@NonNull MediaRouter.RouteInfo route) {
+        MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+        if (selectedGroupRoute == null) {
+            Log.w(TAG, "Ignoring attempt to remove a member route from a selected non-group route");
             return;
         }
-        if (mSelectedRoute.getRoutesInGroup().size() <= 1) {
-            Log.w(TAG, "Ignoring attempt to remove the last member route.");
-            return;
-        }
-        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
-                .onRemoveMemberRoute(route.getDescriptorId());
+        removeRouteFromGroup(selectedGroupRoute, route);
     }
 
     /* package */ void transferToRoute(@NonNull MediaRouter.RouteInfo route) {
-        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
-            throw new IllegalStateException("There is no currently selected dynamic group route.");
-        }
-        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
-        if (state == null || !state.isTransferable()) {
-            Log.w(TAG, "Ignoring attempt to transfer to a non-transferable route.");
+        MediaRouter.GroupRouteInfo selectedGroupRoute = mSelectedRoute.asGroup();
+        if (selectedGroupRoute == null) {
+            Log.w(TAG, "Ignoring attempt to transfer for a selected non-group route");
             return;
         }
-        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
-                .onUpdateMemberRoutes(Collections.singletonList(route.getDescriptorId()));
+        updateRoutesForGroup(selectedGroupRoute, Collections.singletonList(route));
+    }
+
+    @MediaRouter.GroupRouteInfo.AddRouteReason
+    /* package */ int addRouteToGroup(
+            @NonNull MediaRouter.GroupRouteInfo groupRoute,
+            @NonNull MediaRouter.RouteInfo memberRoute) {
+        if (!groupRoute.isGroupable(memberRoute)) {
+            Log.w(TAG, "Ignoring attempt to add a non-groupable member route: " + memberRoute);
+            return ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE;
+        }
+        if (groupRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
+            Log.w(TAG, "Ignoring attempt to add an existing member route: " + memberRoute);
+            return ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP;
+        }
+        if (groupRoute.isSelected()) {
+            if (!(mSelectedRouteController
+                    instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+                throw new IllegalStateException(
+                        "There is no currently selected dynamic group route.");
+            }
+            ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+                    .onAddMemberRoute(memberRoute.getDescriptorId());
+        } else if (groupRoute.isConnected()) {
+            RouteConnection routeConnection = getRouteConnection(groupRoute);
+            if (routeConnection == null) {
+                Log.w(
+                        TAG,
+                        "Ignoring attempt to add a route to a non-available connected route: "
+                                + groupRoute);
+                return ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+            }
+            routeConnection.mController.onAddMemberRoute(memberRoute.getDescriptorId());
+        } else {
+            Log.w(
+                    TAG,
+                    "Ignoring attempt to add a route to an unsupported group route:" + groupRoute);
+            return ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+        }
+        return ADD_ROUTE_SUCCESSFUL;
+    }
+
+    @MediaRouter.GroupRouteInfo.RemoveRouteReason
+    /* package */ int removeRouteFromGroup(
+            @NonNull MediaRouter.GroupRouteInfo groupRoute,
+            @NonNull MediaRouter.RouteInfo memberRoute) {
+        if (!groupRoute.isUnselectable(memberRoute)) {
+            Log.w(
+                    TAG,
+                    "Ignoring attempt to remove a non-unselectable member route: " + memberRoute);
+            return REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE;
+        }
+        if (!groupRoute.getSelectedRoutesInGroup().contains(memberRoute)) {
+            Log.w(TAG, "Ignoring attempt to remove a non-in-group member route: " + memberRoute);
+            return REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP;
+        }
+        if (groupRoute.getSelectedRoutesInGroup().size() <= 1) {
+            Log.w(TAG, "Ignoring attempt to remove the last member route.");
+            return REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP;
+        }
+        if (groupRoute.isSelected()) {
+            if (!(mSelectedRouteController
+                    instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+                throw new IllegalStateException(
+                        "There is no currently selected dynamic group route.");
+            }
+            ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+                    .onRemoveMemberRoute(memberRoute.getDescriptorId());
+        } else if (groupRoute.isConnected()) {
+            RouteConnection routeConnection = getRouteConnection(groupRoute);
+            if (routeConnection == null) {
+                Log.w(
+                        TAG,
+                        "Ignoring attempt to update routes for a non-available connected route: "
+                                + groupRoute);
+                return REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+            }
+            routeConnection.mController.onRemoveMemberRoute(memberRoute.getDescriptorId());
+        } else {
+            Log.w(
+                    TAG,
+                    "Ignoring attempt to remove a route from an unsupported group route:"
+                            + groupRoute);
+            return REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+        }
+        return REMOVE_ROUTE_SUCCESSFUL;
+    }
+
+    @MediaRouter.GroupRouteInfo.UpdateRoutesReason
+    /* package */ int updateRoutesForGroup(
+            @NonNull MediaRouter.GroupRouteInfo groupRoute,
+            @NonNull List<MediaRouter.RouteInfo> memberRoutes) {
+        List<String> memberRouteDescriptorIds = new ArrayList<>();
+        for (MediaRouter.RouteInfo route : memberRoutes) {
+            if (!groupRoute.isTransferable(route)) {
+                Log.w(
+                        TAG,
+                        "Ignoring attempt to update the group with a non-transferable route: "
+                                + route);
+                continue;
+            }
+            memberRouteDescriptorIds.add(route.getDescriptorId());
+        }
+        if (memberRouteDescriptorIds.isEmpty()) {
+            Log.w(TAG, "Ignoring attempt to update the group with non-transferable routes");
+            return UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE;
+        }
+        if (groupRoute.isSelected()) {
+            if (!(mSelectedRouteController
+                    instanceof MediaRouteProvider.DynamicGroupRouteController)) {
+                throw new IllegalStateException(
+                        "There is no currently selected dynamic group route.");
+            }
+            ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
+                    .onUpdateMemberRoutes(memberRouteDescriptorIds);
+        } else if (groupRoute.isConnected()) {
+            RouteConnection routeConnection = getRouteConnection(groupRoute);
+            if (routeConnection == null) {
+                Log.w(
+                        TAG,
+                        "Ignoring attempt to update routes for a non-available connected route: "
+                                + groupRoute);
+                return UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION;
+            }
+            routeConnection.mController.onUpdateMemberRoutes(memberRouteDescriptorIds);
+        } else {
+            Log.w(
+                    TAG,
+                    "Ignoring attempt to update routes for an unsupported group route:"
+                            + groupRoute);
+            return UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE;
+        }
+        return UPDATE_ROUTES_SUCCESSFUL;
     }
 
     /**
@@ -1189,7 +1334,7 @@
         if (!mSelectedRoute.isGroup()) {
             return;
         }
-        List<MediaRouter.RouteInfo> routes = mSelectedRoute.getRoutesInGroup();
+        List<MediaRouter.RouteInfo> routes = mSelectedRoute.getSelectedRoutesInGroup();
         // Build a set of descriptor IDs for the new route group.
         Set<String> idSet = new HashSet<>();
         for (MediaRouter.RouteInfo route : routes) {
@@ -1283,8 +1428,8 @@
                                 String groupId = groupRouteDescriptor.getId();
 
                                 String uniqueId = assignRouteUniqueId(provider, groupId);
-                                MediaRouter.RouteInfo route =
-                                        new MediaRouter.RouteInfo(provider, groupId, uniqueId);
+                                MediaRouter.GroupRouteInfo route =
+                                        new MediaRouter.GroupRouteInfo(provider, groupId, uniqueId);
                                 route.maybeUpdateDescriptor(groupRouteDescriptor);
 
                                 if (mSelectedRoute == route) {
@@ -1307,7 +1452,11 @@
                                     updateRouteDescriptorAndNotify(
                                             mSelectedRoute, groupRouteDescriptor);
                                 }
-                                mSelectedRoute.updateDynamicDescriptors(routes);
+                                MediaRouter.GroupRouteInfo groupRouteInfo =
+                                        mSelectedRoute.asGroup();
+                                if (groupRouteInfo != null) {
+                                    groupRouteInfo.updateDynamicDescriptors(routes);
+                                }
                             }
                         }
                     };
@@ -1509,18 +1658,20 @@
 
         private final MediaRouter.RouteInfo mRequestedRoute;
         private final MediaRouteProvider.DynamicGroupRouteController mController;
+        private final Map<String, MediaRouteProvider.RouteController> mRouteIdToMemberControllerMap;
         private final Handler mHandler;
         private final Runnable mRouteConnectionTimeoutRunnable;
         // Holds the {@link MediaRouter.RouteInfo} of the route that corresponds to the dynamic
         // group created as the result of connecting to {@link mRequestedRoute}. or null if the
         // dynamic group hasn't been created by the provider yet.
-        @Nullable private MediaRouter.RouteInfo mGroupRoute;
+        @Nullable private MediaRouter.GroupRouteInfo mGroupRoute;
 
         /* package */ RouteConnection(
                 MediaRouter.RouteInfo requestedRoute,
                 MediaRouteProvider.DynamicGroupRouteController controller) {
             mRequestedRoute = requestedRoute;
             mController = controller;
+            mRouteIdToMemberControllerMap = new HashMap<>();
             mHandler = new Handler(Looper.getMainLooper());
             mRouteConnectionTimeoutRunnable = this::routeConnectionTimeout;
         }
@@ -1550,7 +1701,9 @@
             if (mController != controller) {
                 return;
             }
-            if (groupRouteDescriptor == null && mGroupRoute == null) {
+            MediaRouteDescriptor updatedGroupRouteDescriptor =
+                    updateGroupMemberIdsIfNeeded(groupRouteDescriptor, routes);
+            if (updatedGroupRouteDescriptor == null && mGroupRoute == null) {
                 // The provider has not yet set a group route descriptor, which is needed to
                 // establish the connection. We need to wait for a group route descriptor.
                 Log.e(
@@ -1559,24 +1712,107 @@
                                 + mRequestedRoute);
                 return;
             }
+
             if (mGroupRoute == null) {
-                // groupRouteDescriptor cannot be null.
-                mGroupRoute = convertFromRouteDescriptorToRouteInfo(groupRouteDescriptor);
+                // updatedGroupRouteDescriptor cannot be null.
+                mGroupRoute = convertFromRouteDescriptorToRouteInfo(updatedGroupRouteDescriptor);
+                mGroupRoute.updateDynamicDescriptors(routes);
                 notifyRouteConnected();
             } else {
                 // mGroupRoute cannot be null.
+                updateRouteDescriptorAndNotify(mGroupRoute, updatedGroupRouteDescriptor);
                 mGroupRoute.updateDynamicDescriptors(routes);
-                updateRouteDescriptorAndNotify(mGroupRoute, groupRouteDescriptor);
+            }
+            updateMemberRouteControllers();
+        }
+
+        @Nullable
+        private MediaRouteDescriptor updateGroupMemberIdsIfNeeded(
+                @Nullable MediaRouteDescriptor groupRouteDescriptor,
+                @NonNull
+                        Collection<
+                                        MediaRouteProvider.DynamicGroupRouteController
+                                                .DynamicRouteDescriptor>
+                                routes) {
+            if (groupRouteDescriptor == null) {
+                return groupRouteDescriptor;
+            }
+            boolean memberRoutesMatched =
+                    groupRouteDescriptor.getGroupMemberIds().size() == routes.size();
+            if (memberRoutesMatched) {
+                for (MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor route :
+                        routes) {
+                    String routeDescriptorId = route.getRouteDescriptor().getId();
+                    if (!groupRouteDescriptor.getGroupMemberIds().contains(routeDescriptorId)) {
+                        memberRoutesMatched = false;
+                        break;
+                    }
+                }
+            }
+            if (memberRoutesMatched) {
+                return groupRouteDescriptor;
+            }
+            List<String> groupMemberIds = new ArrayList<>();
+            for (MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor route :
+                    routes) {
+                groupMemberIds.add(route.getRouteDescriptor().getId());
+            }
+            return new MediaRouteDescriptor.Builder(groupRouteDescriptor)
+                    .clearGroupMemberIds()
+                    .addGroupMemberIds(groupMemberIds)
+                    .build();
+        }
+
+        private void updateMemberRouteControllers() {
+            if (mGroupRoute == null) {
+                return;
+            }
+            Set<String> routeIdsToRemove = new HashSet<>(mRouteIdToMemberControllerMap.keySet());
+            for (MediaRouter.RouteInfo route : mGroupRoute.mSelectedRoutesInGroup) {
+                routeIdsToRemove.remove(route.mUniqueId);
+                if (!mRouteIdToMemberControllerMap.containsKey(route.mUniqueId)) {
+                    createAndConnectMemberRouteController(route);
+                }
+            }
+
+            for (String routeId : routeIdsToRemove) {
+                disconnectAndRemoveMemberRouteController(routeId);
             }
         }
 
-        private MediaRouter.RouteInfo convertFromRouteDescriptorToRouteInfo(
+        private void createAndConnectMemberRouteController(MediaRouter.RouteInfo route) {
+            if (mRouteIdToMemberControllerMap.containsKey(route.mUniqueId) || mGroupRoute == null) {
+                return;
+            }
+            MediaRouteProvider.RouteController routeController =
+                    mRequestedRoute
+                            .getProviderInstance()
+                            .onCreateRouteController(
+                                    route.getDescriptorId(), mGroupRoute.getDescriptorId());
+            if (routeController != null) {
+                mRouteIdToMemberControllerMap.put(route.mUniqueId, routeController);
+                routeController.onSelect();
+            }
+        }
+
+        private void disconnectAndRemoveMemberRouteController(String routeId) {
+            MediaRouteProvider.RouteController routeController =
+                    mRouteIdToMemberControllerMap.get(routeId);
+            if (routeController == null) {
+                return;
+            }
+            routeController.onUnselect(UNSELECT_REASON_DISCONNECTED);
+            routeController.onRelease();
+            mRouteIdToMemberControllerMap.remove(routeId);
+        }
+
+        private MediaRouter.GroupRouteInfo convertFromRouteDescriptorToRouteInfo(
                 MediaRouteDescriptor routeDescriptor) {
             MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
             String descriptorId = routeDescriptor.getId();
             String uniqueId = assignRouteUniqueId(provider, descriptorId);
-            MediaRouter.RouteInfo routeInfo =
-                    new MediaRouter.RouteInfo(provider, descriptorId, uniqueId);
+            MediaRouter.GroupRouteInfo routeInfo =
+                    new MediaRouter.GroupRouteInfo(provider, descriptorId, uniqueId);
             routeInfo.maybeUpdateDescriptor(routeDescriptor);
             return routeInfo;
         }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
index 174169b..931c855 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
@@ -682,21 +682,16 @@
             static final String KEY_IS_GROUPABLE = "isGroupable";
             static final String KEY_IS_TRANSFERABLE = "isTransferable";
 
-            /**
-             */
+            /** */
             @RestrictTo(LIBRARY)
-            @IntDef({
-                    UNSELECTING,
-                    UNSELECTED,
-                    SELECTING,
-                    SELECTED
-            })
+            @IntDef({UNSELECTING, UNSELECTED, SELECTING, SELECTED, NOT_IN_GROUP})
             @Retention(RetentionPolicy.SOURCE)
             public @interface SelectionState {}
+
             /**
              * After a user unselects a route, it might take some time for a provider to complete
-             * the operation. This state is used in this between time. MediaRouter can either
-             * block the UI or show the route as unchecked.
+             * the operation. This state is used in this between time. MediaRouter can either block
+             * the UI or show the route as unchecked.
              */
             public static final int UNSELECTING = 0;
 
@@ -723,6 +718,17 @@
              */
             public static final int SELECTED = 3;
 
+            /**
+             * The route is not in a dynamic group.
+             *
+             * <p>The NOT_IN_GROUP selection state is different from the UNSELECTED state. The
+             * former represents a route that is not in a dynamic group. The latter represents an
+             * unselected route which could be selected to be part of the dynamic group.
+             *
+             * @see MediaRouter.GroupRouteInfo#getSelectionState(MediaRouter.RouteInfo)
+             */
+            public static final int NOT_IN_GROUP = 4;
+
             //TODO: mMediaRouteDescriptor could have an old info. We should provide a way to
             // update it or use only the route ID.
             final MediaRouteDescriptor mMediaRouteDescriptor;
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
index 2bdace0..af4bd6d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter.java
@@ -144,28 +144,28 @@
     /**
      * The route connection is disconnected by {@link RouteInfo#disconnect()}.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_DISCONNECTED = 1;
 
     /**
      * The route connection has failed because the requested route is no longer available.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_ROUTE_NOT_AVAILABLE = 2;
 
     /**
      * The route connection has failed because the requested route is not enabled.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_ROUTE_NOT_ENABLED = 3;
 
     /**
      * The route connection has failed because the requested route is a selected route.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_REJECTED_FOR_SELECTED_ROUTE = 4;
 
@@ -173,7 +173,7 @@
      * The route connection has failed because the provider for the requested route doesn't support
      * dynamic groups.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_UNSUPPORTED_FOR_NON_DYNAMIC_CONTROLLER = 5;
 
@@ -181,14 +181,14 @@
      * The route connection has failed because the provider for the requested route failed to create
      * a dynamic group route controller.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_FAILED_TO_CREATE_DYNAMIC_GROUP_ROUTE_CONTROLLER = 6;
 
     /**
      * The route connection has failed due to a timeout.
      *
-     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, int)
+     * @see Callback#onRouteDisconnected(MediaRouter, RouteInfo, RouteInfo, int)
      */
     public static final int REASON_ROUTE_CONNECTION_TIMEOUT = 7;
 
@@ -501,9 +501,9 @@
      * create dynamic group route connections.
      */
     @MainThread
-    public @NonNull List<RouteInfo> getConnectedRoutes() {
+    public @NonNull List<GroupRouteInfo> getConnectedGroupRoutes() {
         checkCallingThread();
-        return getGlobalRouter().getConnectedRoutes();
+        return getGlobalRouter().getConnectedGroupRoutes();
     }
 
     /**
@@ -587,35 +587,29 @@
         }
     }
 
-    /**
-     * Adds the specified route as a member to the current dynamic group.
-     */
+    /** Adds the specified route as a member to the current selected dynamic group. */
     @RestrictTo(LIBRARY)
     @MainThread
-    public void addMemberToDynamicGroup(@NonNull RouteInfo route) {
+    public void addRouteToSelectedGroup(@NonNull RouteInfo route) {
         if (route == null) {
             throw new NullPointerException("route must not be null");
         }
         checkCallingThread();
-        getGlobalRouter().addMemberToDynamicGroup(route);
+        getGlobalRouter().addRouteToSelectedGroup(route);
     }
 
-    /**
-     * Removes the specified route from the current dynamic group.
-     */
+    /** Removes the specified route from the current selected dynamic group. */
     @RestrictTo(LIBRARY)
     @MainThread
-    public void removeMemberFromDynamicGroup(@NonNull RouteInfo route) {
+    public void removeRouteFromSelectedGroup(@NonNull RouteInfo route) {
         if (route == null) {
             throw new NullPointerException("route must not be null");
         }
         checkCallingThread();
-        getGlobalRouter().removeMemberFromDynamicGroup(route);
+        getGlobalRouter().removeRouteFromSelectedGroup(route);
     }
 
-    /**
-     * Transfers the current dynamic group to the specified route.
-     */
+    /** Transfers the current dynamic group to the specified route. */
     @RestrictTo(LIBRARY)
     @MainThread
     public void transferToRoute(@NonNull RouteInfo route) {
@@ -1166,8 +1160,9 @@
         private IntentSender mSettingsIntent;
         MediaRouteDescriptor mDescriptor;
 
-        private List<RouteInfo> mRoutesInGroup = new ArrayList<>();
-        private Map<String, DynamicRouteDescriptor> mDynamicGroupDescriptors;
+        @RestrictTo(LIBRARY)
+        @NonNull
+        protected List<RouteInfo> mSelectedRoutesInGroup = new ArrayList<>();
 
         @IntDef({
             CONNECTION_STATE_DISCONNECTED,
@@ -1636,20 +1631,6 @@
         }
 
         /**
-         * Returns {@code true} if this route is currently connected.
-         *
-         * <p>Must be called on the main thread.
-         *
-         * @return True if this route is currently connected
-         * @see MediaRouter#getConnectedRoutes()
-         */
-        @MainThread
-        public boolean isConnected() {
-            checkCallingThread();
-            return getGlobalRouter().getConnectedRoutes().contains(this);
-        }
-
-        /**
          * Returns true if this route is the default route.
          *
          * <p>Must be called on the main thread.
@@ -2109,37 +2090,29 @@
         }
 
         /**
-         * Returns true if the route has one or more members
-         */
-        @RestrictTo(LIBRARY)
-        public boolean isGroup() {
-            return !mRoutesInGroup.isEmpty();
-        }
-
-        /**
-         * Gets the dynamic group state of the given route.
+         * Returns a {@link GroupRouteInfo} if the route is a group route or {code null} otherwise.
          */
         @RestrictTo(LIBRARY)
         @Nullable
-        public DynamicGroupState getDynamicGroupState(@NonNull RouteInfo route) {
-            if (route == null) {
-                throw new NullPointerException("route must not be null");
-            }
-            if (mDynamicGroupDescriptors != null
-                    && mDynamicGroupDescriptors.containsKey(route.mUniqueId)) {
-                return new DynamicGroupState(mDynamicGroupDescriptors.get(route.mUniqueId));
-            }
-            return null;
+        public GroupRouteInfo asGroup() {
+            return (this instanceof GroupRouteInfo) ? (GroupRouteInfo) this : null;
+        }
+
+        /** Returns true if the route has one or more members */
+        @RestrictTo(LIBRARY)
+        public boolean isGroup() {
+            return !mSelectedRoutesInGroup.isEmpty();
         }
 
         /**
-         * Returns the routes in this group
+         * Returns the selected routes in this group
          *
-         * @return The list of the routes in this group
+         * @return The list of the selected routes in this group
          */
+        @RestrictTo(LIBRARY)
         @NonNull
-        public List<RouteInfo> getRoutesInGroup() {
-            return Collections.unmodifiableList(mRoutesInGroup);
+        public List<RouteInfo> getSelectedRoutesInGroup() {
+            return Collections.unmodifiableList(mSelectedRoutesInGroup);
         }
 
         /**
@@ -2151,9 +2124,7 @@
             return mDescriptor;
         }
 
-        /**
-         *
-         */
+        /** */
         @MainThread
         @RestrictTo(LIBRARY)
         @Nullable
@@ -2192,11 +2163,11 @@
                     .append(", providerPackageName=").append(mProvider.getPackageName());
             if (isGroup()) {
                 sb.append(", members=[");
-                final int count = mRoutesInGroup.size();
+                final int count = mSelectedRoutesInGroup.size();
                 for (int i = 0; i < count; i++) {
                     if (i > 0) sb.append(", ");
-                    if (mRoutesInGroup.get(i) != this) {
-                        sb.append(mRoutesInGroup.get(i).getId());
+                    if (mSelectedRoutesInGroup.get(i) != this) {
+                        sb.append(mSelectedRoutesInGroup.get(i).getId());
                     }
                 }
                 sb.append(']');
@@ -2338,10 +2309,10 @@
 
                 List<String> groupMemberIds = descriptor.getGroupMemberIds();
                 List<RouteInfo> routes = new ArrayList<>();
-                if (groupMemberIds.size() != mRoutesInGroup.size()) {
+                if (groupMemberIds.size() != mSelectedRoutesInGroup.size()) {
                     memberChanged = true;
                 }
-                //TODO: Clean this up not to reference the global router
+                // TODO: Clean this up not to reference the global router
                 if (!groupMemberIds.isEmpty()) {
                     GlobalMediaRouter globalRouter = getGlobalRouter();
                     for (String groupMemberId : groupMemberIds) {
@@ -2349,14 +2320,14 @@
                         RouteInfo groupMember = globalRouter.getRoute(uniqueId);
                         if (groupMember != null) {
                             routes.add(groupMember);
-                            if (!memberChanged && !mRoutesInGroup.contains(groupMember)) {
+                            if (!memberChanged && !mSelectedRoutesInGroup.contains(groupMember)) {
                                 memberChanged = true;
                             }
                         }
                     }
                 }
                 if (memberChanged) {
-                    mRoutesInGroup = routes;
+                    mSelectedRoutesInGroup = routes;
                     changes |= CHANGE_GENERAL;
                 }
             }
@@ -2373,84 +2344,310 @@
             return mProvider.getProviderInstance();
         }
 
+        RouteInfo findRouteByDynamicRouteDescriptor(DynamicRouteDescriptor dynamicDescriptor) {
+            String descriptorId = dynamicDescriptor.getRouteDescriptor().getId();
+            return getProvider().findRouteByDescriptorId(descriptorId);
+        }
+    }
+
+    /** Provides information about a media route that represents a dynamic group. */
+    public static class GroupRouteInfo extends RouteInfo {
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+            ADD_ROUTE_SUCCESSFUL,
+            ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE,
+            ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP,
+            ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+            ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+        })
+        @interface AddRouteReason {}
+
+        /**
+         * The {@link #addRoute(RouteInfo)} has added a route to the dynamic group.
+         *
+         * @see #addRoute(RouteInfo)
+         */
+        public static final int ADD_ROUTE_SUCCESSFUL = 1;
+
+        /**
+         * Adding a route to a dynamic group has failed because the route is not groupable.
+         *
+         * @see #addRoute(RouteInfo)
+         */
+        public static final int ADD_ROUTE_FAILED_REASON_NOT_GROUPABLE = 2;
+
+        /**
+         * Adding a route to a dynamic group has failed because the route is already in the group.
+         *
+         * @see #addRoute(RouteInfo)
+         */
+        public static final int ADD_ROUTE_FAILED_REASON_ALREADY_IN_GROUP = 3;
+
+        /**
+         * Adding a route to a dynamic group has failed because the group route doesn't support
+         * adding a route.
+         *
+         * @see #addRoute(RouteInfo)
+         */
+        public static final int ADD_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 4;
+
+        /**
+         * Adding a route to a dynamic group has failed because the group route is a connected route
+         * but there is no available route connection for adding a route.
+         *
+         * @see #addRoute(RouteInfo)
+         */
+        public static final int ADD_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 5;
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+            REMOVE_ROUTE_SUCCESSFUL,
+            REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE,
+            REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP,
+            REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP,
+            REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+            REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+        })
+        @interface RemoveRouteReason {}
+
+        /**
+         * The {@link #removeRoute(RouteInfo)} has removed a route from the dynamic group.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_SUCCESSFUL = 1;
+
+        /**
+         * Removing a route from a dynamic group has failed because the route is not unselectable.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_FAILED_REASON_NOT_UNSELECTABLE = 2;
+
+        /**
+         * Removing a route from a dynamic group has failed because the route isn't in the group.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_FAILED_REASON_NOT_IN_GROUP = 3;
+
+        /**
+         * Removing a route from a dynamic group has failed because the route is the last route in
+         * the group.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_FAILED_REASON_LAST_ROUTE_IN_GROUP = 4;
+
+        /**
+         * Removing a route from a dynamic group has failed because the group route doesn't support
+         * removing a route.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 5;
+
+        /**
+         * Removing a route from a dynamic group has failed because the group route is a connected
+         * route but there is no available route connection for removing a route.
+         *
+         * @see #removeRoute(RouteInfo)
+         */
+        public static final int REMOVE_ROUTE_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 6;
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+            UPDATE_ROUTES_SUCCESSFUL,
+            UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE,
+            UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE,
+            UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION
+        })
+        @interface UpdateRoutesReason {}
+
+        /**
+         * The {@link #updateRoutes(List)} has updated routes for the dynamic group.
+         *
+         * @see #updateRoutes(List)
+         */
+        public static final int UPDATE_ROUTES_SUCCESSFUL = 1;
+
+        /**
+         * Updating routes for a dynamic group has failed because the updated routes don't contain
+         * any transferable route.
+         *
+         * @see #updateRoutes(List)
+         */
+        public static final int UPDATE_ROUTES_FAILED_REASON_NOT_TRANSFERABLE = 2;
+
+        /**
+         * Updating routes for a dynamic group has failed because the group route doesn't support
+         * updating routes.
+         *
+         * @see #updateRoutes(List)
+         */
+        public static final int UPDATE_ROUTES_FAILED_REASON_UNSUPPORTED_FOR_GROUP_ROUTE = 3;
+
+        /**
+         * Updating routes for a dynamic group has failed because the group route is a connected
+         * route but there is no available route connection for updating routes.
+         *
+         * @see #updateRoutes(List)
+         */
+        public static final int UPDATE_ROUTES_FAILED_REASON_NOT_AVAILABLE_ROUTE_CONNECTION = 4;
+
+        @NonNull private final List<RouteInfo> mRoutesInGroup = new ArrayList<>();
+
+        @NonNull
+        private final Map<String, DynamicRouteDescriptor> mRouteIdToDynamicRouteDescriptorMap =
+                new ArrayMap<>();
+
+        /* package */ GroupRouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
+            super(provider, descriptorId, uniqueId);
+        }
+
+        /**
+         * Returns {@code true} if this route is currently connected.
+         *
+         * <p>Must be called on the main thread.
+         *
+         * @return True if this route is currently connected
+         * @see MediaRouter#getConnectedGroupRoutes()
+         */
+        @MainThread
+        public boolean isConnected() {
+            checkCallingThread();
+            return getGlobalRouter().getConnectedGroupRoutes().contains(this);
+        }
+
+        /**
+         * Adds the route as a member of the dynamic group if the route is groupable.
+         *
+         * <p>If the route is not groupable or is already in the dynamic group, then adding it to
+         * the dynamic group will do nothing.
+         *
+         * @return The state of adding a route to the dynamic group.
+         * @see #isGroupable(RouteInfo)
+         */
+        @MainThread
+        @AddRouteReason
+        public int addRoute(@NonNull RouteInfo route) {
+            checkCallingThread();
+            return getGlobalRouter().addRouteToGroup(this, route);
+        }
+
+        /**
+         * Removes the route from the dynamic group if the route is unselectable.
+         *
+         * <p>If the route is not unselectable, not in the dynamic group, or is the last route of
+         * the dynamic group, then removing it from the dynamic group will do nothing.
+         *
+         * @return The state of removing a route from the dynamic group.
+         * @see #isUnselectable(RouteInfo)
+         */
+        @MainThread
+        @RemoveRouteReason
+        public int removeRoute(@NonNull RouteInfo route) {
+            checkCallingThread();
+            return getGlobalRouter().removeRouteFromGroup(this, route);
+        }
+
+        /**
+         * Updates the routes to be members of the dynamic group if the routes are transferable.
+         * Non-transferable routes will not be included in the dynamic group.
+         *
+         * @return The state of updating routes for the dynamic group.
+         * @see #isTransferable(RouteInfo)
+         */
+        @UpdateRoutesReason
+        @MainThread
+        public int updateRoutes(@NonNull List<RouteInfo> routes) {
+            checkCallingThread();
+            return getGlobalRouter().updateRoutesForGroup(this, routes);
+        }
+
+        /** Returns the list of {@link RouteInfo}s of the given dynamic group route. */
+        @NonNull
+        public List<RouteInfo> getRoutesInGroup() {
+            return Collections.unmodifiableList(mRoutesInGroup);
+        }
+
+        /**
+         * Gets the selection state of the route when the route is in the dynamic group or {@link
+         * DynamicRouteDescriptor#NOT_IN_GROUP} when the route isn't in the dynamic group.
+         */
+        @DynamicRouteDescriptor.SelectionState
+        public int getSelectionState(@NonNull RouteInfo route) {
+            DynamicRouteDescriptor dynamicRouteDescriptor =
+                    mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+            return (dynamicRouteDescriptor != null)
+                    ? dynamicRouteDescriptor.getSelectionState()
+                    : DynamicRouteDescriptor.NOT_IN_GROUP;
+        }
+
+        /**
+         * Returns {@code true} if the route is in the dynamic group and is unselectable from the
+         * dynamic group with the {@link #removeRoute(RouteInfo)} method.
+         */
+        public boolean isUnselectable(@NonNull RouteInfo route) {
+            DynamicRouteDescriptor dynamicRouteDescriptor =
+                    mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+            return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isUnselectable();
+        }
+
+        /**
+         * Returns {@code true} if the route is groupable and can be added to the dynamic group with
+         * the {@link #addRoute(RouteInfo)} method.
+         */
+        public boolean isGroupable(@NonNull RouteInfo route) {
+            DynamicRouteDescriptor dynamicRouteDescriptor =
+                    mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+            return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isGroupable();
+        }
+
+        /**
+         * Returns {@code true} if the route is transferable and can be updated for the dynamic
+         * group with the {@link #updateRoutes(List)} method.
+         */
+        public boolean isTransferable(@NonNull RouteInfo route) {
+            DynamicRouteDescriptor dynamicRouteDescriptor =
+                    mRouteIdToDynamicRouteDescriptorMap.get(route.getId());
+            return (dynamicRouteDescriptor != null) && dynamicRouteDescriptor.isTransferable();
+        }
+
         void updateDynamicDescriptors(Collection<DynamicRouteDescriptor> dynamicDescriptors) {
+            mSelectedRoutesInGroup.clear();
             mRoutesInGroup.clear();
-            if (mDynamicGroupDescriptors == null) {
-                mDynamicGroupDescriptors = new ArrayMap<>();
-            }
-            mDynamicGroupDescriptors.clear();
+            mRouteIdToDynamicRouteDescriptorMap.clear();
 
             for (DynamicRouteDescriptor dynamicDescriptor : dynamicDescriptors) {
                 RouteInfo route = findRouteByDynamicRouteDescriptor(dynamicDescriptor);
                 if (route == null) {
                     continue;
                 }
-                mDynamicGroupDescriptors.put(route.mUniqueId, dynamicDescriptor);
+                mRoutesInGroup.add(route);
+                mRouteIdToDynamicRouteDescriptorMap.put(route.getId(), dynamicDescriptor);
 
                 if ((dynamicDescriptor.getSelectionState() == DynamicRouteDescriptor.SELECTING)
                         || (dynamicDescriptor.getSelectionState()
                                 == DynamicRouteDescriptor.SELECTED)) {
-                    mRoutesInGroup.add(route);
+                    mSelectedRoutesInGroup.add(route);
                 }
             }
             getGlobalRouter()
                     .mCallbackHandler
                     .post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_CHANGED, this);
         }
-
-        RouteInfo findRouteByDynamicRouteDescriptor(DynamicRouteDescriptor dynamicDescriptor) {
-            String descriptorId = dynamicDescriptor.getRouteDescriptor().getId();
-            return getProvider().findRouteByDescriptorId(descriptorId);
-        }
-
-        /** Represents the dynamic group state of the {@link RouteInfo}. */
-        @RestrictTo(LIBRARY)
-        public static final class DynamicGroupState {
-            final DynamicRouteDescriptor mDynamicDescriptor;
-
-            DynamicGroupState(DynamicRouteDescriptor descriptor) {
-                mDynamicDescriptor = descriptor;
-            }
-
-            /**
-             * Gets the selection state of the route when the {@link MediaRouteProvider} of the
-             * route supports {@link MediaRouteProviderDescriptor#supportsDynamicGroupRoute()
-             * dynamic group}.
-             *
-             * @return The selection state of the route: {@link DynamicRouteDescriptor#UNSELECTED},
-             *     {@link DynamicRouteDescriptor#SELECTING}, or {@link
-             *     DynamicRouteDescriptor#SELECTED}.
-             */
-            @RestrictTo(LIBRARY)
-            public int getSelectionState() {
-                return (mDynamicDescriptor != null)
-                        ? mDynamicDescriptor.getSelectionState()
-                        : DynamicRouteDescriptor.UNSELECTED;
-            }
-
-            @RestrictTo(LIBRARY)
-            public boolean isUnselectable() {
-                return mDynamicDescriptor == null || mDynamicDescriptor.isUnselectable();
-            }
-
-            @RestrictTo(LIBRARY)
-            public boolean isGroupable() {
-                return mDynamicDescriptor != null && mDynamicDescriptor.isGroupable();
-            }
-
-            @RestrictTo(LIBRARY)
-            public boolean isTransferable() {
-                return mDynamicDescriptor != null && mDynamicDescriptor.isTransferable();
-            }
-        }
     }
 
     /**
      * Provides information about a media route provider.
-     * <p>
-     * This object may be used to determine which media route provider has
-     * published a particular route.
-     * </p>
+     *
+     * <p>This object may be used to determine which media route provider has published a particular
+     * route.
      */
     public static final class ProviderInfo {
         // Package private fields to avoid use of a synthetic accessor.
@@ -2992,7 +3189,10 @@
             router.maybeUpdateMemberRouteControllers();
             router.updatePlaybackInfoFromSelectedRoute();
             if (mMemberRoutes != null) {
-                router.mSelectedRoute.updateDynamicDescriptors(mMemberRoutes);
+                GroupRouteInfo groupRouteInfo = router.mSelectedRoute.asGroup();
+                if (groupRouteInfo != null) {
+                    groupRouteInfo.updateDynamicDescriptors(mMemberRoutes);
+                }
             }
         }
     }
diff --git a/metrics/metrics-benchmark/build.gradle b/metrics/metrics-benchmark/build.gradle
index 87d74a1..784e4ae 100644
--- a/metrics/metrics-benchmark/build.gradle
+++ b/metrics/metrics-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -44,3 +46,6 @@
     }
 }
 
+androidx {
+    type = LibraryType.BENCHMARK
+}
diff --git a/navigation/navigation-benchmark/build.gradle b/navigation/navigation-benchmark/build.gradle
index bc91b4d..e05fb56 100644
--- a/navigation/navigation-benchmark/build.gradle
+++ b/navigation/navigation-benchmark/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -51,7 +51,7 @@
 
 androidx {
     name = "Navigation Benchmarks"
-    publish = Publish.NONE
+    type = LibraryType.BENCHMARK
     inceptionYear = "2018"
     description = "Navigation Benchmarks"
 }
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
index 6950b97..e3e8319 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -55,7 +55,7 @@
 
 androidx {
     name = "Compose Navigation Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2020"
     description = "This is a project for Navigation demos."
 }
diff --git a/navigation3/navigation3/build.gradle b/navigation3/navigation3/build.gradle
index c34eafd..796ed34 100644
--- a/navigation3/navigation3/build.gradle
+++ b/navigation3/navigation3/build.gradle
@@ -23,7 +23,6 @@
  */
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.Publish
 import androidx.build.PlatformIdentifier
 
 plugins {
@@ -114,8 +113,7 @@
 
 androidx {
     name = "Androidx Navigation 3"
-    publish = Publish.SNAPSHOT_ONLY
-    type = LibraryType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+    type = LibraryType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
     inceptionYear = "2024"
     description = "Provides the building blocks for a Compose first Navigation solution that " +
             "easily supports extensions."
diff --git a/paging/paging-compose/integration-tests/paging-demos/build.gradle b/paging/paging-compose/integration-tests/paging-demos/build.gradle
index 9ef6e39..eb640ed 100644
--- a/paging/paging-compose/integration-tests/paging-demos/build.gradle
+++ b/paging/paging-compose/integration-tests/paging-demos/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -50,7 +50,7 @@
 
 androidx {
     name = "Compose Paging Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2020"
     description = "This is a project for Paging demos."
 }
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index f79d5be..f6a4b33 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -26,7 +26,6 @@
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import androidx.build.Publish
 import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
diff --git a/performance/performance-annotation/build.gradle b/performance/performance-annotation/build.gradle
index 72993cd..36a6772 100644
--- a/performance/performance-annotation/build.gradle
+++ b/performance/performance-annotation/build.gradle
@@ -16,7 +16,7 @@
 
 import androidx.build.KotlinTarget
 import androidx.build.PlatformIdentifier
-import androidx.build.Publish
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
 
 plugins {
@@ -61,7 +61,7 @@
 
 androidx {
     name = "Performance - Annotation"
-    publish = Publish.NONE
+    type = LibraryType.UNSET
     inceptionYear = "2024"
     description = "Provides source annotations for performance optimizations."
     kotlinTarget = KotlinTarget.KOTLIN_1_9
diff --git a/performance/performance-unsafe/build.gradle b/performance/performance-unsafe/build.gradle
index 9c1b2cc..332949a 100644
--- a/performance/performance-unsafe/build.gradle
+++ b/performance/performance-unsafe/build.gradle
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -28,7 +28,7 @@
 
 androidx {
     name = "Performance - Unsafe"
-    publish = Publish.NONE
+    type = LibraryType.UNSET
     inceptionYear = "2024"
     description = "Compile-time support for sun.misc.Unsafe."
 }
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index 69f7a93..2f69c1d 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -23,7 +23,6 @@
  */
 import androidx.build.KotlinTarget
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 import androidx.build.SdkHelperKt
 import androidx.build.AndroidXConfig
 
diff --git a/profileinstaller/profileinstaller-benchmark/build.gradle b/profileinstaller/profileinstaller-benchmark/build.gradle
index 78d904e..ec8c425 100644
--- a/profileinstaller/profileinstaller-benchmark/build.gradle
+++ b/profileinstaller/profileinstaller-benchmark/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -46,7 +46,7 @@
 
 androidx {
     name = "Profileinstaller Benchmarks"
-    publish = Publish.NONE
+    type = LibraryType.BENCHMARK
     inceptionYear = "2021"
     description = "Profileinstaller Benchmarks"
 }
diff --git a/recyclerview/recyclerview-benchmark/build.gradle b/recyclerview/recyclerview-benchmark/build.gradle
index e627eaf..f7542c7 100644
--- a/recyclerview/recyclerview-benchmark/build.gradle
+++ b/recyclerview/recyclerview-benchmark/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -38,3 +40,6 @@
     namespace = "androidx.recyclerview.benchmark"
 }
 
+androidx {
+    type = LibraryType.BENCHMARK
+}
\ No newline at end of file
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index 8613005..19362ae 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -53,5 +53,5 @@
 }
 
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt
new file mode 100644
index 0000000..488b763
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/InMemoryTrackingModeTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 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.room.integration.kotlintestapp.test
+
+import androidx.kruth.assertThat
+import androidx.room.ExperimentalRoomApi
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.integration.kotlintestapp.TestDatabase
+import androidx.room.util.useCursor
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.test.Test
+import org.junit.Before
+
+class InMemoryTrackingModeTest {
+
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+
+    @Before
+    fun setup() {
+        context.deleteDatabase("test.db")
+    }
+
+    @Test
+    @OptIn(ExperimentalRoomApi::class)
+    fun persistedTrackingTable() {
+        val database =
+            Room.databaseBuilder<TestDatabase>(context, "test.db")
+                .setInMemoryTrackingMode(false)
+                .build()
+
+        assertThat(findCreateSql(database, "sqlite_master")).isNotEmpty()
+        assertThat(findCreateSql(database, "sqlite_temp_master")).isNull()
+        database.close()
+    }
+
+    @Test
+    @OptIn(ExperimentalRoomApi::class)
+    fun temporaryTrackingTable() {
+        val database =
+            Room.databaseBuilder<TestDatabase>(context, "test.db")
+                .setInMemoryTrackingMode(true)
+                .build()
+
+        assertThat(findCreateSql(database, "sqlite_master")).isNull()
+        assertThat(findCreateSql(database, "sqlite_temp_master")).isNotEmpty()
+        database.close()
+    }
+
+    private fun findCreateSql(database: RoomDatabase, masterTable: String) =
+        database.runInTransaction<String?> {
+            database.openHelper.writableDatabase
+                .query("SELECT name, sql FROM $masterTable")
+                .useCursor { c ->
+                    while (c.moveToNext()) {
+                        if (c.getString(0) == TRACKING_TABLE_NAME) {
+                            return@runInTransaction c.getString(1)
+                        }
+                    }
+                }
+            null
+        }
+
+    private companion object {
+        private const val TRACKING_TABLE_NAME = "room_table_modification_log"
+    }
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 321f855..c72e4e6 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -218,6 +218,8 @@
     @CallSuper
     @OptIn(ExperimentalCoroutinesApi::class) // For limitedParallelism(1)
     actual open fun init(configuration: DatabaseConfiguration) {
+        useTempTrackingTable = configuration.useTempTrackingTable
+
         connectionManager = createConnectionManager(configuration)
         internalTracker = createInvalidationTracker()
         validateAutoMigrations(configuration)
@@ -279,8 +281,6 @@
                 configuration.multiInstanceInvalidationServiceIntent
             )
         }
-
-        useTempTrackingTable = configuration.useTempTrackingTable
     }
 
     /**
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
index 188a645..5b9a8ea 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/providers/WrapperMediaRouteProvider.java
@@ -251,9 +251,9 @@
         }
 
         void addOriginalRoute(MediaRouter.@NonNull RouteInfo originalRoute) {
-            if (originalRoute.isSelected() || originalRoute.isConnected()) {
+            if (originalRoute.isSelected() || originalRoute instanceof MediaRouter.GroupRouteInfo) {
                 // The wrapper route provider only wraps discovered routes and it wouldn't wrap
-                // selected routes or connected routes.
+                // selected routes or group routes.
                 return;
             }
             String originalDescriptorId = getDescriptorId(originalRoute.getId());
diff --git a/tracing/tracing/build.gradle b/tracing/tracing/build.gradle
index d62d1a3..39bd6a2 100644
--- a/tracing/tracing/build.gradle
+++ b/tracing/tracing/build.gradle
@@ -26,11 +26,17 @@
 
 plugins {
     id("AndroidXPlugin")
-    id("com.android.library")
 }
 
 androidXMultiplatform {
-    android()
+    androidLibrary {
+        namespace = "androidx.tracing"
+        withAndroidTestOnDeviceBuilder {
+            it.compilationName = "instrumentedTest"
+            it.defaultSourceSetName = "androidInstrumentedTest"
+            it.sourceSetTreeName = "test"
+        }
+    }
 
     defaultPlatform(PlatformIdentifier.ANDROID)
 
@@ -69,7 +75,3 @@
         enableAlsoRunningOnPhysicalDevices = true
     }
 }
-
-android {
-    namespace = "androidx.tracing"
-}
diff --git a/versionedparcelable/versionedparcelable/build.gradle b/versionedparcelable/versionedparcelable/build.gradle
index 5ad222b..f0ec27d 100644
--- a/versionedparcelable/versionedparcelable/build.gradle
+++ b/versionedparcelable/versionedparcelable/build.gradle
@@ -34,6 +34,11 @@
     api("androidx.annotation:annotation:1.8.1")
     implementation("androidx.collection:collection:1.4.2")
 
+    testImplementation(libs.testCore)
+    testImplementation(libs.testRunner)
+    testImplementation(libs.junit)
+    testImplementation(libs.robolectric)
+
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
diff --git a/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java b/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
index 809c0ab..b56562e 100644
--- a/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
+++ b/versionedparcelable/versionedparcelable/src/main/java/androidx/versionedparcelable/VersionedParcel.java
@@ -1599,7 +1599,7 @@
             NoSuchMethodException, ClassNotFoundException {
         Method m = mReadCache.get(parcelCls);
         if (m == null) {
-            Class<?> cls = Class.forName(parcelCls, true, VersionedParcel.class.getClassLoader());
+            Class<?> cls = Class.forName(parcelCls, false, VersionedParcel.class.getClassLoader());
             m = cls.getDeclaredMethod("read", VersionedParcel.class);
             mReadCache.put(parcelCls, m);
         }
diff --git a/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java b/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java
new file mode 100644
index 0000000..1ba07b1
--- /dev/null
+++ b/versionedparcelable/versionedparcelable/src/test/java/androidx/versionedparcelable/ParcelImplTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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.versionedparcelable;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ParcelImplTest {
+
+    @Test
+    public void testFakeParcelableInit_throwsInitializedException() {
+        ExceptionInInitializerError e = assertThrows(
+                ExceptionInInitializerError.class, FakeParcelable::new);
+
+        assertTrue(e.getCause() instanceof InitializedException);
+    }
+
+    @Test
+    public void testCreateFromParcel_withNonVersionedParcelableClass_throwsNoSuchMethodException() {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> {
+            Parcel p = Parcel.obtain();
+            p.writeString("androidx.versionedparcelable.ParcelImplTest$FakeParcelable");
+            p.setDataPosition(0);
+            ParcelImpl.CREATOR.createFromParcel(p);
+        });
+
+        assertTrue(e.getCause() instanceof NoSuchMethodException);
+    }
+
+    @Test
+    public void testCreateFromParcel_withMissingClass_throwsClassNotFoundException() {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> {
+            Parcel p = Parcel.obtain();
+            p.writeString("androidx.versionedparcelable.MissingParcelable");
+            p.setDataPosition(0);
+            ParcelImpl.CREATOR.createFromParcel(p);
+        });
+
+        assertTrue(e.getCause() instanceof ClassNotFoundException);
+    }
+
+    public static class FakeParcelable {
+        static {
+            //noinspection ConstantValue
+            if (true) {
+                throw new InitializedException();
+            }
+        }
+    }
+
+    private static class InitializedException extends RuntimeException { }
+}
diff --git a/wear/compose/compose-foundation/benchmark/build.gradle b/wear/compose/compose-foundation/benchmark/build.gradle
index b69e6d1..d4f9b34 100644
--- a/wear/compose/compose-foundation/benchmark/build.gradle
+++ b/wear/compose/compose-foundation/benchmark/build.gradle
@@ -60,5 +60,5 @@
     androidTestImplementation(libs.truth)
 }
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
index ed96326..696f902 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategyTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -46,7 +45,7 @@
 class TransformingLazyColumnContentPaddingMeasurementStrategyTest {
     private val screenHeight = 100
     private val screenWidth = 120
-    private val density = 1f
+    private val density = Density(1f)
 
     private val containerConstraints =
         Constraints(
@@ -116,13 +115,10 @@
     @Test
     fun twoItemsWithFirstTopAlignedWithPadding_measuredWithCorrectOffsets() {
         val topPadding = 5.dp
-        val topPaddingPx = with(measureScope) { topPadding.roundToPx() }
+        val topPaddingPx = with(density) { topPadding.roundToPx() }
         val strategyWithTopPadding =
-            TransformingLazyColumnContentPaddingMeasurementStrategy(
+            measurementStrategy(
                 PaddingValues(top = topPadding),
-                measureScope,
-                mockGraphicContext,
-                mockItemAnimator
             )
 
         val result = strategyWithTopPadding.measure(listOf(screenHeight / 2, screenHeight / 2))
@@ -137,13 +133,10 @@
     @Test
     fun twoItemsWithLastOneAlignedWithPadding_measuredWithCorrectOffsets() {
         val bottomPadding = 5.dp
-        val bottomPaddingPx = with(measureScope) { bottomPadding.roundToPx() }
+        val bottomPaddingPx = with(density) { bottomPadding.roundToPx() }
         val strategyWithBottomPadding =
-            TransformingLazyColumnContentPaddingMeasurementStrategy(
+            measurementStrategy(
                 PaddingValues(bottom = bottomPadding),
-                measureScope,
-                mockGraphicContext,
-                mockItemAnimator
             )
 
         val result = strategyWithBottomPadding.measure(listOf(screenHeight / 2, screenHeight / 2))
@@ -389,12 +382,9 @@
     @Test
     fun fullSizeBottomContentPadding_doesNotCrash() {
         val strategy =
-            TransformingLazyColumnContentPaddingMeasurementStrategy(
+            measurementStrategy(
                 // Padding takes the full size.
-                PaddingValues(bottom = with(Density(density)) { screenHeight.toDp() }),
-                measureScope,
-                mockGraphicContext,
-                mockItemAnimator
+                PaddingValues(bottom = with(density) { screenHeight.toDp() }),
             )
 
         val itemSize = screenHeight / 4
@@ -409,12 +399,9 @@
     @Test
     fun fullSizeTopContentPadding_doesNotCrash() {
         val strategy =
-            TransformingLazyColumnContentPaddingMeasurementStrategy(
+            measurementStrategy(
                 // Padding takes the full size.
-                PaddingValues(top = with(Density(density)) { screenHeight.toDp() }),
-                measureScope,
-                mockGraphicContext,
-                mockItemAnimator
+                PaddingValues(top = with(density) { screenHeight.toDp() }),
             )
 
         val itemSize = screenHeight / 4
@@ -429,16 +416,6 @@
         assertThat(result.visibleItems.size).isEqualTo(2)
     }
 
-    private val measureScope: IntrinsicMeasureScope =
-        object : IntrinsicMeasureScope {
-            override val fontScale: Float
-                get() = this@TransformingLazyColumnContentPaddingMeasurementStrategyTest.density
-
-            override val layoutDirection: LayoutDirection = LayoutDirection.Ltr
-            override val density: Float
-                get() = this@TransformingLazyColumnContentPaddingMeasurementStrategyTest.density
-        }
-
     private val mockGraphicContext =
         object : GraphicsContext {
             override fun createGraphicsLayer(): GraphicsLayer {
@@ -452,14 +429,17 @@
 
     private val mockItemAnimator = LazyLayoutItemAnimator<TransformingLazyColumnMeasuredItem>()
 
-    private val strategy =
+    private fun measurementStrategy(contentPadding: PaddingValues) =
         TransformingLazyColumnContentPaddingMeasurementStrategy(
-            PaddingValues(0.dp),
-            measureScope,
+            contentPadding,
+            density = density,
+            layoutDirection = LayoutDirection.Ltr,
             mockGraphicContext,
             mockItemAnimator
         )
 
+    private val strategy = measurementStrategy(PaddingValues())
+
     private fun TransformingLazyColumnMeasurementStrategy.measure(
         itemHeights: List<Int>,
         transformedHeight: ((Int, TransformingLazyColumnItemScrollProgress) -> Int)? = null,
@@ -480,7 +460,7 @@
             lastMeasuredAnchorItemHeight = lastMeasuredAnchorItemHeight,
             scrollToBeConsumed = scrollToBeConsumed,
             coroutineScope = CoroutineScope(EmptyCoroutineContext),
-            density = Density(density),
+            density = density,
             layout = { width, height, _ ->
                 object : MeasureResult {
                     override val width = width
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
index 3a57009..bee8429 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumn.kt
@@ -43,7 +43,7 @@
 import androidx.compose.runtime.structuralEqualityPolicy
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalGraphicsContext
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.dp
@@ -92,20 +92,25 @@
     content: TransformingLazyColumnScope.() -> Unit
 ) {
     val graphicsContext = LocalGraphicsContext.current
+    val layoutDirection = LocalLayoutDirection.current
+    val density = LocalDensity.current
+    val measurementStrategy =
+        remember(contentPadding) {
+            TransformingLazyColumnContentPaddingMeasurementStrategy(
+                contentPadding = contentPadding,
+                layoutDirection = layoutDirection,
+                density = density,
+                graphicsContext = graphicsContext,
+                itemAnimator = state.animator,
+            )
+        }
 
     TransformingLazyColumnImpl(
         modifier = modifier,
         state = state,
         verticalArrangement = verticalArrangement,
         horizontalAlignment = horizontalAlignment,
-        measurementStrategyProvider = {
-            TransformingLazyColumnContentPaddingMeasurementStrategy(
-                contentPadding = contentPadding,
-                intrinsicMeasureScope = this,
-                graphicsContext = graphicsContext,
-                itemAnimator = state.animator,
-            )
-        },
+        measurementStrategy = measurementStrategy,
         flingBehavior = flingBehavior,
         userScrollEnabled = userScrollEnabled,
         rotaryScrollableBehavior = rotaryScrollableBehavior,
@@ -146,12 +151,13 @@
     rotaryScrollableBehavior: RotaryScrollableBehavior? = RotaryScrollableDefaults.behavior(state),
     content: TransformingLazyColumnScope.() -> Unit
 ) {
+    val measurementStrategy = remember { TransformingLazyColumnCenterBoundsMeasurementStrategy() }
     TransformingLazyColumnImpl(
         modifier = modifier,
         state = state,
         verticalArrangement = verticalArrangement,
         horizontalAlignment = horizontalAlignment,
-        measurementStrategyProvider = { TransformingLazyColumnCenterBoundsMeasurementStrategy() },
+        measurementStrategy = measurementStrategy,
         flingBehavior = flingBehavior,
         userScrollEnabled = userScrollEnabled,
         rotaryScrollableBehavior = rotaryScrollableBehavior,
@@ -179,8 +185,7 @@
             alignment = Alignment.Top
         ),
     horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
-    measurementStrategyProvider:
-        IntrinsicMeasureScope.() -> TransformingLazyColumnMeasurementStrategy,
+    measurementStrategy: TransformingLazyColumnMeasurementStrategy,
     flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
     userScrollEnabled: Boolean = true,
     rotaryScrollableBehavior: RotaryScrollableBehavior? = RotaryScrollableDefaults.behavior(state),
@@ -213,7 +218,7 @@
             state = state,
             horizontalAlignment = horizontalAlignment,
             verticalArrangement = verticalArrangement,
-            measurementStrategyProvider = measurementStrategyProvider,
+            measurementStrategy = measurementStrategy,
             coroutineScope = coroutineScope,
         )
     val reverseDirection =
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
index 2c9d827..db358e3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnContentPaddingMeasurementStrategy.kt
@@ -19,11 +19,11 @@
 import androidx.collection.mutableObjectIntMapOf
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.ui.graphics.GraphicsContext
-import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
@@ -40,19 +40,16 @@
 
 internal class TransformingLazyColumnContentPaddingMeasurementStrategy(
     contentPadding: PaddingValues,
-    intrinsicMeasureScope: IntrinsicMeasureScope,
+    density: Density,
+    layoutDirection: LayoutDirection,
     private val graphicsContext: GraphicsContext,
     private val itemAnimator: LazyLayoutItemAnimator<TransformingLazyColumnMeasuredItem>
 ) : TransformingLazyColumnMeasurementStrategy {
     override val rightContentPadding: Int =
-        with(intrinsicMeasureScope) {
-            contentPadding.calculateRightPadding(layoutDirection).roundToPx()
-        }
+        with(density) { contentPadding.calculateRightPadding(layoutDirection).roundToPx() }
 
     override val leftContentPadding: Int =
-        with(intrinsicMeasureScope) {
-            contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
-        }
+        with(density) { contentPadding.calculateLeftPadding(layoutDirection).roundToPx() }
 
     override fun measure(
         itemsCount: Int,
@@ -352,10 +349,10 @@
     }
 
     private val beforeContentPadding: Int =
-        with(intrinsicMeasureScope) { contentPadding.calculateTopPadding().roundToPx() }
+        with(density) { contentPadding.calculateTopPadding().roundToPx() }
 
     private val afterContentPadding: Int =
-        with(intrinsicMeasureScope) { contentPadding.calculateBottomPadding().roundToPx() }
+        with(density) { contentPadding.calculateBottomPadding().roundToPx() }
 
     private fun restoreLayoutTopToBottom(
         visibleItems: ArrayDeque<TransformingLazyColumnMeasuredItem>,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
index b9a3318..32a1708 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/TransformingLazyColumnMeasurement.kt
@@ -23,7 +23,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.constrainHeight
@@ -50,19 +49,16 @@
     coroutineScope: CoroutineScope,
     horizontalAlignment: Alignment.Horizontal,
     verticalArrangement: Arrangement.Vertical,
-    measurementStrategyProvider:
-        IntrinsicMeasureScope.() -> TransformingLazyColumnMeasurementStrategy,
+    measurementStrategy: TransformingLazyColumnMeasurementStrategy,
 ): LazyLayoutMeasureScope.(Constraints) -> MeasureResult =
     remember(
         itemProviderLambda,
         state,
         horizontalAlignment,
         verticalArrangement,
-        measurementStrategyProvider
+        measurementStrategy
     ) {
         { containerConstraints ->
-            val measurementStrategy = measurementStrategyProvider(this)
-
             val childConstraints =
                 Constraints(
                     maxHeight = Constraints.Infinity,
diff --git a/wear/compose/compose-material/benchmark/build.gradle b/wear/compose/compose-material/benchmark/build.gradle
index 7d71235..3adc459 100644
--- a/wear/compose/compose-material/benchmark/build.gradle
+++ b/wear/compose/compose-material/benchmark/build.gradle
@@ -62,5 +62,5 @@
     androidTestImplementation(libs.truth)
 }
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/wear/compose/compose-material3/benchmark/build.gradle b/wear/compose/compose-material3/benchmark/build.gradle
index 9e88dd9..836aa4e 100644
--- a/wear/compose/compose-material3/benchmark/build.gradle
+++ b/wear/compose/compose-material3/benchmark/build.gradle
@@ -61,5 +61,5 @@
     androidTestImplementation(libs.truth)
 }
 androidx {
-    type = LibraryType.INTERNAL_TEST_LIBRARY
+    type = LibraryType.BENCHMARK
 }
diff --git a/wear/compose/compose-material3/integration-tests/build.gradle b/wear/compose/compose-material3/integration-tests/build.gradle
index 24d5fb1..956afb7 100644
--- a/wear/compose/compose-material3/integration-tests/build.gradle
+++ b/wear/compose/compose-material3/integration-tests/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -47,7 +47,7 @@
 
 androidx {
     name = "AndroidX Wear Compose Material3 Components Demos"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2023"
     description = "Contains the demo code for the AndroidX Wear Compose Material 3 components."
 }
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
deleted file mode 100644
index 8d25aff..0000000
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeButtonDemo.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright 2023 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.compose.material3.demos
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Home
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material3.FilledIconButton
-import androidx.wear.compose.material3.FilledTonalIconButton
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.IconButton
-import androidx.wear.compose.material3.IconButtonDefaults
-import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.OutlinedIconButton
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.TextButton
-import androidx.wear.compose.material3.TextButtonDefaults
-
-@Composable
-fun AnimatedShapeButtonDemo() {
-    ScalingLazyDemo {
-        item { ListHeader { Text("Animated Text Button") } }
-        item {
-            Row {
-                TextButton(
-                    onClick = {},
-                    shapes = TextButtonDefaults.animatedShapes(),
-                ) {
-                    Text(text = "ABC")
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                TextButton(
-                    onClick = {},
-                    shapes =
-                        TextButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(15.dp),
-                            pressedShape = RoundedCornerShape(15.dp),
-                        ),
-                ) {
-                    Text(text = "ABC")
-                }
-            }
-        }
-        item { ListHeader { Text("Animated Icon Button") } }
-        item {
-            Row {
-                IconButton(
-                    onClick = {},
-                    shapes = IconButtonDefaults.animatedShapes(),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                IconButton(
-                    onClick = {},
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(15.dp),
-                            pressedShape = RoundedCornerShape(15.dp)
-                        ),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-        item { ListHeader { Text("Animated Filled Icon Button") } }
-        item {
-            Row {
-                FilledIconButton(
-                    onClick = {},
-                    shapes = IconButtonDefaults.animatedShapes(),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                FilledIconButton(
-                    onClick = {},
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(15.dp),
-                            pressedShape = RoundedCornerShape(15.dp)
-                        ),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-        item { ListHeader { Text("Animated Filled Tonal Icon Button") } }
-        item {
-            Row {
-                FilledTonalIconButton(
-                    onClick = {},
-                    shapes = IconButtonDefaults.animatedShapes(),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                FilledTonalIconButton(
-                    onClick = {},
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(15.dp),
-                            pressedShape = RoundedCornerShape(15.dp)
-                        ),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-        item { ListHeader { Text("Animated Outlined Icon Button") } }
-        item {
-            Row {
-                OutlinedIconButton(
-                    onClick = {},
-                    shapes = IconButtonDefaults.animatedShapes(),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                OutlinedIconButton(
-                    onClick = {},
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(15.dp),
-                            pressedShape = RoundedCornerShape(15.dp)
-                        ),
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-    }
-}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
deleted file mode 100644
index ed2dbcb..0000000
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/AnimatedShapeToggleButtonDemo.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2023 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.compose.material3.demos
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Home
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.material3.Icon
-import androidx.wear.compose.material3.IconToggleButton
-import androidx.wear.compose.material3.IconToggleButtonDefaults
-import androidx.wear.compose.material3.ListHeader
-import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.TextToggleButton
-import androidx.wear.compose.material3.TextToggleButtonDefaults
-
-@Composable
-fun AnimatedShapeToggleButtonDemo() {
-    ScalingLazyDemo {
-        item { ListHeader { Text("Default Toggle") } }
-        item {
-            Row {
-                val checked = remember { mutableStateOf(false) }
-
-                TextToggleButton(
-                    onCheckedChange = { checked.value = !checked.value },
-                    shapes = TextToggleButtonDefaults.animatedShapes(),
-                    checked = checked.value,
-                ) {
-                    Text(text = "ABC")
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                IconToggleButton(
-                    onCheckedChange = { checked.value = !checked.value },
-                    shapes = IconToggleButtonDefaults.animatedShapes(),
-                    checked = checked.value,
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-        item { ListHeader { Text("Toggle Variant") } }
-        item {
-            Row {
-                val checked = remember { mutableStateOf(false) }
-
-                TextToggleButton(
-                    onCheckedChange = { checked.value = !checked.value },
-                    shapes = TextToggleButtonDefaults.variantAnimatedShapes(),
-                    checked = checked.value,
-                ) {
-                    Text(text = "ABC")
-                }
-
-                Spacer(modifier = Modifier.width(5.dp))
-
-                IconToggleButton(
-                    onCheckedChange = { checked.value = !checked.value },
-                    shapes = IconToggleButtonDefaults.variantAnimatedShapes(),
-                    checked = checked.value,
-                ) {
-                    Icon(imageVector = Icons.Rounded.Home, contentDescription = null)
-                }
-            }
-        }
-    }
-}
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
index ba9730e..15ac940 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/IconButtonDemo.kt
@@ -19,8 +19,6 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -105,7 +103,7 @@
         }
         item { ListHeader { Text("With onLongClick") } }
         item { IconButtonWithOnLongClickSample { showOnLongClickToast(context) } }
-        item { ListHeader { Text("Corner Animation") } }
+        item { ListHeader { Text("Animated") } }
         item {
             Row {
                 IconButtonWithCornerAnimationSample()
@@ -115,33 +113,6 @@
                 )
             }
         }
-        item { ListHeader { Text("Morphed Animation") } }
-        item {
-            Row {
-                FilledIconButton(
-                    onClick = {},
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(5.dp),
-                            pressedShape = RoundedCornerShape(5.dp)
-                        ),
-                ) {
-                    FavoriteIcon(ButtonDefaults.IconSize)
-                }
-                Spacer(modifier = Modifier.width(5.dp))
-                FilledIconButton(
-                    onClick = {},
-                    colors = IconButtonDefaults.filledVariantIconButtonColors(),
-                    shapes =
-                        IconButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(5.dp),
-                            pressedShape = RoundedCornerShape(5.dp)
-                        ),
-                ) {
-                    FavoriteIcon(ButtonDefaults.IconSize)
-                }
-            }
-        }
         item { ListHeader { Text("Sizes") } }
         item {
             Row(verticalAlignment = Alignment.CenterVertically) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
index 088d524..0648dcc 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextButtonDemo.kt
@@ -19,8 +19,6 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CutCornerShape
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -113,7 +111,7 @@
         }
         item { ListHeader { Text("With onLongClick") } }
         item { TextButtonWithOnLongClickSample { showOnLongClickToast(context) } }
-        item { ListHeader { Text("Corner Animation") } }
+        item { ListHeader { Text("Animated") } }
         item {
             Row(verticalAlignment = Alignment.CenterVertically) {
                 TextButton(
@@ -133,34 +131,6 @@
                 }
             }
         }
-        item { ListHeader { Text("Morphed Animation") } }
-        item {
-            Row(verticalAlignment = Alignment.CenterVertically) {
-                TextButton(
-                    onClick = {},
-                    colors = TextButtonDefaults.filledTextButtonColors(),
-                    shapes =
-                        TextButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(5.dp),
-                            pressedShape = RoundedCornerShape(5.dp)
-                        ),
-                ) {
-                    Text(text = "ABC")
-                }
-                Spacer(modifier = Modifier.width(5.dp))
-                TextButton(
-                    onClick = {},
-                    colors = TextButtonDefaults.filledVariantTextButtonColors(),
-                    shapes =
-                        TextButtonDefaults.animatedShapes(
-                            shape = CutCornerShape(5.dp),
-                            pressedShape = RoundedCornerShape(5.dp)
-                        ),
-                ) {
-                    Text(text = "ABC")
-                }
-            }
-        }
         item { ListHeader { Text("Sizes") } }
         item {
             Row(verticalAlignment = Alignment.CenterVertically) {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
index 6ae2091..58ca9b8 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/TextToggleButtonDemo.kt
@@ -57,9 +57,7 @@
                 TextToggleButtonsDemo(enabled = false, initialChecked = false)
             }
         }
-        item {
-            ListHeader { Text("Text Toggle Button Shape morphing", textAlign = TextAlign.Center) }
-        }
+        item { ListHeader { Text("Shape morphing", textAlign = TextAlign.Center) } }
         item {
             Row {
                 AnimatedTextToggleButtonsDemo(enabled = true, initialChecked = true)
@@ -74,11 +72,7 @@
                 AnimatedTextToggleButtonsDemo(enabled = false, initialChecked = false)
             }
         }
-        item {
-            ListHeader {
-                Text("Text Toggle Button Shape morphing variant", textAlign = TextAlign.Center)
-            }
-        }
+        item { ListHeader { Text("Shape morphing variant", textAlign = TextAlign.Center) } }
         item {
             Row {
                 VariantAnimatedTextToggleButtonsDemo(enabled = true, initialChecked = true)
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index 03d28c9..b70eeef 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -116,8 +116,6 @@
                 ComposableDemo("List Header") { Centralize { ListHeaderSample() } },
                 Material3DemoCategory("Time Text", TimeTextDemos),
                 ComposableDemo("Card") { CardDemo() },
-                ComposableDemo("Animated Shape Buttons") { AnimatedShapeButtonDemo() },
-                ComposableDemo("Animated Shape Toggle Buttons") { AnimatedShapeToggleButtonDemo() },
                 ComposableDemo("Text Toggle Button") { TextToggleButtonDemo() },
                 ComposableDemo("Icon Toggle Button") { IconToggleButtonDemo() },
                 ComposableDemo("Checkbox Button") { CheckboxButtonDemo() },
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
index fb631f4..a6c2594 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ButtonTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.shape.CutCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.assertContainsColor
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
@@ -34,6 +35,8 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -63,6 +66,7 @@
 import androidx.compose.ui.unit.height
 import androidx.wear.compose.material3.samples.FilledTonalCompactButtonSample
 import androidx.wear.compose.material3.samples.SimpleButtonSample
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -1187,6 +1191,54 @@
         assertEquals(TextAlign.Center, labelAlignment)
     }
 
+    @Test
+    fun button_long_click_triggers_haptic() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                Button(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    Text("Test")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
+    fun compactbutton_long_click_triggers_haptic() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                CompactButton(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    Text("Test")
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
     private fun responds_to_long_click(
         enabled: Boolean,
         onLongClick: () -> Unit,
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
index 8c48118..98b8213 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/CardTest.kt
@@ -24,10 +24,13 @@
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.assertContainsColor
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -48,6 +51,7 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -167,6 +171,29 @@
     }
 
     @Test
+    fun card_long_click_triggers_haptic() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                Card(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    enabled = true,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {}
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
     fun card_does_not_respond_to_long_click_when_disabled() {
         var longClicked = false
 
@@ -209,6 +236,33 @@
     }
 
     @Test
+    fun appCard_triggers_haptic_when_long_clicked() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                AppCard(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    appName = {},
+                    title = {},
+                    enabled = true,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    TestImage()
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
     fun appCard_does_not_respond_to_long_click_when_disabled() {
         var longClicked = false
 
@@ -252,6 +306,32 @@
     }
 
     @Test
+    fun titleCard_triggers_haptic_when_long_clicked() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                TitleCard(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    title = {},
+                    enabled = true,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {
+                    TestImage()
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
     fun titleCard_does_not_respond_to_long_click_when_disabled() {
         var longClicked = false
 
@@ -433,7 +513,7 @@
     }
 
     @Test
-    public fun title_card_with_time_and_subtitle_gives_default_colors() {
+    fun title_card_with_time_and_subtitle_gives_default_colors() {
         var expectedTimeColor = Color.Transparent
         var expectedSubtitleColor = Color.Transparent
         var expectedTitleColor = Color.Transparent
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
index e13753d..ae3a245 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/IconButtonTest.kt
@@ -25,10 +25,13 @@
 import androidx.compose.foundation.shape.CutCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -51,6 +54,7 @@
 import androidx.wear.compose.material3.IconButtonDefaults.ExtraSmallButtonSize
 import androidx.wear.compose.material3.IconButtonDefaults.LargeButtonSize
 import androidx.wear.compose.material3.IconButtonDefaults.SmallButtonSize
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -151,6 +155,29 @@
     }
 
     @Test
+    fun triggers_haptic_when_long_clicked() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                IconButton(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    enabled = true,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {}
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
     fun onLongClickLabel_includedInSemantics() {
         val testLabel = "Long click action"
 
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
index 3a64620..884643a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/TextButtonTest.kt
@@ -25,10 +25,13 @@
 import androidx.compose.foundation.shape.CutCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.testutils.assertShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -53,6 +56,7 @@
 import androidx.wear.compose.material3.TextButtonDefaults.DefaultButtonSize
 import androidx.wear.compose.material3.TextButtonDefaults.LargeButtonSize
 import androidx.wear.compose.material3.TextButtonDefaults.SmallButtonSize
+import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -181,6 +185,29 @@
     }
 
     @Test
+    fun triggers_haptic_when_long_clicked() {
+        val results = mutableMapOf<HapticFeedbackType, Int>()
+        val haptics = hapticFeedback(collectResultsFromHapticFeedback(results))
+
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalHapticFeedback provides haptics) {
+                TextButton(
+                    onClick = { /* Do nothing */ },
+                    onLongClick = {},
+                    enabled = true,
+                    modifier = Modifier.testTag(TEST_TAG)
+                ) {}
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performTouchInput { longClick() }
+
+        assertThat(results).hasSize(1)
+        assertThat(results).containsKey(HapticFeedbackType.LongPress)
+        assertThat(results[HapticFeedbackType.LongPress]).isEqualTo(1)
+    }
+
+    @Test
     fun onLongClickLabel_includedInSemantics() {
         val testLabel = "Long click action"
 
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 0f94a7d..7ef944f2 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -51,8 +51,6 @@
 import androidx.compose.ui.graphics.painter.ColorPainter
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextAlign
@@ -1853,7 +1851,6 @@
     interactionSource: MutableInteractionSource?,
     content: @Composable RowScope.() -> Unit
 ) {
-    val hapticFeedback = LocalHapticFeedback.current
     Row(
         verticalAlignment = Alignment.CenterVertically,
         // Fill the container height but not its width as buttons have fixed size height but we
@@ -1866,14 +1863,7 @@
                 .combinedClickable(
                     enabled = enabled,
                     onClick = onClick,
-                    onLongClick =
-                        onLongClick?.let {
-                            {
-                                hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
-                                it()
-                            }
-                        },
+                    onLongClick = onLongClick, // NB CombinedClickable calls LongPress haptic
                     onLongClickLabel = onLongClickLabel,
                     role = Role.Button,
                     indication = ripple(),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
index a11d41f..cfe3743 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Card.kt
@@ -49,8 +49,6 @@
 import androidx.compose.ui.graphics.painter.ColorPainter
 import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.LocalTransformingLazyColumnItemScope
@@ -850,7 +848,6 @@
     interactionSource: MutableInteractionSource?,
     content: @Composable ColumnScope.() -> Unit,
 ) {
-    val hapticFeedback = LocalHapticFeedback.current
     Column(
         modifier =
             modifier
@@ -859,14 +856,7 @@
                 .combinedClickable(
                     enabled = enabled,
                     onClick = onClick,
-                    onLongClick =
-                        onLongClick?.let {
-                            {
-                                hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
-                                it()
-                            }
-                        },
+                    onLongClick = onLongClick, // NB combinedClickable calls LongPress haptic
                     onLongClickLabel = onLongClickLabel,
                     role = null,
                     indication = ripple(),
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
index 3409695..92016f0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RoundButton.kt
@@ -40,8 +40,6 @@
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
@@ -67,7 +65,6 @@
     content: @Composable BoxScope.() -> Unit,
 ) {
     val borderStroke = border(enabled)
-    val hapticFeedback = LocalHapticFeedback.current
 
     Box(
         contentAlignment = Alignment.Center,
@@ -77,13 +74,7 @@
                 .clip(shape) // Clip for the touch area (e.g. for Ripple).
                 .combinedClickable(
                     onClick = onClick,
-                    onLongClick =
-                        onLongClick?.let {
-                            {
-                                hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-                                it()
-                            }
-                        },
+                    onLongClick = onLongClick, // NB combinedClickable calls LongPress haptic
                     onLongClickLabel = onLongClickLabel,
                     enabled = enabled,
                     interactionSource = interactionSource,
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index c3306bc..24b5e35 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -383,7 +383,7 @@
   @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ProtoLayoutExperimental {
   }
 
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PARAMETER}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
     method public abstract int major();
     method public abstract int minor();
   }
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index c3306bc..24b5e35 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -383,7 +383,7 @@
   @SuppressCompatibility @RequiresOptIn(level=androidx.annotation.RequiresOptIn.Level.ERROR) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD}) public @interface ProtoLayoutExperimental {
   }
 
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PARAMETER}) @kotlin.annotation.MustBeDocumented public @interface RequiresSchemaVersion {
     method public abstract int major();
     method public abstract int minor();
   }
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
index 6041b9f..84e995d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/RequiresSchemaVersion.java
@@ -19,6 +19,7 @@
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -36,7 +37,7 @@
  */
 @MustBeDocumented
 @Retention(CLASS)
-@Target({TYPE, METHOD, CONSTRUCTOR, FIELD})
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER})
 public @interface RequiresSchemaVersion {
     int major();
 
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 179ee74..7e28703 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -67,9 +67,9 @@
   }
 
   public final class CardKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
   }
 
   public final class ColorScheme {
@@ -139,8 +139,8 @@
   }
 
   public final class EdgeButtonKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
   }
 
   public final class EdgeButtonStyle {
@@ -206,7 +206,7 @@
   }
 
   public final class TextKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.TypeBuilders.StringProp text, optional androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint stringLayoutConstraint, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.ModifiersBuilders.Modifiers modifiers);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.types.LayoutString text, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.modifiers.LayoutModifier modifiers);
   }
 
   public final class TitleCardDefaults {
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 179ee74..7e28703 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -67,9 +67,9 @@
   }
 
   public final class CardKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement appCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? avatar, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? label, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.AppCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement card(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension width, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.types.LayoutColor? backgroundColor, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> content);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement titleCard(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> title, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? content, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? time, optional androidx.wear.protolayout.DimensionBuilders.ContainerDimension height, optional androidx.wear.protolayout.ModifiersBuilders.Corner shape, optional androidx.wear.protolayout.material3.CardColors colors, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? background, optional androidx.wear.protolayout.material3.TitleCardStyle style, optional androidx.wear.protolayout.ModifiersBuilders.Padding contentPadding, optional int horizontalAlignment);
   }
 
   public final class ColorScheme {
@@ -139,8 +139,8 @@
   }
 
   public final class EdgeButtonKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, androidx.wear.protolayout.TypeBuilders.StringProp contentDescription, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement iconEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> iconContent);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement textEdgeButton(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.ModifiersBuilders.Clickable onClick, optional androidx.wear.protolayout.modifiers.LayoutModifier modifier, optional androidx.wear.protolayout.material3.ButtonColors colors, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> labelContent);
   }
 
   public final class EdgeButtonStyle {
@@ -206,7 +206,7 @@
   }
 
   public final class TextKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.TypeBuilders.StringProp text, optional androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint stringLayoutConstraint, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.ModifiersBuilders.Modifiers modifiers);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement text(androidx.wear.protolayout.material3.MaterialScope, androidx.wear.protolayout.types.LayoutString text, optional int typography, optional androidx.wear.protolayout.types.LayoutColor color, optional boolean italic, optional boolean underline, optional boolean scalable, optional int maxLines, optional int multilineAlignment, optional int overflow, optional androidx.wear.protolayout.modifiers.LayoutModifier modifiers);
   }
 
   public final class TitleCardDefaults {
diff --git a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
index 970f2cc..f20c349 100644
--- a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
+++ b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
@@ -24,8 +24,6 @@
 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
 import androidx.wear.protolayout.ModifiersBuilders
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
-import androidx.wear.protolayout.TypeBuilders.StringProp
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.material3.AppCardStyle
 import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
@@ -40,16 +38,20 @@
 import androidx.wear.protolayout.material3.iconEdgeButton
 import androidx.wear.protolayout.material3.materialScope
 import androidx.wear.protolayout.material3.primaryLayout
-import androidx.wear.protolayout.material3.prop
 import androidx.wear.protolayout.material3.text
 import androidx.wear.protolayout.material3.textEdgeButton
 import androidx.wear.protolayout.material3.titleCard
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
 
 /** Builds Material3 text element with default options. */
 @Sampled
 fun helloWorldTextDefault(context: Context, deviceConfiguration: DeviceParameters): LayoutElement =
     materialScope(context, deviceConfiguration) {
-        text(text = "Hello Material3".prop(), typography = Typography.DISPLAY_LARGE)
+        text(text = "Hello Material3".layoutString, typography = Typography.DISPLAY_LARGE)
     }
 
 /** Builds Material3 text element with some of the overridden defaults. */
@@ -61,10 +63,11 @@
     materialScope(context, deviceConfiguration) {
         text(
             text =
-                StringProp.Builder("Static")
-                    .setDynamicValue(DynamicString.constant("Dynamic"))
-                    .build(),
-            stringLayoutConstraint = StringLayoutConstraint.Builder("Constraint").build(),
+                LayoutString(
+                    "Static",
+                    DynamicString.constant("Dynamic"),
+                    "LongestConstraint".asLayoutConstraint()
+                ),
             typography = Typography.DISPLAY_LARGE,
             color = colorScheme.tertiary,
             underline = true,
@@ -79,7 +82,10 @@
     clickable: Clickable
 ): LayoutElement =
     materialScope(context, deviceConfiguration) {
-        iconEdgeButton(onClick = clickable, contentDescription = "Description of a button".prop()) {
+        iconEdgeButton(
+            onClick = clickable,
+            modifier = LayoutModifier.contentDescription("Description of a button")
+        ) {
             icon(protoLayoutResourceId = "id")
         }
     }
@@ -91,8 +97,11 @@
     clickable: Clickable
 ): LayoutElement =
     materialScope(context, deviceConfiguration) {
-        textEdgeButton(onClick = clickable, contentDescription = "Description of a button".prop()) {
-            text("Hello".prop())
+        textEdgeButton(
+            onClick = clickable,
+            modifier = LayoutModifier.contentDescription("Description of a button")
+        ) {
+            text("Hello".layoutString)
         }
     }
 
@@ -104,7 +113,7 @@
 ): LayoutElement =
     materialScope(context, deviceConfiguration) {
         primaryLayout(
-            titleSlot = { text("App title".prop()) },
+            titleSlot = { text("App title".layoutString) },
             mainSlot = {
                 buttonGroup {
                     // To be populated with proper components
@@ -123,7 +132,14 @@
                     }
                 }
             },
-            bottomSlot = { iconEdgeButton(clickable, "Description".prop()) { icon("id") } }
+            bottomSlot = {
+                iconEdgeButton(
+                    clickable,
+                    modifier = LayoutModifier.contentDescription("Description")
+                ) {
+                    icon("id")
+                }
+            }
         )
     }
 
@@ -138,12 +154,12 @@
             mainSlot = {
                 card(
                     onClick = clickable,
-                    contentDescription = "Card with image background".prop(),
+                    modifier = LayoutModifier.contentDescription("Card with image background"),
                     width = expand(),
                     height = expand(),
                     background = { backgroundImage(protoLayoutResourceId = "id") }
                 ) {
-                    text("Content of the Card!".prop())
+                    text("Content of the Card!".layoutString)
                 }
             }
         )
@@ -160,13 +176,13 @@
             mainSlot = {
                 titleCard(
                     onClick = clickable,
-                    contentDescription = "Title Card".prop(),
+                    modifier = LayoutModifier.contentDescription("Title Card"),
                     height = expand(),
                     colors = filledVariantCardColors(),
                     style = largeTitleCardStyle(),
-                    title = { text("This is title of the title card".prop()) },
-                    time = { text("NOW".prop()) },
-                    content = { text("Content of the Card!".prop()) }
+                    title = { text("This is title of the title card".layoutString) },
+                    time = { text("NOW".layoutString) },
+                    content = { text("Content of the Card!".layoutString) }
                 )
             }
         )
@@ -183,14 +199,14 @@
             mainSlot = {
                 appCard(
                     onClick = clickable,
-                    contentDescription = "App Card".prop(),
+                    modifier = LayoutModifier.contentDescription("App Card"),
                     height = expand(),
                     colors = filledTonalCardColors(),
                     style = AppCardStyle.largeAppCardStyle(),
-                    title = { text("This is title of the app card".prop()) },
-                    time = { text("NOW".prop()) },
-                    label = { text("Label".prop()) },
-                    content = { text("Content of the Card!".prop()) },
+                    title = { text("This is title of the app card".layoutString) },
+                    time = { text("NOW".layoutString) },
+                    label = { text("Label".layoutString) },
+                    content = { text("Content of the Card!".layoutString) },
                 )
             }
         )
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index ed7c966..ff49447 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -32,7 +32,10 @@
 import androidx.wear.protolayout.material3.ButtonDefaults.filledVariantButtonColors
 import androidx.wear.protolayout.material3.CardDefaults.filledVariantCardColors
 import androidx.wear.protolayout.material3.MaterialGoldenTest.Companion.pxToDp
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
 import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.layoutString
 import com.google.common.collect.ImmutableMap
 
 private const val CONTENT_DESCRIPTION_PLACEHOLDER = "Description"
@@ -79,19 +82,20 @@
                 primaryLayoutWithOverrideIcon(
                     mainSlot = {
                         text(
-                            text = "Text in the main slot that overflows".prop(),
+                            text = "Text in the main slot that overflows".layoutString,
                             color = colorScheme.secondary
                         )
                     },
                     bottomSlot = {
                         textEdgeButton(
                             onClick = clickable,
-                            labelContent = { text("Action".prop()) },
-                            contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+                            labelContent = { text("Action".layoutString) },
+                            modifier =
+                                LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
                             colors = filledButtonColors()
                         )
                     },
-                    titleSlot = { text("Title".prop()) },
+                    titleSlot = { text("Title".layoutString) },
                     overrideIcon = true
                 )
             }
@@ -121,8 +125,9 @@
                     bottomSlot = {
                         textEdgeButton(
                             onClick = clickable,
-                            labelContent = { text("Action that overflows".prop()) },
-                            contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+                            labelContent = { text("Action that overflows".layoutString) },
+                            modifier =
+                                LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
                             colors = filledVariantButtonColors()
                         )
                     },
@@ -139,13 +144,13 @@
                     mainSlot = {
                         card(
                             onClick = clickable,
-                            contentDescription = "Card".prop(),
+                            modifier = LayoutModifier.contentDescription("Card"),
                             width = expand(),
                             height = expand(),
                             background = { backgroundImage(protoLayoutResourceId = IMAGE_ID) }
                         ) {
                             text(
-                                "Card with image background".prop(),
+                                "Card with image background".layoutString,
                                 color = colorScheme.onBackground
                             )
                         }
@@ -154,11 +159,14 @@
                         iconEdgeButton(
                             onClick = clickable,
                             iconContent = { icon(ICON_ID) },
-                            contentDescription = CONTENT_DESCRIPTION_PLACEHOLDER.prop(),
+                            modifier =
+                                LayoutModifier.contentDescription(CONTENT_DESCRIPTION_PLACEHOLDER),
                             colors = filledTonalButtonColors()
                         )
                     },
-                    titleSlot = { text("Title that overflows".prop(), color = colorScheme.error) }
+                    titleSlot = {
+                        text("Title that overflows".layoutString, color = colorScheme.error)
+                    }
                 )
             }
         testCases["primarylayout_titlecard_bottomslot_golden$goldenSuffix"] =
@@ -171,21 +179,21 @@
                     mainSlot = {
                         titleCard(
                             onClick = clickable,
-                            contentDescription = "Card".prop(),
+                            modifier = LayoutModifier.contentDescription("Card"),
                             height = expand(),
                             title = {
                                 text(
                                     "Title Card text that will overflow after 2 max lines of text"
-                                        .prop()
+                                        .layoutString
                                 )
                             },
-                            time = { text("Now".prop()) },
-                            content = { text("Default title card".prop()) },
+                            time = { text("Now".layoutString) },
+                            content = { text("Default title card".layoutString) },
                             colors = filledVariantCardColors()
                         )
                     },
-                    bottomSlot = { text("Bottom Slot that overflows".prop()) },
-                    titleSlot = { text("TitleCard".prop(), color = colorScheme.secondaryDim) }
+                    bottomSlot = { text("Bottom Slot that overflows".layoutString) },
+                    titleSlot = { text("TitleCard".layoutString, color = colorScheme.secondaryDim) }
                 )
             }
         testCases["primarylayout_bottomslot_withlabel_golden$goldenSuffix"] =
@@ -198,9 +206,11 @@
                     mainSlot = {
                         coloredBox(color = colorScheme.errorContainer, shape = shapes.extraLarge)
                     },
-                    bottomSlot = { text("Bottom Slot".prop()) },
-                    labelForBottomSlot = { text("Label in bottom slot overflows".prop()) },
-                    titleSlot = { text("Title".prop(), color = colorScheme.secondaryContainer) }
+                    bottomSlot = { text("Bottom Slot".layoutString) },
+                    labelForBottomSlot = { text("Label in bottom slot overflows".layoutString) },
+                    titleSlot = {
+                        text("Title".layoutString, color = colorScheme.secondaryContainer)
+                    }
                 )
             }
         testCases["primarylayout_nobottomslot_golden$goldenSuffix"] =
@@ -213,8 +223,10 @@
                     mainSlot = {
                         coloredBox(color = colorScheme.tertiaryContainer, shape = shapes.extraLarge)
                     },
-                    labelForBottomSlot = { text("Ignored Label in bottom slot".prop()) },
-                    titleSlot = { text("Title".prop(), color = colorScheme.secondaryContainer) }
+                    labelForBottomSlot = { text("Ignored Label in bottom slot".layoutString) },
+                    titleSlot = {
+                        text("Title".layoutString, color = colorScheme.secondaryContainer)
+                    }
                 )
             }
         testCases["primarylayout_nobottomslotnotitle_golden$NORMAL_SCALE_SUFFIX"] =
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index 39391fe..84f52a6 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -29,9 +29,8 @@
 import androidx.wear.protolayout.ModifiersBuilders.Background
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
 import androidx.wear.protolayout.ModifiersBuilders.Corner
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
 import androidx.wear.protolayout.material3.AppCardDefaults.buildContentForAppCard
 import androidx.wear.protolayout.material3.AppCardStyle.Companion.defaultAppCardStyle
 import androidx.wear.protolayout.material3.CardDefaults.DEFAULT_CONTENT_PADDING
@@ -39,6 +38,10 @@
 import androidx.wear.protolayout.material3.CardDefaults.filledCardColors
 import androidx.wear.protolayout.material3.TitleCardDefaults.buildContentForTitleCard
 import androidx.wear.protolayout.material3.TitleCardStyle.Companion.defaultTitleCardStyle
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.semanticsRole
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
 import androidx.wear.protolayout.types.LayoutColor
 
 /**
@@ -48,9 +51,10 @@
  *
  * @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
  *   associated action.
- * @param contentDescription The content description to be read by Talkback.
  * @param title A slot for displaying the title of the card, expected to be one or two lines of
  *   text. Uses [CardColors.title] color by default.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param content The optional body content of the card. Uses [CardColors.content] color by default.
  * @param time An optional slot for displaying the time relevant to the contents of the card,
  *   expected to be a short piece of text. Uses [CardColors.time] color by default.
@@ -83,8 +87,8 @@
 // TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.titleCard(
     onClick: Clickable,
-    contentDescription: StringProp,
     title: (MaterialScope.() -> LayoutElement),
+    modifier: LayoutModifier = LayoutModifier,
     content: (MaterialScope.() -> LayoutElement)? = null,
     time: (MaterialScope.() -> LayoutElement)? = null,
     height: ContainerDimension = wrapWithMinTapTargetDimension(),
@@ -99,7 +103,7 @@
 ): LayoutElement =
     card(
         onClick = onClick,
-        contentDescription = contentDescription,
+        modifier = modifier,
         width = expand(),
         height = height,
         shape = shape,
@@ -167,9 +171,10 @@
  *
  * @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
  *   associated action.
- * @param contentDescription The content description to be read by Talkback.
  * @param title A slot for displaying the title of the card, expected to be one line of text. Uses
  *   [CardColors.title] color by default.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param content The optional body content of the card. Uses [CardColors.content] color by default.
  * @param avatar An optional slot in header for displaying small image, such as [avatarImage].
  * @param label An optional slot in header for displaying short, label text. Uses [CardColors.label]
@@ -203,8 +208,8 @@
 // TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.appCard(
     onClick: Clickable,
-    contentDescription: StringProp,
     title: (MaterialScope.() -> LayoutElement),
+    modifier: LayoutModifier = LayoutModifier,
     content: (MaterialScope.() -> LayoutElement)? = null,
     avatar: (MaterialScope.() -> LayoutElement)? = null,
     label: (MaterialScope.() -> LayoutElement)? = null,
@@ -219,7 +224,7 @@
 ): LayoutElement =
     card(
         onClick = onClick,
-        contentDescription = contentDescription,
+        modifier = modifier,
         width = expand(),
         height = height,
         shape = shape,
@@ -302,7 +307,8 @@
  *
  * @param onClick Associated [Clickable] for click events. When the card is clicked it will fire the
  *   associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param shape Defines the card's shape, in other words the corner radius for this card.
  * @param backgroundColor The color to be used as a background of this card. If the background image
  *   is also specified, the image will be laid out on top of this color. In case of the fully opaque
@@ -323,7 +329,7 @@
 // TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.card(
     onClick: Clickable,
-    contentDescription: StringProp,
+    modifier: LayoutModifier = LayoutModifier,
     width: ContainerDimension = wrapWithMinTapTargetDimension(),
     height: ContainerDimension = wrapWithMinTapTargetDimension(),
     shape: Corner = shapes.large,
@@ -336,10 +342,11 @@
 
     backgroundColor?.let { backgroundBuilder.setColor(it.prop) }
 
+    val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
     val modifiers =
-        Modifiers.Builder()
+        defaultModifier
+            .toProtoLayoutModifiersBuilder()
             .setClickable(onClick)
-            .setSemantics(contentDescription.buttonRoleSemantics())
             .setMetadata(METADATA_TAG.toElementMetadata())
             .setBackground(backgroundBuilder.build())
 
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
index 65142f6..227f92c 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/EdgeButton.kt
@@ -28,7 +28,7 @@
 import androidx.wear.protolayout.ModifiersBuilders.Corner
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
 import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
@@ -41,6 +41,10 @@
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.TOP_CORNER_RADIUS
 import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.DEFAULT
 import androidx.wear.protolayout.material3.EdgeButtonStyle.Companion.TOP_ALIGN
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.modifiers.semanticsRole
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiersBuilder
 
 /**
  * ProtoLayout Material3 component edge button that offers a single slot to take an icon or similar
@@ -56,7 +60,8 @@
  *
  * @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
  *   the associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
  *   will be used as high emphasis button. Other recommended colors are
  *   [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
@@ -69,16 +74,11 @@
 // TODO: b/346958146 - link EdgeButton visuals in DAC
 public fun MaterialScope.iconEdgeButton(
     onClick: Clickable,
-    contentDescription: StringProp,
+    modifier: LayoutModifier = LayoutModifier,
     colors: ButtonColors = filledButtonColors(),
     iconContent: (MaterialScope.() -> LayoutElement)
 ): LayoutElement =
-    edgeButton(
-        onClick = onClick,
-        contentDescription = contentDescription,
-        colors = colors,
-        style = DEFAULT
-    ) {
+    edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = DEFAULT) {
         withStyle(defaultIconStyle = IconStyle(size = ICON_SIZE_DP.toDp(), tintColor = colors.icon))
             .iconContent()
     }
@@ -97,7 +97,8 @@
  *
  * @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
  *   the associated action.
- * @param contentDescription The content description to be read by Talkback.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
  *   will be used as high emphasis button. Other recommended colors are
  *   [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
@@ -110,16 +111,11 @@
 // TODO(b/346958146): link EdgeButton visuals in DAC
 public fun MaterialScope.textEdgeButton(
     onClick: Clickable,
-    contentDescription: StringProp,
+    modifier: LayoutModifier = LayoutModifier,
     colors: ButtonColors = filledButtonColors(),
     labelContent: (MaterialScope.() -> LayoutElement)
 ): LayoutElement =
-    edgeButton(
-        onClick = onClick,
-        contentDescription = contentDescription,
-        colors = colors,
-        style = TOP_ALIGN
-    ) {
+    edgeButton(onClick = onClick, modifier = modifier, colors = colors, style = TOP_ALIGN) {
         withStyle(
                 defaultTextElementStyle =
                     TextElementStyle(
@@ -144,12 +140,13 @@
  *
  * @param onClick Associated [Clickable] for click events. When the button is clicked it will fire
  *   the associated action.
- * @param contentDescription The content description to be read by Talkback.
  * @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
  *   will be used as high emphasis button. Other recommended colors are
  *   [ButtonDefaults.filledTonalButtonColors] and [ButtonDefaults.filledVariantButtonColors]. If
  *   using custom colors, it is important to choose a color pair from same role to ensure
  *   accessibility with sufficient color contrast.
+ * @param modifier Modifiers to set to this element. It's highly recommended to set a content
+ *   description using [contentDescription].
  * @param style The style used for the inner content, specifying how the content should be aligned.
  *   It is recommended to use [EdgeButtonStyle.TOP_ALIGN] for long, wide content. If not set,
  *   defaults to [EdgeButtonStyle.DEFAULT] which center-aligns the content.
@@ -159,8 +156,8 @@
 // TODO(b/346958146): link EdgeButton visuals in DAC
 private fun MaterialScope.edgeButton(
     onClick: Clickable,
-    contentDescription: StringProp,
     colors: ButtonColors,
+    modifier: LayoutModifier = LayoutModifier,
     style: EdgeButtonStyle = DEFAULT,
     content: MaterialScope.() -> LayoutElement
 ): LayoutElement {
@@ -173,10 +170,11 @@
     val bottomCornerRadiusX = dp(edgeButtonWidth / 2f)
     val bottomCornerRadiusY = dp(EDGE_BUTTON_HEIGHT_DP - TOP_CORNER_RADIUS.value)
 
-    val modifiers: Modifiers.Builder =
-        Modifiers.Builder()
+    val defaultModifier = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON) then modifier
+    val modifiers =
+        defaultModifier
+            .toProtoLayoutModifiersBuilder()
             .setClickable(onClick)
-            .setSemantics(contentDescription.buttonRoleSemantics())
             .setBackground(
                 Background.Builder()
                     .setColor(colors.container.prop)
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index 36c3f6f..4cf5b75 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -22,8 +22,6 @@
 import androidx.annotation.Dimension.Companion.DP
 import androidx.annotation.Dimension.Companion.SP
 import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
-import androidx.wear.protolayout.ColorBuilders.argb
 import androidx.wear.protolayout.DimensionBuilders.DpProp
 import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
 import androidx.wear.protolayout.DimensionBuilders.dp
@@ -43,9 +41,6 @@
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.ModifiersBuilders.Padding
-import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
-import androidx.wear.protolayout.ModifiersBuilders.Semantics
-import androidx.wear.protolayout.TypeBuilders.StringProp
 import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory
 import androidx.wear.protolayout.types.LayoutColor
 import androidx.wear.protolayout.types.argb
@@ -85,9 +80,6 @@
 
 @Dimension(unit = SP) private fun Float.dpToSpLinear(fontScale: Float): Float = this / fontScale
 
-internal fun StringProp.buttonRoleSemantics() =
-    Semantics.Builder().setContentDescription(this).setRole(SEMANTICS_ROLE_BUTTON).build()
-
 internal fun Int.toDp() = dp(this.toFloat())
 
 internal fun String.toElementMetadata() = ElementMetadata.Builder().setTagData(toTagBytes()).build()
@@ -100,9 +92,6 @@
 internal fun verticalSpacer(@Dimension(unit = DP) widthDp: Int): Spacer =
     Spacer.Builder().setWidth(widthDp.toDp()).setHeight(expand()).build()
 
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun String.prop(): StringProp = StringProp.Builder(this).build()
-
 /**
  * Returns [wrap] but with minimum dimension of [MINIMUM_TAP_TARGET_SIZE] for accessibility
  * requirements of tap targets.
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
index 5faf4b5..074afc8 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Text.kt
@@ -17,14 +17,13 @@
 package androidx.wear.protolayout.material3
 
 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
-import androidx.wear.protolayout.LayoutElementBuilders.Text
 import androidx.wear.protolayout.LayoutElementBuilders.TextAlignment
 import androidx.wear.protolayout.LayoutElementBuilders.TextOverflow
-import androidx.wear.protolayout.ModifiersBuilders.Modifiers
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
-import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.layout.basicText
 import androidx.wear.protolayout.material3.Typography.TypographyToken
+import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.LayoutString
 
 /**
  * ProtoLayout component that represents text object holding any information.
@@ -33,8 +32,6 @@
  * [Typography].
  *
  * @param text The text content for this component.
- * @param stringLayoutConstraint The layout constraints used to correctly measure Text view size and
- *   align text when `text` has dynamic value.
  * @param typography The typography from [Typography] to be applied to this text. This will have
  *   predefined default value specified by each components that uses this text, to achieve the
  *   recommended look.
@@ -46,14 +43,12 @@
  * @param maxLines The maximum number of lines that text can occupy.
  * @param multilineAlignment The horizontal alignment of the multiple lines of text.
  * @param overflow The overflow strategy when text doesn't have enough space to be shown.
- * @param modifiers The additional [Modifiers] for this text.
+ * @param modifiers Modifiers to set to this element.
  * @sample androidx.wear.protolayout.material3.samples.helloWorldTextDefault
  * @sample androidx.wear.protolayout.material3.samples.helloWorldTextDynamicCustom
  */
 public fun MaterialScope.text(
-    text: StringProp,
-    stringLayoutConstraint: StringLayoutConstraint =
-        StringLayoutConstraint.Builder(text.value).build(),
+    text: LayoutString,
     @TypographyToken typography: Int = defaultTextElementStyle.typography,
     color: LayoutColor = defaultTextElementStyle.color,
     italic: Boolean = defaultTextElementStyle.italic,
@@ -62,20 +57,18 @@
     maxLines: Int = defaultTextElementStyle.maxLines,
     @TextAlignment multilineAlignment: Int = defaultTextElementStyle.multilineAlignment,
     @TextOverflow overflow: Int = defaultTextElementStyle.overflow,
-    modifiers: Modifiers = Modifiers.Builder().build()
+    modifiers: LayoutModifier = LayoutModifier
 ): LayoutElement =
-    Text.Builder()
-        .setText(text)
-        .setLayoutConstraintsForDynamicText(stringLayoutConstraint)
-        .setFontStyle(
+    basicText(
+        text = text,
+        fontStyle =
             createFontStyleBuilder(typographyToken = typography, deviceConfiguration, scalable)
                 .setColor(color.prop)
                 .setItalic(italic)
                 .setUnderline(underline)
-                .build()
-        )
-        .setMaxLines(maxLines)
-        .setMultilineAlignment(multilineAlignment)
-        .setOverflow(overflow)
-        .setModifiers(modifiers)
-        .build()
+                .build(),
+        maxLines = maxLines,
+        multilineAlignment = multilineAlignment,
+        overflow = overflow,
+        modifier = modifiers
+    )
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
index f96a668..9d2b8aa 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CardTest.kt
@@ -22,6 +22,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.wear.protolayout.DeviceParametersBuilders
 import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
 import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
 import androidx.wear.protolayout.testing.hasClickable
 import androidx.wear.protolayout.testing.hasColor
@@ -32,6 +34,7 @@
 import androidx.wear.protolayout.testing.hasText
 import androidx.wear.protolayout.testing.hasWidth
 import androidx.wear.protolayout.types.argb
+import androidx.wear.protolayout.types.layoutString
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.internal.DoNotInstrument
@@ -151,10 +154,10 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 card(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                     background = { backgroundImage(IMAGE_ID) }
                 ) {
-                    text(TEXT.prop())
+                    text(TEXT.layoutString)
                 }
             }
 
@@ -169,10 +172,10 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 card(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                     backgroundColor = color.argb
                 ) {
-                    text(TEXT.prop())
+                    text(TEXT.layoutString)
                 }
             }
 
@@ -190,7 +193,7 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 titleCard(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                     colors =
                         CardColors(
                             background = backgroundColor.argb,
@@ -198,9 +201,9 @@
                             content = contentColor.argb,
                             time = timeColor.argb
                         ),
-                    title = { text(TEXT.prop()) },
-                    content = { text(TEXT2.prop()) },
-                    time = { text(TEXT3.prop()) },
+                    title = { text(TEXT.layoutString) },
+                    content = { text(TEXT2.layoutString) },
+                    time = { text(TEXT3.layoutString) },
                 )
             }
 
@@ -224,7 +227,7 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 appCard(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                     colors =
                         CardColors(
                             background = backgroundColor.argb,
@@ -233,10 +236,10 @@
                             time = timeColor.argb,
                             label = labelColor.argb
                         ),
-                    title = { text(TEXT.prop()) },
-                    content = { text(TEXT2.prop()) },
-                    time = { text(TEXT3.prop()) },
-                    label = { text(TEXT4.prop()) },
+                    title = { text(TEXT.layoutString) },
+                    content = { text(TEXT2.layoutString) },
+                    time = { text(TEXT3.layoutString) },
+                    label = { text(TEXT4.layoutString) },
                 )
             }
 
@@ -259,11 +262,11 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 card(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                     width = expand(),
                     height = height.toDp()
                 ) {
-                    text(TEXT.prop())
+                    text(TEXT.layoutString)
                 }
             }
 
@@ -297,8 +300,11 @@
 
         private val DEFAULT_CONTAINER_CARD_WITH_TEXT =
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
-                card(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION.prop()) {
-                    text(TEXT.prop())
+                card(
+                    onClick = CLICKABLE,
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+                ) {
+                    text(TEXT.layoutString)
                 }
             }
 
@@ -306,10 +312,10 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 titleCard(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
-                    title = { text(TEXT.prop()) },
-                    content = { text(TEXT2.prop()) },
-                    time = { text(TEXT3.prop()) },
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
+                    title = { text(TEXT.layoutString) },
+                    content = { text(TEXT2.layoutString) },
+                    time = { text(TEXT3.layoutString) },
                 )
             }
 
@@ -317,12 +323,12 @@
             materialScope(CONTEXT, DEVICE_CONFIGURATION) {
                 appCard(
                     onClick = CLICKABLE,
-                    contentDescription = CONTENT_DESCRIPTION.prop(),
-                    title = { text(TEXT.prop()) },
-                    content = { text(TEXT2.prop()) },
-                    time = { text(TEXT3.prop()) },
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
+                    title = { text(TEXT.layoutString) },
+                    content = { text(TEXT2.layoutString) },
+                    time = { text(TEXT3.layoutString) },
                     avatar = { avatarImage(AVATAR_ID) },
-                    label = { text(TEXT4.prop()) }
+                    label = { text(TEXT4.layoutString) }
                 )
             }
     }
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
index 0e0f7b7..7a85fae 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/EdgeButtonTest.kt
@@ -23,11 +23,12 @@
 import androidx.wear.protolayout.DeviceParametersBuilders
 import androidx.wear.protolayout.LayoutElementBuilders.Image
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringProp
 import androidx.wear.protolayout.expression.AppDataKey
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.BOTTOM_MARGIN_DP
 import androidx.wear.protolayout.material3.EdgeButtonDefaults.EDGE_BUTTON_HEIGHT_DP
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
 import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
 import androidx.wear.protolayout.testing.LayoutElementMatcher
 import androidx.wear.protolayout.testing.hasColor
@@ -37,6 +38,9 @@
 import androidx.wear.protolayout.testing.hasText
 import androidx.wear.protolayout.testing.hasWidth
 import androidx.wear.protolayout.testing.isClickable
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.internal.DoNotInstrument
@@ -63,7 +67,7 @@
     fun contentDescription() {
         LayoutElementAssertionsProvider(ICON_EDGE_BUTTON)
             .onElement(isClickable())
-            .assert(hasContentDescription(CONTENT_DESCRIPTION.value))
+            .assert(hasContentDescription(CONTENT_DESCRIPTION))
     }
 
     @Test
@@ -92,7 +96,7 @@
                 materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
                     iconEdgeButton(
                         onClick = CLICKABLE,
-                        contentDescription = CONTENT_DESCRIPTION,
+                        modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION),
                         colors =
                             ButtonColors(
                                 container = COLOR_SCHEME.tertiaryContainer,
@@ -117,8 +121,11 @@
         val label = "static text"
         val textEdgeButton =
             materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
-                textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
-                    text(label.prop())
+                textEdgeButton(
+                    onClick = CLICKABLE,
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+                ) {
+                    text(label.layoutString)
                 }
             }
 
@@ -132,14 +139,19 @@
         val label = "test text"
         val stateKey = AppDataKey<DynamicInt32>("testKey")
         val dynamicLabel =
-            StringProp.Builder(label)
-                .setDynamicValue(DynamicInt32.from(stateKey).times(2).format())
-                .build()
+            LayoutString(
+                label,
+                DynamicInt32.from(stateKey).times(2).format(),
+                label.asLayoutConstraint()
+            )
 
         val queryProvider =
             LayoutElementAssertionsProvider(
                 materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
-                    textEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+                    textEdgeButton(
+                        onClick = CLICKABLE,
+                        modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+                    ) {
                         text(dynamicLabel)
                     }
                 }
@@ -166,12 +178,15 @@
                 .setId("action_id")
                 .build()
 
-        private val CONTENT_DESCRIPTION = "it is an edge button".prop()
+        private const val CONTENT_DESCRIPTION = "it is an edge button"
 
-        private val RES_ID = "resId"
+        private const val RES_ID = "resId"
         private val ICON_EDGE_BUTTON =
             materialScope(CONTEXT, DEVICE_CONFIGURATION, allowDynamicTheme = false) {
-                iconEdgeButton(onClick = CLICKABLE, contentDescription = CONTENT_DESCRIPTION) {
+                iconEdgeButton(
+                    onClick = CLICKABLE,
+                    modifier = LayoutModifier.contentDescription(CONTENT_DESCRIPTION)
+                ) {
                     icon(RES_ID)
                 }
             }
diff --git a/wear/protolayout/protolayout-testing/api/current.txt b/wear/protolayout/protolayout-testing/api/current.txt
index 47ceaab..1503d9d1 100644
--- a/wear/protolayout/protolayout-testing/api/current.txt
+++ b/wear/protolayout/protolayout-testing/api/current.txt
@@ -12,7 +12,7 @@
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension height);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp height);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasImage(String protolayoutResId);
-    method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.TypeBuilders.StringProp value);
+    method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.types.LayoutString value);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString, optional boolean ignoreCase);
diff --git a/wear/protolayout/protolayout-testing/api/restricted_current.txt b/wear/protolayout/protolayout-testing/api/restricted_current.txt
index 47ceaab..1503d9d1 100644
--- a/wear/protolayout/protolayout-testing/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-testing/api/restricted_current.txt
@@ -12,7 +12,7 @@
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ContainerDimension height);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasHeight(androidx.wear.protolayout.DimensionBuilders.ProportionalDimensionProp height);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasImage(String protolayoutResId);
-    method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.TypeBuilders.StringProp value);
+    method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(androidx.wear.protolayout.types.LayoutString value);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString);
     method public static androidx.wear.protolayout.testing.LayoutElementMatcher hasText(String value, optional boolean subString, optional boolean ignoreCase);
diff --git a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
index 2e024cc..8a8a141 100644
--- a/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
+++ b/wear/protolayout/protolayout-testing/src/main/java/androidx/wear/protolayout/testing/filters.kt
@@ -33,8 +33,8 @@
 import androidx.wear.protolayout.LayoutElementBuilders.Spannable
 import androidx.wear.protolayout.LayoutElementBuilders.Text
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
-import androidx.wear.protolayout.TypeBuilders.StringProp
 import androidx.wear.protolayout.proto.DimensionProto
+import androidx.wear.protolayout.types.LayoutString
 
 /** Returns a [LayoutElementMatcher] which checks whether the element is clickable. */
 public fun isClickable(): LayoutElementMatcher =
@@ -89,12 +89,12 @@
 /**
  * Returns a [LayoutElementMatcher] which checks whether the element's text equals the given value.
  */
-public fun hasText(value: StringProp): LayoutElementMatcher =
+public fun hasText(value: LayoutString): LayoutElementMatcher =
     LayoutElementMatcher("Element text = '$value'") {
         it is Text &&
             // TODO: b/375448507 - Add dynamic data evaluation and compare the current string value
-            it.text?.toProto()?.value == value.toProto().value &&
-            it.text?.toProto()?.dynamicValue == value.toProto().dynamicValue
+            it.text?.toProto()?.value == value.staticValue &&
+            it.text?.toProto()?.dynamicValue == value.dynamicValue?.toDynamicStringProto()
     }
 
 /**
diff --git a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
index 6ca1a48..ba732a8 100644
--- a/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
+++ b/wear/protolayout/protolayout-testing/src/test/java/androidx/wear/protolayout/testing/FiltersTest.kt
@@ -31,16 +31,18 @@
 import androidx.wear.protolayout.LayoutElementBuilders.Image
 import androidx.wear.protolayout.LayoutElementBuilders.Row
 import androidx.wear.protolayout.LayoutElementBuilders.Spacer
-import androidx.wear.protolayout.LayoutElementBuilders.Text
 import androidx.wear.protolayout.ModifiersBuilders.Background
 import androidx.wear.protolayout.ModifiersBuilders.Clickable
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.ModifiersBuilders.Semantics
 import androidx.wear.protolayout.StateBuilders
-import androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint
 import androidx.wear.protolayout.TypeBuilders.StringProp
 import androidx.wear.protolayout.expression.DynamicBuilders
+import androidx.wear.protolayout.layout.basicText
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.asLayoutConstraint
+import androidx.wear.protolayout.types.layoutString
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -142,7 +144,7 @@
     @Test
     fun hasText() {
         val textContent = "random test content"
-        val testElement = Text.Builder().setText(textContent).build()
+        val testElement = basicText(textContent.layoutString)
 
         assertThat(hasText(textContent).matches(testElement)).isTrue()
         assertThat(hasText("blabla").matches(testElement)).isFalse()
@@ -151,16 +153,12 @@
     @Test
     fun hasDynamicText() {
         val textContent =
-            StringProp.Builder("static content")
-                .setDynamicValue(DynamicBuilders.DynamicString.constant("dynamic content"))
-                .build()
-        val testElement =
-            Text.Builder()
-                .setText(textContent)
-                .setLayoutConstraintsForDynamicText(
-                    StringLayoutConstraint.Builder("static content").build()
-                )
-                .build()
+            LayoutString(
+                "static content",
+                DynamicBuilders.DynamicString.constant("dynamic content"),
+                "static content".asLayoutConstraint()
+            )
+        val testElement = basicText(textContent)
 
         assertThat(hasText(textContent).matches(testElement)).isTrue()
         assertThat(hasText("blabla").matches(testElement)).isFalse()
@@ -197,12 +195,11 @@
     @Test
     fun hasColor_onTextStyle() {
         val testText =
-            Text.Builder()
-                .setText("text")
-                .setFontStyle(
+            basicText(
+                "text".layoutString,
+                fontStyle =
                     FontStyle.Builder().setColor(ColorProp.Builder(Color.CYAN).build()).build()
-                )
-                .build()
+            )
 
         assertThat(hasColor(Color.CYAN).matches(testText)).isTrue()
         assertThat(hasColor(Color.GREEN).matches(testText)).isFalse()
@@ -340,7 +337,7 @@
                 .addContent(
                     Column.Builder()
                         .setWidth(width)
-                        .addContent(Text.Builder().setText("text").build())
+                        .addContent(basicText("text".layoutString))
                         .build()
                 )
                 .build()
@@ -370,7 +367,7 @@
                 .addContent(
                     Column.Builder()
                         .setWidth(width)
-                        .addContent(Text.Builder().setText("text").build())
+                        .addContent(basicText("text".layoutString))
                         .build()
                 )
                 .build()
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index 8ca5dc7..73620fa 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1481,6 +1481,42 @@
 
 }
 
+package androidx.wear.protolayout.layout {
+
+  public final class TextKt {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.Text basicText(androidx.wear.protolayout.types.LayoutString text, optional androidx.wear.protolayout.LayoutElementBuilders.FontStyle? fontStyle, optional androidx.wear.protolayout.modifiers.LayoutModifier? modifier, optional int maxLines, optional int multilineAlignment, optional int overflow, optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float lineHeight);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle fontStyle(optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float size, optional boolean italic, optional boolean underline, optional androidx.wear.protolayout.types.LayoutColor? color, optional int weight, optional float letterSpacingEm, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) java.util.List<java.lang.Float> additionalSizesSp, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<? extends androidx.wear.protolayout.LayoutElementBuilders.FontSetting> settings, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<java.lang.String> preferredFontFamilies);
+  }
+
+}
+
+package androidx.wear.protolayout.modifiers {
+
+  public interface LayoutModifier {
+    method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+    method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
+    field public static final androidx.wear.protolayout.modifiers.LayoutModifier.Companion Companion;
+  }
+
+  public static final class LayoutModifier.Companion implements androidx.wear.protolayout.modifiers.LayoutModifier {
+    method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+  }
+
+  public static interface LayoutModifier.Element extends androidx.wear.protolayout.modifiers.LayoutModifier {
+    method public default <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+  }
+
+  public final class ModifierAppliersKt {
+    method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
+  }
+
+  public final class SemanticsKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
+  }
+
+}
+
 package androidx.wear.protolayout.types {
 
   public final class LayoutColor {
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index 8ca5dc7..73620fa 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1481,6 +1481,42 @@
 
 }
 
+package androidx.wear.protolayout.layout {
+
+  public final class TextKt {
+    method public static androidx.wear.protolayout.LayoutElementBuilders.Text basicText(androidx.wear.protolayout.types.LayoutString text, optional androidx.wear.protolayout.LayoutElementBuilders.FontStyle? fontStyle, optional androidx.wear.protolayout.modifiers.LayoutModifier? modifier, optional int maxLines, optional int multilineAlignment, optional int overflow, optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float lineHeight);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.FontStyle fontStyle(optional @Dimension(unit=androidx.annotation.Dimension.Companion.SP) float size, optional boolean italic, optional boolean underline, optional androidx.wear.protolayout.types.LayoutColor? color, optional int weight, optional float letterSpacingEm, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=300) java.util.List<java.lang.Float> additionalSizesSp, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<? extends androidx.wear.protolayout.LayoutElementBuilders.FontSetting> settings, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=400) java.util.List<java.lang.String> preferredFontFamilies);
+  }
+
+}
+
+package androidx.wear.protolayout.modifiers {
+
+  public interface LayoutModifier {
+    method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+    method public default infix androidx.wear.protolayout.modifiers.LayoutModifier then(androidx.wear.protolayout.modifiers.LayoutModifier other);
+    field public static final androidx.wear.protolayout.modifiers.LayoutModifier.Companion Companion;
+  }
+
+  public static final class LayoutModifier.Companion implements androidx.wear.protolayout.modifiers.LayoutModifier {
+    method public <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+  }
+
+  public static interface LayoutModifier.Element extends androidx.wear.protolayout.modifiers.LayoutModifier {
+    method public default <R> R foldIn(R initial, kotlin.jvm.functions.Function2<? super R,? super androidx.wear.protolayout.modifiers.LayoutModifier.Element,? extends R> operation);
+  }
+
+  public final class ModifierAppliersKt {
+    method public static androidx.wear.protolayout.ModifiersBuilders.Modifiers toProtoLayoutModifiers(androidx.wear.protolayout.modifiers.LayoutModifier);
+  }
+
+  public final class SemanticsKt {
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier contentDescription(androidx.wear.protolayout.modifiers.LayoutModifier, String staticValue, optional @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? dynamicValue);
+    method public static androidx.wear.protolayout.modifiers.LayoutModifier semanticsRole(androidx.wear.protolayout.modifiers.LayoutModifier, int semanticsRole);
+  }
+
+}
+
 package androidx.wear.protolayout.types {
 
   public final class LayoutColor {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt
new file mode 100644
index 0000000..45b96f7
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/layout/Text.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.layout
+
+import android.annotation.SuppressLint
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.SP
+import androidx.annotation.OptIn
+import androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.FontSetting
+import androidx.wear.protolayout.LayoutElementBuilders.FontStyle
+import androidx.wear.protolayout.LayoutElementBuilders.FontWeight
+import androidx.wear.protolayout.LayoutElementBuilders.TEXT_ALIGN_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_UNDEFINED
+import androidx.wear.protolayout.LayoutElementBuilders.Text
+import androidx.wear.protolayout.LayoutElementBuilders.TextAlignment
+import androidx.wear.protolayout.LayoutElementBuilders.TextOverflow
+import androidx.wear.protolayout.expression.ProtoLayoutExperimental
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.LayoutColor
+import androidx.wear.protolayout.types.LayoutString
+import androidx.wear.protolayout.types.em
+import androidx.wear.protolayout.types.sp
+import java.util.stream.Collectors.toList
+import java.util.stream.Stream
+
+/**
+ * Builds a text string.
+ *
+ * @param text The text to render.
+ * @param fontStyle The style of font to use (size, bold etc). If not specified, defaults to the
+ *   platform's default body font.
+ * @param modifier Modifiers to set to this element..
+ * @param maxLines The maximum number of lines that can be represented by the [Text] element. If not
+ *   defined, the [Text] element will be treated as a single-line element.
+ * @param multilineAlignment Alignment of the text within its bounds. Note that a [Text] element
+ *   will size itself to wrap its contents, so this option is meaningless for single-line text (for
+ *   that, use alignment of the outer container). For multi-line text, however, this will set the
+ *   alignment of lines relative to the [Text] element bounds. If not defined, defaults to
+ *   TEXT_ALIGN_CENTER.
+ * @param overflow How to handle text which overflows the bound of the [Text] element. A [Text]
+ *   element will grow as large as possible inside its parent container (while still respecting
+ *   max_lines); if it cannot grow large enough to render all of its text, the text which cannot fit
+ *   inside its container will be truncated. If not defined, defaults to TEXT_OVERFLOW_TRUNCATE.
+ * @param lineHeight The explicit height between lines of text. This is equivalent to the vertical
+ *   distance between subsequent baselines. If not specified, defaults the font's recommended
+ *   interline spacing.
+ */
+@SuppressLint("ProtoLayoutMinSchema")
+@Suppress("MissingJvmstatic") // Kotlin-friendly version of already available Java Apis
+fun basicText(
+    text: LayoutString,
+    fontStyle: FontStyle? = null,
+    modifier: LayoutModifier? = null,
+    maxLines: Int = 0,
+    @TextAlignment multilineAlignment: Int = TEXT_ALIGN_UNDEFINED,
+    @TextOverflow overflow: Int = TEXT_OVERFLOW_UNDEFINED,
+    @Dimension(SP) lineHeight: Float = Float.NaN,
+) =
+    Text.Builder()
+        .setText(text.prop)
+        .apply {
+            text.layoutConstraint?.let { setLayoutConstraintsForDynamicText(it) }
+            fontStyle?.let { setFontStyle(it) }
+            modifier?.let { setModifiers(it.toProtoLayoutModifiers()) }
+            if (maxLines != 0) {
+                setMaxLines(maxLines)
+            }
+            if (multilineAlignment != TEXT_ALIGN_UNDEFINED) {
+                setMultilineAlignment(multilineAlignment)
+            }
+            if (overflow != TEXT_OVERFLOW_UNDEFINED) {
+                setOverflow(overflow)
+            }
+            if (!lineHeight.isNaN()) {
+                setLineHeight(lineHeight.sp)
+            }
+        }
+        .build()
+
+/**
+ * Builds the styling of a font (e.g. font size, and metrics).
+ *
+ * @param size The size of the font, in scaled pixels (sp). If not specified, defaults to the size
+ *   of the system's "body" font.
+ * @param italic Whether the text should be rendered in a italic typeface.
+ * @param underline Whether the text should be rendered with an underline.
+ * @param color The text color. If not defined, defaults to white.
+ * @param weight The weight of the font. If the provided value is not supported on a platform, the
+ *   nearest supported value will be used. If not defined, or when set to an invalid value, defaults
+ *   to "normal".
+ * @param letterSpacingEm The text letter-spacing. Positive numbers increase the space between
+ *   letters while negative numbers tighten the space. If not specified, defaults to 0.
+ * @param additionalSizesSp when this [FontStyle] is applied to a [Text] element with static text,
+ *   the text size will be automatically picked from the provided sizes to try to perfectly fit
+ *   within its parent bounds. In other words, the largest size from the specified preset sizes that
+ *   can fit the most text within the parent bounds will be used.
+ * @param settings The collection of font settings to be applied. If more than one Setting with the
+ *   same axis tag is specified, the first one will be used. Supported settings depend on the font
+ *   used and renderer version.
+ * @param preferredFontFamilies is the ordered list of font families to pick from for this
+ *   [FontStyle]. If the given font family is not available on a device, the fallback values will be
+ *   attempted to use, in order in which they are given. Note that support for font family
+ *   customization is dependent on the target platform.
+ */
+@OptIn(ProtoLayoutExperimental::class)
+@SuppressLint("ProtoLayoutMinSchema")
+@Suppress("MissingJvmstatic") // Kotlin-friendly version of already available Java Apis
+fun fontStyle(
+    @Dimension(SP) size: Float = 0f,
+    italic: Boolean = false,
+    underline: Boolean = false,
+    color: LayoutColor? = null,
+    @FontWeight weight: Int = FONT_WEIGHT_UNDEFINED,
+    letterSpacingEm: Float = Float.NaN,
+    @RequiresSchemaVersion(major = 1, minor = 300) additionalSizesSp: List<Float> = listOf(),
+    @RequiresSchemaVersion(major = 1, minor = 400) settings: List<FontSetting> = listOf(),
+    @RequiresSchemaVersion(major = 1, minor = 400) preferredFontFamilies: List<String> = listOf()
+): FontStyle =
+    FontStyle.Builder()
+        .apply {
+            if (size != 0f) {
+                setSize(size.sp)
+            }
+            setItalic(italic)
+            setUnderline(underline)
+            color?.let { setColor(it.prop) }
+            if (weight != FONT_WEIGHT_UNDEFINED) {
+                setWeight(weight)
+            }
+            if (settings.isNotEmpty()) {
+                setSettings(*settings.toTypedArray())
+            }
+            if (!letterSpacingEm.isNaN()) {
+                setLetterSpacing(letterSpacingEm.em)
+            }
+            if (preferredFontFamilies.isNotEmpty()) {
+                setPreferredFontFamilies(
+                    preferredFontFamilies.first(),
+                    *preferredFontFamilies.subList(1, preferredFontFamilies.size).toTypedArray()
+                )
+            }
+            if (additionalSizesSp.isNotEmpty()) {
+                setSizes(
+                    *Stream.concat(
+                            if (size != 0f) Stream.of(size.toInt()) else Stream.empty(),
+                            additionalSizesSp.stream().map { it.toInt() }
+                        )
+                        .collect(toList())
+                        .toIntArray()
+                )
+            }
+        }
+        .build()
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt
new file mode 100644
index 0000000..74657a0
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/LayoutModifier.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import java.util.Objects
+
+/**
+ * An ordered, immutable collection of [modifier elements][LayoutModifier.Element] that decorate or
+ * add behavior to ProtoLayout layout elements. For example, backgrounds, padding and click actions.
+ * When a single modifier is applied multiple times, the last one wins.
+ *
+ * @sample androidx.wear.protolayout.material3.samples.edgeButtonSampleIcon
+ */
+interface LayoutModifier {
+    /**
+     * Accumulates a value starting with [initial] and applying [operation] to the current value and
+     * each element from outside in.
+     *
+     * Elements wrap one another in a chain from left to right; an [Element] that appears to the
+     * left of another in a `+` expression or in [operation]'s parameter order affects all of the
+     * elements that appear after it. [foldIn] may be used to accumulate a value starting from the
+     * parent or head of the modifier chain to the final wrapped child.
+     */
+    fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
+
+    /**
+     * Concatenates this modifier with another.
+     *
+     * Returns a [LayoutModifier] representing this modifier followed by [other] in sequence.
+     */
+    infix fun then(other: LayoutModifier): LayoutModifier =
+        if (other === LayoutModifier) this else CombinedLayoutModifier(this, other)
+
+    /** A single element contained within a [LayoutModifier] chain. */
+    interface Element : LayoutModifier {
+        override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
+            operation(initial, this)
+    }
+
+    /**
+     * The companion object `LayoutModifier` is the empty, default, or starter [LayoutModifier] that
+     * contains no [elements][Element]. Use it to create a new [LayoutModifier] using modifier
+     * extension factory functions.
+     */
+    companion object : LayoutModifier {
+        @Suppress("MissingJvmstatic")
+        override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
+
+        @Suppress("MissingJvmstatic")
+        override infix fun then(other: LayoutModifier): LayoutModifier = other
+
+        @Suppress("MissingJvmstatic") override fun toString(): String = "LayoutModifier"
+    }
+}
+
+/**
+ * A node in a [LayoutModifier] chain. A CombinedModifier always contains at least two elements; a
+ * * Modifier [outer] that wraps around the Modifier [inner].
+ */
+internal class CombinedLayoutModifier(
+    private val outer: LayoutModifier,
+    private val inner: LayoutModifier
+) : LayoutModifier {
+    override fun <R> foldIn(initial: R, operation: (R, LayoutModifier.Element) -> R): R =
+        inner.foldIn(outer.foldIn(initial, operation), operation)
+
+    override fun equals(other: Any?): Boolean =
+        other is CombinedLayoutModifier && outer == other.outer && inner == other.inner
+
+    override fun hashCode(): Int = Objects.hash(outer, inner)
+
+    override fun toString(): String =
+        "[" +
+            foldIn("") { acc, element ->
+                if (acc.isEmpty()) element.toString() else "$acc, $element"
+            } +
+            "]"
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
new file mode 100644
index 0000000..194809c
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/ModifierAppliers.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.ModifiersBuilders
+import androidx.wear.protolayout.ModifiersBuilders.Semantics
+
+/** Creates a [ModifiersBuilders.Modifiers] from a [LayoutModifier]. */
+fun LayoutModifier.toProtoLayoutModifiers(): ModifiersBuilders.Modifiers =
+    toProtoLayoutModifiersBuilder().build()
+
+// TODO: b/384921198 - Remove when M3 elements can use LayoutModifier chain for everything.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+/** Creates a [ModifiersBuilders.Modifiers.Builder] from a [LayoutModifier]. */
+fun LayoutModifier.toProtoLayoutModifiersBuilder(): ModifiersBuilders.Modifiers.Builder {
+    data class AccumulatingModifier(val semantics: Semantics.Builder? = null)
+
+    val accumulatingModifier =
+        this.foldIn(AccumulatingModifier()) { acc, e ->
+            when (e) {
+                is BaseSemanticElement -> AccumulatingModifier(semantics = e.foldIn(acc.semantics))
+                else -> acc
+            }
+        }
+
+    return ModifiersBuilders.Modifiers.Builder().apply {
+        accumulatingModifier.semantics?.let { setSemantics(it.build()) }
+    }
+}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
new file mode 100644
index 0000000..3bf0caf
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import android.annotation.SuppressLint
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
+import androidx.wear.protolayout.ModifiersBuilders.Semantics
+import androidx.wear.protolayout.ModifiersBuilders.SemanticsRole
+import androidx.wear.protolayout.TypeBuilders.StringProp
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.RequiresSchemaVersion
+import java.util.Objects
+
+internal class BaseSemanticElement(
+    val contentDescription: StringProp? = null,
+    @SemanticsRole val semanticsRole: Int = SEMANTICS_ROLE_NONE
+) : LayoutModifier.Element {
+    @SuppressLint("ProtoLayoutMinSchema")
+    fun foldIn(initial: Semantics.Builder?): Semantics.Builder =
+        (initial ?: Semantics.Builder()).apply {
+            contentDescription?.let { setContentDescription(it) }
+            if (semanticsRole != SEMANTICS_ROLE_NONE) {
+                setRole(semanticsRole)
+            }
+        }
+
+    override fun equals(other: Any?): Boolean =
+        other is BaseSemanticElement &&
+            contentDescription == other.contentDescription &&
+            semanticsRole == other.semanticsRole
+
+    override fun hashCode(): Int = Objects.hash(contentDescription, semanticsRole)
+
+    override fun toString(): String =
+        "BaseSemanticElement[contentDescription=$contentDescription, semanticRole=$semanticsRole"
+}
+
+/**
+ * Adds content description to be read by Talkback.
+ *
+ * @param staticValue The static content description. This value will be used if [dynamicValue] is
+ *   null, or if can't be resolved.
+ * @param dynamicValue The dynamic content description. This is useful when content of the element
+ *   itself is dynamic.
+ */
+@SuppressLint("ProtoLayoutMinSchema") // 1.2 schema only used when dynamicValue is non-null
+fun LayoutModifier.contentDescription(
+    staticValue: String,
+    @RequiresSchemaVersion(major = 1, minor = 200) dynamicValue: DynamicString? = null
+): LayoutModifier =
+    this then
+        BaseSemanticElement(
+            contentDescription =
+                StringProp.Builder(staticValue)
+                    .apply { dynamicValue?.let { setDynamicValue(it) } }
+                    .build()
+        )
+
+/**
+ * Adds the semantic role of user interface element. Accessibility services might use this to
+ * describe the element or do customizations.
+ */
+fun LayoutModifier.semanticsRole(@SemanticsRole semanticsRole: Int): LayoutModifier =
+    this then BaseSemanticElement(semanticsRole = semanticsRole)
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
new file mode 100644
index 0000000..2f4295e
--- /dev/null
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.types
+
+import androidx.wear.protolayout.DimensionBuilders.EmProp
+import androidx.wear.protolayout.DimensionBuilders.SpProp
+import androidx.wear.protolayout.TypeBuilders.BoolProp
+
+internal val Float.sp: SpProp
+    get() = SpProp.Builder().setValue(this).build()
+
+internal val Float.em: EmProp
+    get() = EmProp.Builder().setValue(this).build()
+
+internal val Boolean.prop: BoolProp
+    get() = BoolProp.Builder(this).build()
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
new file mode 100644
index 0000000..f91621c
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/modifiers/ModifiersTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.modifiers
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
+import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_NONE
+import androidx.wear.protolayout.expression.DynamicBuilders
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ModifiersTest {
+
+    @Test
+    fun contentDescription_toModifier() {
+        val modifiers =
+            LayoutModifier.contentDescription(
+                    STATIC_CONTENT_DESCRIPTION,
+                    DYNAMIC_CONTENT_DESCRIPTION
+                )
+                .toProtoLayoutModifiers()
+
+        assertThat(modifiers.semantics?.contentDescription?.value)
+            .isEqualTo(STATIC_CONTENT_DESCRIPTION)
+        assertThat(modifiers.semantics?.contentDescription?.dynamicValue?.toDynamicStringProto())
+            .isEqualTo(DYNAMIC_CONTENT_DESCRIPTION.toDynamicStringProto())
+        assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_NONE)
+    }
+
+    @Test
+    fun semanticsRole_toModifier() {
+        val modifiers = LayoutModifier.semanticsRole(SEMANTICS_ROLE_BUTTON).toProtoLayoutModifiers()
+
+        assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_BUTTON)
+        assertThat(modifiers.semantics?.contentDescription).isNull()
+    }
+
+    @Test
+    fun contentDescription_semanticRole_toModifier() {
+        val modifiers =
+            LayoutModifier.contentDescription(
+                    STATIC_CONTENT_DESCRIPTION,
+                    DYNAMIC_CONTENT_DESCRIPTION
+                )
+                .semanticsRole(SEMANTICS_ROLE_BUTTON)
+                .toProtoLayoutModifiers()
+
+        assertThat(modifiers.semantics?.contentDescription?.value)
+            .isEqualTo(STATIC_CONTENT_DESCRIPTION)
+        assertThat(modifiers.semantics?.contentDescription?.dynamicValue?.toDynamicStringProto())
+            .isEqualTo(DYNAMIC_CONTENT_DESCRIPTION.toDynamicStringProto())
+        assertThat(modifiers.semantics?.role).isEqualTo(SEMANTICS_ROLE_BUTTON)
+    }
+
+    companion object {
+        const val STATIC_CONTENT_DESCRIPTION = "content desc"
+        val DYNAMIC_CONTENT_DESCRIPTION = DynamicBuilders.DynamicString.constant("dynamic content")
+    }
+}
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
index b201000..968f80b 100644
--- a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/types/LayoutColorTest.kt
@@ -16,11 +16,14 @@
 
 package androidx.wear.protolayout.types
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.wear.protolayout.ColorBuilders.ColorProp
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
+import org.junit.runner.RunWith
 
+@RunWith(AndroidJUnit4::class)
 class LayoutColorTest {
     @Test
     fun staticColor_asLayoutColor() {
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index 3d1239a..b4eb6a6 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -27,9 +27,11 @@
 import androidx.wear.protolayout.material3.avatarImage
 import androidx.wear.protolayout.material3.materialScope
 import androidx.wear.protolayout.material3.primaryLayout
-import androidx.wear.protolayout.material3.prop
 import androidx.wear.protolayout.material3.text
 import androidx.wear.protolayout.material3.textEdgeButton
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.contentDescription
+import androidx.wear.protolayout.types.layoutString
 import androidx.wear.tiles.RequestBuilders
 import androidx.wear.tiles.TileBuilders
 import androidx.wear.tiles.TileService
@@ -89,7 +91,7 @@
             mainSlot = {
                 appCard(
                     onClick = EMPTY_LOAD_CLICKABLE,
-                    contentDescription = "Sample Card".prop(),
+                    modifier = LayoutModifier.contentDescription("Sample Card"),
                     colors =
                         CardColors(
                             background = colorScheme.tertiary,
@@ -99,25 +101,25 @@
                         ),
                     title = {
                         text(
-                            "Title Card!".prop(),
+                            "Title Card!".layoutString,
                             maxLines = 1,
                         )
                     },
                     content = {
                         text(
-                            "Content of this Card!".prop(),
+                            "Content of this Card!".layoutString,
                             maxLines = 1,
                         )
                     },
                     label = {
                         text(
-                            "Hello and welcome Tiles in AndroidX!".prop(),
+                            "Hello and welcome Tiles in AndroidX!".layoutString,
                         )
                     },
                     avatar = { avatarImage("id") },
                     time = {
                         text(
-                            "NOW".prop(),
+                            "NOW".layoutString,
                         )
                     }
                 )
@@ -125,9 +127,9 @@
             bottomSlot = {
                 textEdgeButton(
                     onClick = EMPTY_LOAD_CLICKABLE,
-                    contentDescription = "EdgeButton".prop(),
+                    modifier = LayoutModifier.contentDescription("EdgeButton"),
                 ) {
-                    text("Edge".prop())
+                    text("Edge".layoutString)
                 }
             }
         )
diff --git a/wear/tiles/tiles-testing/build.gradle b/wear/tiles/tiles-testing/build.gradle
index b7c9811..3b3f44b 100644
--- a/wear/tiles/tiles-testing/build.gradle
+++ b/wear/tiles/tiles-testing/build.gradle
@@ -21,7 +21,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
 import androidx.build.LibraryType
 
 plugins {
diff --git a/wear/tiles/tiles-tooling-preview/build.gradle b/wear/tiles/tiles-tooling-preview/build.gradle
index dc02618..ea458da 100644
--- a/wear/tiles/tiles-tooling-preview/build.gradle
+++ b/wear/tiles/tiles-tooling-preview/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/wear/watchface/watchface-client-guava/build.gradle b/wear/watchface/watchface-client-guava/build.gradle
index eba4a21..012b9e03 100644
--- a/wear/watchface/watchface-client-guava/build.gradle
+++ b/wear/watchface/watchface-client-guava/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index 579d3a7..021c9e3 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -21,7 +21,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.RunApiTasks
 
 import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
diff --git a/wear/watchface/watchface-complications-rendering/build.gradle b/wear/watchface/watchface-complications-rendering/build.gradle
index 995d4ad..f398b88 100644
--- a/wear/watchface/watchface-complications-rendering/build.gradle
+++ b/wear/watchface/watchface-complications-rendering/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.RunApiTasks
+
 import androidx.build.LibraryType
 
 plugins {
diff --git a/wear/watchface/watchface-complications/build.gradle b/wear/watchface/watchface-complications/build.gradle
index 0ce442d..c780293 100644
--- a/wear/watchface/watchface-complications/build.gradle
+++ b/wear/watchface/watchface-complications/build.gradle
@@ -21,7 +21,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.RunApiTasks
 
 import androidx.build.LibraryType
 
diff --git a/wear/watchface/watchface-editor-guava/build.gradle b/wear/watchface/watchface-editor-guava/build.gradle
index be06686..a05c3e8 100644
--- a/wear/watchface/watchface-editor-guava/build.gradle
+++ b/wear/watchface/watchface-editor-guava/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/wear/watchface/watchface-guava/build.gradle b/wear/watchface/watchface-guava/build.gradle
index d975a8c..8fdfbf2 100644
--- a/wear/watchface/watchface-guava/build.gradle
+++ b/wear/watchface/watchface-guava/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
diff --git a/wear/watchface/watchface/build.gradle b/wear/watchface/watchface/build.gradle
index 8efd472..910148f 100644
--- a/wear/watchface/watchface/build.gradle
+++ b/wear/watchface/watchface/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index 74df165..b6afc79 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -22,7 +22,7 @@
  * modifying its settings.
  */
 import androidx.build.ApkCopyHelperKt
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -71,7 +71,7 @@
 
 androidx {
     name = "WebKit Test App"
-    publish = Publish.NONE
+    type = LibraryType.TEST_APPLICATION
     inceptionYear = "2017"
     description = "The WebKit Support Library test application is a demonstration of the APIs provided in the androidx.webkit library."
     additionalDeviceTestApkKeys.add("chrome")
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
index 82a2eb7..6b84532 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ProfileImpl.java
@@ -28,12 +28,9 @@
 import androidx.webkit.SpeculativeLoadingParameters;
 
 import org.chromium.support_lib_boundary.ProfileBoundaryInterface;
-import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
-import java.lang.reflect.InvocationHandler;
-
 
 /**
  * Internal implementation of Profile.
@@ -108,52 +105,17 @@
             @Nullable CancellationSignal cancellationSignal,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
-        ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
-        if (feature.isSupportedByWebView()) {
-            InvocationHandler paramsBoundaryInterface =
-                    BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
-                            new SpeculativeLoadingParametersAdapter(params));
-
-            mProfileImpl.prefetchUrl(url, paramsBoundaryInterface,
-                    PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
-
-            if (cancellationSignal != null) {
-                cancellationSignal.setOnCancelListener(() -> mProfileImpl.cancelPrefetch(url,
-                        null));
-            }
-        } else {
-            throw WebViewFeatureInternal.getUnsupportedOperationException();
-        }
     }
 
     @Override
     public void prefetchUrlAsync(@NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
-        ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
-        if (feature.isSupportedByWebView()) {
-            mProfileImpl.prefetchUrl(url,
-                    PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
-
-            if (cancellationSignal != null) {
-                cancellationSignal.setOnCancelListener(() -> mProfileImpl.cancelPrefetch(url,
-                        null));
-            }
-        } else {
-            throw WebViewFeatureInternal.getUnsupportedOperationException();
-        }
     }
 
     @Override
     public void clearPrefetchAsync(@NonNull String url,
             @NonNull OutcomeReceiverCompat<Void, PrefetchException> callback) {
-        ApiFeature.NoFramework feature = WebViewFeatureInternal.PROFILE_URL_PREFETCH;
-        if (feature.isSupportedByWebView()) {
-            mProfileImpl.clearPrefetch(url,
-                    PrefetchOperationCallbackAdapter.buildInvocationHandler(callback));
-        } else {
-            throw WebViewFeatureInternal.getUnsupportedOperationException();
-        }
     }
 
 }
diff --git a/window/extensions/extensions/build.gradle b/window/extensions/extensions/build.gradle
index 7781812..26acee1 100644
--- a/window/extensions/extensions/build.gradle
+++ b/window/extensions/extensions/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
@@ -52,7 +51,6 @@
 androidx {
     name = "WindowManager Extensions"
     type = LibraryType.PUBLISHED_LIBRARY // Only to generate per-project-zips
-    runApiTasks = new RunApiTasks.Yes("Need to track API surface before moving to publish")
     inceptionYear = "2020"
     description = "OEM extension interface definition for the Jetpack WindowManager. " +
             "This module declares the interface the the core component of this library " +
diff --git a/window/sidecar/sidecar/build.gradle b/window/sidecar/sidecar/build.gradle
index e902d344..c7d41342 100644
--- a/window/sidecar/sidecar/build.gradle
+++ b/window/sidecar/sidecar/build.gradle
@@ -22,7 +22,6 @@
  * modifying its settings.
  */
 import androidx.build.LibraryType
-import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
@@ -37,7 +36,6 @@
 androidx {
     name = "WindowManager Sidecar"
     type = LibraryType.PUBLISHED_LIBRARY // Only to generate per-project-zips
-    runApiTasks = new RunApiTasks.Yes("Need to track API surface but should never publish")
     inceptionYear = "2020"
     description = "This version of the OEM extension is deprecated. Please use window:extensions:extensions." +
             "module instead."
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index 695cac9..04d2a36 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -21,7 +21,7 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -47,7 +47,7 @@
 
 androidx {
     name = "WorkManager Benchmarks"
-     publish = Publish.NONE
+     type = LibraryType.BENCHMARK
     inceptionYear = "2019"
     description = "Android WorkManager Benchmark Library"
 }
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
index 3d30602..958e1a6 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
@@ -251,6 +251,7 @@
     }
 
     @Test
+    @Ignore
     public void initRuntimePerceptionFailure() {
         ListenableFuture<Session> sessionFuture =
                 immediateFailedFuture(
@@ -276,12 +277,14 @@
     }
 
     @Test
+    @Ignore
     public void requestHomeSpaceMode_callsExtensions() {
         realityCoreRuntime.requestHomeSpaceMode();
         assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.HOME_SPACE);
     }
 
     @Test
+    @Ignore
     public void requestFullSpaceMode_callsExtensions() {
         realityCoreRuntime.requestFullSpaceMode();
         assertThat(fakeExtensions.getSpaceMode()).isEqualTo(SpaceMode.FULL_SPACE);
@@ -299,6 +302,7 @@
     }
 
     @Test
+    @Ignore
     public void loggingEntitySetParent() {
         Pose pose = new Pose();
         LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
@@ -314,6 +318,7 @@
     }
 
     @Test
+    @Ignore
     public void loggingEntityUpdateParent() {
         Pose pose = new Pose();
         LoggingEntity childEntity = realityCoreRuntime.createLoggingEntity(pose);
@@ -332,6 +337,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_setsSpatialCapabilities() {
         realityCoreRuntime =
                 JxrPlatformAdapterAxr.create(
@@ -371,6 +377,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_setsEnvironmentVisibility() {
         SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
         assertThat(environment.isSpatialEnvironmentPreferenceActive()).isFalse();
@@ -395,6 +402,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_callsEnvironmentListenerOnlyForChanges() {
         SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
         @SuppressWarnings(value = "unchecked")
@@ -431,6 +439,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_setsPassthroughOpacity() {
         SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
         assertThat(environment.getCurrentPassthroughOpacity()).isZero();
@@ -461,6 +470,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_callsPassthroughListenerOnlyForChanges() {
         SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
         @SuppressWarnings(value = "unchecked")
@@ -506,6 +516,7 @@
     }
 
     @Test
+    @Ignore
     public void currentPassthroughOpacity_isSetDuringRuntimeCreation() {
         fakeExtensions.fakeSpatialState.setPassthroughVisibility(
                 new FakePassthroughVisibilityState(PassthroughVisibilityState.APP, 0.5f));
@@ -527,6 +538,7 @@
     }
 
     @Test
+    @Ignore
     public void onSpatialStateChanged_firesSpatialCapabilitiesChangedListener() {
         realityCoreRuntime =
                 JxrPlatformAdapterAxr.create(
@@ -577,6 +589,7 @@
     }
 
     @Test
+    @Ignore
     public void getHeadPoseInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
         when(perceptionLibrary.getSession()).thenReturn(null);
         assertThat(((JxrPlatformAdapterAxr) realityCoreRuntime).getHeadPoseInOpenXrUnboundedSpace())
@@ -584,6 +597,7 @@
     }
 
     @Test
+    @Ignore
     public void getHeadPoseInOpenXrUnboundedSpace_returnsPose() {
         when(session.getHeadPose())
                 .thenReturn(
@@ -595,6 +609,7 @@
     }
 
     @Test
+    @Ignore
     public void
             getStereoViewsInOpenXrUnboundedSpace_returnsNullWhenPerceptionSessionUninitialized() {
         when(perceptionLibrary.getSession()).thenReturn(null);
@@ -605,6 +620,7 @@
     }
 
     @Test
+    @Ignore
     public void getStereoViewsInOpenXrUnboundedSpace_returnsViewProjections() {
         ViewProjection leftViewProjection =
                 new ViewProjection(
@@ -626,6 +642,7 @@
     }
 
     @Test
+    @Ignore
     public void loggingEntity_getActivitySpacePose_returnsIdentityPose() {
         Pose identityPose = new Pose();
         LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
@@ -633,6 +650,7 @@
     }
 
     @Test
+    @Ignore
     public void loggingEntity_transformPoseTo_returnsIdentityPose() {
         Pose identityPose = new Pose();
         LoggingEntity loggingEntity = realityCoreRuntime.createLoggingEntity(identityPose);
@@ -640,6 +658,7 @@
     }
 
     @Test
+    @Ignore
     public void getPose_returnsSetPose() throws Exception {
         Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
         Pose identityPose = new Pose();
@@ -665,6 +684,7 @@
     }
 
     @Test
+    @Ignore
     public void getPose_returnsFactoryMethodPose() throws Exception {
         Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
         PanelEntity panelEntity = createPanelEntity(pose);
@@ -679,6 +699,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withParentChainTranslation_returnsOffsetPositionFromRoot()
             throws Exception {
         // Create a simple pose with only a small translation on all axes
@@ -703,6 +724,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withParentChainRotation_returnsOffsetRotationFromRoot()
             throws Exception {
         // Create a pose with a translation and one with 90 degree rotation around the y axis.
@@ -736,6 +758,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
             throws Exception {
         // Create a pose with a 1D translation and a 90 degree rotation around the z axis.
@@ -775,6 +798,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withActivitySpaceParent_returnsScaledPose()
             throws Exception {
         Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
@@ -796,6 +820,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withScale_returnsPose() throws Exception {
         Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
 
@@ -844,6 +869,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpacePose_withParentChainTranslation_returnsOffsetPositionFromRoot()
             throws Exception {
         // Create a simple pose with only a small translation on all axes
@@ -867,6 +893,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpacePose_withParentChainRotation_returnsOffsetRotationFromRoot()
             throws Exception {
         // Create a pose with a translation and one with 90 degree rotation around the y axis.
@@ -893,6 +920,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpacePose_withParentChainPoseOffsets_returnsOffsetPoseFromRoot()
             throws Exception {
         // Create a pose with a 1D translation and a 90 degree rotation around the z axis.
@@ -929,6 +957,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpacePose_withDefaultParent_returnsPose() throws Exception {
         Pose pose = new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f));
 
@@ -943,6 +972,7 @@
     }
 
     @Test
+    @Ignore
     public void getPoseInActivitySpace_withScale_returnsScaledPose() throws Exception {
         Pose localPose = new Pose(new Vector3(1f, 2f, 1f), Quaternion.Identity);
 
@@ -979,6 +1009,7 @@
     }
 
     @Test
+    @Ignore
     public void transformPoseTo_sameDestAndSourceEntity_returnsUnchangedPose() throws Exception {
         Pose pose =
                 new Pose(new Vector3(1f, 2f, 3f), new Quaternion(1f, 2f, 3f, 4f).toNormalized());
@@ -997,6 +1028,7 @@
     }
 
     @Test
+    @Ignore
     public void transformPoseTo_withOnlyTranslationOffset_returnsTranslationDifference()
             throws Exception {
         PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
@@ -1017,6 +1049,7 @@
     }
 
     @Test
+    @Ignore
     public void transformPoseTo_withOnlyRotationOffset_returnsRotationDifference()
             throws Exception {
         PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
@@ -1044,6 +1077,7 @@
     }
 
     @Test
+    @Ignore
     public void transformPoseTo_withDifferentTranslationAndRotation_returnsTransformedPose() {
         // Assume the source and destination entities are in the same coordinate space.
         Vector3 sourceVector = new Vector3(1f, 2f, 3f);
@@ -1107,6 +1141,7 @@
     }
 
     @Test
+    @Ignore
     public void getAlpha_returnsSetAlpha() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1131,6 +1166,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpaceAlpha_returnsTotalAncestorAlpha() throws Exception {
         PanelEntity grandparent = createPanelEntity();
         GltfEntity parent = createGltfEntity();
@@ -1157,6 +1193,7 @@
     }
 
     @Test
+    @Ignore
     public void transformPoseTo_withScale_returnsPose() throws Exception {
         PanelEntityImpl sourceEntity = (PanelEntityImpl) createPanelEntity();
         GltfEntityImpl destinationEntity = (GltfEntityImpl) createGltfEntity();
@@ -1196,6 +1233,7 @@
     }
 
     @Test
+    @Ignore
     public void isHidden_returnsSetHidden() throws Exception {
         PanelEntity parentEntity = createPanelEntity();
         assertThat(parentEntity.isHidden(true)).isFalse();
@@ -1235,6 +1273,7 @@
     }
 
     @Test
+    @Ignore
     public void setHidden_modifiesReforms() throws Exception {
         PanelEntity testEntity = createPanelEntity();
         FakeNode testNode = (FakeNode) ((AndroidXrEntity) testEntity).getNode();
@@ -1255,6 +1294,7 @@
     }
 
     @Test
+    @Ignore
     public void loggingEntityAddChildren() {
         Pose pose = new Pose();
         LoggingEntity childEntity1 = realityCoreRuntime.createLoggingEntity(pose);
@@ -1273,6 +1313,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpace_returnsEntity() {
         ActivitySpace activitySpace = realityCoreRuntime.getActivitySpace();
 
@@ -1283,6 +1324,7 @@
     }
 
     @Test
+    @Ignore
     public void getActivitySpaceRootImpl_returnsEntity() {
         Entity activitySpaceRoot = realityCoreRuntime.getActivitySpaceRootImpl();
         assertThat(activitySpaceRoot).isNotNull();
@@ -1293,12 +1335,14 @@
     }
 
     @Test
+    @Ignore
     public void getEnvironment_returnsEnvironment() {
         SpatialEnvironment environment = realityCoreRuntime.getSpatialEnvironment();
         assertThat(environment).isNotNull();
     }
 
     @Test
+    @Ignore
     public void getHeadActivityPose_returnsNullIfNotReady() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.getHeadPose()).thenReturn(null);
@@ -1308,6 +1352,7 @@
     }
 
     @Test
+    @Ignore
     public void getHeadActivityPose_returnsActivityPose() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.getHeadPose())
@@ -1318,6 +1363,7 @@
     }
 
     @Test
+    @Ignore
     public void getCameraViewActivityPose_returnsNullIfNotReady() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.getStereoViews()).thenReturn(new ViewProjections(null, null));
@@ -1334,6 +1380,7 @@
     }
 
     @Test
+    @Ignore
     public void getLeftCameraViewActivityPose_returnsActivityPose() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         ViewProjection viewProjection =
@@ -1349,6 +1396,7 @@
     }
 
     @Test
+    @Ignore
     public void getRightCameraViewActivityPose_returnsActivityPose() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         ViewProjection viewProjection =
@@ -1364,6 +1412,7 @@
     }
 
     @Test
+    @Ignore
     public void getUnknownCameraViewActivityPose_returnsEmptyOptional() {
         CameraViewActivityPose cameraViewActivityPose =
                 realityCoreRuntime.getCameraViewActivityPose(555);
@@ -1372,6 +1421,7 @@
     }
 
     @Test
+    @Ignore
     public void loadExrImageByAssetName_returnsImage() throws Exception {
         ListenableFuture<ExrImageResource> imageFuture =
                 realityCoreRuntime.loadExrImageByAssetName("FakeAsset.exr");
@@ -1388,6 +1438,7 @@
     }
 
     @Test
+    @Ignore
     public void loadGltfByAssetName_returnsModel() throws Exception {
         ListenableFuture<GltfModelResource> modelFuture =
                 realityCoreRuntime.loadGltfByAssetName("FakeAsset.glb");
@@ -1404,16 +1455,19 @@
     }
 
     @Test
+    @Ignore
     public void createGltfEntity_returnsEntity() throws Exception {
         assertThat(createGltfEntity()).isNotNull();
     }
 
     @Test
+    @Ignore
     public void createGltfEntitySplitEngine_returnsEntity() throws Exception {
         assertThat(createGltfEntitySplitEngine()).isNotNull();
     }
 
     @Test
+    @Ignore
     public void animateGltfEntitySplitEngine_gltfEntityIsAnimating() throws Exception {
         GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
         gltfEntitySplitEngine.startAnimation(false, "animation_name");
@@ -1429,6 +1483,7 @@
     }
 
     @Test
+    @Ignore
     public void animateLoopGltfEntitySplitEngine_gltfEntityIsAnimatingInLoop() throws Exception {
         GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
         gltfEntitySplitEngine.startAnimation(true, "animation_name");
@@ -1442,6 +1497,7 @@
     }
 
     @Test
+    @Ignore
     public void stopAnimateGltfEntitySplitEngine_gltfEntityStopsAnimating() throws Exception {
         GltfEntity gltfEntitySplitEngine = createGltfEntitySplitEngine();
         gltfEntitySplitEngine.startAnimation(true, "animation_name");
@@ -1456,6 +1512,7 @@
     }
 
     @Test
+    @Ignore
     public void gltfEntitySetParent() throws Exception {
         GltfEntity childEntity = createGltfEntity();
         GltfEntity parentEntity = createGltfEntity();
@@ -1474,6 +1531,7 @@
     }
 
     @Test
+    @Ignore
     public void gltfEntityUpdateParent() throws Exception {
         GltfEntity childEntity = createGltfEntity();
         GltfEntity parentEntity1 = createGltfEntity();
@@ -1495,6 +1553,7 @@
     }
 
     @Test
+    @Ignore
     public void gltfEntityAddChildren() throws Exception {
         GltfEntity childEntity1 = createGltfEntity();
         GltfEntity childEntity2 = createGltfEntity();
@@ -1517,11 +1576,13 @@
     }
 
     @Test
+    @Ignore
     public void createPanelEntity_returnsEntity() throws Exception {
         assertThat(createPanelEntity()).isNotNull();
     }
 
     @Test
+    @Ignore
     public void allPanelEnities_haveActivitySpaceRootImplAsParentByDefault() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
 
@@ -1530,6 +1591,7 @@
     }
 
     @Test
+    @Ignore
     public void panelEntitySetParent_setsParent() throws Exception {
         PanelEntity childEntity = createPanelEntity();
         PanelEntity parentEntity = createPanelEntity();
@@ -1546,6 +1608,7 @@
     }
 
     @Test
+    @Ignore
     public void panelEntityUpdateParent_updatesParent() throws Exception {
         PanelEntity childEntity = createPanelEntity();
         PanelEntity parentEntity1 = createPanelEntity();
@@ -1567,6 +1630,7 @@
     }
 
     @Test
+    @Ignore
     public void panelEntityAddChildren_addsChildren() throws Exception {
         PanelEntity childEntity1 = createPanelEntity();
         PanelEntity childEntity2 = createPanelEntity();
@@ -1589,6 +1653,7 @@
     }
 
     @Test
+    @Ignore
     public void createAnchorEntity_returnsAndInitsAnchor() throws Exception {
         Dimensions anchorDimensions = new Dimensions(2f, 5f, 0f);
         androidx.xr.scenecore.impl.perception.Pose perceptionPose =
@@ -1615,11 +1680,13 @@
     }
 
     @Test
+    @Ignore
     public void getMainPanelEntity_returnsPanelEntity() throws Exception {
         assertThat(realityCoreRuntime.getMainPanelEntity()).isNotNull();
     }
 
     @Test
+    @Ignore
     public void getMainPanelEntity_usesWindowLeashNode() throws Exception {
         PanelEntity mainPanel = realityCoreRuntime.getMainPanelEntity();
 
@@ -1628,6 +1695,7 @@
     }
 
     @Test
+    @Ignore
     public void addInputEventConsumerToEntity_setsUpNodeListener() {
         InputEventListener mockConsumer = mock(InputEventListener.class);
         PanelEntity panelEntity = createPanelEntity();
@@ -1649,6 +1717,7 @@
     }
 
     @Test
+    @Ignore
     public void inputEvent_hasHitInfo() {
         InputEventListener mockConsumer = mock(InputEventListener.class);
         PanelEntity panelEntity = createPanelEntity();
@@ -1676,6 +1745,7 @@
     }
 
     @Test
+    @Ignore
     public void passingNullExecutorWhenAddingConsumer_usesInternalExecutor() {
         InputEventListener mockConsumer = mock(InputEventListener.class);
         PanelEntity panelEntity = createPanelEntity();
@@ -1687,6 +1757,7 @@
     }
 
     @Test
+    @Ignore
     public void addMultipleInputEventConsumerToEntity_setsUpInputCallbacksForAll() {
         InputEventListener mockConsumer1 = mock(InputEventListener.class);
         InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1707,6 +1778,7 @@
     }
 
     @Test
+    @Ignore
     public void addMultipleInputEventConsumersToEntity_setsUpInputCallbacksOnGivenExecutors() {
         InputEventListener mockConsumer1 = mock(InputEventListener.class);
         InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1734,6 +1806,7 @@
     }
 
     @Test
+    @Ignore
     public void removeInputEventConsumerToEntity_removesFromCallbacks() {
         InputEventListener mockConsumer1 = mock(InputEventListener.class);
         InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1759,6 +1832,7 @@
     }
 
     @Test
+    @Ignore
     public void removeAllInputEventConsumers_stopsInputListening() {
         InputEventListener mockConsumer1 = mock(InputEventListener.class);
         InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1786,6 +1860,7 @@
     }
 
     @Test
+    @Ignore
     public void dispose_stopsInputListening() {
         InputEventListener mockConsumer1 = mock(InputEventListener.class);
         InputEventListener mockConsumer2 = mock(InputEventListener.class);
@@ -1812,17 +1887,20 @@
     }
 
     @Test
+    @Ignore
     public void createContentlessEntity_returnsEntity() throws Exception {
         assertThat(createContentlessEntity()).isNotNull();
     }
 
     @Test
+    @Ignore
     public void contentlessEntity_hasActivitySpaceRootImplAsParentByDefault() throws Exception {
         Entity entity = createContentlessEntity();
         assertThat(entity.getParent()).isEqualTo(realityCoreRuntime.getActivitySpaceRootImpl());
     }
 
     @Test
+    @Ignore
     public void contentlessEntityAddChildren_addsChildren() throws Exception {
         Entity childEntity1 = createContentlessEntity();
         Entity childEntity2 = createContentlessEntity();
@@ -1845,6 +1923,7 @@
     }
 
     @Test
+    @Ignore
     public void addComponent_callsOnAttach() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1863,6 +1942,7 @@
     }
 
     @Test
+    @Ignore
     public void addComponent_failsIfOnAttachFails() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1881,6 +1961,7 @@
     }
 
     @Test
+    @Ignore
     public void removeComponent_callsOnDetach() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1908,6 +1989,7 @@
     }
 
     @Test
+    @Ignore
     public void addingSameComponentTypeAgain_addsComponent() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1934,6 +2016,7 @@
     }
 
     @Test
+    @Ignore
     public void addingDifferentComponentType_addComponentSucceeds() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1960,6 +2043,7 @@
     }
 
     @Test
+    @Ignore
     public void removeAll_callsOnDetachOnAll() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -1998,6 +2082,7 @@
     }
 
     @Test
+    @Ignore
     public void addSameComponentTwice_callsOnAttachTwice() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -2019,6 +2104,7 @@
     }
 
     @Test
+    @Ignore
     public void removeSameComponentTwice_callsOnDetachOnce() throws Exception {
         PanelEntity panelEntity = createPanelEntity();
         GltfEntity gltfEntity = createGltfEntity();
@@ -2049,6 +2135,7 @@
     }
 
     @Test
+    @Ignore
     public void createInteractableComponent_returnsComponent() {
         InputEventListener mockConsumer = mock(InputEventListener.class);
         InteractableComponent interactableComponent =
@@ -2057,6 +2144,7 @@
     }
 
     @Test
+    @Ignore
     public void createPersistedAnchorEntity_returnsEntityInNominalCase() throws Exception {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.createAnchorFromUuid(any())).thenReturn(anchor);
@@ -2067,6 +2155,7 @@
     }
 
     @Test
+    @Ignore
     public void createPersistedAnchorEntity_returnsEntityForNullSession() throws Exception {
         when(perceptionLibrary.getSession()).thenReturn(null);
         assertThat(
@@ -2076,6 +2165,7 @@
     }
 
     @Test
+    @Ignore
     public void createPersistedAnchorEntity_returnsEntityForNullAnchor() throws Exception {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.createAnchorFromUuid(any())).thenReturn(null);
@@ -2086,6 +2176,7 @@
     }
 
     @Test
+    @Ignore
     public void createPersistedAnchorEntity_returnsEntityForNullAnchorToken() throws Exception {
         when(perceptionLibrary.getSession()).thenReturn(session);
         when(session.createAnchorFromUuid(any())).thenReturn(anchor);
@@ -2101,12 +2192,14 @@
     }
 
     @Test
+    @Ignore
     public void unpersistAnchor_failsWhenSessionIsNotInitialized() {
         when(perceptionLibrary.getSession()).thenReturn(null);
         assertThat(realityCoreRuntime.unpersistAnchor(UUID.randomUUID())).isFalse();
     }
 
     @Test
+    @Ignore
     public void unpersistAnchor_sessionIsInitialized_operationSucceeds() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         UUID uuid = UUID.randomUUID();
@@ -2115,6 +2208,7 @@
     }
 
     @Test
+    @Ignore
     public void unpersistAnchor_sessionIsInitialized_operationFails() {
         when(perceptionLibrary.getSession()).thenReturn(session);
         UUID uuid = UUID.randomUUID();
@@ -2123,6 +2217,7 @@
     }
 
     @Test
+    @Ignore
     public void createMovableComponent_returnsComponent() {
         MovableComponent movableComponent =
                 realityCoreRuntime.createMovableComponent(
@@ -2131,6 +2226,7 @@
     }
 
     @Test
+    @Ignore
     public void createAnchorPlacement_returnsAnchorPlacement() {
         AnchorPlacement anchorPlacement =
                 realityCoreRuntime.createAnchorPlacementForPlanes(
@@ -2139,6 +2235,7 @@
     }
 
     @Test
+    @Ignore
     public void createResizableComponent_returnsComponent() {
         ResizableComponent resizableComponent =
                 realityCoreRuntime.createResizableComponent(
@@ -2147,6 +2244,7 @@
     }
 
     @Test
+    @Ignore
     public void createPointerCaptureComponent_returnsComponent() {
         PointerCaptureComponent pointerCaptureComponent =
                 realityCoreRuntime.createPointerCaptureComponent(
@@ -2155,6 +2253,7 @@
     }
 
     @Test
+    @Ignore
     public void dispose_clearsReformOptions() {
         AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
         FakeNode node = (FakeNode) entity.getNode();
@@ -2168,6 +2267,7 @@
     }
 
     @Test
+    @Ignore
     public void dispose_clearsParents() {
         AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
         entity.setParent(realityCoreRuntime.getActivitySpaceRootImpl());
@@ -2178,6 +2278,7 @@
     }
 
     @Test
+    @Ignore
     public void setFullSpaceMode_callsExtensions() {
         Bundle bundle = Bundle.EMPTY;
         bundle = realityCoreRuntime.setFullSpaceMode(bundle);
@@ -2185,6 +2286,7 @@
     }
 
     @Test
+    @Ignore
     public void setFullSpaceModeWithEnvironmentInherited_callsExtensions() {
         Bundle bundle = Bundle.EMPTY;
         bundle = realityCoreRuntime.setFullSpaceModeWithEnvironmentInherited(bundle);
@@ -2192,12 +2294,14 @@
     }
 
     @Test
+    @Ignore
     public void setPreferredAspectRatio_callsExtensions() {
         realityCoreRuntime.setPreferredAspectRatio(activity, 1.23f);
         assertThat(fakeExtensions.getPreferredAspectRatio()).isEqualTo(1.23f);
     }
 
     @Test
+    @Ignore
     public void createStereoSurface_returnsStereoSurface() {
         // Not a great test, since it returns the (non-SplitEngine) StereoSurfaceEntityImpl
         // and that throws this from its Ctor.
@@ -2213,6 +2317,7 @@
     }
 
     @Test
+    @Ignore
     public void getSurfaceFromStereoSurface_returnsSurface() {
         assertThrows(
                 IllegalArgumentException.class,
@@ -2220,6 +2325,7 @@
     }
 
     @Test
+    @Ignore
     public void setStereoModeForStereoSurface_callsExtensions() {
         assertThrows(
                 IllegalArgumentException.class,
@@ -2229,6 +2335,7 @@
     }
 
     @Test
+    @Ignore
     public void injectRootNodeAndTaskWindowLeashNode_runtimeImplUsesThoseNodes() {
         FakeNode rootNode = (FakeNode) fakeExtensions.createNode();
         FakeNode taskWindowLeashNode = (FakeNode) fakeExtensions.createNode();
@@ -2252,6 +2359,7 @@
     }
 
     @Test
+    @Ignore
     public void dispose_clearsResources() {
         AndroidXrEntity entity = (AndroidXrEntity) createContentlessEntity();
         FakeNode node = (FakeNode) entity.getNode();
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
index 2cef0e9..2f9e930 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/MovableComponentImplTest.java
@@ -83,6 +83,7 @@
 import com.google.common.truth.Expect;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -187,6 +188,7 @@
     }
 
     @Test
+    @Ignore
     public void addMovableComponent_addsReformOptionsToNode() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -215,6 +217,7 @@
     }
 
     @Test
+    @Ignore
     public void addMovableComponent_addsSystemMovableFlagToNode() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -243,6 +246,7 @@
     }
 
     @Test
+    @Ignore
     public void addMovableComponent_addsScaleInZFlagToNode() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -271,6 +275,7 @@
     }
 
     @Test
+    @Ignore
     public void addMovableComponent_addsAllFlagsToNode() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -300,6 +305,7 @@
     }
 
     @Test
+    @Ignore
     public void setSystemMovableFlag_alsoUpdatesEntityPoseAndScale() {
         AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
         MovableComponentImpl movableComponent =
@@ -341,6 +347,7 @@
     }
 
     @Test
+    @Ignore
     public void systemMovableFlagNotSet_doesNotUpdateEntityPoseAndScale() {
         AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
         MovableComponent movableComponent =
@@ -380,6 +387,7 @@
     }
 
     @Test
+    @Ignore
     public void setSizeOnMovableComponent_setsSizeOnNodeReformOptions() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -407,6 +415,7 @@
     }
 
     @Test
+    @Ignore
     public void scaleWithDistanceOnMovableComponent_defaultsToDefaultMode() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -434,6 +443,7 @@
     }
 
     @Test
+    @Ignore
     public void setScaleWithDistanceOnMovableComponent_setsScaleWithDistanceOnNodeReformOptions() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -462,6 +472,7 @@
     }
 
     @Test
+    @Ignore
     public void setPropertiesOnMovableComponentAttachLater_setsPropertiesOnNodeReformOptions() {
         AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
         MovableComponentImpl movableComponent =
@@ -495,6 +506,7 @@
     }
 
     @Test
+    @Ignore
     public void addMoveEventListener_onlyInvokedOnMoveEvent() {
         AndroidXrEntity entity = (AndroidXrEntity) createTestEntity();
         Vector3 initialTranslation = new Vector3(1f, 2f, 3f);
@@ -548,6 +560,7 @@
     }
 
     @Test
+    @Ignore
     public void addMoveEventListenerWithExecutor_invokesListenerOnGivenExecutor() {
         Entity entity = createTestEntity();
         MovableComponent movableComponent =
@@ -587,6 +600,7 @@
     }
 
     @Test
+    @Ignore
     public void addMoveEventListenerMultiple_invokesAllListeners() {
         Entity entity = createTestEntity();
         MovableComponentImpl movableComponent =
@@ -626,6 +640,7 @@
     }
 
     @Test
+    @Ignore
     public void removeMoveEventListenerMultiple_removesGivenListener() {
         Entity entity = createTestEntity();
         MovableComponentImpl movableComponent =
@@ -677,6 +692,7 @@
     }
 
     @Test
+    @Ignore
     public void removeMovableComponent_clearsMoveReformOptionsAndMoveEventListeners() {
         Entity entity = createTestEntity();
         MovableComponentImpl movableComponent =
@@ -715,6 +731,7 @@
     }
 
     @Test
+    @Ignore
     public void movableComponent_canAttachAgainAfterDetach() {
         Entity entity = createTestEntity();
         assertThat(entity).isNotNull();
@@ -739,6 +756,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_updatesThePoseBasedOnPlanes() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -823,6 +841,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_nullParent_updatesThePoseBasedOnPlanes() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -903,6 +922,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_updatesPoseButDoesNotMove_ifNotSystemMovable() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -988,6 +1008,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_withNonActivityParent_updatesPoseBasedOnPlanesAndParent() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1074,6 +1095,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorableAndScaledParent_updatesThePoseBasedOnPlanes() {
         // Set the activity space pose to be 1 unit to the left of the origin. with a scale of 2.
         setActivitySpacePose(
@@ -1151,6 +1173,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_withinAnchorDistance_setsAnchorEntity() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1238,6 +1261,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_withinAnchorDistanceAboveAnchor_resetsPose() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1327,6 +1351,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_withIncorrectPlaneType_doesNotCreateAnchor() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1417,6 +1442,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_withinAnchorDistanceAndScale_setsAnchorEntityAndScales() {
         // Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
         // of
@@ -1510,6 +1536,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_noPlanes_keepsProposedPose() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1567,6 +1594,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_noPlaneData_keepsProposedPose() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1627,6 +1655,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_outsideExtents_keepsProposedPose() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1698,6 +1727,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_resetsToActivityPoseAfterAnchoring() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -1810,6 +1840,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_resetsAndScaleToActivityPoseAfterAnchoring() {
         // Set the activity space pose to be 1 unit to the left of the OpenXR origin and add a scale
         // of
@@ -1932,6 +1963,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorableChildOfEntity_resetsToActivityPoseAfterAnchoring() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -2050,6 +2082,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_shouldDispose_disposesAnchorAfterUnparenting() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -2168,6 +2201,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorable_shouldDispose_doeNotDisposeIfAnchorHasChildren() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -2289,6 +2323,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorablePanelEntity_nearPlane_rendersShadow() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -2352,6 +2387,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorablePanelEntity_awayFromPlane_hidesShadow() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
@@ -2410,6 +2446,7 @@
     }
 
     @Test
+    @Ignore
     public void anchorablePanelEntity_endMovement_callsDestroy() {
         // Set the activity space pose to be 1 unit to the left of the origin.
         setActivitySpacePose(
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
index dd24f63..4169854 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/OpenXrActivityPoseTest.java
@@ -38,6 +38,7 @@
 import androidx.xr.scenecore.testing.FakeXrExtensions.FakeGltfModelToken;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.ParameterizedRobolectricTestRunner;
@@ -167,6 +168,7 @@
         assertPose(testActivityPose.getPoseInActivitySpace(), new Pose());
     }
 
+    @Ignore("b/384930655")
     @Test
     public void getPoseInActivitySpace_returnsDifferencePose() {
         testActivityPose = createTestActivityPose();