[go: nahoru, domu]

Merge "Remove robolectric SDK restriction." into androidx-main
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index c38f2d5..7602e65 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.activity {
 
-  public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
+  public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
     ctor public ComponentActivity();
     ctor @ContentView public ComponentActivity(@LayoutRes int);
     method public void addMenuProvider(androidx.core.view.MenuProvider);
@@ -9,6 +9,7 @@
     method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State);
     method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
@@ -28,6 +29,7 @@
     method public void removeMenuProvider(androidx.core.view.MenuProvider);
     method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index c38f2d5..7602e65 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.activity {
 
-  public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
+  public class ComponentActivity extends android.app.Activity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
     ctor public ComponentActivity();
     ctor @ContentView public ComponentActivity(@LayoutRes int);
     method public void addMenuProvider(androidx.core.view.MenuProvider);
@@ -9,6 +9,7 @@
     method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State);
     method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
@@ -28,6 +29,7 @@
     method public void removeMenuProvider(androidx.core.view.MenuProvider);
     method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 0fdb2e6..50e9fa1 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.activity {
 
-  public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
+  public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.activity.result.ActivityResultCaller androidx.activity.result.ActivityResultRegistryOwner androidx.activity.contextaware.ContextAware androidx.lifecycle.HasDefaultViewModelProviderFactory androidx.lifecycle.LifecycleOwner androidx.core.view.MenuHost androidx.activity.OnBackPressedDispatcherOwner androidx.core.content.OnConfigurationChangedProvider androidx.core.app.OnNewIntentProvider androidx.core.content.OnTrimMemoryProvider androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
     ctor public ComponentActivity();
     ctor @ContentView public ComponentActivity(@LayoutRes int);
     method public void addMenuProvider(androidx.core.view.MenuProvider);
@@ -9,6 +9,7 @@
     method public void addMenuProvider(androidx.core.view.MenuProvider, androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Lifecycle.State);
     method public final void addOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void addOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void addOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method public final androidx.activity.result.ActivityResultRegistry getActivityResultRegistry();
     method public androidx.lifecycle.ViewModelProvider.Factory getDefaultViewModelProviderFactory();
@@ -27,6 +28,7 @@
     method public void removeMenuProvider(androidx.core.view.MenuProvider);
     method public final void removeOnConfigurationChangedListener(androidx.core.util.Consumer<android.content.res.Configuration!>);
     method public final void removeOnContextAvailableListener(androidx.activity.contextaware.OnContextAvailableListener);
+    method public final void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
     method public final void removeOnTrimMemoryListener(androidx.core.util.Consumer<java.lang.Integer!>);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int);
     method @Deprecated public void startActivityForResult(android.content.Intent!, int, android.os.Bundle?);
diff --git a/activity/activity/src/androidTest/AndroidManifest.xml b/activity/activity/src/androidTest/AndroidManifest.xml
index ca437c7..d7e4d08 100644
--- a/activity/activity/src/androidTest/AndroidManifest.xml
+++ b/activity/activity/src/androidTest/AndroidManifest.xml
@@ -50,6 +50,10 @@
             android:name="androidx.activity.EmptyContentActivity"
             android:exported="true" />
         <activity
+            android:name="androidx.activity.SingleTopActivity"
+            android:launchMode="singleTop"
+            android:exported="true" />
+        <activity
             android:name="androidx.activity.AutoRestarterActivity"
             android:exported="true" />
         <activity
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index b5d20b5..814c66f 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
@@ -17,6 +17,7 @@
 package androidx.activity
 
 import android.content.ComponentCallbacks2
+import android.content.Intent
 import android.content.res.Configuration
 import androidx.core.util.Consumer
 import androidx.test.core.app.ActivityScenario
@@ -24,6 +25,7 @@
 import androidx.test.filters.LargeTest
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -183,4 +185,121 @@
             assertThat(receivedLevel).isEqualTo(ComponentCallbacks2.TRIM_MEMORY_MODERATE)
         }
     }
+
+    @Test
+    fun onNewIntent() {
+        with(ActivityScenario.launch(SingleTopActivity::class.java)) {
+            val receivedIntents = mutableListOf<Intent>()
+
+            val listener = Consumer<Intent> { intent ->
+                receivedIntents += intent
+            }
+            withActivity {
+                addOnNewIntentListener(listener)
+                startActivity(
+                    Intent(this, SingleTopActivity::class.java).apply {
+                        putExtra("newExtra", 5)
+                    }
+                )
+            }
+
+            withActivity {
+                assertWithMessage("Should have received one intent")
+                    .that(receivedIntents)
+                    .hasSize(1)
+                val receivedIntent = receivedIntents.first()
+                assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                    .isEqualTo(5)
+            }
+        }
+    }
+
+    @Test
+    fun onNewIntentRemove() {
+        with(ActivityScenario.launch(SingleTopActivity::class.java)) {
+            val receivedIntents = mutableListOf<Intent>()
+
+            val listener = Consumer<Intent> { intent ->
+                receivedIntents += intent
+            }
+            withActivity {
+                addOnNewIntentListener(listener)
+                startActivity(
+                    Intent(this, SingleTopActivity::class.java).apply {
+                        putExtra("newExtra", 5)
+                    }
+                )
+            }
+
+            withActivity {
+                assertWithMessage("Should have received one intent")
+                    .that(receivedIntents)
+                    .hasSize(1)
+                val receivedIntent = receivedIntents.first()
+                assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                    .isEqualTo(5)
+            }
+
+            withActivity {
+                removeOnNewIntentListener(listener)
+                startActivity(
+                    Intent(this, SingleTopActivity::class.java).apply {
+                        putExtra("newExtra", 10)
+                    }
+                )
+            }
+
+            withActivity {
+                assertWithMessage("Should have received only one intent")
+                    .that(receivedIntents)
+                    .hasSize(1)
+            }
+        }
+    }
+
+    @Test
+    fun onNewIntentReentrant() {
+        with(ActivityScenario.launch(SingleTopActivity::class.java)) {
+            val activity = withActivity { this }
+            val receivedIntents = mutableListOf<Intent>()
+
+            val listener = object : Consumer<Intent> {
+                override fun accept(intent: Intent) {
+                    receivedIntents += intent
+                    activity.removeOnNewIntentListener(this)
+                }
+            }
+            withActivity {
+                addOnNewIntentListener(listener)
+                // Add a second listener to force a ConcurrentModificationException
+                // if not properly handled by ComponentActivity
+                addOnNewIntentListener { }
+                startActivity(
+                    Intent(this, SingleTopActivity::class.java).apply {
+                        putExtra("newExtra", 5)
+                    }
+                )
+            }
+
+            withActivity {
+                startActivity(
+                    Intent(this, SingleTopActivity::class.java).apply {
+                        putExtra("newExtra", 10)
+                    }
+                )
+            }
+
+            withActivity {
+                // Only the first Intent should be received
+                assertWithMessage("Should have received only one intent")
+                    .that(receivedIntents)
+                    .hasSize(1)
+                val receivedIntent = receivedIntents.first()
+                assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                    .isEqualTo(5)
+            }
+        }
+    }
 }
+
+class SingleTopActivity : ComponentActivity()
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index a881c8d..4672265 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -67,6 +67,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.core.app.ActivityCompat;
 import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.app.OnNewIntentProvider;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.OnConfigurationChangedProvider;
 import androidx.core.content.OnTrimMemoryProvider;
@@ -113,6 +114,7 @@
         ActivityResultCaller,
         OnConfigurationChangedProvider,
         OnTrimMemoryProvider,
+        OnNewIntentProvider,
         MenuHost {
 
     static final class NonConfigurationInstances {
@@ -231,6 +233,8 @@
             new CopyOnWriteArrayList<>();
     private final CopyOnWriteArrayList<Consumer<Integer>> mOnTrimMemoryListeners =
             new CopyOnWriteArrayList<>();
+    private final CopyOnWriteArrayList<Consumer<Intent>> mOnNewIntentListeners =
+            new CopyOnWriteArrayList<>();
 
     /**
      * Default constructor for ComponentActivity. All Activities must have a default constructor
@@ -839,6 +843,37 @@
         mOnTrimMemoryListeners.remove(listener);
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * Dispatches this call to all listeners added via
+     * {@link #addOnNewIntentListener(Consumer)}.
+     */
+    @CallSuper
+    @Override
+    protected void onNewIntent(
+            @SuppressLint({"UnknownNullness", "MissingNullability"}) Intent intent
+    ) {
+        super.onNewIntent(intent);
+        for (Consumer<Intent> listener : mOnNewIntentListeners) {
+            listener.accept(intent);
+        }
+    }
+
+    @Override
+    public final void addOnNewIntentListener(
+            @NonNull Consumer<Intent> listener
+    ) {
+        mOnNewIntentListeners.add(listener);
+    }
+
+    @Override
+    public final void removeOnNewIntentListener(
+            @NonNull Consumer<Intent> listener
+    ) {
+        mOnNewIntentListeners.remove(listener);
+    }
+
     @Override
     public void reportFullyDrawn() {
         try {
diff --git a/ads/ads-identifier-testing/build.gradle b/ads/ads-identifier-testing/build.gradle
index e4458f8..e7d05c0 100644
--- a/ads/ads-identifier-testing/build.gradle
+++ b/ads/ads-identifier-testing/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")
@@ -29,3 +32,7 @@
         disable "InvalidPackage" // Lint is unhappy about mockito package
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
index 18a6539..d50d170 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
@@ -18,10 +18,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.text.method.DigitsKeyListener;
 import android.text.method.KeyListener;
 import android.text.method.NumberKeyListener;
 import android.view.KeyEvent;
 
+import androidx.appcompat.test.R;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -47,7 +49,7 @@
     public void respectsFocusableAndEditableAttribute() {
         AppCompatEditText notFocusable =
                 mActivityTestRule.getActivity()
-                        .findViewById(androidx.appcompat.test.R.id.not_focusable);
+                        .findViewById(R.id.not_focusable);
 
         assertThat(notFocusable.isEnabled()).isFalse();
         assertThat(notFocusable.isFocusable()).isFalse();
@@ -57,7 +59,7 @@
     @UiThreadTest
     public void respectsDigits() {
         AppCompatEditText textWithDigits = mActivityTestRule.getActivity()
-                        .findViewById(androidx.appcompat.test.R.id.text_with_digits);
+                        .findViewById(R.id.text_with_digits);
 
         int[] acceptedKeyCodes = {KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2,
                 KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_4};
@@ -82,7 +84,7 @@
     @UiThreadTest
     public void respectsDigitsAndComma() {
         AppCompatEditText textWithDigitsAndComma = mActivityTestRule.getActivity()
-                .findViewById(androidx.appcompat.test.R.id.text_with_digits_and_comma);
+                .findViewById(R.id.text_with_digits_and_comma);
 
         int[] acceptedKeyCodes = {KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2,
                 KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_NUMPAD_COMMA};
@@ -104,6 +106,18 @@
         assertThat(textWithDigitsAndComma.getKeyListener()).isInstanceOf(NumberKeyListener.class);
     }
 
+    @Test
+    @UiThreadTest
+    public void setKeyListener_doesNotWrap_numberKeyListener() {
+        KeyListener digitsKeyListener = DigitsKeyListener.getInstance("123456");
+        AppCompatEditText textWithDigits = mActivityTestRule.getActivity()
+                .findViewById(R.id.text_with_digits);
+
+        textWithDigits.setKeyListener(digitsKeyListener);
+        assertThat(textWithDigits.getKeyListener()).isSameInstanceAs(digitsKeyListener);
+        assertThat(textWithDigits.getKeyListener()).isInstanceOf(DigitsKeyListener.class);
+    }
+
 
     private boolean listenerHandlesKeyEvent(AppCompatEditText textWithDigits, int action,
             int keycode) {
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
index 5162e15..9243af2 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
@@ -141,7 +141,13 @@
      */
     @Nullable
     KeyListener getKeyListener(@Nullable KeyListener keyListener) {
-        return mEmojiEditTextHelper.getKeyListener(keyListener);
+        // add a guard for NumberkeyListener both here and in emoji2 to avoid release dependency.
+        // this allows appcompat 1.4.1 to ship without a dependency on emoji2 1.1.
+        if (isEmojiCapableKeyListener(keyListener)) {
+            return mEmojiEditTextHelper.getKeyListener(keyListener);
+        } else {
+            return keyListener;
+        }
     }
 
     /**
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedCapabilities.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
similarity index 67%
rename from appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedCapabilities.java
rename to appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index 34c4084..2ac95f0 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedCapabilities.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -15,20 +15,24 @@
  */
 package androidx.appsearch.localstorage;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.app.Capabilities;
+import androidx.appsearch.app.Features;
 
 /**
- * An implementation of {@link Capabilities}. This implementation always returns true. This is
+ * An implementation of {@link Features}. This implementation always returns true. This is
  * sufficient for the use in the local backend because all features are always available on the
  * local backend.
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class AlwaysSupportedCapabilities implements Capabilities {
+public class AlwaysSupportedFeatures implements Features {
 
     @Override
-    public boolean isSubmatchSupported() {
-        return true;
+    public boolean isFeatureSupported(@NonNull String feature) {
+        if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
+            return true;
+        }
+        return false;
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
index 3406886..2a8e176 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/GlobalSearchSessionImpl.java
@@ -21,7 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.app.Capabilities;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResults;
@@ -44,7 +44,7 @@
 class GlobalSearchSessionImpl implements GlobalSearchSession {
     private final AppSearchImpl mAppSearchImpl;
     private final Executor mExecutor;
-    private final Capabilities mCapabilities;
+    private final Features mFeatures;
     private final Context mContext;
 
     private boolean mIsClosed = false;
@@ -55,12 +55,12 @@
     GlobalSearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull Executor executor,
-            @NonNull Capabilities capabilities,
+            @NonNull Features features,
             @NonNull Context context,
             @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
-        mCapabilities = Preconditions.checkNotNull(capabilities);
+        mFeatures = Preconditions.checkNotNull(features);
         mContext = Preconditions.checkNotNull(context);
         mLogger = logger;
     }
@@ -102,8 +102,8 @@
 
     @NonNull
     @Override
-    public Capabilities getCapabilities() {
-        return mCapabilities;
+    public Features getFeatures() {
+        return mFeatures;
     }
 
     @Override
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 1bdb76b..3ffb93d 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -381,7 +381,7 @@
         return new SearchSessionImpl(
                 mAppSearchImpl,
                 context.mExecutor,
-                new AlwaysSupportedCapabilities(),
+                new AlwaysSupportedFeatures(),
                 context.mContext.getPackageName(),
                 context.mDatabaseName,
                 context.mLogger);
@@ -391,6 +391,6 @@
     private GlobalSearchSession doCreateGlobalSearchSession(
             @NonNull GlobalSearchContext context) {
         return new GlobalSearchSessionImpl(mAppSearchImpl, context.mExecutor,
-                new AlwaysSupportedCapabilities(), context.mContext, context.mLogger);
+                new AlwaysSupportedFeatures(), context.mContext, context.mLogger);
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index ebdfe96..c74cae5 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -25,7 +25,7 @@
 import androidx.annotation.Nullable;
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Capabilities;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
@@ -71,7 +71,7 @@
     private static final String TAG = "AppSearchSessionImpl";
     private final AppSearchImpl mAppSearchImpl;
     private final Executor mExecutor;
-    private final Capabilities mCapabilities;
+    private final Features mFeatures;
     private final String mPackageName;
     private final String mDatabaseName;
     private volatile boolean mIsMutated = false;
@@ -81,13 +81,13 @@
     SearchSessionImpl(
             @NonNull AppSearchImpl appSearchImpl,
             @NonNull Executor executor,
-            @NonNull Capabilities capabilities,
+            @NonNull Features features,
             @NonNull String packageName,
             @NonNull String databaseName,
             @Nullable AppSearchLogger logger) {
         mAppSearchImpl = Preconditions.checkNotNull(appSearchImpl);
         mExecutor = Preconditions.checkNotNull(executor);
-        mCapabilities = Preconditions.checkNotNull(capabilities);
+        mFeatures = Preconditions.checkNotNull(features);
         mPackageName = packageName;
         mDatabaseName = Preconditions.checkNotNull(databaseName);
         mLogger = logger;
@@ -439,8 +439,8 @@
 
     @NonNull
     @Override
-    public Capabilities getCapabilities() {
-        return mCapabilities;
+    public Features getFeatures() {
+        return mFeatures;
     }
 
     @Override
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/CapabilitiesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/CapabilitiesImpl.java
deleted file mode 100644
index 4d21fbad..0000000
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/CapabilitiesImpl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.appsearch.platformstorage;
-
-import androidx.appsearch.app.Capabilities;
-
-/**
- * An implementation of {@link Capabilities}. Feature availability is dependent on Android API
- * level.
- */
-final class CapabilitiesImpl implements Capabilities {
-
-    @Override
-    public boolean isSubmatchSupported() {
-        // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is synced
-        // over into service-appsearch.
-        return false;
-    }
-}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
new file mode 100644
index 0000000..5bbf8bc
--- /dev/null
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -0,0 +1,36 @@
+/*
+ * 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.appsearch.platformstorage;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.Features;
+
+/**
+ * An implementation of {@link Features}. Feature availability is dependent on Android API
+ * level.
+ */
+final class FeaturesImpl implements Features {
+
+    @Override
+    public boolean isFeatureSupported(@NonNull String feature) {
+        if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
+            // TODO(b/201316758) : Update to reflect support in Android T+ once this feature is
+            // synced over into service-appsearch.
+            return false;
+        }
+        return false;
+    }
+}
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
index 102cf91..110d593 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/GlobalSearchSessionImpl.java
@@ -20,7 +20,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
-import androidx.appsearch.app.Capabilities;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GlobalSearchSession;
 import androidx.appsearch.app.ReportSystemUsageRequest;
 import androidx.appsearch.app.SearchResults;
@@ -45,15 +45,15 @@
 class GlobalSearchSessionImpl implements GlobalSearchSession {
     private final android.app.appsearch.GlobalSearchSession mPlatformSession;
     private final Executor mExecutor;
-    private final Capabilities mCapabilities;
+    private final Features mFeatures;
 
     GlobalSearchSessionImpl(
             @NonNull android.app.appsearch.GlobalSearchSession platformSession,
             @NonNull Executor executor,
-            @NonNull Capabilities capabilities) {
+            @NonNull Features features) {
         mPlatformSession = Preconditions.checkNotNull(platformSession);
         mExecutor = Preconditions.checkNotNull(executor);
-        mCapabilities = Preconditions.checkNotNull(capabilities);
+        mFeatures = Preconditions.checkNotNull(features);
     }
 
     @Override
@@ -85,8 +85,8 @@
 
     @NonNull
     @Override
-    public Capabilities getCapabilities() {
-        return mCapabilities;
+    public Features getFeatures() {
+        return mFeatures;
     }
 
     @Override
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
index 9cd4b06..ad345db 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/PlatformStorage.java
@@ -222,7 +222,7 @@
                     if (result.isSuccess()) {
                         future.set(
                                 new SearchSessionImpl(result.getResultValue(), context.mExecutor,
-                                new CapabilitiesImpl()));
+                                new FeaturesImpl()));
                     } else {
                         future.setException(
                                 new AppSearchException(
@@ -248,7 +248,7 @@
                     if (result.isSuccess()) {
                         future.set(new GlobalSearchSessionImpl(
                                 result.getResultValue(), context.mExecutor,
-                                new CapabilitiesImpl()));
+                                new FeaturesImpl()));
                     } else {
                         future.setException(
                                 new AppSearchException(
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
index 25b137c..4d8f2f2 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/SearchSessionImpl.java
@@ -22,7 +22,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchBatchResult;
 import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.Capabilities;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
@@ -62,15 +62,15 @@
 class SearchSessionImpl implements AppSearchSession {
     private final android.app.appsearch.AppSearchSession mPlatformSession;
     private final Executor mExecutor;
-    private final Capabilities mCapabilities;
+    private final Features mFeatures;
 
     SearchSessionImpl(
             @NonNull android.app.appsearch.AppSearchSession platformSession,
             @NonNull Executor executor,
-            @NonNull Capabilities capabilities) {
+            @NonNull Features features) {
         mPlatformSession = Preconditions.checkNotNull(platformSession);
         mExecutor = Preconditions.checkNotNull(executor);
-        mCapabilities = Preconditions.checkNotNull(capabilities);
+        mFeatures = Preconditions.checkNotNull(features);
     }
 
     @Override
@@ -290,8 +290,8 @@
 
     @NonNull
     @Override
-    public Capabilities getCapabilities() {
-        return mCapabilities;
+    public Features getFeatures() {
+        return mFeatures;
     }
 
     @Override
diff --git a/appsearch/appsearch-test-util/build.gradle b/appsearch/appsearch-test-util/build.gradle
index c6fdcaa..107b4b0 100644
--- a/appsearch/appsearch-test-util/build.gradle
+++ b/appsearch/appsearch-test-util/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 import androidx.build.LibraryVersions
 import androidx.build.Publish
 
@@ -37,6 +38,7 @@
 
 androidx {
     name = 'AppSearch Test Util'
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     mavenGroup = LibraryGroups.APPSEARCH
     inceptionYear = '2021'
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index c375403..865cad6 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -178,7 +178,7 @@
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
@@ -191,10 +191,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface Capabilities {
-    method public boolean isSubmatchSupported();
-  }
-
   public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
@@ -202,6 +198,11 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  public interface Features {
+    method public boolean isFeatureSupported(String);
+    field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+  }
+
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
     method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
@@ -277,7 +278,7 @@
 
   public interface GlobalSearchSession extends java.io.Closeable {
     method public void close();
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
   }
@@ -371,8 +372,8 @@
     method public String getPropertyPath();
     method public CharSequence getSnippet();
     method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
-    method public CharSequence getSubmatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public CharSequence getSubmatch();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
   }
 
   public static final class SearchResult.MatchInfo.Builder {
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index c375403..865cad6 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -178,7 +178,7 @@
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
@@ -191,10 +191,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface Capabilities {
-    method public boolean isSubmatchSupported();
-  }
-
   public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
@@ -202,6 +198,11 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  public interface Features {
+    method public boolean isFeatureSupported(String);
+    field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+  }
+
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
     method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
@@ -277,7 +278,7 @@
 
   public interface GlobalSearchSession extends java.io.Closeable {
     method public void close();
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
   }
@@ -371,8 +372,8 @@
     method public String getPropertyPath();
     method public CharSequence getSnippet();
     method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
-    method public CharSequence getSubmatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public CharSequence getSubmatch();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
   }
 
   public static final class SearchResult.MatchInfo.Builder {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index c375403..865cad6 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -178,7 +178,7 @@
   public interface AppSearchSession extends java.io.Closeable {
     method public void close();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByDocumentId(androidx.appsearch.app.GetByDocumentIdRequest);
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.util.Set<java.lang.String!>!> getNamespaces();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.GetSchemaResponse!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.StorageInfo!> getStorageInfo();
@@ -191,10 +191,6 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.SetSchemaResponse!> setSchema(androidx.appsearch.app.SetSchemaRequest);
   }
 
-  public interface Capabilities {
-    method public boolean isSubmatchSupported();
-  }
-
   public interface DocumentClassFactory<T> {
     method public T fromGenericDocument(androidx.appsearch.app.GenericDocument) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.AppSearchSchema getSchema() throws androidx.appsearch.exceptions.AppSearchException;
@@ -202,6 +198,11 @@
     method public androidx.appsearch.app.GenericDocument toGenericDocument(T) throws androidx.appsearch.exceptions.AppSearchException;
   }
 
+  public interface Features {
+    method public boolean isFeatureSupported(String);
+    field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+  }
+
   public class GenericDocument {
     ctor protected GenericDocument(androidx.appsearch.app.GenericDocument);
     method public static androidx.appsearch.app.GenericDocument fromDocumentClass(Object) throws androidx.appsearch.exceptions.AppSearchException;
@@ -277,7 +278,7 @@
 
   public interface GlobalSearchSession extends java.io.Closeable {
     method public void close();
-    method public androidx.appsearch.app.Capabilities getCapabilities();
+    method public androidx.appsearch.app.Features getFeatures();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> reportSystemUsage(androidx.appsearch.app.ReportSystemUsageRequest);
     method public androidx.appsearch.app.SearchResults search(String, androidx.appsearch.app.SearchSpec);
   }
@@ -371,8 +372,8 @@
     method public String getPropertyPath();
     method public CharSequence getSnippet();
     method public androidx.appsearch.app.SearchResult.MatchRange getSnippetRange();
-    method public CharSequence getSubmatch();
-    method public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public CharSequence getSubmatch();
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH) public androidx.appsearch.app.SearchResult.MatchRange getSubmatchRange();
   }
 
   public static final class SearchResult.MatchInfo.Builder {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index 765b4f3..470aae0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -36,6 +36,7 @@
 import androidx.appsearch.app.AppSearchSchema.PropertyConfig;
 import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.GetByDocumentIdRequest;
 import androidx.appsearch.app.GetSchemaResponse;
@@ -1901,7 +1902,8 @@
                 new SearchResult.MatchRange(/*lower=*/26,  /*upper=*/33));
         assertThat(matchInfo.getSnippet()).isEqualTo("is foo.");
 
-        if (!mDb1.getCapabilities().isSubmatchSupported()) {
+        if (!mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
             assertThrows(
                     UnsupportedOperationException.class,
                     () -> matchInfo.getSubmatchRange());
@@ -2005,9 +2007,9 @@
 
         String japanese =
                 "差し出されたのが今日ランドセルでした普通の子であれば満面の笑みで俺を言うでしょうしかし私は赤いランド"
-                + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言"
-                + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ"
-                + "ンポンと叩きピンクが良かったんだもん";
+                        + "セルを見て笑うことができませんでしたどうしたのと心配そうな仕事ガラスながら渋い顔する私書いたこと言"
+                        + "うんじゃないのカードとなる声を聞きたい私は目から涙をこぼしながらおじいちゃんの近くにかけおり頭をポ"
+                        + "ンポンと叩きピンクが良かったんだもん";
         // Index a document
         GenericDocument document =
                 new GenericDocument.Builder<>("namespace", "id", "Generic")
@@ -2036,7 +2038,8 @@
                 new SearchResult.MatchRange(/*lower=*/44,  /*upper=*/45));
         assertThat(matchInfo.getExactMatch()).isEqualTo("は");
 
-        if (!mDb1.getCapabilities().isSubmatchSupported()) {
+        if (!mDb1.getFeatures().isFeatureSupported(
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
             assertThrows(
                     UnsupportedOperationException.class,
                     () -> matchInfo.getSubmatchRange());
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index f01bed2d..8d45573 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -28,6 +28,7 @@
 import androidx.appsearch.app.AppSearchResult;
 import androidx.appsearch.app.AppSearchSchema;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.Migrator;
 import androidx.appsearch.app.PutDocumentsRequest;
@@ -495,6 +496,7 @@
         AppSearchSession db2 = LocalStorage.createSearchSession(
                 new LocalStorage.SearchContext.Builder(context, DB_NAME_2).build()).get();
 
-        assertThat(db2.getCapabilities().isSubmatchSupported()).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isTrue();
     }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
index 2490a03..a0cd21a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionPlatformCtsTest.java
@@ -23,6 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.Features;
 import androidx.appsearch.platformstorage.PlatformStorage;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SdkSuppress;
@@ -59,6 +60,7 @@
 
         // TODO(b/201316758) Update to reflect support in Android T+ once this feature is synced
         // over into service-appsearch.
-        assertThat(db2.getCapabilities().isSubmatchSupported()).isFalse();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)).isFalse();
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index c1279d39..d423da0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -262,10 +262,11 @@
     ListenableFuture<Void> requestFlush();
 
     /**
-     * Returns the {@link Capabilities} to check for the availability of certain features
+     * Returns the {@link Features} to check for the availability of certain features
      * for this session.
      */
-    @NonNull Capabilities getCapabilities();
+    @NonNull
+    Features getFeatures();
 
     /**
      * Closes the {@link AppSearchSession} to persist all schema and document updates, additions,
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Capabilities.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Capabilities.java
deleted file mode 100644
index 445f91f..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Capabilities.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.appsearch.app;
-
-/**
- * A class that encapsulates all features that are only supported with certain combinations of
- * backend and Android API Level.
- * <!--@exportToFramework:hide-->
- */
-public interface Capabilities {
-
-    /**
-     * Returns whether or not {@link SearchResult.MatchInfo#getSubmatchRange} and
-     * {@link SearchResult.MatchInfo#getSubmatch} are available.
-     */
-    boolean isSubmatchSupported();
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
new file mode 100644
index 0000000..0f42a09
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -0,0 +1,47 @@
+/*
+ * 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.appsearch.app;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A class that encapsulates all features that are only supported with certain combinations of
+ * backend and Android API Level.
+ * <!--@exportToFramework:hide-->
+ */
+public interface Features {
+
+    /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link SearchResult.MatchInfo#getSubmatchRange} and
+     * {@link SearchResult.MatchInfo#getSubmatch}.
+     */
+    String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+
+    /**
+     * Returns whether a feature is supported at run-time. Feature support depends on the
+     * feature in question, the AppSearch backend being used and the Android version of the
+     * device.
+     *
+     * <p class="note"><b>Note:</b> If this method returns {@code false}, it is not safe to invoke
+     * the methods requiring the desired feature.
+     *
+     * @param feature the feature to be checked
+     * @return whether the capability is supported given the Android API level and AppSearch
+     * backend.
+     */
+    boolean isFeatureSupported(@NonNull String feature);
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
index 1d8f921..2cbff4a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GlobalSearchSession.java
@@ -78,10 +78,11 @@
     ListenableFuture<Void> reportSystemUsage(@NonNull ReportSystemUsageRequest request);
 
     /**
-     * Returns the {@link Capabilities} to check for the availability of certain features
+     * Returns the {@link Features} to check for the availability of certain features
      * for this session.
      */
-    @NonNull Capabilities getCapabilities();
+    @NonNull
+    Features getFeatures();
 
     /** Closes the {@link GlobalSearchSession}. */
     @Override
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
index f27addc..b9d71ad 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchResult.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.core.util.ObjectsCompat;
@@ -426,12 +427,15 @@
          *
          * <!--@exportToFramework:ifJetpack()-->
          * <p>This information may not be available depending on the backend and Android API
-         * level. To ensure it is available, call {@link Capabilities#isSubmatchSupported}.
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
          *
-         * @throws UnsupportedOperationException if {@link Capabilities#isSubmatchSupported} is
+         * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is
          * false.
          * <!--@exportToFramework:else()-->
          */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
         @NonNull
         public MatchRange getSubmatchRange() {
             checkSubmatchSupported();
@@ -451,12 +455,15 @@
          *
          * <!--@exportToFramework:ifJetpack()-->
          * <p>This information may not be available depending on the backend and Android API
-         * level. To ensure it is available, call {@link Capabilities#isSubmatchSupported}.
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
          *
-         * @throws UnsupportedOperationException if {@link Capabilities#isSubmatchSupported} is
+         * @throws UnsupportedOperationException if {@link Features#isFeatureSupported} is
          * false.
          * <!--@exportToFramework:else()-->
          */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)
         @NonNull
         public CharSequence getSubmatch() {
             checkSubmatchSupported();
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 05effe5..53d7c42 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -233,7 +233,15 @@
         // We run lint on each library, so we don't want transitive checking of each dependency
         checkDependencies = false
 
-        fatal.add("VisibleForTests")
+        if (
+            extension.type == LibraryType.PUBLISHED_TEST_LIBRARY ||
+            extension.type == LibraryType.INTERNAL_TEST_LIBRARY
+        ) {
+            // Test libraries are allowed to call @VisibleForTests code
+            disable.add("VisibleForTests")
+        } else {
+            fatal.add("VisibleForTests")
+        }
 
         // Disable dependency checks that suggest to change them. We want libraries to be
         // intentional with their dependency version bumps.
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index 276651e..5579270 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -66,6 +66,14 @@
         sourceJars = true,
         checkApi = RunApiTasks.Yes()
     ),
+    PUBLISHED_TEST_LIBRARY(
+        publish = Publish.SNAPSHOT_AND_RELEASE,
+        sourceJars = true,
+        checkApi = RunApiTasks.Yes()
+    ),
+    INTERNAL_TEST_LIBRARY(
+        checkApi = RunApiTasks.No("Internal Library")
+    ),
     SAMPLES(
         publish = Publish.SNAPSHOT_AND_RELEASE,
         sourceJars = true,
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index 3cdad88..a881683 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -43,7 +43,7 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.junit)
     testImplementation(libs.truth)
-    testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/205731854): fix tests to work with SDK 31 and robolectric 4.7
+    testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/209062465): fix tests to work with SDK 31 and robolectric 4.7
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation(project(":camera:camera-camera2-pipe-testing"))
     testImplementation(project(":internal-testutils-truth"))
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 386a9c0..d18a2f3 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -38,7 +38,7 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.junit)
     testImplementation(libs.truth)
-    testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/205731854): fix tests to work with SDK 31 and robolectric 4.7
+    testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/209062465): fix tests to work with SDK 31 and robolectric 4.7
     testImplementation(libs.mockitoCore)
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation("androidx.annotation:annotation-experimental:1.1.0")
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index d6c1368..4e709b5 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -16,6 +16,7 @@
 
 
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -79,6 +80,7 @@
 
 androidx {
     name = "Jetpack Camera Testing Library"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     mavenGroup = LibraryGroups.CAMERA
     inceptionYear = "2019"
diff --git a/car/app/app-automotive/src/main/AndroidManifest.xml b/car/app/app-automotive/src/main/AndroidManifest.xml
index 4abc219..43b025b 100644
--- a/car/app/app-automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-automotive/src/main/AndroidManifest.xml
@@ -31,6 +31,7 @@
             android:name="androidx.car.app.CarAppMetadataHolderService"
             android:exported="false"
             android:enabled="false"
+            android:process=""
             tools:ignore="Instantiatable"
             tools:node="merge">
             <meta-data
diff --git a/car/app/app-projected/src/main/AndroidManifest.xml b/car/app/app-projected/src/main/AndroidManifest.xml
index a56441a..186a4fd 100644
--- a/car/app/app-projected/src/main/AndroidManifest.xml
+++ b/car/app/app-projected/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
             android:name="androidx.car.app.CarAppMetadataHolderService"
             android:exported="false"
             android:enabled="false"
+            android:process=""
             tools:ignore="Instantiatable"
             tools:node="merge">
             <meta-data
diff --git a/car/app/app-testing/build.gradle b/car/app/app-testing/build.gradle
index 9ab6dbb..ec190c2 100644
--- a/car/app/app-testing/build.gradle
+++ b/car/app/app-testing/build.gradle
@@ -54,7 +54,7 @@
 
 androidx {
     name = "androidx.car.app:app-testing"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.CAR_APP
     inceptionYear = "2021"
     description = "androidx.car.app:app-testing"
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index 704251b..ec72e80 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -39,7 +39,7 @@
         api(project(":compose:animation:animation"))
         api("androidx.compose.foundation:foundation-layout:1.0.0")
         api(project(":compose:runtime:runtime"))
-        api(project(":compose:ui:ui"))
+        api("androidx.compose.ui:ui:1.1.0-rc01")
         api("androidx.compose.ui:ui-geometry:1.0.0")
 
         implementation("androidx.compose.ui:ui-util:1.0.0")
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 5d7055a..58e5c02 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -67,6 +67,7 @@
 abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
     open val liveLiteralsEnabled get() = false
     open val liveLiteralsV2Enabled get() = false
+    open val generateFunctionKeyMetaClasses get() = false
     open val sourceInformationEnabled get() = true
     open val decoysEnabled get() = false
     open val metricsDestination: String? get() = null
@@ -74,6 +75,7 @@
     protected val extension = ComposeIrGenerationExtension(
         liveLiteralsEnabled,
         liveLiteralsV2Enabled,
+        generateFunctionKeyMetaClasses,
         sourceInformationEnabled,
         intrinsicRememberEnabled = true,
         decoysEnabled,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index a062ac0..6633510 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -26,6 +26,7 @@
 import androidx.compose.compiler.plugins.kotlin.lower.CopyDefaultValuesFromExpectLowering
 import androidx.compose.compiler.plugins.kotlin.lower.DurableKeyVisitor
 import androidx.compose.compiler.plugins.kotlin.lower.KlibAssignableParamTransformer
+import androidx.compose.compiler.plugins.kotlin.lower.DurableFunctionKeyTransformer
 import androidx.compose.compiler.plugins.kotlin.lower.LiveLiteralTransformer
 import androidx.compose.compiler.plugins.kotlin.lower.decoys.CreateDecoysTransformer
 import androidx.compose.compiler.plugins.kotlin.lower.decoys.RecordDecoySignaturesTransformer
@@ -46,6 +47,7 @@
 class ComposeIrGenerationExtension(
     @Suppress("unused") private val liveLiteralsEnabled: Boolean = false,
     @Suppress("unused") private val liveLiteralsV2Enabled: Boolean = false,
+    private val generateFunctionKeyMetaClasses: Boolean = false,
     private val sourceInformationEnabled: Boolean = true,
     private val intrinsicRememberEnabled: Boolean = true,
     private val decoysEnabled: Boolean = false,
@@ -97,6 +99,15 @@
 
         ComposableFunInterfaceLowering(pluginContext).lower(moduleFragment)
 
+        val functionKeyTransformer = DurableFunctionKeyTransformer(
+            pluginContext,
+            symbolRemapper,
+            bindingTrace,
+            metrics
+        )
+
+        functionKeyTransformer.lower(moduleFragment)
+
         // Memoize normal lambdas and wrap composable lambdas
         ComposerLambdaMemoization(
             pluginContext,
@@ -189,6 +200,10 @@
             ).lower(moduleFragment)
         }
 
+        if (generateFunctionKeyMetaClasses) {
+            functionKeyTransformer.includeFunctionKeyMetaClasses()
+        }
+
         if (metricsDestination != null) {
             metrics.saveMetricsTo(metricsDestination)
         }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index 0095ce0..fd485fd 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -41,6 +41,10 @@
         CompilerConfigurationKey<Boolean>(
             "Enable Live Literals code generation (with per-file enabled flags)"
         )
+    val GENERATE_FUNCTION_KEY_META_CLASSES_KEY =
+        CompilerConfigurationKey<Boolean>(
+            "Generate function key meta classes"
+        )
     val SOURCE_INFORMATION_ENABLED_KEY =
         CompilerConfigurationKey<Boolean>("Include source information in generated code")
     val METRICS_DESTINATION_KEY =
@@ -72,6 +76,14 @@
             required = false,
             allowMultipleOccurrences = false
         )
+        val GENERATE_FUNCTION_KEY_META_CLASSES_OPTION = CliOption(
+            "generateFunctionKeyMetaClasses",
+            "<true|false>",
+            "Generate function key meta classes with annotations indicating the " +
+                "functions and their group keys. Generally used for tooling.",
+            required = false,
+            allowMultipleOccurrences = false
+        )
         val SOURCE_INFORMATION_ENABLED_OPTION = CliOption(
             "sourceInformation",
             "<true|false>",
@@ -120,6 +132,7 @@
     override val pluginOptions = listOf(
         LIVE_LITERALS_ENABLED_OPTION,
         LIVE_LITERALS_V2_ENABLED_OPTION,
+        GENERATE_FUNCTION_KEY_META_CLASSES_OPTION,
         SOURCE_INFORMATION_ENABLED_OPTION,
         METRICS_DESTINATION_OPTION,
         REPORTS_DESTINATION_OPTION,
@@ -141,6 +154,10 @@
             ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY,
             value == "true"
         )
+        GENERATE_FUNCTION_KEY_META_CLASSES_OPTION -> configuration.put(
+            ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY,
+            value == "true"
+        )
         SOURCE_INFORMATION_ENABLED_OPTION -> configuration.put(
             ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY,
             value == "true"
@@ -220,6 +237,10 @@
                 ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY,
                 false
             )
+            val generateFunctionKeyMetaClasses = configuration.get(
+                ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY,
+                false
+            )
             val sourceInformationEnabled = configuration.get(
                 ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY,
                 false
@@ -268,6 +289,7 @@
                 ComposeIrGenerationExtension(
                     liveLiteralsEnabled = liveLiteralsEnabled,
                     liveLiteralsV2Enabled = liveLiteralsV2Enabled,
+                    generateFunctionKeyMetaClasses = generateFunctionKeyMetaClasses,
                     sourceInformationEnabled = sourceInformationEnabled,
                     intrinsicRememberEnabled = intrinsicRememberEnabled,
                     decoysEnabled = decoysEnabled,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposeWritableSlices.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposeWritableSlices.kt
index f813c6e..49643e1 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposeWritableSlices.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposeWritableSlices.kt
@@ -1,5 +1,6 @@
 package androidx.compose.compiler.plugins.kotlin.analysis
 
+import androidx.compose.compiler.plugins.kotlin.lower.KeyInfo
 import org.jetbrains.kotlin.descriptors.FunctionDescriptor
 import org.jetbrains.kotlin.ir.declarations.IrAttributeContainer
 import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -26,4 +27,6 @@
         BasicWritableSlice(RewritePolicy.DO_NOTHING)
     val IS_COMPOSABLE_SINGLETON_CLASS: WritableSlice<IrAttributeContainer, Boolean> =
         BasicWritableSlice(RewritePolicy.DO_NOTHING)
+    val DURABLE_FUNCTION_KEY: WritableSlice<IrAttributeContainer, KeyInfo> =
+        BasicWritableSlice(RewritePolicy.DO_NOTHING)
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 285ddca..9f054bb 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -129,6 +129,7 @@
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
 import org.jetbrains.kotlin.ir.util.constructedClass
+import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.getArguments
 import org.jetbrains.kotlin.ir.util.getPrimitiveArrayElementType
@@ -136,6 +137,7 @@
 import org.jetbrains.kotlin.ir.util.isFunction
 import org.jetbrains.kotlin.ir.util.isNoinline
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.load.kotlin.computeJvmDescriptor
 import org.jetbrains.kotlin.name.FqName
 import org.jetbrains.kotlin.name.Name
 import org.jetbrains.kotlin.platform.jvm.isJvm
@@ -213,14 +215,22 @@
     }
 
     fun getTopLevelClass(fqName: FqName): IrClassSymbol {
-        return context.referenceClass(fqName) ?: error("Class not found in the classpath: $fqName")
+        return getTopLevelClassOrNull(fqName) ?: error("Class not found in the classpath: $fqName")
+    }
+
+    fun getTopLevelClassOrNull(fqName: FqName): IrClassSymbol? {
+        return context.referenceClass(fqName)
     }
 
     fun getTopLevelFunction(fqName: FqName): IrFunctionSymbol {
-        return context.referenceFunctions(fqName).firstOrNull()
+        return getTopLevelFunctionOrNull(fqName)
             ?: error("Function not found in the classpath: $fqName")
     }
 
+    fun getTopLevelFunctionOrNull(fqName: FqName): IrFunctionSymbol? {
+        return context.referenceFunctions(fqName).firstOrNull()
+    }
+
     fun getTopLevelFunctions(fqName: FqName): List<IrSimpleFunctionSymbol> {
         return context.referenceFunctions(fqName).toList()
     }
@@ -237,6 +247,10 @@
         ComposeFqNames.internalFqNameFor(name)
     )
 
+    fun getInternalClassOrNull(name: String) = getTopLevelClassOrNull(
+        ComposeFqNames.internalFqNameFor(name)
+    )
+
     fun getTopLevelPropertyGetter(fqName: FqName): IrFunctionSymbol {
         val propertySymbol = context.referenceProperties(fqName).firstOrNull()
             ?: error("Property was not found $fqName")
@@ -1254,6 +1268,22 @@
             null
         }
     }
+
+    @OptIn(ObsoleteDescriptorBasedAPI::class)
+    fun IrSimpleFunction.sourceKey(): Int {
+        val info = context.irTrace[
+            ComposeWritableSlices.DURABLE_FUNCTION_KEY,
+            this
+        ]
+        if (info != null) {
+            info.used = true
+            return info.key
+        }
+        val signature = symbol.descriptor.computeJvmDescriptor(withName = false)
+        val name = fqNameForIrSerialization
+        val stringKey = "$name$signature"
+        return stringKey.hashCode()
+    }
 }
 
 private val unsafeSymbolsRegex = "[ <>]".toRegex()
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index 95eadcc..425b1cd8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -65,6 +65,7 @@
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
 import org.jetbrains.kotlin.ir.declarations.IrPackageFragment
 import org.jetbrains.kotlin.ir.declarations.IrProperty
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
 import org.jetbrains.kotlin.ir.declarations.IrTypeAlias
 import org.jetbrains.kotlin.ir.declarations.IrTypeParameter
 import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration
@@ -829,14 +830,14 @@
                         irStartReplaceableGroup(
                             body,
                             scope,
-                            declaration.irSourceKey()
+                            irFunctionSourceKey()
                         )
                     collectSourceInformation &&
                         !declaration.descriptor.hasExplicitGroupsAnnotation() ->
                         irSourceInformationMarkerStart(
                             body,
                             scope,
-                            declaration.irSourceKey()
+                            irFunctionSourceKey()
                         )
                     else -> null
                 },
@@ -972,7 +973,7 @@
                 body.endOffset,
                 listOfNotNull(
                     if (collectSourceInformation && scope.isInlinedLambda)
-                        irStartReplaceableGroup(body, scope)
+                        irStartReplaceableGroup(body, scope, irFunctionSourceKey())
                     else null,
                     *sourceInformationPreamble.statements.toTypedArray(),
                     *skipPreamble.statements.toTypedArray(),
@@ -1147,7 +1148,7 @@
                 irStartRestartGroup(
                     body,
                     scope,
-                    declaration.irSourceKey()
+                    irFunctionSourceKey()
                 ),
                 *skipPreamble.statements.toTypedArray(),
                 transformedBody,
@@ -1818,6 +1819,16 @@
         return hash
     }
 
+    @OptIn(ObsoleteDescriptorBasedAPI::class)
+    private fun functionSourceKey(): Int {
+        val fn = currentFunctionScope.function
+        if (fn is IrSimpleFunction) {
+            return fn.sourceKey()
+        } else {
+            error("expected simple function: ${fn::class}")
+        }
+    }
+
     private fun IrElement.irSourceKey(): IrConst<Int> {
         return IrConstImpl(
             UNDEFINED_OFFSET,
@@ -1828,6 +1839,16 @@
         )
     }
 
+    private fun irFunctionSourceKey(): IrConst<Int> {
+        return IrConstImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            context.irBuiltIns.intType,
+            IrConstKind.Int,
+            functionSourceKey()
+        )
+    }
+
     private fun irStartReplaceableGroup(
         element: IrElement,
         scope: Scope.BlockScope,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index bb88a82..ddb5af3 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -94,7 +94,6 @@
 import org.jetbrains.kotlin.platform.js.isJs
 import org.jetbrains.kotlin.platform.jvm.isJvm
 import org.jetbrains.kotlin.resolve.BindingTrace
-import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
 import org.jetbrains.kotlin.types.typeUtil.isUnit
 
 private class CaptureCollector {
@@ -730,10 +729,7 @@
             // key parameter
             putValueArgument(
                 index++,
-                irBuilder.irInt(
-                    @Suppress("DEPRECATION")
-                    symbol.descriptor.fqNameSafe.hashCode() xor expression.startOffset
-                )
+                irBuilder.irInt(expression.function.sourceKey())
             )
 
             // tracked parameter
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index 1287a2d..b4ce529 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -391,6 +391,7 @@
             containerSource
         ).also { fn ->
             if (this is IrSimpleFunction) {
+                fn.copyAttributes(this)
                 val propertySymbol = correspondingPropertySymbol
                 if (propertySymbol != null) {
                     fn.correspondingPropertySymbol = propertySymbol
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt
new file mode 100644
index 0000000..71cf8f4
--- /dev/null
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableFunctionKeyTransformer.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.compiler.plugins.kotlin.lower
+
+import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
+import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices.DURABLE_FUNCTION_KEY
+import androidx.compose.compiler.plugins.kotlin.irTrace
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.backend.common.ir.addChild
+import org.jetbrains.kotlin.backend.common.ir.createParameterDeclarations
+import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
+import org.jetbrains.kotlin.backend.common.push
+import org.jetbrains.kotlin.descriptors.ClassKind
+import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
+import org.jetbrains.kotlin.ir.builders.declarations.addConstructor
+import org.jetbrains.kotlin.ir.builders.declarations.buildClass
+import org.jetbrains.kotlin.ir.builders.irBlockBody
+import org.jetbrains.kotlin.ir.builders.irDelegatingConstructorCall
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
+import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
+import org.jetbrains.kotlin.ir.types.defaultType
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.constructors
+import org.jetbrains.kotlin.ir.util.primaryConstructor
+import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils
+import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.resolve.BindingTrace
+
+class KeyInfo(
+    val name: String,
+    val startOffset: Int,
+    val endOffset: Int,
+    val hasDuplicates: Boolean,
+) {
+    var used: Boolean = false
+    val key: Int get() = name.hashCode()
+}
+
+/**
+ * This transform will generate a "durable" and mostly unique key for every function in the module.
+ * In this case "durable" means that when the code is edited over time, a function with the same
+ * semantic identity will usually have the same key each time it is compiled. This is important so
+ * that new code can be recompiled and the key that the function gets after that recompile ought to
+ * be the same as before, so one could inject this new code and signal to the runtime that
+ * composable functions with that key should be considered invalid.
+ *
+ * This transform runs early on in the lowering pipeline, and stores the keys for every function in
+ * the file in the BindingTrace for each function. These keys are then retrieved later on by other
+ * lowerings and marked as used. After all lowerings have completed, one can use the
+ * [includeFunctionKeyMetaClasses] method to generate additional empty classes that include annotations
+ * with the keys of each function and their source locations for tooling to utilize.
+ *
+ * For example, this transform will run on code like the following:
+ *
+ *     @Composable fun Example() {
+ *       Box {
+ *          Text("Hello WOrld")
+ *       }
+ *     }
+ *
+ * And produce code like the following:
+ *
+ *     @Composable fun Example() {
+ *       startGroup(123)
+ *       Box {
+ *         startGroup(345)
+ *         Text("Hello World")
+ *         endGroup()
+ *       }
+ *       endGroup()
+ *     }
+ *
+ *     @FunctionKeyMetaClass
+ *     @FunctionKeyMeta(key=123, startOffset=24, endOffset=56)
+ *     @FunctionKeyMeta(key=345, startOffset=32, endOffset=43)
+ *     class Example-KeyMeta
+ *
+ * @see DurableKeyVisitor
+ */
+class DurableFunctionKeyTransformer(
+    context: IrPluginContext,
+    symbolRemapper: DeepCopySymbolRemapper,
+    bindingTrace: BindingTrace,
+    metrics: ModuleMetrics,
+) : DurableKeyTransformer(
+    DurableKeyVisitor(),
+    context,
+    symbolRemapper,
+    bindingTrace,
+    metrics
+) {
+    inner class Meta(
+        val file: IrFile,
+        val metaClass: IrClass,
+    ) {
+        val keys = mutableListOf<KeyInfo>()
+        fun includeFunctionKeyMetaClass() {
+            val usedKeys = keys.filter { it.used }
+            if (usedKeys.isEmpty()) {
+                // If none of the keys were used, don't generate a class
+                return
+            }
+            metaClass.annotations += usedKeys.map { irKeyMetaAnnotation(it) }
+            file.addChild(metaClass)
+        }
+    }
+
+    val metas = mutableListOf<Meta>()
+
+    var current: Meta? = null
+
+    fun includeFunctionKeyMetaClasses() {
+        if (keyMetaAnnotation == null || metaClassAnnotation == null) {
+            // if the generate key meta flag was passed in to the compiler but the annotations
+            // aren't in the runtime, we are just going to silently ignore it.
+            return
+        }
+        metas.forEach { it.includeFunctionKeyMetaClass() }
+    }
+
+    private val keyMetaAnnotation =
+        getInternalClassOrNull("FunctionKeyMeta")
+    private val metaClassAnnotation =
+        getInternalClassOrNull("FunctionKeyMetaClass")
+
+    private fun irKeyMetaAnnotation(
+        key: KeyInfo
+    ): IrConstructorCall = IrConstructorCallImpl(
+        UNDEFINED_OFFSET,
+        UNDEFINED_OFFSET,
+        keyMetaAnnotation!!.defaultType,
+        keyMetaAnnotation.constructors.single(),
+        0,
+        0,
+        2
+    ).apply {
+        putValueArgument(0, irConst(key.key.hashCode()))
+        putValueArgument(1, irConst(key.startOffset))
+        putValueArgument(1, irConst(key.endOffset))
+    }
+
+    private fun irMetaClassAnnotation(
+        file: String
+    ): IrConstructorCall = IrConstructorCallImpl(
+        UNDEFINED_OFFSET,
+        UNDEFINED_OFFSET,
+        metaClassAnnotation!!.defaultType,
+        metaClassAnnotation.constructors.single(),
+        0,
+        0,
+        1
+    ).apply {
+        putValueArgument(0, irConst(file))
+    }
+
+    private fun buildClass(filePath: String): IrClass {
+        val fileName = filePath.split('/').last()
+        return context.irFactory.buildClass {
+            kind = ClassKind.CLASS
+            visibility = DescriptorVisibilities.INTERNAL
+            val shortName = PackagePartClassUtils.getFilePartShortName(fileName)
+            // the name of the LiveLiterals class is per-file, so we use the same name that
+            // the kotlin file class lowering produces, prefixed with `LiveLiterals$`.
+            name = Name.identifier("$shortName\$KeyMeta")
+        }.also {
+            it.createParameterDeclarations()
+
+            // store the full file path to the file that this class is associated with in an
+            // annotation on the class. This will be used by tooling to associate the keys
+            // inside of this class with actual PSI in the editor.
+            if (metaClassAnnotation != null) {
+                it.annotations += irMetaClassAnnotation(filePath)
+            }
+            it.addConstructor {
+                isPrimary = true
+            }.also { ctor ->
+                ctor.body = DeclarationIrBuilder(context, it.symbol).irBlockBody {
+                    +irDelegatingConstructorCall(
+                        context
+                            .irBuiltIns
+                            .anyClass
+                            .owner
+                            .primaryConstructor!!
+                    )
+                }
+            }
+        }
+    }
+
+    override fun visitFile(declaration: IrFile): IrFile {
+        val stringKeys = mutableSetOf<String>()
+        return root(stringKeys) {
+            val prev = current
+            val next = Meta(
+                declaration,
+                buildClass(declaration.fileEntry.name)
+            )
+            metas.push(next)
+            try {
+                current = next
+                super.visitFile(declaration)
+            } finally {
+                current = prev
+            }
+        }
+    }
+
+    override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
+        val signature = declaration.signatureString()
+        val (fullName, success) = buildKey("fun-$signature")
+        val info = KeyInfo(
+            fullName,
+            declaration.startOffset,
+            declaration.endOffset,
+            success,
+        )
+        current?.keys?.add(info)
+        context.irTrace.record(DURABLE_FUNCTION_KEY, declaration, info)
+        return super.visitSimpleFunction(declaration)
+    }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt
new file mode 100644
index 0000000..e638de4
--- /dev/null
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/DurableKeyTransformer.kt
@@ -0,0 +1,463 @@
+/*
+ * 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.compose.compiler.plugins.kotlin.lower
+
+import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
+import org.jetbrains.kotlin.ir.declarations.IrEnumEntry
+import org.jetbrains.kotlin.ir.declarations.IrField
+import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrPackageFragment
+import org.jetbrains.kotlin.ir.declarations.IrProperty
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.declarations.IrVariable
+import org.jetbrains.kotlin.ir.expressions.IrBlock
+import org.jetbrains.kotlin.ir.expressions.IrBlockBody
+import org.jetbrains.kotlin.ir.expressions.IrBody
+import org.jetbrains.kotlin.ir.expressions.IrBranch
+import org.jetbrains.kotlin.ir.expressions.IrCall
+import org.jetbrains.kotlin.ir.expressions.IrComposite
+import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
+import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall
+import org.jetbrains.kotlin.ir.expressions.IrElseBranch
+import org.jetbrains.kotlin.ir.expressions.IrEnumConstructorCall
+import org.jetbrains.kotlin.ir.expressions.IrExpression
+import org.jetbrains.kotlin.ir.expressions.IrLoop
+import org.jetbrains.kotlin.ir.expressions.IrSetField
+import org.jetbrains.kotlin.ir.expressions.IrSetValue
+import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
+import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation
+import org.jetbrains.kotlin.ir.expressions.IrTry
+import org.jetbrains.kotlin.ir.expressions.IrVararg
+import org.jetbrains.kotlin.ir.expressions.IrVarargElement
+import org.jetbrains.kotlin.ir.expressions.IrWhen
+import org.jetbrains.kotlin.ir.expressions.impl.IrBranchImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrElseBranchImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrStringConcatenationImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl
+import org.jetbrains.kotlin.ir.types.IrDynamicType
+import org.jetbrains.kotlin.ir.types.IrErrorType
+import org.jetbrains.kotlin.ir.types.IrSimpleType
+import org.jetbrains.kotlin.ir.types.IrType
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
+import org.jetbrains.kotlin.ir.util.isAnnotationClass
+import org.jetbrains.kotlin.ir.util.parentAsClass
+import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
+import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.resolve.BindingTrace
+
+open class DurableKeyTransformer(
+    private val keyVisitor: DurableKeyVisitor,
+    context: IrPluginContext,
+    symbolRemapper: DeepCopySymbolRemapper,
+    bindingTrace: BindingTrace,
+    metrics: ModuleMetrics,
+) :
+    AbstractComposeLowering(context, symbolRemapper, bindingTrace, metrics),
+    ModuleLoweringPass {
+
+    override fun lower(module: IrModuleFragment) {
+        module.transformChildrenVoid(this)
+    }
+
+    protected fun buildKey(
+        prefix: String,
+        pathSeparator: String = "/",
+        siblingSeparator: String = ":"
+    ): Pair<String, Boolean> = keyVisitor.buildPath(prefix, pathSeparator, siblingSeparator)
+
+    protected fun <T> root(keys: MutableSet<String>, block: () -> T): T =
+        keyVisitor.root(keys, block)
+    protected fun <T> enter(key: String, block: () -> T) = keyVisitor.enter(key, block)
+    protected fun <T> siblings(key: String, block: () -> T) = keyVisitor.siblings(key, block)
+    protected fun <T> siblings(block: () -> T) = keyVisitor.siblings(block)
+
+    protected fun Name.asJvmFriendlyString(): String {
+        return if (!isSpecial) identifier
+        else asString()
+            .replace('<', '$')
+            .replace('>', '$')
+            .replace(' ', '-')
+    }
+
+    override fun visitClass(declaration: IrClass): IrStatement {
+        // constants in annotations need to be compile-time values, so we can never transform them
+        if (declaration.isAnnotationClass) return declaration
+        return siblings("class-${declaration.name.asJvmFriendlyString()}") {
+            super.visitClass(declaration)
+        }
+    }
+
+    override fun visitFile(declaration: IrFile): IrFile {
+        val filePath = declaration.fileEntry.name
+        val fileName = filePath.split('/').last()
+        return enter("file-$fileName") { super.visitFile(declaration) }
+    }
+
+    override fun visitPackageFragment(declaration: IrPackageFragment): IrPackageFragment {
+        return enter("pkg-${declaration.fqNameForIrSerialization}") {
+            super.visitPackageFragment(declaration)
+        }
+    }
+
+    override fun visitTry(aTry: IrTry): IrExpression {
+        aTry.tryResult = enter("try") {
+            aTry.tryResult.transform(this, null)
+        }
+        siblings {
+            aTry.catches.forEach {
+                it.result = enter("catch") { it.result.transform(this, null) }
+            }
+        }
+        aTry.finallyExpression = enter("finally") {
+            aTry.finallyExpression?.transform(this, null)
+        }
+        return aTry
+    }
+
+    override fun visitDelegatingConstructorCall(
+        expression: IrDelegatingConstructorCall
+    ): IrExpression {
+        val owner = expression.symbol.owner
+
+        // annotations are represented as constructor calls in IR, but the parameters need to be
+        // compile-time values only, so we can't transform them at all.
+        if (owner.parentAsClass.isAnnotationClass) return expression
+
+        val name = owner.name.asJvmFriendlyString()
+
+        return enter("call-$name") {
+            expression.dispatchReceiver = enter("\$this") {
+                expression.dispatchReceiver?.transform(this, null)
+            }
+            expression.extensionReceiver = enter("\$\$this") {
+                expression.extensionReceiver?.transform(this, null)
+            }
+
+            for (i in 0 until expression.valueArgumentsCount) {
+                val arg = expression.getValueArgument(i)
+                if (arg != null) {
+                    enter("arg-$i") {
+                        expression.putValueArgument(i, arg.transform(this, null))
+                    }
+                }
+            }
+            expression
+        }
+    }
+
+    override fun visitEnumConstructorCall(expression: IrEnumConstructorCall): IrExpression {
+        val owner = expression.symbol.owner
+        val name = owner.name.asJvmFriendlyString()
+
+        return enter("call-$name") {
+            expression.dispatchReceiver = enter("\$this") {
+                expression.dispatchReceiver?.transform(this, null)
+            }
+            expression.extensionReceiver = enter("\$\$this") {
+                expression.extensionReceiver?.transform(this, null)
+            }
+
+            for (i in 0 until expression.valueArgumentsCount) {
+                val arg = expression.getValueArgument(i)
+                if (arg != null) {
+                    enter("arg-$i") {
+                        expression.putValueArgument(i, arg.transform(this, null))
+                    }
+                }
+            }
+            expression
+        }
+    }
+
+    override fun visitConstructorCall(expression: IrConstructorCall): IrExpression {
+        val owner = expression.symbol.owner
+
+        // annotations are represented as constructor calls in IR, but the parameters need to be
+        // compile-time values only, so we can't transform them at all.
+        if (owner.parentAsClass.isAnnotationClass) return expression
+
+        val name = owner.name.asJvmFriendlyString()
+
+        return enter("call-$name") {
+            expression.dispatchReceiver = enter("\$this") {
+                expression.dispatchReceiver?.transform(this, null)
+            }
+            expression.extensionReceiver = enter("\$\$this") {
+                expression.extensionReceiver?.transform(this, null)
+            }
+
+            for (i in 0 until expression.valueArgumentsCount) {
+                val arg = expression.getValueArgument(i)
+                if (arg != null) {
+                    enter("arg-$i") {
+                        expression.putValueArgument(i, arg.transform(this, null))
+                    }
+                }
+            }
+            expression
+        }
+    }
+
+    override fun visitCall(expression: IrCall): IrExpression {
+        val owner = expression.symbol.owner
+        val name = owner.name.asJvmFriendlyString()
+
+        return enter("call-$name") {
+            expression.dispatchReceiver = enter("\$this") {
+                expression.dispatchReceiver?.transform(this, null)
+            }
+            expression.extensionReceiver = enter("\$\$this") {
+                expression.extensionReceiver?.transform(this, null)
+            }
+
+            for (i in 0 until expression.valueArgumentsCount) {
+                val arg = expression.getValueArgument(i)
+                if (arg != null) {
+                    enter("arg-$i") {
+                        expression.putValueArgument(i, arg.transform(this, null))
+                    }
+                }
+            }
+            expression
+        }
+    }
+
+    override fun visitEnumEntry(declaration: IrEnumEntry): IrStatement {
+        return enter("entry-${declaration.name.asJvmFriendlyString()}") {
+            super.visitEnumEntry(declaration)
+        }
+    }
+
+    override fun visitVararg(expression: IrVararg): IrExpression {
+        if (expression !is IrVarargImpl) return expression
+        return enter("vararg") {
+            expression.elements.forEachIndexed { i, arg ->
+                expression.elements[i] = enter("$i") {
+                    arg.transform(this, null) as IrVarargElement
+                }
+            }
+            expression
+        }
+    }
+
+    @OptIn(ObsoleteDescriptorBasedAPI::class)
+    protected fun IrType.asString(): String {
+        return when (this) {
+            is IrDynamicType -> "dynamic"
+            is IrErrorType -> "IrErrorType"
+            is IrSimpleType -> classifier.descriptor.name.asString()
+            else -> "{${javaClass.simpleName} $this}"
+        }
+    }
+
+    protected fun IrSimpleFunction.signatureString(): String {
+        return buildString {
+            extensionReceiverParameter?.let {
+                append(it.type.asString())
+                append(".")
+            }
+            append(name.asJvmFriendlyString())
+            append('(')
+            append(valueParameters.joinToString(",") { it.type.asString() })
+            append(')')
+            append(returnType.asString())
+        }
+    }
+
+    override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
+        val path = "fun-${declaration.signatureString()}"
+        return enter(path) { super.visitSimpleFunction(declaration) }
+    }
+
+    override fun visitLoop(loop: IrLoop): IrExpression {
+        return when (loop.origin) {
+            // in these cases, the compiler relies on a certain structure for the condition
+            // expression, so we only touch the body
+            IrStatementOrigin.WHILE_LOOP,
+            IrStatementOrigin.FOR_LOOP_INNER_WHILE -> enter("loop") {
+                loop.body = enter("body") { loop.body?.transform(this, null) }
+                loop
+            }
+            else -> enter("loop") {
+                loop.condition = enter("cond") { loop.condition.transform(this, null) }
+                loop.body = enter("body") { loop.body?.transform(this, null) }
+                loop
+            }
+        }
+    }
+
+    override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression {
+        if (expression !is IrStringConcatenationImpl) return expression
+        return enter("str") {
+            siblings {
+                expression.arguments.forEachIndexed { index, expr ->
+                    expression.arguments[index] = enter("$index") {
+                        expr.transform(this, null)
+                    }
+                }
+                expression
+            }
+        }
+    }
+
+    override fun visitWhen(expression: IrWhen): IrExpression {
+        return when (expression.origin) {
+            // ANDAND needs to have an 'if true then false' body on its second branch, so only
+            // transform the first branch
+            IrStatementOrigin.ANDAND -> {
+                expression.branches[0] = expression.branches[0].transform(this, null)
+                expression
+            }
+
+            // OROR condition should have an 'if a then true' body on its first branch, so only
+            // transform the second branch
+            IrStatementOrigin.OROR -> {
+                expression.branches[1] = expression.branches[1].transform(this, null)
+                expression
+            }
+
+            IrStatementOrigin.IF -> siblings("if") {
+                super.visitWhen(expression)
+            }
+
+            else -> siblings("when") {
+                super.visitWhen(expression)
+            }
+        }
+    }
+
+    override fun visitValueParameter(declaration: IrValueParameter): IrStatement {
+        return enter("param-${declaration.name.asJvmFriendlyString()}") {
+            super.visitValueParameter(declaration)
+        }
+    }
+
+    override fun visitElseBranch(branch: IrElseBranch): IrElseBranch {
+        return IrElseBranchImpl(
+            startOffset = branch.startOffset,
+            endOffset = branch.endOffset,
+            // the condition of an else branch is a constant boolean but we don't want
+            // to convert it into a live literal, so we don't transform it
+            condition = branch.condition,
+            result = enter("else") {
+                branch.result.transform(this, null)
+            }
+        )
+    }
+
+    override fun visitBranch(branch: IrBranch): IrBranch {
+        return IrBranchImpl(
+            startOffset = branch.startOffset,
+            endOffset = branch.endOffset,
+            condition = enter("cond") {
+                branch.condition.transform(this, null)
+            },
+            // only translate the result, as the branch is a constant boolean but we don't want
+            // to convert it into a live literal
+            result = enter("branch") {
+                branch.result.transform(this, null)
+            }
+        )
+    }
+
+    override fun visitComposite(expression: IrComposite): IrExpression {
+        return siblings {
+            super.visitComposite(expression)
+        }
+    }
+
+    override fun visitBlock(expression: IrBlock): IrExpression {
+        return when (expression.origin) {
+            // The compiler relies on a certain structure for the "iterator" instantiation in For
+            // loops, so we avoid transforming the first statement in this case
+            IrStatementOrigin.FOR_LOOP,
+            IrStatementOrigin.FOR_LOOP_INNER_WHILE -> {
+                expression.statements[1] =
+                    expression.statements[1].transform(this, null) as IrStatement
+                expression
+            }
+//            IrStatementOrigin.SAFE_CALL
+//            IrStatementOrigin.WHEN
+//            IrStatementOrigin.IF
+//            IrStatementOrigin.ELVIS
+//            IrStatementOrigin.ARGUMENTS_REORDERING_FOR_CALL
+            else -> siblings {
+                super.visitBlock(expression)
+            }
+        }
+    }
+
+    override fun visitSetValue(expression: IrSetValue): IrExpression {
+        val owner = expression.symbol.owner
+        val name = owner.name
+        return when (owner.origin) {
+            // for these synthetic variable declarations we want to avoid transforming them since
+            // the compiler will rely on their compile time value in some cases.
+            IrDeclarationOrigin.FOR_LOOP_IMPLICIT_VARIABLE -> expression
+            IrDeclarationOrigin.IR_TEMPORARY_VARIABLE -> expression
+            IrDeclarationOrigin.FOR_LOOP_VARIABLE -> expression
+            else -> enter("set-$name") { super.visitSetValue(expression) }
+        }
+    }
+
+    override fun visitSetField(expression: IrSetField): IrExpression {
+        val name = expression.symbol.owner.name
+        return enter("set-$name") { super.visitSetField(expression) }
+    }
+
+    override fun visitBlockBody(body: IrBlockBody): IrBody {
+        return siblings {
+            super.visitBlockBody(body)
+        }
+    }
+
+    override fun visitVariable(declaration: IrVariable): IrStatement {
+        return enter("val-${declaration.name.asJvmFriendlyString()}") {
+            super.visitVariable(declaration)
+        }
+    }
+
+    override fun visitProperty(declaration: IrProperty): IrStatement {
+        val backingField = declaration.backingField
+        val getter = declaration.getter
+        val setter = declaration.setter
+        val name = declaration.name.asJvmFriendlyString()
+
+        return enter("val-$name") {
+            // turn them into live literals. We should consider transforming some simple cases like
+            // `val foo = 123`, but in general turning this initializer into a getter is not a
+            // safe operation. We should figure out a way to do this for "static" expressions
+            // though such as `val foo = 16.dp`.
+            declaration.backingField = backingField?.transform(this, null) as? IrField
+            declaration.getter = enter("get") {
+                getter?.transform(this, null) as? IrSimpleFunction
+            }
+            declaration.setter = enter("set") {
+                setter?.transform(this, null) as? IrSimpleFunction
+            }
+            declaration
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 05efe11..27abb26 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -36,8 +36,8 @@
          */
 
         api("androidx.annotation:annotation:1.1.0")
-        api(project(":compose:ui:ui"))
-        api(project(":compose:ui:ui-unit"))
+        api("androidx.compose.ui:ui:1.1.0-rc01")
+        api("androidx.compose.ui:ui-unit:1.1.0-rc01")
 
         implementation(project(":compose:runtime:runtime"))
         implementation("androidx.compose.ui:ui-util:1.0.0")
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index c1cce87..410c22a 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,13 +36,13 @@
          * corresponding block above
          */
         api("androidx.annotation:annotation:1.1.0")
-        api(project(':compose:animation:animation'))
+        api("androidx.compose.animation:animation:1.1.0-rc01")
         api(project(':compose:runtime:runtime'))
-        api(project(':compose:ui:ui'))
+        api("androidx.compose.ui:ui:1.1.0-rc01")
 
         implementation(libs.kotlinStdlibCommon)
         implementation(project(":compose:foundation:foundation-layout"))
-        implementation(project(":compose:ui:ui-graphics"))
+        implementation("androidx.compose.ui:ui-graphics:1.1.0-rc01")
         implementation("androidx.compose.ui:ui-text:1.0.0")
         implementation("androidx.compose.ui:ui-util:1.0.0")
 
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
index 62c1c68..33ae8af 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/internal/ExposedDropdownMenuPopup.kt
@@ -163,7 +163,7 @@
     }
 }
 
-// TODO(b/142431825): This is a hack to work around Popups not using Semantics for test tags
+// TODO(b/139861182): This is a hack to work around Popups not using Semantics for test tags
 //  We should either remove it, or come up with an abstracted general solution that isn't specific
 //  to Popup
 internal val LocalPopupTestTag = compositionLocalOf { "DEFAULT_TEST_TAG" }
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index ac7294f..42fbd0d 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -37,16 +37,16 @@
          * corresponding block below
          */
         implementation(libs.kotlinStdlibCommon)
-        implementation(project(":compose:animation:animation-core"))
-        implementation(project(":compose:foundation:foundation-layout"))
+        implementation("androidx.compose.animation:animation-core:1.1.0-rc01")
+        implementation("androidx.compose.foundation:foundation-layout:1.1.0-rc01")
         implementation("androidx.compose.ui:ui-util:1.0.0")
 
-        api(project(":compose:foundation:foundation"))
+        api("androidx.compose.foundation:foundation:1.1.0-rc01")
         api("androidx.compose.material:material-icons-core:1.0.2")
         api("androidx.compose.material:material-ripple:1.0.0")
         api("androidx.compose.runtime:runtime:1.0.1")
         api("androidx.compose.ui:ui-graphics:1.0.1")
-        api(project(":compose:ui:ui"))
+        api("androidx.compose.ui:ui:1.1.0-rc01")
         api("androidx.compose.ui:ui-text:1.0.1")
 
         testImplementation(libs.testRules)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 30d8f34..27fddc4 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -662,6 +662,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public static Void illegalDecoyCallException(String fName);
   }
 
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMeta {
+    method public abstract int endOffset();
+    method public abstract int key();
+    method public abstract int startOffset();
+    property public abstract int endOffset;
+    property public abstract int key;
+    property public abstract int startOffset;
+  }
+
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMetaClass {
+    method public abstract String file();
+    property public abstract String file;
+  }
+
   @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface LiveLiteralFileInfo {
     method public abstract String file();
     property public abstract String file;
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 4b4a119..2f9a57e 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -693,6 +693,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public static Void illegalDecoyCallException(String fName);
   }
 
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMeta {
+    method public abstract int endOffset();
+    method public abstract int key();
+    method public abstract int startOffset();
+    property public abstract int endOffset;
+    property public abstract int key;
+    property public abstract int startOffset;
+  }
+
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMetaClass {
+    method public abstract String file();
+    property public abstract String file;
+  }
+
   @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface LiveLiteralFileInfo {
     method public abstract String file();
     property public abstract String file;
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index eba8918..5b450a1 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -690,6 +690,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public static Void illegalDecoyCallException(String fName);
   }
 
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMeta {
+    method public abstract int endOffset();
+    method public abstract int key();
+    method public abstract int startOffset();
+    property public abstract int endOffset;
+    property public abstract int key;
+    property public abstract int startOffset;
+  }
+
+  @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface FunctionKeyMetaClass {
+    method public abstract String file();
+    property public abstract String file;
+  }
+
   @androidx.compose.runtime.ComposeCompilerApi @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.RUNTIME) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface LiveLiteralFileInfo {
     method public abstract String file();
     property public abstract String file;
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/HotReloadIntegrationTests.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/HotReloadIntegrationTests.kt
deleted file mode 100644
index 621cadd..0000000
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/HotReloadIntegrationTests.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.runtime
-
-import android.app.Activity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.ModalDrawer
-import androidx.compose.material.Text
-import androidx.compose.runtime.benchmark.ComposeActivity
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-
-/**
- * Test the hot reload with sub-composition (specifically BoxWithConstraints).
- *
- * It is a bit odd for this to be in the benchmark project but, for one test, it seemed overkill
- * to create a separate integration test project.
- *
- * If we end up adding more tests a new project should be created.
- *
- * Regression test for b/148818582
- */
-@RunWith(AndroidJUnit4::class)
-@LargeTest
-class HotReloadIntegrationTests {
-    @Suppress("DEPRECATION")
-    @get:Rule
-    val activityRule = androidx.test.rule.ActivityTestRule(ComposeActivity::class.java)
-
-    @Test
-    fun testSubComposition() {
-        val activity = activityRule.activity
-        activity.uiThread {
-            activity.setContent {
-                Column {
-                    BoxWithConstraints {
-                        ModalDrawer(
-                            drawerContent = { },
-                            content = { Text(text = "Hello") }
-                        )
-                    }
-                }
-            }
-        }
-
-        activity.onNextFrame {
-            simulateHotReload(activity)
-        }
-    }
-}
-
-fun Activity.uiThread(block: () -> Unit) {
-    val latch = CountDownLatch(1)
-    var exception: Throwable? = null
-    runOnUiThread {
-        try {
-            block()
-        } catch (e: Throwable) {
-            exception = e
-        }
-        latch.countDown()
-    }
-    latch.await(5, TimeUnit.SECONDS) || error("UI thread work didn't complete in 5 secs")
-    exception?.let { throw it }
-}
-
-fun Activity.onNextFrame(block: () -> Unit) {
-    uiThread {
-        android.view.Choreographer.getInstance().postFrameCallback(
-            object : android.view.Choreographer.FrameCallback {
-                override fun doFrame(frameTimeNanos: Long) {
-                    block()
-                }
-            })
-    }
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index b794fa3..4d92637 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -2386,6 +2386,15 @@
         return false
     }
 
+    @TestOnly
+    internal fun parentKey(): Int {
+        return if (inserting) {
+            writer.groupKey(writer.parent)
+        } else {
+            reader.groupKey(reader.parent)
+        }
+    }
+
     /**
      * Skip a group. Skips the group at the current location. This is only valid to call if the
      * composition is not inserting.
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index df49cd8..a9bfd41 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -433,6 +433,10 @@
         parent.composeInitial(this, composable)
     }
 
+    fun invalidateGroupsWithKey(key: Int): Boolean {
+        return slotTable.invalidateGroupsWithKey(key)
+    }
+
     @Suppress("UNCHECKED_CAST")
     private fun drainPendingModificationsForCompositionLocked() {
         // Recording modifications may race for lock. If there are pending modifications
@@ -684,6 +688,8 @@
         val location = anchor.toIndexFor(slotTable)
         if (location < 0)
             return InvalidationResult.IGNORED // The scope was removed from the composition
+        if (!scope.canRecompose)
+            return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated
         if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
             // The invalidation was redirected to the composer.
             return InvalidationResult.IMMINENT
@@ -852,6 +858,11 @@
         internal fun simulateHotReload(context: Any) {
             loadStateAndCompose(saveStateAndDispose(context))
         }
+
+        @TestOnly
+        internal fun invalidateGroupsWithKey(key: Int): Boolean {
+            return Recomposer.invalidateGroupsWithKey(key)
+        }
     }
 }
 
@@ -861,6 +872,12 @@
 @TestOnly
 fun simulateHotReload(context: Any) = HotReloader.simulateHotReload(context)
 
+/**
+ * @suppress
+ */
+@TestOnly
+fun invalidateGroupsWithKey(key: Int) = HotReloader.invalidateGroupsWithKey(key)
+
 private fun <K : Any, V : Any> IdentityArrayMap<K, IdentityArraySet<V>?>.addValue(
     key: K,
     value: V
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 97c7b8d..fe36d27 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -65,6 +65,8 @@
      */
     val valid: Boolean get() = composition != null && anchor?.valid ?: false
 
+    val canRecompose: Boolean get() = block != null
+
     /**
      * Used is set when the [RecomposeScopeImpl] is used by, for example, [currentRecomposeScope].
      * This is used as the result of [Composer.endRestartGroup] and indicates whether the lambda
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 29d8d66..8f7f5e7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.snapshots.fastMapNotNull
 import androidx.compose.runtime.tooling.CompositionData
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
+import androidx.compose.runtime.snapshots.fastAny
 import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
@@ -309,6 +310,14 @@
             get() = this@Recomposer.hasPendingWork
         override val changeCount: Long
             get() = this@Recomposer.changeCount
+        fun invalidateGroupsWithKey(key: Int): Boolean {
+            val compositions: List<ControlledComposition> = synchronized(stateLock) {
+                knownCompositions.toMutableList()
+            }
+            return compositions
+                .fastMapNotNull { it as? CompositionImpl }
+                .fastAny { it.invalidateGroupsWithKey(key) }
+        }
         fun saveStateAndDisposeForHotReload(): List<HotReloadable> {
             val compositions: List<ControlledComposition> = synchronized(stateLock) {
                 knownCompositions.toMutableList()
@@ -952,6 +961,14 @@
             holders.fastForEach { it.resetContent() }
             holders.fastForEach { it.recompose() }
         }
+
+        internal fun invalidateGroupsWithKey(key: Int): Boolean {
+            var result = false
+            _runningRecomposers.value.forEach {
+                result = it.invalidateGroupsWithKey(key) || result
+            }
+            return result
+        }
     }
 }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index 10ffa2c..adcb14c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -262,6 +262,72 @@
     }
 
     /**
+     * Modifies the current slot table such that every group with the target key will be invalidated, and
+     * when recomposed, the content of those groups will be disposed and re-inserted.
+     *
+     * This is currently only used for developer tooling such as Live Edit to invalidate groups which
+     * we know will no longer have the same structure so we want to remove them before recomposing.
+     */
+    internal fun invalidateGroupsWithKey(target: Int): Boolean {
+        val anchors = mutableListOf<Anchor>()
+        // invalidate groups
+        read { reader ->
+            fun scanGroup() {
+                val key = reader.groupKey
+                if (key == target) {
+                    anchors.add(reader.anchor())
+                    invalidateGroup(reader.currentGroup)
+                    reader.skipGroup()
+                    return
+                }
+                reader.startGroup()
+                while (!reader.isGroupEnd) {
+                    scanGroup()
+                }
+                reader.endGroup()
+            }
+            scanGroup()
+        }
+        // bash keys
+        write { writer ->
+            writer.startGroup()
+            anchors.fastForEach { anchor ->
+                if (anchor.toIndexFor(writer) >= writer.currentGroup) {
+                    writer.seek(anchor)
+                    writer.bashGroup()
+                }
+            }
+            writer.skipToGroupEnd()
+            writer.endGroup()
+        }
+
+        return true
+    }
+
+    /**
+     * Finds the nearest recompose scope to the provided group and invalidates it
+     */
+    private fun invalidateGroup(group: Int): Anchor? {
+        var current = group
+        // for each parent up the spine
+        while (current >= 0) {
+            for (data in DataIterator(this, current)) {
+                if (data is RecomposeScopeImpl) {
+                    data.requiresRecompose = true
+                    val result = data.invalidateForResult(null)
+                    if (result != InvalidationResult.IGNORED) {
+                        // even though this is nullable, the anchor will not be null if
+                        // the invalidation wasn't ignored
+                        return data.anchor
+                    }
+                }
+            }
+            current = groups.parentAnchor(current)
+        }
+        return null
+    }
+
+    /**
      * A debugging aid to validate the internal structure of the slot table. Throws an exception
      * if the slot table is not in the expected shape.
      */
@@ -1450,6 +1516,18 @@
     }
 
     /**
+     * Wraps every child group of the current group with a group of a different key.
+     */
+    internal fun bashGroup() {
+        startGroup()
+        while (!isGroupEnd) {
+            insertParentGroup(-3)
+            skipGroup()
+        }
+        endGroup()
+    }
+
+    /**
      * If the start of a group was skipped using [skip], calling [ensureStarted] puts the writer
      * into the same state as if [startGroup] or [startNode] was called on the group starting at
      * [index]. If, after starting, the group, [currentGroup] is not a the end of the group or
@@ -1806,6 +1884,68 @@
     }
 
     /**
+     * Insert a parent group for the rest of the children in the current group. After this call
+     * all remaining children of the current group will be parented by a new group and the
+     * [currentSlot] will be moved to after the group inserted.
+     */
+    fun insertParentGroup(key: Int) {
+        runtimeCheck(insertCount == 0) { "Writer cannot be inserting" }
+        if (isGroupEnd) {
+            beginInsert()
+            startGroup(key)
+            endGroup()
+            endInsert()
+        } else {
+            val currentGroup = currentGroup
+            val parent = groups.parent(currentGroup)
+            val currentGroupEnd = parent + groupSize(parent)
+            val remainingSize = currentGroupEnd - currentGroup
+            var nodeCount = 0
+            var currentNewChild = currentGroup
+            while (currentNewChild < currentGroupEnd) {
+                val newChildAddress = groupIndexToAddress(currentNewChild)
+                nodeCount += groups.nodeCount(newChildAddress)
+                currentNewChild += groups.groupSize(newChildAddress)
+            }
+            val currentSlot = groups.dataAnchor(groupIndexToAddress(currentGroup))
+            beginInsert()
+            insertGroups(1)
+            endInsert()
+            val currentAddress = groupIndexToAddress(currentGroup)
+            groups.initGroup(
+                address = currentAddress,
+                key = key,
+                isNode = false,
+                hasDataKey = false,
+                hasData = false,
+                parentAnchor = parent,
+                dataAnchor = currentSlot
+            )
+
+            // Update the size of the group to cover the remaining children
+            groups.updateGroupSize(currentAddress, remainingSize + 1)
+            groups.updateNodeCount(currentAddress, nodeCount)
+
+            // Update the parent to account for the new group
+            val parentAddress = groupIndexToAddress(parent)
+            addToGroupSizeAlongSpine(parentAddress, 1)
+            fixParentAnchorsFor(parent, currentGroupEnd, currentGroup)
+            this.currentGroup = currentGroupEnd
+        }
+    }
+
+    fun addToGroupSizeAlongSpine(address: Int, amount: Int) {
+        var addr = address
+        while (addr > 0) {
+            groups.updateGroupSize(addr, groups.groupSize(addr) + amount)
+            val parentAnchor = groups.parentAnchor(addr)
+            val parentGroup = parentAnchorToIndex(parentAnchor)
+            val parentAddress = groupIndexToAddress(parentGroup)
+            addr = parentAddress
+        }
+    }
+
+    /**
      * Allocate an anchor to the current group or [index].
      */
     fun anchor(index: Int = currentGroup): Anchor = anchors.getOrAdd(index, size) {
@@ -2438,22 +2578,7 @@
                     table.slots[table.groups.nodeIndex(group)] else
                     null
 
-            override val data: Iterable<Any?> get() {
-                val start = table.groups.dataAnchor(group)
-                val end = if (group + 1 < table.groupsSize)
-                    table.groups.dataAnchor(group + 1) else table.slotsSize
-                return object : Iterable<Any?>, Iterator<Any?> {
-                    var index = start
-                    override fun iterator(): Iterator<Any?> = this
-                    override fun hasNext(): Boolean = index < end
-                    override fun next(): Any? =
-                        (
-                            if (index >= 0 && index < table.slots.size)
-                                table.slots[index]
-                            else null
-                            ).also { index++ }
-                }
-            }
+            override val data: Iterable<Any?> get() = DataIterator(table, group)
 
             override val compositionGroups: Iterable<CompositionGroup> get() = this
 
@@ -2475,6 +2600,23 @@
     }
 }
 
+private class DataIterator(
+    val table: SlotTable,
+    val group: Int,
+) : Iterable<Any?>, Iterator<Any?> {
+    val start = table.groups.dataAnchor(group)
+    val end = if (group + 1 < table.groupsSize)
+        table.groups.dataAnchor(group + 1) else table.slotsSize
+    var index = start
+    override fun iterator(): Iterator<Any?> = this
+    override fun hasNext(): Boolean = index < end
+    override fun next(): Any? = (
+        if (index >= 0 && index < table.slots.size)
+            table.slots[index]
+        else null
+    ).also { index++ }
+}
+
 // Parent -1 is reserved to be the root parent index so the anchor must pivot on -2.
 private const val parentAnchorPivot = -2
 
@@ -2644,6 +2786,14 @@
     this[arrayIndex + DataAnchor_Offset] = dataAnchor
 }
 
+private fun IntArray.updateGroupKey(
+    address: Int,
+    key: Int,
+) {
+    val arrayIndex = address * Group_Fields_Size
+    this[arrayIndex + Key_Offset] = key
+}
+
 private inline fun ArrayList<Anchor>.getOrAdd(
     index: Int,
     effectiveSize: Int,
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt
new file mode 100644
index 0000000..5056419
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/FunctionKeyMeta.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.internal
+
+import androidx.compose.runtime.ComposeCompilerApi
+
+/**
+ * This annotation is applied to the FunctionKeyMeta classes created by the Compose
+ * Compiler. These classes will have multiple of these annotations, each one corresponding to a
+ * single composable function. The annotation holds some metadata about the function itself and is
+ * intended to be used to provide information useful to tooling.
+ *
+ * @param key The key used for the function's group.
+ * @param startOffset The startOffset of the function in the source file at the time of compilation.
+ * @param endOffset The startOffset of the function in the source file at the time of compilation.
+ */
+@ComposeCompilerApi
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+@Repeatable
+annotation class FunctionKeyMeta(
+    val key: Int,
+    val startOffset: Int,
+    val endOffset: Int
+)
+
+/**
+ * This annotation is applied to the FunctionKeyMeta classes created by the Compose
+ * Compiler. This is intended to be used to provide information useful to tooling.
+ *
+ * @param file The file path of the file the associated class was produced for
+ */
+@ComposeCompilerApi
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FunctionKeyMetaClass(
+    val file: String
+)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
index cb0552e..21a2e7ac 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/ListUtils.kt
@@ -84,6 +84,15 @@
     return target
 }
 
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T> List<T>.fastAny(predicate: (T) -> Boolean): Boolean {
+    contract { callsInPlace(predicate) }
+    fastForEach {
+        if (predicate(it)) return true
+    }
+    return false
+}
+
 /**
  * Creates a string from all the elements separated using [separator] and using the given [prefix]
  * and [postfix] if supplied.
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LiveEditTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LiveEditTests.kt
new file mode 100644
index 0000000..3f74c3d
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LiveEditTests.kt
@@ -0,0 +1,289 @@
+/*
+ * 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.compose.runtime
+
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.compositionTest
+import org.junit.Assert
+import org.junit.Ignore
+import org.junit.Test
+
+class LiveEditTests {
+
+    @Test
+    fun testRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+        ensureStatePreservedAndNotRecomposed("a")
+        RestartGroup {
+            Text("Hello World")
+            ensureStatePreservedAndNotRecomposed("b")
+            Target("c")
+        }
+    }
+
+    // TODO: This should pass but doesn't. Need to investigate why.
+    @Ignore
+    fun testNonRestartableTargetAtRootScope() = liveEditTest {
+        Target("b", restartable = false)
+    }
+
+    @Test
+    fun testTargetSiblings() = liveEditTest {
+        Target("a")
+        Target("b")
+    }
+
+    @Test
+    fun testMultipleFunctionPreservesParentAndSiblingState() = liveEditTest {
+        ensureStatePreservedAndNotRecomposed("a")
+        Target("b")
+        RestartGroup {
+            Text("Hello World")
+            ensureStatePreservedAndNotRecomposed("c")
+            Target("d")
+            Target("e")
+        }
+        Target("f")
+    }
+
+    @Test
+    fun testChildGroupStateIsDestroyed() = liveEditTest {
+        ensureStatePreservedAndNotRecomposed("a")
+        RestartGroup {
+            Text("Hello World")
+            ensureStatePreservedAndNotRecomposed("b")
+            Target("c") {
+                Text("Hello World")
+                ensureStateLost("d")
+            }
+        }
+    }
+
+    @Test
+    fun testTargetWithinTarget() = liveEditTest {
+        ensureStatePreservedAndNotRecomposed("a")
+        RestartGroup {
+            Text("Hello World")
+            ensureStatePreservedAndNotRecomposed("b")
+            Target("c") {
+                Text("Hello World")
+                ensureStateLost("d")
+                RestartGroup {
+                    markAsTarget()
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+        ensureStatePreservedAndNotRecomposed("a")
+        RestartGroup {
+            Text("Hello World")
+            ensureStatePreservedButRecomposed("b")
+            Target("c", restartable = false)
+        }
+    }
+
+    @Test
+    fun testMultipleNonRestartableFunctionPreservesParentAndSiblingState() = liveEditTest {
+        RestartGroup {
+            ensureStatePreservedButRecomposed("a")
+            Target("b", restartable = false)
+            RestartGroup {
+                Text("Hello World")
+                ensureStatePreservedButRecomposed("c")
+                Target("d", restartable = false)
+                Target("e", restartable = false)
+            }
+            Target("f", restartable = false)
+        }
+    }
+
+    @Test
+    fun testLambda() = liveEditTest {
+        RestartGroup {
+            markAsTarget()
+            ensureStateLost("a")
+            Text("Hello World")
+        }
+    }
+
+    @Test
+    fun testInlineComposableLambda() = liveEditTest {
+        RestartGroup {
+            InlineTarget("a")
+            ensureStatePreservedButRecomposed("b")
+            Text("Hello World")
+        }
+    }
+}
+
+@Composable
+@NonRestartableComposable
+fun LiveEditTestScope.ensureStatePreservedButRecomposed(ref: String) {
+    expect(
+        ref,
+        compose = 2,
+        >
+        >
+        >
+    )
+}
+
+@Composable
+@NonRestartableComposable
+fun LiveEditTestScope.ensureStatePreservedAndNotRecomposed(ref: String) {
+    expect(
+        ref,
+        compose = 1,
+        >
+        >
+        >
+    )
+}
+
+@Composable
+@NonRestartableComposable
+fun LiveEditTestScope.ensureStateLost(ref: String) {
+    expect(
+        ref,
+        compose = 2,
+        >
+        >
+        >
+    )
+}
+
+@Composable
+@NonRestartableComposable
+fun LiveEditTestScope.expect(
+    ref: String,
+    compose: Int,
+    onRememberd: Int,
+    onForgotten: Int,
+    onAbandoned: Int,
+) {
+    log(ref, "compose")
+    remember {
+        object : RememberObserver {
+            override fun onRemembered() {
+                log(ref, "onRemembered")
+            }
+
+            override fun onForgotten() {
+                log(ref, "onForgotten")
+            }
+
+            override fun onAbandoned() {
+                log(ref, "onAbandoned")
+            }
+        }
+    }
+    expectLogCount(ref, "compose", compose)
+    expectLogCount(ref, "onRemembered", onRememberd)
+    expectLogCount(ref, "onForgotten", onForgotten)
+    expectLogCount(ref, "onAbandoned", onAbandoned)
+}
+
+@Composable fun LiveEditTestScope.Target(
+    ref: String,
+    restartable: Boolean = true,
+    content: @Composable () -> Unit = {}
+) {
+    if (restartable) currentRecomposeScope
+    markAsTarget()
+    expect(
+        ref,
+        compose = 2,
+        >
+        >
+        >
+    )
+    content()
+}
+
+@Composable fun LiveEditTestScope.InlineTarget(
+    ref: String,
+    content: @Composable () -> Unit = {}
+) {
+    markAsTarget()
+    expect(
+        ref,
+        compose = 2,
+        >
+        >
+        >
+    )
+    content()
+}
+
+@Composable
+@ExplicitGroupsComposable
+fun LiveEditTestScope.markAsTarget() {
+    addTargetKey((currentComposer as ComposerImpl).parentKey())
+}
+
+fun liveEditTest(fn: @Composable LiveEditTestScope.() -> Unit) = compositionTest {
+    with(LiveEditTestScope()) {
+        compose { fn(this) }
+        invalidateTargets()
+        advance()
+        runChecks()
+    }
+}
+
+@Stable
+class LiveEditTestScope {
+    private val targetKeys = mutableSetOf<Int>()
+    private val checks = mutableListOf<() -> Unit>()
+    private val logs = mutableListOf<Pair<String, String>>()
+
+    fun invalidateTargets() {
+        for (key in targetKeys) {
+            invalidateGroupsWithKey(key)
+        }
+    }
+
+    fun runChecks() {
+        for (check in checks) {
+            check()
+        }
+    }
+
+    fun addTargetKey(key: Int) {
+        targetKeys.add(key)
+    }
+
+    fun log(ref: String, msg: String) {
+        logs.add(ref to msg)
+    }
+    fun addLogCheck(ref: String, validate: (List<String>) -> Unit) {
+        checks.add {
+            validate(logs.filter { it.first == ref }.map { it.second }.toList())
+        }
+    }
+    fun expectLogCount(ref: String, msg: String, expected: Int) {
+        addLogCheck(ref) { logs ->
+            val actual = logs.filter { m -> m == msg }.count()
+            Assert.assertEquals(
+                "Ref $ref had an unexpected # of '$msg' logs",
+                expected,
+                actual
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index bf81a8d..e792129 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -15,6 +15,7 @@
  */
 
 import androidx.build.AndroidXComposePlugin
+import androidx.build.LibraryType
 import androidx.build.Publish
 
 plugins {
@@ -99,6 +100,7 @@
 
 androidx {
     name = "Compose Internal Test Utils"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     inceptionYear = "2020"
     description = "Compose internal test utils."
diff --git a/compose/test-utils/lint-baseline.xml b/compose/test-utils/lint-baseline.xml
deleted file mode 100644
index a49e666..0000000
--- a/compose/test-utils/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta02)" variant="all" version="7.1.0-beta02">
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        view = findViewRootForTest(activity)!!.view"
-        errorLine2="                                               ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
-            line="126"
-            column="48"/>
-    </issue>
-
-</issues>
diff --git a/compose/ui/ui-test-font/build.gradle b/compose/ui/ui-test-font/build.gradle
index e8c0acd..d4d503e 100644
--- a/compose/ui/ui-test-font/build.gradle
+++ b/compose/ui/ui-test-font/build.gradle
@@ -16,6 +16,7 @@
 
 import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 import androidx.build.Publish
 import androidx.build.RunApiTasks
 
@@ -49,6 +50,7 @@
 
 androidx {
     name = "Compose Test Font resources"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     mavenGroup = LibraryGroups.Compose.UI
     inceptionYear = "2020"
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 21e551d..752cb1e 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -141,7 +141,7 @@
 
 androidx {
     name = "Compose Testing for JUnit4"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.Compose.UI
     inceptionYear = "2020"
     description = "Compose testing integration with JUnit4"
diff --git a/compose/ui/ui-test-junit4/lint-baseline.xml b/compose/ui/ui-test-junit4/lint-baseline.xml
deleted file mode 100644
index cd86c96..0000000
--- a/compose/ui/ui-test-junit4/lint-baseline.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta02)" variant="all" version="7.1.0-beta02">
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="            hadPendingMeasureLayout = composeRoots.any { it.hasPendingMeasureOrLayout }"
-        errorLine2="                                                            ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResource.android.kt"
-            line="67"
-            column="61"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        return view.rootView.parent != null &amp;&amp; !view.isAttachedToWindow"
-        errorLine2="               ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResource.android.kt"
-            line="111"
-            column="16"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        return view.rootView.parent != null &amp;&amp; !view.isAttachedToWindow"
-        errorLine2="                                                ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResource.android.kt"
-            line="111"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        get() = ViewRootForTest. ::onViewRootCreated"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistry.android.kt"
-            line="53"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        ViewRootForTest.>
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistry.android.kt"
-            line="59"
-            column="25"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="                root.view.addOnAttachStateChangeListener(StateChangeHandler(root))"
-        errorLine2="                     ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistry.android.kt"
-            line="87"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="            if (composeRoot == this.composeRoot &amp;&amp; !registered) {"
-        errorLine2="                            ~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/ComposeRootRegistry.android.kt"
-            line="222"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        return composeRoots.filter { it.hasPendingMeasureOrLayout }"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/RobolectricIdlingStrategy.android.kt"
-            line="91"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="            .onEach { it.view.requestLayout() }"
-        errorLine2="                         ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/junit4/RobolectricIdlingStrategy.android.kt"
-            line="92"
-            column="26"/>
-    </issue>
-
-</issues>
diff --git a/compose/ui/ui-test-manifest/build.gradle b/compose/ui/ui-test-manifest/build.gradle
index ce28047..d162a53 100644
--- a/compose/ui/ui-test-manifest/build.gradle
+++ b/compose/ui/ui-test-manifest/build.gradle
@@ -29,7 +29,7 @@
 
 androidx {
     name = "Compose Testing manifest dependency"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.Compose.UI
     inceptionYear = "2021"
     description = "Compose testing library that should be added as a debugImplementation dependency to add properties to the debug manifest necessary for testing an application"
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 0e9a2ba..1a5db16 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -143,7 +143,7 @@
 
 androidx {
     name = "Compose Testing"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.Compose.UI
     inceptionYear = "2019"
     description = "Compose testing library"
diff --git a/compose/ui/ui-test/lint-baseline.xml b/compose/ui/ui-test/lint-baseline.xml
deleted file mode 100644
index 071d07e..0000000
--- a/compose/ui/ui-test/lint-baseline.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.0-beta03)" variant="all" version="7.1.0-beta03">
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="        if (!ViewMatchers.isDisplayed().matches(it.view)) {"
-        errorLine2="                                                   ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.android.kt"
-            line="41"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="    val composeView = (root as ViewRootForTest).view"
-        errorLine2="                                                ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.android.kt"
-            line="56"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="    val composeView = (root as ViewRootForTest).view"
-        errorLine2="                                                ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.android.kt"
-            line="65"
-            column="49"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="    val view = root.view"
-        errorLine2="                    ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt"
-            line="48"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="                root.view.getLocationOnScreen(array)"
-        errorLine2="                     ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt"
-            line="221"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="VisibleForTests"
-        message="This method should only be accessed from tests or within private scope"
-        errorLine1="                root.view.getLocationOnScreen(array)"
-        errorLine2="                     ~~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.android.kt"
-            line="306"
-            column="22"/>
-    </issue>
-
-</issues>
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
index 9b39bd5..70047e6 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.android.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.test
 
-import android.annotation.SuppressLint
 import android.app.Activity
 import android.content.Context
 import android.content.ContextWrapper
@@ -56,7 +55,6 @@
         )
     }
 
-    @SuppressLint("VisibleForTests")
     val view = (node.root as ViewRootForTest).view
 
     // If we are in dialog use its window to capture the bitmap
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index ec6d0e0..f1e7484 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -33,7 +33,7 @@
         implementation(libs.kotlinStdlib)
 
         api("androidx.annotation:annotation:1.1.0")
-        implementation(project(":compose:animation:animation"))
+        implementation("androidx.compose.animation:animation:1.1.0-rc01")
 
         api(project(":compose:runtime:runtime"))
         api(project(":compose:ui:ui"))
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 1e133f1..0de865e 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -61,7 +61,7 @@
         implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
         implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
         implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
-        implementation(project(":profileinstaller:profileinstaller"))
+        implementation("androidx.profileinstaller:profileinstaller:1.1.0-rc01")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 175c408..1dc4b98 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -3025,6 +3025,8 @@
             MotionEvent.BUTTON_FORWARD to ButtonValidation(4, forward = true),
             MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_TERTIARY to
                 ButtonValidation(0, 2, primary = true, tertiary = true),
+            MotionEvent.BUTTON_BACK or MotionEvent.BUTTON_STYLUS_PRIMARY to
+                ButtonValidation(0, 3, primary = true, back = true),
             0 to ButtonValidation(anyPressed = false)
         )
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
index 82ac8cb..0d64d23 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
@@ -147,31 +147,24 @@
         return -1
     }
     var index = 0
-    var shifted = packedValue
+    // shift stylus primary and secondary to primary and secondary
+    var shifted = ((packedValue and 0x60) ushr 5) or (packedValue and 0x60.inv())
     while (shifted and 1 == 0) {
         index++
         shifted = shifted ushr 1
     }
-    return indexRemovingStylusPrimaryAndSecondary(index)
+    return index
 }
 
 actual fun PointerButtons.indexOfLastPressed(): Int {
-    var shifted = packedValue
+    // shift stylus primary and secondary to primary and secondary
+    var shifted = ((packedValue and 0x60) ushr 5) or (packedValue and 0x60.inv())
     var index = -1
     while (shifted != 0) {
         index++
         shifted = shifted ushr 1
     }
-    return indexRemovingStylusPrimaryAndSecondary(index)
-}
-
-private fun indexRemovingStylusPrimaryAndSecondary(buttonIndex: Int): Int {
-    return when (buttonIndex) {
-        -1, 0, 1, 2, 3, 4 -> buttonIndex
-        5 -> 0 // stylus primary is just primary
-        6 -> 1 // stylus secondary is just secondary
-        else -> buttonIndex - 2
-    }
+    return index
 }
 
 actual val PointerKeyboardModifiers.isCtrlPressed: Boolean
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
index 60fbbcc..b680907 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
@@ -319,7 +319,7 @@
     }
 }
 
-// TODO(b/142431825): This is a hack to work around Popups not using Semantics for test tags
+// TODO(b/139861182): This is a hack to work around Popups not using Semantics for test tags
 //  We should either remove it, or come up with an abstracted general solution that isn't specific
 //  to Popup
 internal val LocalPopupTestTag = compositionLocalOf { "DEFAULT_TEST_TAG" }
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index a28a310..0f35e71 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -772,6 +772,11 @@
     field public static final int IMPORTANCE_UNSPECIFIED = -1000; // 0xfffffc18
   }
 
+  public interface OnNewIntentProvider {
+    method public void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+  }
+
   public class Person {
     method public static androidx.core.app.Person fromBundle(android.os.Bundle);
     method public androidx.core.graphics.drawable.IconCompat? getIcon();
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index d26610b..72026a6 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -772,6 +772,11 @@
     field public static final int IMPORTANCE_UNSPECIFIED = -1000; // 0xfffffc18
   }
 
+  public interface OnNewIntentProvider {
+    method public void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+  }
+
   public class Person {
     method public static androidx.core.app.Person fromBundle(android.os.Bundle);
     method public androidx.core.graphics.drawable.IconCompat? getIcon();
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index e7c35ad..902fd540 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -858,6 +858,11 @@
     field public static final int IMPORTANCE_UNSPECIFIED = -1000; // 0xfffffc18
   }
 
+  public interface OnNewIntentProvider {
+    method public void addOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+    method public void removeOnNewIntentListener(androidx.core.util.Consumer<android.content.Intent!>);
+  }
+
   public class Person {
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.core.app.Person fromAndroidPerson(android.app.Person);
     method public static androidx.core.app.Person fromBundle(android.os.Bundle);
diff --git a/core/core/src/main/java/androidx/core/app/OnNewIntentProvider.java b/core/core/src/main/java/androidx/core/app/OnNewIntentProvider.java
new file mode 100644
index 0000000..7907310
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/app/OnNewIntentProvider.java
@@ -0,0 +1,46 @@
+/*
+ * 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.core.app;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+/**
+ * Interface for components that can dispatch calls from
+ * {@link android.app.Activity#onNewIntent(Intent)}.
+ */
+public interface OnNewIntentProvider {
+    /**
+     * Add a new listener that will get a callback associated with
+     * {@link android.app.Activity#onNewIntent(Intent)} with the
+     * new {@link Intent}.
+     *
+     * @param listener The listener that should be called whenever
+     * {@link android.app.Activity#onNewIntent(Intent)} was called.
+     */
+    void addOnNewIntentListener(@NonNull Consumer<Intent> listener);
+
+    /**
+     * Remove a previously added listener. It will not receive any future callbacks.
+     *
+     * @param listener The listener previously added with
+     * {@link #addOnNewIntentListener(Consumer)} that should be removed.
+     */
+    void removeOnNewIntentListener(@NonNull Consumer<Intent> listener);
+}
diff --git a/docs/testing.md b/docs/testing.md
index d6ad752..5378606 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -137,9 +137,9 @@
 following contents:
 
 ```
-# Robolectric currently doesn't support API 31, so we have to explicitly specify 30 as the target
+# Robolectric currently doesn't support API 32, so we have to explicitly specify 30 as the target
 # sdk for now. Remove when no longer necessary.
-sdk=30
+sdk=31
 ```
 
 ## Using the emulator {#emulator}
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditTextHelperTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditTextHelperTest.java
index 9f46572..c371814 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditTextHelperTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiEditTextHelperTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.text.TextWatcher;
+import android.text.method.DigitsKeyListener;
 import android.text.method.KeyListener;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
@@ -81,6 +82,14 @@
     }
 
     @Test
+    public void testGetKeyListener_doesNotWrap_numberKeyListener() {
+        KeyListener digitsKeyListener = DigitsKeyListener.getInstance("123456");
+        KeyListener wrapped = mEmojiEditTextHelper.getKeyListener(digitsKeyListener);
+        assertSame(digitsKeyListener, wrapped);
+        assertTrue(wrapped instanceof DigitsKeyListener);
+    }
+
+    @Test
     public void testGetOnCreateInputConnection_withNullAttrs_returnsInputConnection() {
         final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(
                 mock(InputConnection.class), null);
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
index 5136889..ee94b74 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiEditTextHelper.java
@@ -17,6 +17,7 @@
 
 import android.os.Build;
 import android.text.method.KeyListener;
+import android.text.method.NumberKeyListener;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
@@ -289,6 +290,11 @@
                 // possible, and the EmojiKeyListener is not required)
                 return null;
             }
+            if (keyListener instanceof NumberKeyListener) {
+                // don't wrap NumberKeyListener as it will never allow emoji input and TextView
+                // needs the original type to do correct locale setting (b/207119921)
+                return keyListener;
+            }
             // make a KeyListener as it's always correct even if disabled
             return new EmojiKeyListener(keyListener);
         }
diff --git a/external/libyuv/build.gradle b/external/libyuv/build.gradle
index 1456863..1f6f967 100644
--- a/external/libyuv/build.gradle
+++ b/external/libyuv/build.gradle
@@ -78,4 +78,20 @@
     tasks.named("reportLibraryMetrics").configure {
         it.dependsOn("prefabReleaseConfigurePackage")
     }
+    tasks.named("prefabReleasePackage").configure {
+        def releaseAbiJson = project.file("$buildDir/intermediates/prefab_package/release/prefab/modules/yuv/libs/android.armeabi-v7a/abi.json")
+        it.doLast {
+            if (!releaseAbiJson.exists()) {
+                throw new GradleException("$releaseAbiJson does not exist")
+            }
+        }
+    }
+    tasks.named("prefabDebugPackage").configure {
+        def debugAbiJson = project.file("$buildDir/intermediates/prefab_package/debug/prefab/modules/yuv/libs/android.armeabi-v7a/abi.json")
+        it.doLast {
+            if (!debugAbiJson.exists()) {
+                throw new GradleException("$debugAbiJson does not exist")
+            }
+        }
+    }
 }
diff --git a/fragment/fragment/src/androidTest/AndroidManifest.xml b/fragment/fragment/src/androidTest/AndroidManifest.xml
index 51eaed7..e845180 100644
--- a/fragment/fragment/src/androidTest/AndroidManifest.xml
+++ b/fragment/fragment/src/androidTest/AndroidManifest.xml
@@ -35,6 +35,8 @@
 
         <activity android:name="androidx.fragment.app.test.NewIntentActivity"
                   android:launchMode="singleInstance"/>
+        <activity android:name="androidx.fragment.app.test.NewIntentProviderActivity"
+            android:launchMode="singleInstance"/>
 
         <activity android:name="androidx.fragment.app.test.NonConfigOnStopActivity"/>
         <activity android:name="androidx.fragment.app.test.HangingFragmentActivity"/>
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
index dc5514b..95b9313 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
@@ -447,6 +447,36 @@
         }
     }
 
+    /**
+     * onNewIntent() should note that the state is not saved so that child fragment
+     * managers can execute transactions in an Consumer for OnNewIntent
+     */
+    @Test
+    fun newIntentProviderUnlocks() {
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
+        val intent1 = Intent(activity, NewIntentProviderActivity::class.java)
+            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        val newIntentActivity = instrumentation.startActivitySync(intent1)
+            as NewIntentProviderActivity
+        activityRule.waitForExecution()
+
+        val intent2 = Intent(activity, FragmentTestActivity::class.java)
+        intent2.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        instrumentation.startActivitySync(intent2)
+        activityRule.waitForExecution()
+
+        val intent3 = Intent(activity, NewIntentProviderActivity::class.java)
+            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        activity.startActivity(intent3)
+        assertThat(newIntentActivity.newIntent.await(1, TimeUnit.SECONDS)).isTrue()
+        activityRule.waitForExecution()
+
+        for (fragment in newIntentActivity.supportFragmentManager.fragments) {
+            // There really should only be one fragment in newIntentActivity.
+            assertThat(fragment.childFragmentManager.fragments.size).isEqualTo(1)
+        }
+    }
+
     private fun getFragmentsUntilSize(expectedSize: Int) {
         val endTime = SystemClock.uptimeMillis() + 3000
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/NewIntentProviderActivity.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/NewIntentProviderActivity.kt
new file mode 100644
index 0000000..77e5966
--- /dev/null
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/test/NewIntentProviderActivity.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.fragment.app.test
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import java.util.concurrent.CountDownLatch
+
+class NewIntentProviderActivity : FragmentActivity() {
+    val newIntent = CountDownLatch(1)
+
+    init {
+        addOnNewIntentListener {
+            // Test a child fragment transaction -
+            supportFragmentManager
+                .findFragmentByTag("derp")!!
+                .childFragmentManager
+                .beginTransaction()
+                .add(FooFragment(), "derp4")
+                .commitNow()
+            newIntent.countDown()
+        }
+    }
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (savedInstanceState == null) {
+            supportFragmentManager
+                .beginTransaction()
+                .add(FooFragment(), "derp")
+                .commitNow()
+        }
+    }
+
+    class FooFragment : Fragment()
+}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index f21970f..bb42cd0 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -131,6 +131,9 @@
         // Ensure that the first OnConfigurationChangedListener
         // marks the FragmentManager's state as not saved
         addOnConfigurationChangedListener(newConfig -> mFragments.noteStateNotSaved());
+        // Ensure that the first OnNewIntentListener
+        // marks the FragmentManager's state as not saved
+        addOnNewIntentListener(newConfig -> mFragments.noteStateNotSaved());
         addOnContextAvailableListener(context -> mFragments.attachHost(null /*parent*/));
     }
 
@@ -353,25 +356,6 @@
     /**
      * {@inheritDoc}
      *
-     * Handle onNewIntent() to inform the fragment manager that the
-     * state is not saved.  If you are handling new intents and may be
-     * making changes to the fragment state, you want to be sure to call
-     * through to the super-class here first.  Otherwise, if your state
-     * is saved but the activity is not stopped, you could get an
-     * onNewIntent() call which happens before onResume() and trying to
-     * perform fragment operations at that point will throw IllegalStateException
-     * because the fragment manager thinks the state is still saved.
-     */
-    @Override
-    @CallSuper
-    protected void onNewIntent(@SuppressLint("UnknownNullness") Intent intent) {
-        mFragments.noteStateNotSaved();
-        super.onNewIntent(intent);
-    }
-
-    /**
-     * {@inheritDoc}
-     *
      * Hook in to note that fragment state is no longer saved.
      */
     @SuppressWarnings("deprecation")
diff --git a/inspection/inspection-testing/build.gradle b/inspection/inspection-testing/build.gradle
index b93b52d..d57636b 100644
--- a/inspection/inspection-testing/build.gradle
+++ b/inspection/inspection-testing/build.gradle
@@ -15,6 +15,7 @@
  */
 
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -38,6 +39,7 @@
 
 androidx {
     name = "AndroidX Inspection Testing"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     mavenGroup = LibraryGroups.INSPECTION
     inceptionYear = "2019"
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
index 179e97c..64e823d 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -376,6 +376,27 @@
             .isFalse()
     }
 
+    // Ensure case when matching the exact argument query (i.e. param names in braces) is handled
+    @Test
+    fun deepLinkQueryParamArgumentMatchParamsInBracesSameName() {
+        val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users?myarg={myarg}"
+        val deepLink = NavDeepLink(deepLinkArgument)
+
+        val matchArgs = deepLink.getMatchingArguments(
+            Uri.parse(deepLinkArgument.replace("{myarg}", "myarg")),
+            mapOf("myarg" to NavArgument.Builder()
+                .setType(NavType.StringType)
+                .setIsNullable(true)
+                .build())
+        )
+        assertWithMessage("Args should not be null")
+            .that(matchArgs)
+            .isNotNull()
+        assertWithMessage("Args should contain the argument and it should be null")
+            .that(matchArgs?.getString("myarg"))
+            .isEqualTo("myarg")
+    }
+
     @Test
     fun deepLinkQueryParamMultipleArgumentMatchOptionalDefault() {
         val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users?id={id}&optional={optional}"
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
index e591671..f2ddc11 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
@@ -194,7 +194,10 @@
                     }
                     val argName = storedParam.getArgumentName(index)
                     val argument = arguments[argName]
-                    if (value != null && value.replace("[{}]".toRegex(), "") != argName &&
+                    // Passing in a value the exact same as the placeholder will be treated the
+                    // as if no value was passed, being replaced if it is optional or throwing an
+                    // error if it is required.
+                    if (value != null && value != "{$argName}" &&
                         parseArgument(bundle, argName, value, argument)
                     ) {
                         return null
diff --git a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinNavWriter.kt b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinNavWriter.kt
index 57cac57..2ab4454 100644
--- a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinNavWriter.kt
+++ b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinNavWriter.kt
@@ -303,7 +303,7 @@
                     arg.type.typeName().copy(nullable = true)
                 )
                 beginControlFlow("if (%L.contains(%S))", savedStateParamName, arg.name)
-                addStatement("%L = %L[%S]", tempVal, savedStateParamName, arg.name)
+                arg.type.addSavedStateGetStatement(this, arg, tempVal, savedStateParamName)
                 if (!arg.isNullable) {
                     beginControlFlow("if (%L == null)", tempVal)
                     val errorMessage = if (arg.type.allowsNullable()) {
diff --git a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
index 67551f9..8f35ae4 100644
--- a/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
+++ b/navigation/navigation-safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/kotlin/KotlinTypes.kt
@@ -155,6 +155,47 @@
     )
 }
 
+internal fun NavType.addSavedStateGetStatement(
+    builder: FunSpec.Builder,
+    arg: Argument,
+    lValue: String,
+    savedStateHandle: String
+): FunSpec.Builder = when (this) {
+    is ObjectType -> builder.apply {
+        beginControlFlow(
+            "if (%T::class.java.isAssignableFrom(%T::class.java) " +
+                "|| %T::class.java.isAssignableFrom(%T::class.java))",
+            PARCELABLE_CLASSNAME, arg.type.typeName(),
+            SERIALIZABLE_CLASSNAME, arg.type.typeName()
+        )
+        addStatement(
+            "%L = %L.get<%T>(%S)",
+            lValue, savedStateHandle, arg.type.typeName().copy(nullable = true), arg.name
+        )
+        nextControlFlow("else")
+        addStatement(
+            "throw·%T(%T::class.java.name + %S)",
+            UnsupportedOperationException::class.asTypeName(),
+            arg.type.typeName(),
+            " must implement Parcelable or Serializable or must be an Enum."
+        )
+        endControlFlow()
+    }
+    is ObjectArrayType -> builder.apply {
+        val baseType = (arg.type.typeName() as ParameterizedTypeName).typeArguments.first()
+        addStatement(
+            "%L = %L.get<Array<%T>>(%S)?.map { it as %T }?.toTypedArray()",
+            lValue, savedStateHandle, PARCELABLE_CLASSNAME, arg.name, baseType
+        )
+    }
+    else -> builder.addStatement(
+        "%L = %L[%S]",
+        lValue,
+        savedStateHandle,
+        arg.name
+    )
+}
+
 internal fun NavType.addSavedStateSetStatement(
     builder: FunSpec.Builder,
     arg: Argument,
diff --git a/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt b/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
index 90bfdeb..b3ac929 100644
--- a/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
+++ b/navigation/navigation-safe-args-generator/src/test/test-data/expected/kotlin_nav_writer_test/MainFragmentArgs.kt
@@ -230,7 +230,8 @@
       }
       val __objectArrayArg : Array<ActivityInfo>?
       if (savedStateHandle.contains("objectArrayArg")) {
-        __objectArrayArg = savedStateHandle["objectArrayArg"]
+        __objectArrayArg = savedStateHandle.get<Array<Parcelable>>("objectArrayArg")?.map { it as
+            ActivityInfo }?.toTypedArray()
         if (__objectArrayArg == null) {
           throw IllegalArgumentException("Argument \"objectArrayArg\" is marked as non-null but was passed a null value")
         }
@@ -248,13 +249,25 @@
       }
       val __optionalParcelable : ActivityInfo?
       if (savedStateHandle.contains("optionalParcelable")) {
-        __optionalParcelable = savedStateHandle["optionalParcelable"]
+        if (Parcelable::class.java.isAssignableFrom(ActivityInfo::class.java) ||
+            Serializable::class.java.isAssignableFrom(ActivityInfo::class.java)) {
+          __optionalParcelable = savedStateHandle.get<ActivityInfo?>("optionalParcelable")
+        } else {
+          throw UnsupportedOperationException(ActivityInfo::class.java.name +
+              " must implement Parcelable or Serializable or must be an Enum.")
+        }
       } else {
         __optionalParcelable = null
       }
       val __enumArg : AccessMode?
       if (savedStateHandle.contains("enumArg")) {
-        __enumArg = savedStateHandle["enumArg"]
+        if (Parcelable::class.java.isAssignableFrom(AccessMode::class.java) ||
+            Serializable::class.java.isAssignableFrom(AccessMode::class.java)) {
+          __enumArg = savedStateHandle.get<AccessMode?>("enumArg")
+        } else {
+          throw UnsupportedOperationException(AccessMode::class.java.name +
+              " must implement Parcelable or Serializable or must be an Enum.")
+        }
         if (__enumArg == null) {
           throw IllegalArgumentException("Argument \"enumArg\" is marked as non-null but was passed a null value")
         }
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
index b4f9ea7..34e06f5 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -582,7 +582,7 @@
 
 /**
  * Helper for [GenerationalViewportHint] prioritization in cases where item accesses are being sent
- * to PageFetcherSnapshot] faster than they can be processed. A [GenerationalViewportHint] is
+ * to [PageFetcherSnapshot] faster than they can be processed. A [GenerationalViewportHint] is
  * prioritized if it represents an update to presenter state or if it would cause
  * [PageFetcherSnapshot] to load more items.
  *
diff --git a/security/security-app-authenticator-testing/build.gradle b/security/security-app-authenticator-testing/build.gradle
index 4bee868..7d1d4da 100644
--- a/security/security-app-authenticator-testing/build.gradle
+++ b/security/security-app-authenticator-testing/build.gradle
@@ -40,7 +40,7 @@
 
 androidx {
     name = "Android Security App Package Authenticator Testing"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenVersion = LibraryVersions.SECURITY_APP_AUTHENTICATOR_TESTING
     mavenGroup = LibraryGroups.SECURITY
     inceptionYear = "2021"
diff --git a/security/security-app-authenticator-testing/src/main/java/androidx/security/app/authenticator/TestAppAuthenticatorBuilder.java b/security/security-app-authenticator-testing/src/main/java/androidx/security/app/authenticator/TestAppAuthenticatorBuilder.java
index 8460b83..372a166 100644
--- a/security/security-app-authenticator-testing/src/main/java/androidx/security/app/authenticator/TestAppAuthenticatorBuilder.java
+++ b/security/security-app-authenticator-testing/src/main/java/androidx/security/app/authenticator/TestAppAuthenticatorBuilder.java
@@ -312,7 +312,7 @@
      */
     // This class is provided so that apps can inject a configurable AppAuthenticator for their
     // tests, so it needs access to the restricted test APIs.
-    @SuppressLint({"RestrictedApi", "VisibleForTests"})
+    @SuppressLint("RestrictedApi")
     @NonNull
     public AppAuthenticator build() throws AppAuthenticatorXmlException, IOException {
         // Obtain the config from the AppAuthenticator class to ensure that the provided XML is
diff --git a/slice/slice-test/build.gradle b/slice/slice-test/build.gradle
index 901074e..582583a 100644
--- a/slice/slice-test/build.gradle
+++ b/slice/slice-test/build.gradle
@@ -16,6 +16,7 @@
 
 import androidx.build.LibraryVersions
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 import androidx.build.Publish
 
 plugins {
@@ -40,6 +41,7 @@
 
 androidx {
     name = "Slice test code"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     publish = Publish.NONE
     mavenVersion = LibraryVersions.SLICE
     mavenGroup = LibraryGroups.SLICE
diff --git a/test/screenshot/screenshot/build.gradle b/test/screenshot/screenshot/build.gradle
index 08a239a..3d10daa 100644
--- a/test/screenshot/screenshot/build.gradle
+++ b/test/screenshot/screenshot/build.gradle
@@ -15,6 +15,7 @@
  */
 
 import androidx.build.LibraryGroups
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -40,5 +41,6 @@
 
 androidx {
     name = "AndroidX Library Screenshot Test"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
     mavenGroup = LibraryGroups.TESTSCREENSHOT
 }
diff --git a/testutils/testutils-appcompat/build.gradle b/testutils/testutils-appcompat/build.gradle
index ee9a25e..832c172 100644
--- a/testutils/testutils-appcompat/build.gradle
+++ b/testutils/testutils-appcompat/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -36,3 +38,7 @@
         disable "InvalidPackage" // Lint is unhappy about junit package
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-common/build.gradle b/testutils/testutils-common/build.gradle
index dccc2a9..19480c5 100644
--- a/testutils/testutils-common/build.gradle
+++ b/testutils/testutils-common/build.gradle
@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("kotlin")
@@ -32,3 +34,7 @@
         freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-espresso/build.gradle b/testutils/testutils-espresso/build.gradle
index 8ac3572..db5fcf70 100644
--- a/testutils/testutils-espresso/build.gradle
+++ b/testutils/testutils-espresso/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -33,3 +35,7 @@
         disable "InvalidPackage" // Lint is unhappy about junit package
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-gradle-plugin/build.gradle b/testutils/testutils-gradle-plugin/build.gradle
index 127a4bf..e8d7ab2 100644
--- a/testutils/testutils-gradle-plugin/build.gradle
+++ b/testutils/testutils-gradle-plugin/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("kotlin")
@@ -25,3 +27,7 @@
     implementation(libs.testCore)
     implementation(libs.testRules)
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-ktx/build.gradle b/testutils/testutils-ktx/build.gradle
index eda09a6..1998abd 100644
--- a/testutils/testutils-ktx/build.gradle
+++ b/testutils/testutils-ktx/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -34,3 +35,7 @@
         freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-macrobenchmark/build.gradle b/testutils/testutils-macrobenchmark/build.gradle
index f698e9a..a239cb8 100644
--- a/testutils/testutils-macrobenchmark/build.gradle
+++ b/testutils/testutils-macrobenchmark/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 @@
         minSdkVersion 23
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-mockito/build.gradle b/testutils/testutils-mockito/build.gradle
index 40d784c..36831bb 100644
--- a/testutils/testutils-mockito/build.gradle
+++ b/testutils/testutils-mockito/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -30,4 +32,8 @@
     lintOptions {
         disable "InvalidPackage" // Lint is unhappy about java.lang.instrument package
     }
-}
\ No newline at end of file
+}
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-navigation/build.gradle b/testutils/testutils-navigation/build.gradle
index 4123407..78d5517 100644
--- a/testutils/testutils-navigation/build.gradle
+++ b/testutils/testutils-navigation/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
@@ -35,3 +37,7 @@
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation(libs.truth)
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-paging/build.gradle b/testutils/testutils-paging/build.gradle
index 7287ed7..3be7e84 100644
--- a/testutils/testutils-paging/build.gradle
+++ b/testutils/testutils-paging/build.gradle
@@ -15,6 +15,7 @@
  */
 
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
@@ -34,3 +35,7 @@
         freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-runtime/build.gradle b/testutils/testutils-runtime/build.gradle
index 795f410..310a3ab 100644
--- a/testutils/testutils-runtime/build.gradle
+++ b/testutils/testutils-runtime/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 @@
         testInstrumentationRunner "androidx.testutils.ActivityRecyclingAndroidJUnitRunner"
     }
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/testutils/testutils-truth/build.gradle b/testutils/testutils-truth/build.gradle
index 2c984ec..1e46a9e 100644
--- a/testutils/testutils-truth/build.gradle
+++ b/testutils/testutils-truth/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+import androidx.build.LibraryType
+
 plugins {
     id("AndroidXPlugin")
     id("kotlin")
@@ -23,3 +25,7 @@
     api(libs.truth)
     api(libs.kotlinStdlib)
 }
+
+androidx {
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+}
diff --git a/wear/tiles/tiles-testing/build.gradle b/wear/tiles/tiles-testing/build.gradle
index 4c78089..210a2ca 100644
--- a/wear/tiles/tiles-testing/build.gradle
+++ b/wear/tiles/tiles-testing/build.gradle
@@ -61,7 +61,7 @@
 
 androidx {
     name = "Android Wear Tiles Testing Utilities"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.WEAR_TILES
     inceptionYear = "2021"
     description = "Testing utilities for Android Wear Tiles."
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
index c98549d..4af7b10 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
@@ -65,13 +65,13 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
             if (mUriIdlingResource != null) {
-                mUriIdlingResource.beginLoad(request);
+                mUriIdlingResource.beginLoad(url);
             }
-            WebResourceResponse response = mAssetLoader.shouldInterceptRequest(Uri.parse(request));
+            WebResourceResponse response = mAssetLoader.shouldInterceptRequest(Uri.parse(url));
             if (mUriIdlingResource != null) {
-                mUriIdlingResource.endLoad(request);
+                mUriIdlingResource.endLoad(url);
             }
             return response;
         }
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderInternalStorageActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderInternalStorageActivity.java
index 35f53be..0a823feb 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderInternalStorageActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderInternalStorageActivity.java
@@ -66,8 +66,8 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
-            return mAssetLoader.shouldInterceptRequest(Uri.parse(request));
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+            return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
         }
     }
 
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderSimpleActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderSimpleActivity.java
index 78c8e2b..0af0721 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderSimpleActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderSimpleActivity.java
@@ -51,8 +51,8 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
-            return mAssetLoader.shouldInterceptRequest(Uri.parse(request));
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+            return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
         }
     }
 
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
index df27a0e..068b76d 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DocumentStartJavaScriptActivity.java
@@ -70,8 +70,8 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
-            return mAssetLoader.shouldInterceptRequest(Uri.parse(request));
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+            return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
         }
     }
 
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerActivity.java
index d6ca419..553a605 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerActivity.java
@@ -80,8 +80,8 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
-            return mAssetLoader.shouldInterceptRequest(Uri.parse(request));
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+            return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
         }
     }
 
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerMaliciousWebsiteActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerMaliciousWebsiteActivity.java
index 8c9450e..a02603b 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerMaliciousWebsiteActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/WebMessageListenerMaliciousWebsiteActivity.java
@@ -76,9 +76,9 @@
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
-        public WebResourceResponse shouldInterceptRequest(WebView view, String request) {
+        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
             for (WebViewAssetLoader loader : mAssetLoaders) {
-                WebResourceResponse response = loader.shouldInterceptRequest(Uri.parse(request));
+                WebResourceResponse response = loader.shouldInterceptRequest(Uri.parse(url));
                 if (response != null) {
                     return response;
                 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
index 87047f7..d9a60d6 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewAssetLoader.java
@@ -73,7 +73,7 @@
  *     {@literal @}Override
  *     {@literal @}SuppressWarnings("deprecation") // for API < 21
  *     public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
- *         return assetLoader.shouldInterceptRequest(Uri.parse(request));
+ *         return assetLoader.shouldInterceptRequest(Uri.parse(url));
  *     }
  * });
  *
diff --git a/window/window-testing/build.gradle b/window/window-testing/build.gradle
index e9ad9b4..bedbf25 100644
--- a/window/window-testing/build.gradle
+++ b/window/window-testing/build.gradle
@@ -48,7 +48,7 @@
 
 androidx {
     name = "WindowManager Test Library"
-    type = LibraryType.PUBLISHED_LIBRARY
+    type = LibraryType.PUBLISHED_TEST_LIBRARY
     mavenGroup = LibraryGroups.WINDOW
     inceptionYear = "2021"
     description = "WindowManager Test Library"