Merge "Reorder methods in API files based on metalava update" into androidx-main
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index db2e46a..cb49084 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -25,6 +25,12 @@
           gcp-token: ${{ secrets.GCP_SA_KEY }}
           github-token: ${{ secrets.GITHUB_TOKEN }}
           output-folder: ${{ steps.dirs.outputs.output-dir }}
+          gcp-bucket-name: "androidx-ftl-test-results"
+          gcp-bucket-path: "github-ci-action"
+          log-file: ${{ steps.dirs.outputs.output-dir }}/ftl-logs.txt
+          device-specs: Pixel2.arm:30
+          use-test-config-files: true
+          test-suite-tags: androidx_unit_tests
       - uses: actions/upload-artifact@v2
         if: always()
         with:
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ReportDrawnTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ReportDrawnTest.kt
index 75be5f1..db1fee8 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ReportDrawnTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/ReportDrawnTest.kt
@@ -265,4 +265,15 @@
         }
         assertThat(localValue).isSameInstanceAs(fullyDrawnReporterOwner)
     }
+
+    @Test
+    fun testDisposingBeforeReporting() {
+        rule.setContent {
+            // Reporting never finishes
+            ReportDrawnWhen { false }
+            // Report that finishes immediatelly
+            ReportDrawn()
+        }
+        // By going out of the scope, both reporters call onDismiss
+    }
 }
diff --git a/activity/activity/src/main/java/androidx/activity/FullyDrawnReporter.kt b/activity/activity/src/main/java/androidx/activity/FullyDrawnReporter.kt
index 8c57628..dd87c18 100644
--- a/activity/activity/src/main/java/androidx/activity/FullyDrawnReporter.kt
+++ b/activity/activity/src/main/java/androidx/activity/FullyDrawnReporter.kt
@@ -103,10 +103,7 @@
      */
     fun removeReporter() {
         synchronized(lock) {
-            if (!reportedFullyDrawn) {
-                check(reporterCount > 0) {
-                    "removeReporter() called when all reporters have already been removed."
-                }
+            if (!reportedFullyDrawn && reporterCount > 0) {
                 reporterCount--
                 postWhenReportersAreDone()
             }
@@ -189,4 +186,4 @@
     } finally {
         removeReporter()
     }
-}
\ No newline at end of file
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.kt
similarity index 81%
rename from appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java
rename to appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.kt
index 6d2ca04..e807783 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FutureCallback.kt
@@ -13,20 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package androidx.appactions.interaction.capabilities.core.impl.concurrent;
-
-import androidx.annotation.NonNull;
+package androidx.appactions.interaction.capabilities.core.impl.concurrent
 
 /**
  * A FutureCallback that can be attached to a ListenableFuture with Futures#addCallback.
- *
- * @param <V>
  */
-public interface FutureCallback<V> {
-    /** Called with the ListenableFuture's result if it completes successfully. */
-    void onSuccess(V result);
+interface FutureCallback<V> {
+    /** Called with the ListenableFuture's result if it completes successfully.  */
+    fun onSuccess(result: V)
 
-    /** Called with the ListenableFuture's exception if it fails. */
-    void onFailure(@NonNull Throwable t);
-}
+    /** Called with the ListenableFuture's exception if it fails.  */
+    fun onFailure(t: Throwable)
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java
deleted file mode 100644
index 1486f0b..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.impl.concurrent;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
-import java.util.function.Function;
-
-/** Future/ListenableFuture related utility methods. */
-public final class Futures {
-    private Futures() {
-    }
-
-    /** Attach a FutureCallback to a ListenableFuture instance. */
-    public static <V> void addCallback(
-            @NonNull final ListenableFuture<V> future,
-            @NonNull final FutureCallback<? super V> callback,
-            @NonNull Executor executor) {
-        Utils.checkNotNull(callback);
-        future.addListener(new CallbackListener<>(future, callback), executor);
-    }
-
-    /**
-     * Transforms an input ListenableFuture into a second ListenableFuture by applying a
-     * transforming
-     * function to the result of the input ListenableFuture.
-     */
-    @NonNull
-    @SuppressLint("LambdaLast")
-    public static <I, O> ListenableFuture<O> transform(
-            @NonNull ListenableFuture<I> input,
-            @NonNull Function<I, O> function,
-            @NonNull Executor executor,
-            @Nullable String tag) {
-        return CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    addCallback(input, transformFutureCallback(completer, function), executor);
-                    return tag;
-                });
-    }
-
-    /**
-     * Transforms an input ListenableFuture into a second ListenableFuture by applying an
-     * asynchronous
-     * transforming function to the result of the input ListenableFuture.
-     */
-    @NonNull
-    @SuppressLint("LambdaLast")
-    public static <I, O> ListenableFuture<O> transformAsync(
-            @NonNull ListenableFuture<I> input,
-            @NonNull Function<I, ListenableFuture<O>> asyncFunction,
-            @NonNull Executor executor,
-            @NonNull String tag) {
-
-        return CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    addCallback(input, asyncTransformFutureCallback(completer, asyncFunction),
-                            executor);
-                    return tag;
-                });
-    }
-
-    /** Returns a Future that is immediately complete with the given value. */
-    @NonNull
-    public static <V> ListenableFuture<V> immediateFuture(V value) {
-        return CallbackToFutureAdapter.getFuture(completer -> completer.set(value));
-    }
-
-    /** Returns a Future that is immediately complete with null value. */
-    @NonNull
-    public static ListenableFuture<Void> immediateVoidFuture() {
-        return CallbackToFutureAdapter.getFuture(completer -> completer.set(null));
-    }
-
-    /** Returns a Future that is immediately complete with an exception. */
-    @NonNull
-    public static <V> ListenableFuture<V> immediateFailedFuture(@NonNull Throwable throwable) {
-        return CallbackToFutureAdapter.getFuture(completer -> completer.setException(throwable));
-    }
-
-    /**
-     * Returns a FutureCallback that transform the result in onSuccess, and then set the result in
-     * completer.
-     */
-    static <I, O> FutureCallback<I> transformFutureCallback(
-            Completer<O> completer, Function<I, O> function) {
-        return new FutureCallback<I>() {
-            @Override
-            public void onSuccess(I result) {
-                try {
-                    completer.set(function.apply(result));
-                } catch (Throwable t) {
-                    if (t instanceof InterruptedException) {
-                        Thread.currentThread().interrupt();
-                    }
-                    completer.setException(t);
-                }
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable failure) {
-                completer.setException(failure);
-            }
-        };
-    }
-
-    /** Returns a FutureCallback that asynchronously transform the result. */
-    private static <I, O> FutureCallback<I> asyncTransformFutureCallback(
-            Completer<O> completer, Function<I, ListenableFuture<O>> asyncFunction) {
-        return new FutureCallback<I>() {
-            @Override
-            public void onSuccess(I inputResult) {
-                try {
-                    addCallback(
-                            asyncFunction.apply(inputResult),
-                            transformFutureCallback(completer, Function.identity()),
-                            Runnable::run);
-                } catch (Throwable t) {
-                    if (t instanceof InterruptedException) {
-                        Thread.currentThread().interrupt();
-                    }
-                    completer.setException(t);
-                }
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable failure) {
-                completer.setException(failure);
-            }
-        };
-    }
-
-    static <V> V getDone(Future<V> future) throws ExecutionException {
-        Utils.checkState(future.isDone(), "future is expected to be done already.");
-        boolean interrupted = false;
-        try {
-            while (true) {
-                try {
-                    return future.get();
-                } catch (InterruptedException e) {
-                    interrupted = true;
-                }
-            }
-        } finally {
-            if (interrupted) {
-                Thread.currentThread().interrupt();
-            }
-        }
-    }
-
-    private static final class CallbackListener<V> implements Runnable {
-        final Future<V> mFuture;
-        final FutureCallback<? super V> mCallback;
-
-        CallbackListener(Future<V> future, FutureCallback<? super V> callback) {
-            this.mFuture = future;
-            this.mCallback = callback;
-        }
-
-        @Override
-        public void run() {
-            final V value;
-            try {
-                value = getDone(mFuture);
-            } catch (ExecutionException e) {
-                Throwable cause = e.getCause();
-                mCallback.onFailure(cause != null ? cause : e);
-                return;
-            } catch (RuntimeException | Error e) {
-                mCallback.onFailure(e);
-                return;
-            }
-            mCallback.onSuccess(value);
-        }
-    }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.kt
new file mode 100644
index 0000000..0005982
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Futures.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.appactions.interaction.capabilities.core.impl.concurrent
+
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executor
+import java.util.concurrent.Future
+import java.util.function.Function
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+/** Future/ListenableFuture related utility methods.  */
+object Futures {
+    /** Attach a FutureCallback to a ListenableFuture instance.  */
+    fun <V> addCallback(
+        future: ListenableFuture<V>,
+        callback: FutureCallback<in V>,
+        executor: Executor
+    ) {
+        future.addListener(CallbackListener(future, callback), executor)
+    }
+
+    /**
+     * Transforms an input ListenableFuture into a second ListenableFuture by applying a
+     * transforming
+     * function to the result of the input ListenableFuture.
+     */
+    fun <I, O> transform(
+        input: ListenableFuture<I>,
+        function: Function<I, O>,
+        executor: Executor,
+        tag: String?
+    ): ListenableFuture<O> {
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<O> ->
+            addCallback(input, transformFutureCallback(completer, function), executor)
+            tag
+        }
+    }
+
+    /**
+     * Transforms an input ListenableFuture into a second ListenableFuture by applying an
+     * asynchronous
+     * transforming function to the result of the input ListenableFuture.
+     */
+    fun <I, O> transformAsync(
+        input: ListenableFuture<I>,
+        asyncFunction: Function<I, ListenableFuture<O>>,
+        executor: Executor,
+        tag: String
+    ): ListenableFuture<O> {
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<O> ->
+            addCallback(
+                input, asyncTransformFutureCallback(completer, asyncFunction),
+                executor
+            )
+            tag
+        }
+    }
+
+    /** Returns a Future that is immediately complete with the given value.  */
+    fun <V> immediateFuture(value: V): ListenableFuture<V> {
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<V> ->
+            completer.set(
+                value
+            )
+        }
+    }
+
+    /** Returns a Future that is immediately complete with null value.  */
+    fun immediateVoidFuture(): ListenableFuture<Void> {
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<Void> ->
+            completer.set(
+                null
+            )
+        }
+    }
+
+    /** Returns a Future that is immediately complete with an exception.  */
+    fun <V> immediateFailedFuture(throwable: Throwable): ListenableFuture<V> {
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<V> ->
+            completer.setException(
+                throwable
+            )
+        }
+    }
+
+    /**
+     * Returns a FutureCallback that transform the result in onSuccess, and then set the result in
+     * completer.
+     */
+    fun <I, O> transformFutureCallback(
+        completer: CallbackToFutureAdapter.Completer<O>,
+        function: Function<I, O>
+    ): FutureCallback<I> {
+        return object : FutureCallback<I> {
+            override fun onSuccess(result: I) {
+                try {
+                    completer.set(function.apply(result))
+                } catch (t: Throwable) {
+                    if (t is InterruptedException) {
+                        Thread.currentThread().interrupt()
+                    }
+                    completer.setException(t)
+                }
+            }
+
+            override fun onFailure(t: Throwable) {
+                completer.setException(t)
+            }
+        }
+    }
+
+    /** Returns a FutureCallback that asynchronously transform the result.  */
+    private fun <I, O> asyncTransformFutureCallback(
+        completer: CallbackToFutureAdapter.Completer<O>,
+        asyncFunction: Function<I, ListenableFuture<O>>
+    ): FutureCallback<I> {
+        return object : FutureCallback<I> {
+            override fun onSuccess(result: I) {
+                try {
+                    addCallback(
+                        asyncFunction.apply(result),
+                        transformFutureCallback(completer, Function.identity())
+                    ) { obj: Runnable -> obj.run() }
+                } catch (t: Throwable) {
+                    if (t is InterruptedException) {
+                        Thread.currentThread().interrupt()
+                    }
+                    completer.setException(t)
+                }
+            }
+
+            override fun onFailure(t: Throwable) {
+                completer.setException(t)
+            }
+        }
+    }
+
+    @Throws(ExecutionException::class)
+    fun <V> getDone(future: Future<V>): V {
+        if (!future.isDone) {
+            throw IllegalStateException("future is expected to be done already.")
+        }
+        var interrupted = false
+        try {
+            while (true) {
+                interrupted = try {
+                    return future.get()
+                } catch (e: InterruptedException) {
+                    true
+                }
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt()
+            }
+        }
+    }
+
+    private class CallbackListener<V> internal constructor(
+        val mFuture: Future<V>,
+        val mCallback: FutureCallback<in V>
+    ) : Runnable {
+        override fun run() {
+            val value: V
+            value = try {
+                getDone(mFuture)
+            } catch (e: ExecutionException) {
+                val cause = e.cause
+                mCallback.onFailure(cause ?: e)
+                return
+            } catch (e: RuntimeException) {
+                mCallback.onFailure(e)
+                return
+            } catch (e: Error) {
+                mCallback.onFailure(e)
+                return
+            }
+            mCallback.onSuccess(value)
+        }
+    }
+}
+
+fun <T> convertToListenableFuture(
+    tag: String,
+    block: suspend CoroutineScope.() -> T,
+): ListenableFuture<T> {
+    return CallbackToFutureAdapter.getFuture { completer ->
+        val job = CoroutineScope(Dispatchers.Unconfined).launch {
+                try {
+                    completer.set(block())
+                } catch (t: Throwable) {
+                    completer.setException(t)
+                }
+            }
+        completer.addCancellationListener(
+            { job.cancel() },
+            Runnable::run,
+        )
+        "ListenableFutureHelper#convertToListenableFuture for '$tag'"
+    }
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt
deleted file mode 100644
index b640f22..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelper.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package androidx.appactions.interaction.capabilities.core.impl.concurrent
-
-import androidx.annotation.RestrictTo
-import androidx.concurrent.futures.CallbackToFutureAdapter
-import com.google.common.util.concurrent.ListenableFuture
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-// TODO(b/269525385): merge this into Futures utility class once it's migrated to Kotlin.
-/** @suppress */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun <T> convertToListenableFuture(
-    tag: String,
-    block: suspend CoroutineScope.() -> T,
-): ListenableFuture<T> {
-    return CallbackToFutureAdapter.getFuture { completer ->
-        val job = CoroutineScope(Dispatchers.Unconfined).launch {
-                try {
-                    completer.set(block())
-                } catch (t: Throwable) {
-                    completer.setException(t)
-                }
-            }
-        completer.addCancellationListener(
-            { job.cancel() },
-            Runnable::run,
-        )
-        "ListenableFutureHelper#convertToListenableFuture for '$tag'"
-    }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java
deleted file mode 100644
index aeb0e9b..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/Utils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.impl.concurrent;
-
-import androidx.annotation.Nullable;
-
-final class Utils {
-
-    private Utils() {
-    }
-
-    public static <T> T checkNotNull(@Nullable T reference) {
-        if (reference == null) {
-            throw new NullPointerException();
-        }
-        return reference;
-    }
-
-    public static <T> T checkNotNull(@Nullable T reference, String errorMessage) {
-        if (reference == null) {
-            throw new NullPointerException(errorMessage);
-        }
-        return reference;
-    }
-
-    public static void checkState(boolean b, String message) {
-        if (!b) {
-            throw new IllegalStateException(message);
-        }
-    }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/package-info.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/package-info.java
index ea6e4cc..8d0f6b6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/package-info.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/package-info.java
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-/** @hide */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 package androidx.appactions.interaction.capabilities.core.impl;
 
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ConvertToListenableFutureTest.kt
similarity index 98%
rename from appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt
rename to appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ConvertToListenableFutureTest.kt
index 9b7b0b91..2422b97 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ListenableFutureHelperTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/ConvertToListenableFutureTest.kt
@@ -29,7 +29,7 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class ListenableFutureHelperTest {
+class ConvertToListenableFutureTest {
     val TAG = "tag"
 
     @Test
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.kt
index d0c26db..c7140ba 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/concurrent/FuturesTest.kt
@@ -173,7 +173,7 @@
         Futures.addCallback(
             transformedFuture,
             object : FutureCallback<Int> {
-                override fun onSuccess(value: Int) {}
+                override fun onSuccess(result: Int) {}
 
                 override fun onFailure(t: Throwable) {
                     errorDeferred.complete(t)
@@ -203,7 +203,7 @@
         Futures.addCallback(
             transformedFuture,
             object : FutureCallback<Int> {
-                override fun onSuccess(value: Int) {}
+                override fun onSuccess(result: Int) {}
 
                 override fun onFailure(t: Throwable) {
                     errorDeferred.complete(t)
diff --git a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionServiceGrpcImpl.kt b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionServiceGrpcImpl.kt
index d1ac8d1..e3a60b6 100644
--- a/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionServiceGrpcImpl.kt
+++ b/appactions/interaction/interaction-service/src/main/java/androidx/appactions/interaction/service/AppInteractionServiceGrpcImpl.kt
@@ -238,9 +238,9 @@
         Futures.addCallback(
             executeFulfillmentRequest(currentSession, selectedFulfillment),
             object : FutureCallback<FulfillmentResponse> {
-                override fun onSuccess(fulfillmentResponse: FulfillmentResponse) {
+                override fun onSuccess(result: FulfillmentResponse) {
                     val responseBuilder =
-                        convertFulfillmentResponse(fulfillmentResponse, capability)
+                        convertFulfillmentResponse(result, capability)
                             .toBuilder()
                     val uiCache = UiSessions.getUiCacheOrNull(sessionId)
                     if (uiCache != null && uiCache.hasUnreadUiResponse) {
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 22296ee..7eab234 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -9,7 +9,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.3.0")
-    api("androidx.core:core:1.9.0")
+    api(project(":core:core"))
 
     // Required to make activity 1.5.0-rc01 dependencies resolve.
     implementation("androidx.core:core-ktx:1.8.0")
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
index fa0c892..c00c10b 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesPersistTestCase.kt
@@ -18,16 +18,20 @@
 
 package androidx.appcompat.app
 
+import android.content.Context
 import android.content.Intent
 import androidx.appcompat.testutils.LocalesActivityTestRule
 import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
 import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWait
 import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
+import androidx.core.app.LocaleManagerCompat
 import androidx.core.os.LocaleListCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
+import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertNull
 import org.junit.After
 import org.junit.Before
@@ -117,6 +121,16 @@
         assertConfigurationLocalesEquals(systemLocales, secondActivity)
     }
 
+    @Test
+    fun testGetAppLocalesFromNonActivityContext() {
+        AppCompatDelegate.setIsAutoStoreLocalesOptedIn(true)
+
+        setLocalesAndWait(rule, CUSTOM_LOCALE_LIST)
+
+        val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals(CUSTOM_LOCALE_LIST, LocaleManagerCompat.getApplicationLocales(appContext))
+    }
+
     @After
     fun teardown() {
         rule.runOnUiThread {
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
index 536e283..da2a2b3 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesSyncToFrameworkTestCase.kt
@@ -26,6 +26,7 @@
 import androidx.appcompat.testutils.LocalesActivityTestRule
 import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
 import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
+import androidx.core.app.AppLocalesStorageHelper
 import androidx.core.os.LocaleListCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -72,7 +73,7 @@
 
         appLocalesComponent = ComponentName(
             instrumentation.context,
-            AppLocalesStorageHelper.APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME
+            AppCompatDelegate.APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME
         )
     }
 
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index ec995d7..df731da1 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -16,16 +16,17 @@
 
 package androidx.appcompat.app;
 
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-import static androidx.appcompat.app.AppLocalesStorageHelper.persistLocales;
-import static androidx.appcompat.app.AppLocalesStorageHelper.readLocales;
-import static androidx.appcompat.app.AppLocalesStorageHelper.syncLocalesToFramework;
 
 import static java.util.Objects.requireNonNull;
 
 import android.app.Activity;
 import android.app.Dialog;
 import android.app.LocaleManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
@@ -57,6 +58,7 @@
 import androidx.appcompat.widget.Toolbar;
 import androidx.appcompat.widget.VectorEnabledTintResources;
 import androidx.collection.ArraySet;
+import androidx.core.app.AppLocalesStorageHelper;
 import androidx.core.os.LocaleListCompat;
 import androidx.core.view.WindowCompat;
 import androidx.fragment.app.FragmentActivity;
@@ -64,7 +66,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
 import java.util.Iterator;
+import java.util.Queue;
+import java.util.concurrent.Executor;
 
 /**
  * This class represents a delegate which you can use to extend AppCompat's support to any
@@ -110,9 +115,62 @@
     static final boolean DEBUG = false;
     static final String TAG = "AppCompatDelegate";
 
-    static AppLocalesStorageHelper.SerialExecutor sSerialExecutorForLocalesStorage = new
-            AppLocalesStorageHelper.SerialExecutor(
-                    new AppLocalesStorageHelper.ThreadPerTaskExecutor());
+    static SerialExecutor sSerialExecutorForLocalesStorage = new
+            SerialExecutor(new ThreadPerTaskExecutor());
+
+    static final String APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME = "androidx.appcompat.app"
+            + ".AppLocalesMetadataHolderService";
+
+    /**
+     * Implementation of {@link java.util.concurrent.Executor} that executes runnables serially
+     * by synchronizing the {@link Executor#execute(Runnable)} method and maintaining a tasks
+     * queue.
+     */
+    static class SerialExecutor implements Executor {
+        private final Object mLock = new Object();
+        final Queue<Runnable> mTasks = new ArrayDeque<>();
+        final Executor mExecutor;
+        Runnable mActive;
+
+        SerialExecutor(Executor executor) {
+            this.mExecutor = executor;
+        }
+
+        @Override
+        public void execute(final Runnable r) {
+            synchronized (mLock) {
+                mTasks.add(() -> {
+                    try {
+                        r.run();
+                    } finally {
+                        scheduleNext();
+                    }
+                });
+                if (mActive == null) {
+                    scheduleNext();
+                }
+            }
+        }
+
+        protected void scheduleNext() {
+            synchronized (mLock) {
+                if ((mActive = mTasks.poll()) != null) {
+                    mExecutor.execute(mActive);
+                }
+            }
+        }
+    }
+
+    /**
+     * Implementation of {@link java.util.concurrent.Executor} that executes each runnable on a
+     * new thread.
+     */
+    static class ThreadPerTaskExecutor implements Executor {
+        @Override
+        public void execute(Runnable r) {
+            new Thread(r).start();
+        }
+    }
 
     /**
      * Mode which uses the system's night mode setting to determine if it is night or not.
@@ -714,6 +772,12 @@
      *     this transition on their end.</li>
      * </ul>
      *
+     * <p><b>Note: This API work with the AppCompatActivity context, not for others context, for
+     * Android 12 (API level 32) and earlier. If there is a requirement to get the localized
+     * string which respects the per-app locale in non-AppCompatActivity context, please consider
+     * using {@link androidx.core.content.ContextCompat#getString(Context, int)} or
+     * {@link androidx.core.content.ContextCompat#getContextForLanguage(Context)}. </b></p>
+     *
      * @param locales a list of locales.
      */
     public static void setApplicationLocales(@NonNull LocaleListCompat locales) {
@@ -749,7 +813,8 @@
      * <p>Returns a {@link LocaleListCompat#getEmptyLocaleList()} if no app-specific locales are
      * set.
      *
-     * <p><b>Note: This API should always be called after Activity.onCreate().</b></p>
+     * <p><b>Note: This API only work at AppCompatDelegate and it should always be called after
+     * Activity.onCreate().</b></p>
      */
     @AnyThread
     @NonNull
@@ -913,7 +978,8 @@
                 if (sRequestedAppLocales == null) {
                     if (sStoredAppLocales == null) {
                         sStoredAppLocales =
-                                LocaleListCompat.forLanguageTags(readLocales(context));
+                                LocaleListCompat.forLanguageTags(
+                                        AppLocalesStorageHelper.readLocales(context));
                     }
                     if (sStoredAppLocales.isEmpty()) {
                         // if both requestedLocales and storedLocales not set, then the user has not
@@ -926,7 +992,8 @@
                     // if requestedLocales is set and is not equal to the storedLocales then in this
                     // case we need to store these locales in storage.
                     sStoredAppLocales = sRequestedAppLocales;
-                    persistLocales(context, sRequestedAppLocales.toLanguageTags());
+                    AppLocalesStorageHelper.persistLocales(context,
+                            sRequestedAppLocales.toLanguageTags());
                 }
             }
         }
@@ -996,6 +1063,53 @@
         }
     }
 
+    /**
+     * Syncs app-specific locales from androidX to framework. This is used to maintain a smooth
+     * transition for a device that updates from pre-T API versions to T.
+     *
+     * <p><b>NOTE:</b> This should only be called when auto-storage is opted-in. This method
+     * uses the meta-data service provided during the opt-in and hence if the service is not found
+     * this method will throw an error.</p>
+     */
+    static void syncLocalesToFramework(Context context) {
+        if (Build.VERSION.SDK_INT >= 33) {
+            ComponentName app_locales_component = new ComponentName(
+                    context, APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME);
+
+            if (context.getPackageManager().getComponentEnabledSetting(app_locales_component)
+                    != COMPONENT_ENABLED_STATE_ENABLED) {
+                // ComponentEnabledSetting for the app component app_locales_component is used as a
+                // marker to represent that the locales has been synced from AndroidX to framework
+                // If this marker is found in ENABLED state then we do not need to sync again.
+                if (AppCompatDelegate.getApplicationLocales().isEmpty()) {
+                    // We check if some locales are applied by the framework or not (this is done to
+                    // ensure that we don't overwrite newer locales set by the framework). If no
+                    // app-locales are found then we need to sync the app-specific locales from
+                    // androidX to framework.
+
+                    String appLocales = AppLocalesStorageHelper.readLocales(context);
+                    // if locales are present in storage, call the setApplicationLocales() API. As
+                    // the API version is >= 33, this call will be directed to the framework API and
+                    // the locales will be persisted there.
+                    Object localeManager = context.getSystemService(Context.LOCALE_SERVICE);
+                    if (localeManager != null) {
+                        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
+                                localeManager,
+                                AppCompatDelegate.Api24Impl.localeListForLanguageTags(appLocales));
+                    }
+                }
+                // setting ComponentEnabledSetting for app component using
+                // AppLocalesMetadataHolderService (used only for locales, thus minimizing
+                // the chances of conflicts). Setting it as ENABLED marks the success of app-locales
+                // sync from AndroidX to framework.
+                // Flag DONT_KILL_APP indicates that you don't want to kill the app containing the
+                // component.
+                context.getPackageManager().setComponentEnabledSetting(app_locales_component,
+                        COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ DONT_KILL_APP);
+            }
+        }
+    }
+
     private static void removeDelegateFromActives(@NonNull AppCompatDelegate toRemove) {
         synchronized (sActivityDelegatesLock) {
             final Iterator<WeakReference<AppCompatDelegate>> i = sActivityDelegates.iterator();
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
deleted file mode 100644
index 2a0cb70..0000000
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppLocalesStorageHelper.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appcompat.app;
-
-import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-import static android.content.pm.PackageManager.DONT_KILL_APP;
-
-import static androidx.appcompat.app.AppCompatDelegate.getApplicationLocales;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Build;
-import android.util.Log;
-import android.util.Xml;
-
-import androidx.annotation.NonNull;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.Queue;
-import java.util.concurrent.Executor;
-
-/**
- * Helper class to manage storage of locales in app's persistent files.
- */
-class AppLocalesStorageHelper {
-    static final String APPLICATION_LOCALES_RECORD_FILE =
-            "androidx.appcompat.app.AppCompatDelegate.application_locales_record_file";
-    static final String LOCALE_RECORD_ATTRIBUTE_TAG = "application_locales";
-    static final String LOCALE_RECORD_FILE_TAG = "locales";
-    static final String APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME = "androidx.appcompat.app"
-            + ".AppLocalesMetadataHolderService";
-    static final String TAG = "AppLocalesStorageHelper";
-    static final boolean DEBUG = false;
-
-    private AppLocalesStorageHelper() {}
-
-    /**
-     * Returns app locales after reading from storage, fetched using the application context.
-     */
-    @NonNull
-    static String readLocales(@NonNull Context context) {
-        String appLocales = "";
-
-        FileInputStream fis;
-        try {
-            fis = context.openFileInput(APPLICATION_LOCALES_RECORD_FILE);
-        } catch (FileNotFoundException fnfe) {
-            if (DEBUG) {
-                Log.d(TAG, "Reading app Locales : Locales record file not found: "
-                        + APPLICATION_LOCALES_RECORD_FILE);
-            }
-            return appLocales;
-        }
-        try {
-            XmlPullParser parser = Xml.newPullParser();
-            parser.setInput(fis, "UTF-8");
-            int type;
-            int outerDepth = parser.getDepth();
-            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
-                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
-                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
-                    continue;
-                }
-
-                String tagName = parser.getName();
-                if (tagName.equals(LOCALE_RECORD_FILE_TAG)) {
-                    appLocales =  parser.getAttributeValue(/*namespace= */ null,
-                            LOCALE_RECORD_ATTRIBUTE_TAG);
-                    break;
-                }
-            }
-        } catch (XmlPullParserException | IOException e) {
-            Log.w(TAG,
-                    "Reading app Locales : Unable to parse through file :"
-                            + APPLICATION_LOCALES_RECORD_FILE);
-        } finally {
-            if (fis != null) {
-                try {
-                    fis.close();
-                } catch (IOException e) {
-                    /* ignore */
-                }
-            }
-        }
-
-        if (!appLocales.isEmpty()) {
-            if (DEBUG) {
-                Log.d(TAG,
-                        "Reading app Locales : Locales read from file: "
-                                + APPLICATION_LOCALES_RECORD_FILE + " ," + " appLocales: "
-                                + appLocales);
-            }
-        } else {
-            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
-        }
-        return appLocales;
-    }
-
-    /**
-     * Stores the provided locales in internal app file, using the application context.
-     */
-    static void persistLocales(@NonNull Context context, @NonNull String locales) {
-        if (locales.equals("")) {
-            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
-            return;
-        }
-
-        FileOutputStream fos;
-        try {
-            fos = context.openFileOutput(APPLICATION_LOCALES_RECORD_FILE, Context.MODE_PRIVATE);
-        } catch (FileNotFoundException fnfe) {
-            Log.w(TAG, String.format("Storing App Locales : FileNotFoundException: Cannot open "
-                    + "file %s for writing ", APPLICATION_LOCALES_RECORD_FILE));
-            return;
-        }
-        XmlSerializer serializer = Xml.newSerializer();
-        try {
-            serializer.setOutput(fos, /* encoding= */ null);
-            serializer.startDocument("UTF-8", true);
-            serializer.startTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
-            serializer.attribute(/* namespace= */ null, LOCALE_RECORD_ATTRIBUTE_TAG, locales);
-            serializer.endTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
-            serializer.endDocument();
-            if (DEBUG) {
-                Log.d(TAG, "Storing App Locales : app-locales: "
-                        + locales + " persisted successfully.");
-            }
-        } catch (Exception e) {
-            Log.w(TAG, "Storing App Locales : Failed to persist app-locales in storage ",
-                    e);
-        } finally {
-            if (fos != null) {
-                try {
-                    fos.close();
-                } catch (IOException e) {
-                    /* ignore */
-                }
-            }
-        }
-    }
-
-    /**
-     * Syncs app-specific locales from androidX to framework. This is used to maintain a smooth
-     * transition for a device that updates from pre-T API versions to T.
-     *
-     * <p><b>NOTE:</b> This should only be called when auto-storage is opted-in. This method
-     * uses the meta-data service provided during the opt-in and hence if the service is not found
-     * this method will throw an error.</p>
-     */
-    static void syncLocalesToFramework(Context context) {
-        if (Build.VERSION.SDK_INT >= 33) {
-            ComponentName app_locales_component = new ComponentName(
-                    context, APP_LOCALES_META_DATA_HOLDER_SERVICE_NAME);
-
-            if (context.getPackageManager().getComponentEnabledSetting(app_locales_component)
-                    != COMPONENT_ENABLED_STATE_ENABLED) {
-                // ComponentEnabledSetting for the app component app_locales_component is used as a
-                // marker to represent that the locales has been synced from AndroidX to framework
-                // If this marker is found in ENABLED state then we do not need to sync again.
-                if (getApplicationLocales().isEmpty()) {
-                    // We check if some locales are applied by the framework or not (this is done to
-                    // ensure that we don't overwrite newer locales set by the framework). If no
-                    // app-locales are found then we need to sync the app-specific locales from
-                    // androidX to framework.
-
-                    String appLocales = readLocales(context);
-                    // if locales are present in storage, call the setApplicationLocales() API. As
-                    // the API version is >= 33, this call will be directed to the framework API and
-                    // the locales will be persisted there.
-                    Object localeManager = context.getSystemService(Context.LOCALE_SERVICE);
-                    if (localeManager != null) {
-                        AppCompatDelegate.Api33Impl.localeManagerSetApplicationLocales(
-                                localeManager,
-                                AppCompatDelegate.Api24Impl.localeListForLanguageTags(appLocales));
-                    }
-                }
-                // setting ComponentEnabledSetting for app component using
-                // AppLocalesMetadataHolderService (used only for locales, thus minimizing
-                // the chances of conflicts). Setting it as ENABLED marks the success of app-locales
-                // sync from AndroidX to framework.
-                // Flag DONT_KILL_APP indicates that you don't want to kill the app containing the
-                // component.
-                context.getPackageManager().setComponentEnabledSetting(app_locales_component,
-                        COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ DONT_KILL_APP);
-            }
-        }
-    }
-
-    /**
-     * Implementation of {@link java.util.concurrent.Executor} that executes each runnable on a
-     * new thread.
-     */
-    static class ThreadPerTaskExecutor implements Executor {
-        @Override
-        public void execute(Runnable r) {
-            new Thread(r).start();
-        }
-    }
-
-    /**
-     * Implementation of {@link java.util.concurrent.Executor} that executes runnables serially
-     * by synchronizing the {@link Executor#execute(Runnable)} method and maintaining a tasks
-     * queue.
-     */
-    static class SerialExecutor implements Executor {
-        private final Object mLock = new Object();
-        final Queue<Runnable> mTasks = new ArrayDeque<>();
-        final Executor mExecutor;
-        Runnable mActive;
-
-        SerialExecutor(Executor executor) {
-            this.mExecutor = executor;
-        }
-
-        @Override
-        public void execute(final Runnable r) {
-            synchronized (mLock) {
-                mTasks.add(() -> {
-                    try {
-                        r.run();
-                    } finally {
-                        scheduleNext();
-                    }
-                });
-                if (mActive == null) {
-                    scheduleNext();
-                }
-            }
-        }
-
-        protected void scheduleNext() {
-            synchronized (mLock) {
-                if ((mActive = mTasks.poll()) != null) {
-                    mExecutor.execute(mActive);
-                }
-            }
-        }
-    }
-}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index 8b61e56..193adf1 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -17,7 +17,11 @@
 package androidx.bluetooth.integration.testapp.ui.advertiser
 
 // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once Gatt Server API is in place
+import android.Manifest
 import android.annotation.SuppressLint
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import android.content.pm.PackageManager
 import android.os.Bundle
 import android.util.Log
 import android.view.LayoutInflater
@@ -34,6 +38,7 @@
 import androidx.bluetooth.integration.testapp.ui.common.getColor
 import androidx.bluetooth.integration.testapp.ui.common.setViewEditText
 import androidx.bluetooth.integration.testapp.ui.common.toast
+import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
@@ -79,7 +84,6 @@
                 advertiseJob?.cancel()
                 advertiseJob = null
             }
-            _binding?.textInputEditTextDisplayName?.isEnabled = !value
             _binding?.checkBoxIncludeDeviceName?.isEnabled = !value
             _binding?.checkBoxConnectable?.isEnabled = !value
             _binding?.checkBoxDiscoverable?.isEnabled = !value
@@ -200,6 +204,17 @@
     }
 
     private fun initData() {
+        if (ContextCompat.checkSelfPermission(
+                requireContext(),
+                Manifest.permission.BLUETOOTH_CONNECT
+            )
+            == PackageManager.PERMISSION_GRANTED
+        ) {
+            binding.textInputEditTextDisplayName.setText(
+                (requireContext().getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
+                    .adapter.name
+            )
+        }
         binding.checkBoxIncludeDeviceName.isChecked = advertiserViewModel.includeDeviceName
         binding.checkBoxConnectable.isChecked = advertiserViewModel.connectable
         binding.checkBoxDiscoverable.isChecked = advertiserViewModel.discoverable
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
index f18e982..024d732 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
@@ -70,6 +70,7 @@
                 android:id="@+id/text_input_edit_text_display_name"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
+                android:enabled="false"
                 android:inputType="text"
                 android:maxLines="1" />
 
diff --git a/buildSrc/OWNERS b/buildSrc/OWNERS
index e866ad4..ec63c9a 100644
--- a/buildSrc/OWNERS
+++ b/buildSrc/OWNERS
@@ -6,3 +6,4 @@
 
 per-file *AndroidXPlaygroundRootPlugin.kt = dustinlam@google.com, rahulrav@google.com, yboyar@google.com
 per-file *LintConfiguration.kt = juliamcclellan@google.com, tiem@google.com
+per-file *AndroidXComposeLintIssues.kt = anbailey@google.com, lelandr@google.com
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index c596da5..97382e3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -148,26 +148,7 @@
                     // Lint tries to apply this rule to modules that do not have this lint check, so
                     // we disable that check too
                     disable.add("UnknownIssueId")
-                    error.add("ComposableNaming")
-                    error.add("ComposableLambdaParameterNaming")
-                    error.add("ComposableLambdaParameterPosition")
-                    error.add("CompositionLocalNaming")
-                    error.add("ComposableModifierFactory")
-                    error.add("AutoboxingStateCreation")
-                    error.add("AutoboxingStateValueProperty")
-                    error.add("InvalidColorHexValue")
-                    error.add("MissingColorAlphaChannel")
-                    error.add("ModifierFactoryReturnType")
-                    error.add("ModifierFactoryExtensionFunction")
-                    error.add("ModifierNodeInspectableProperties")
-                    error.add("ModifierParameter")
-                    error.add("MutableCollectionMutableState")
-                    error.add("OpaqueUnitKey")
-                    error.add("UnnecessaryComposedModifier")
-                    error.add("FrequentlyChangedStateReadInComposition")
-                    error.add("ReturnFromAwaitPointerEventScope")
-                    error.add("UseOfNonLambdaOffsetOverload")
-                    error.add("MultipleAwaitPointerEventScopes")
+                    error.addAll(ComposeLintWarningIdsToTreatAsErrors)
 
                     // Paths we want to enable ListIterator checks for - for higher level
                     // libraries it won't have a noticeable performance impact, and we don't want
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt
new file mode 100644
index 0000000..4f039ec
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+/**
+ * These lint checks are normally a warning (or lower), but in AndroidX we ignore warnings in Lint.
+ * We want these errors to be reported, so they'll be promoted from a warning to an error in
+ * modules that use the [AndroidXComposeImplPlugin].
+ */
+internal val ComposeLintWarningIdsToTreatAsErrors = listOf(
+    "ComposableNaming",
+    "ComposableLambdaParameterNaming",
+    "ComposableLambdaParameterPosition",
+    "CompositionLocalNaming",
+    "ComposableModifierFactory",
+    "AutoboxingStateCreation",
+    "AutoboxingStateValueProperty",
+    "InvalidColorHexValue",
+    "MissingColorAlphaChannel",
+    "ModifierFactoryReturnType",
+    "ModifierFactoryExtensionFunction",
+    "ModifierNodeInspectableProperties",
+    "ModifierParameter",
+    "MutableCollectionMutableState",
+    "OpaqueUnitKey",
+    "UnnecessaryComposedModifier",
+    "FrequentlyChangedStateReadInComposition",
+    "ReturnFromAwaitPointerEventScope",
+    "UseOfNonLambdaOffsetOverload",
+    "MultipleAwaitPointerEventScopes",
+)
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 7a4f3c0..1a36ae6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -69,6 +69,8 @@
         configureKtlintCheckFile()
         tasks.register(CheckExternalDependencyLicensesTask.TASK_NAME)
 
+        maybeRegisterFilterableTask()
+
         // If we're running inside Studio, validate the Android Gradle Plugin version.
         val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
         if (properties.containsKey("android.injected.invoked.from.ide")) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
new file mode 100644
index 0000000..c40a47d
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import androidx.build.FilteredAnchorTask.Companion.GLOBAL_TASK_NAME
+import androidx.build.FilteredAnchorTask.Companion.PROP_PATH_PREFIX
+import androidx.build.FilteredAnchorTask.Companion.PROP_TASK_NAME
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.work.DisableCachingByDefault
+
+@DisableCachingByDefault(because = "This is an anchor task that does no work.")
+abstract class FilteredAnchorTask : DefaultTask() {
+    init {
+        group = "Help"
+        description = "Runs tasks with a name specified by -P$PROP_TASK_NAME= for projects with " +
+            "a path prefix specified by -P$PROP_PATH_PREFIX="
+    }
+
+    @get:Input
+    abstract var pathPrefix: String
+
+    @get:Input
+    abstract var taskName: String
+
+    @TaskAction
+    fun exec() {
+        if (dependsOn.isEmpty()) {
+            throw GradleException("Failed to find any filterable tasks with name \"$taskName\" " +
+                "and path prefixed with \"$pathPrefix\"")
+        }
+    }
+
+    companion object {
+        const val GLOBAL_TASK_NAME = "filterTasks"
+        const val PROP_PATH_PREFIX = "androidx.pathPrefix"
+        const val PROP_TASK_NAME = "androidx.taskName"
+    }
+}
+
+/**
+ * Offers the specified [taskProviders] to the global [FilteredAnchorTask], adding them if they match
+ * the requested path prefix and task name.
+ */
+internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
+    if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+        val pathPrefix = properties[PROP_PATH_PREFIX] as String
+        if (relativePathForFiltering().startsWith(pathPrefix)) {
+            val taskName = properties[PROP_TASK_NAME] as String
+            taskProviders.find { taskProvider ->
+                taskName == taskProvider?.name
+            }?.let { taskProvider ->
+                rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->
+                    task.dependsOn(taskProvider)
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Registers the global [FilteredAnchorTask] if the required command-line properties are set.
+ *
+ * For example, to run `checkApi` for all projects under `core/core/`:
+ * ./gradlew filterTasks -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
+ */
+internal fun Project.maybeRegisterFilterableTask() {
+    if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+        tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
+            task.pathPrefix = properties[PROP_PATH_PREFIX] as String
+            task.taskName = properties[PROP_TASK_NAME] as String
+        }
+    }
+}
+
+/**
+ * Returns an AndroidX-relative path for the [Project], inserting the root project directory when
+ * run in a Playground context such that paths are consistent with the AndroidX context.
+ */
+internal fun Project.relativePathForFiltering(): String =
+    if (ProjectLayoutType.isPlayground(project)) {
+        "${rootProject.projectDir.name}/"
+    } else {
+        ""
+    } + "${projectDir.relativeTo(rootDir)}/"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
index d7c4c07..54597b4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
@@ -258,12 +258,7 @@
          * of the build that is released.  Thus, we use frameworks/support to get the sha
          */
         fun Project.getFrameworksSupportCommitShaAtHead(): String {
-            val gitClient = GitClient.create(
-                project.getSupportRootFolder(),
-                logger,
-                GitClient.getChangeInfoPath(project).get(),
-                GitClient.getManifestPath(project).get()
-            )
+            val gitClient = GitClient.forProject(project)
             return gitClient.getHeadSha(getSupportRootFolder())
         }
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index feb0a6f..756da81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -278,7 +278,7 @@
                 logger.info("using base commit override $baseCommitOverride")
             }
             val gitClient = GitClient.create(
-                rootProjectDir = parameters.rootDir,
+                projectDir = parameters.rootDir,
                 logger = logger.toLogger(),
                 changeInfoPath = parameters.changeInfoPath.get(),
                 manifestPath = parameters.manifestPath.get()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
index 471f537..bfec803 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
@@ -204,12 +204,7 @@
                 it.additionalDocumentation.set(
                     project.files("homepage.md")
                 )
-                val gitClient = GitClient.create(
-                    project.getSupportRootFolder(),
-                    project.logger,
-                    GitClient.getChangeInfoPath(project).get(),
-                    GitClient.getManifestPath(project).get()
-                )
+                val gitClient = GitClient.forProject(project)
                 it.replacementUrl.set(
                     DokkaUtils.createCsAndroidUrl(
                         gitClient.getHeadSha(project.getSupportRootFolder())
@@ -253,4 +248,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
index 9b1ecee..25c6a81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -19,6 +19,7 @@
 import androidx.build.releasenotes.getBuganizerLink
 import androidx.build.releasenotes.getChangeIdAOSPLink
 import java.io.File
+import java.util.concurrent.ConcurrentHashMap
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.logging.Logger
@@ -83,8 +84,16 @@
         fun getManifestPath(project: Project): Provider<String> {
             return project.providers.environmentVariable("MANIFEST").orElse("")
         }
+        fun forProject(project: Project): GitClient {
+            return create(
+                project.projectDir,
+                project.logger,
+                GitClient.getChangeInfoPath(project).get(),
+                GitClient.getManifestPath(project).get()
+            )
+        }
         fun create(
-            rootProjectDir: File,
+            projectDir: File,
             logger: Logger,
             changeInfoPath: String,
             manifestPath: String
@@ -107,8 +116,41 @@
                     "manifest $manifestPath")
                 return ChangeInfoGitClient(changeInfoText, manifestText)
             }
+            val gitRoot = findGitDirInParentFilepath(projectDir)
+            check(gitRoot != null) {
+                "Could not find .git dir for $projectDir"
+            }
             logger.info("UsingGitRunnerGitClient")
-            return GitRunnerGitClient(rootProjectDir, logger)
+            return GitRunnerGitClient(gitRoot, logger)
+        }
+    }
+}
+
+data class MultiGitClient(
+    val logger: Logger,
+    val changeInfoPath: String,
+    val manifestPath: String
+) {
+    // Map from the root of the git repository to a GitClient for that repository
+    // In AndroidX this directory could be frameworks/support, external/noto-fonts, or others
+    @Transient // We don't want Gradle to persist GitClient in the configuration cache
+    val cache: MutableMap<File, GitClient> = ConcurrentHashMap()
+
+    fun getGitClient(projectDir: File): GitClient {
+        return cache.getOrPut(
+            key = projectDir
+        ) {
+            GitClient.create(projectDir, logger, changeInfoPath, manifestPath)
+        }
+    }
+
+    companion object {
+        fun create(project: Project): MultiGitClient {
+            return MultiGitClient(
+                project.logger,
+                GitClient.getChangeInfoPath(project).get(),
+                GitClient.getManifestPath(project).get()
+            )
         }
     }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
index 2057187..342d2e5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
@@ -66,17 +66,6 @@
             ?.firstOrNull()
     }
 
-    private fun findGitDirInParentFilepath(filepath: File): File? {
-        var curDirectory: File = filepath
-        while (curDirectory.path != "/") {
-            if (File("$curDirectory/.git").exists()) {
-                return curDirectory
-            }
-            curDirectory = curDirectory.parentFile
-        }
-        return null
-    }
-
     private fun parseCommitLogString(
         commitLogString: String,
         commitStartDelimiter: String,
@@ -214,3 +203,17 @@
         const val GIT_LOG_CMD_PREFIX = "git log --name-only"
     }
 }
+
+/**
+ * Finds the git directory containing the given File by checking parent directories
+ */
+internal fun findGitDirInParentFilepath(filepath: File): File? {
+    var curDirectory: File = filepath
+    while (curDirectory.path != "/") {
+        if (File("$curDirectory/.git").exists()) {
+            return curDirectory
+        }
+        curDirectory = curDirectory.parentFile
+    }
+    return null
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
index c8458bc..63e1edb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
@@ -17,7 +17,7 @@
 package androidx.build.metalava
 
 import androidx.build.AndroidXExtension
-import androidx.build.ProjectLayoutType.Companion.isPlayground
+import androidx.build.addFilterableTasks
 import androidx.build.addToBuildOnServer
 import androidx.build.addToCheckTask
 import androidx.build.checkapi.ApiBaselinesLocation
@@ -26,6 +26,7 @@
 import androidx.build.getSuppressCompatibilityOptInPathPrefixes
 import androidx.build.getSuppressCompatibilityOptOutPathPrefixes
 import androidx.build.java.JavaCompileInputs
+import androidx.build.relativePathForFiltering
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import com.android.build.gradle.tasks.ProcessLibraryManifest
 import org.gradle.api.Project
@@ -113,7 +114,7 @@
             }
         }
 
-        project.tasks.register(
+        val updateApiLintBaseline = project.tasks.register(
             "updateApiLintBaseline",
             UpdateApiLintBaselineTask::class.java
         ) { task ->
@@ -189,7 +190,7 @@
         // Make sure it always runs *after* the updateApi task.
         ignoreApiChanges?.configure { it.mustRunAfter(updateApi) }
 
-        project.tasks.register("regenerateApis") { task ->
+        val regenerateApis = project.tasks.register("regenerateApis") { task ->
             task.group = "API"
             task.description = "Regenerates current and historic API .txt files using the " +
                 "corresponding prebuilt and the latest Metalava, then updates API ignore files"
@@ -200,6 +201,14 @@
 
         project.addToCheckTask(checkApi)
         project.addToBuildOnServer(checkApi)
+        project.addFilterableTasks(
+            ignoreApiChanges,
+            updateApiLintBaseline,
+            checkApi,
+            regenerateOldApis,
+            updateApi,
+            regenerateApis,
+        )
     }
 
     private fun applyInputs(inputs: JavaCompileInputs, task: MetalavaTask) {
@@ -214,11 +223,7 @@
  * Returns whether the project has been opted-in to the Suppress Compatibility migration.
  */
 internal fun Project.isOptedInToSuppressCompatibilityMigration(): Boolean {
-    val dir = if (isPlayground(project)) {
-        "${rootProject.projectDir.name}/"
-    } else {
-        ""
-    } + "${projectDir.relativeTo(rootDir)}/"
+    val dir = relativePathForFiltering()
     return getSuppressCompatibilityOptOutPathPrefixes().none { pathPrefix ->
         dir.startsWith(pathPrefix)
     } && getSuppressCompatibilityOptInPathPrefixes().any { pathPrefix ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 6849ca4..8889212 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -37,7 +37,11 @@
     return when (configurationName) {
         BundleInsideHelper.CONFIGURATION_NAME -> true
         "shadowed" -> true
-        "compileClasspath" -> appliesShadowPlugin()
+        // compileClasspath is included by the Shadow plugin by default but projects that
+        // declare a "shadowed" configuration exclude the "compileClasspath" configuration from
+        // the shadowJar task
+        "compileClasspath" ->
+            appliesShadowPlugin() && project.configurations.findByName("shadowed") == null
         EXPORT_INSPECTOR_DEPENDENCIES -> true
         IMPORT_INSPECTOR_DEPENDENCIES -> true
         else -> false
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index b6c5c29..51a2301 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -36,17 +36,33 @@
  *
  * The possible values of LibraryType are as follows:
  * PUBLISHED_LIBRARY: a conventional library, published, sourced, documented, and versioned.
- * SAMPLES: a library of samples, published as additional properties to a conventional library,
- *      including published source. Documented in a special way, not API tracked.
+ * PUBLISHED_TEST_LIBRARY: PUBLISHED_LIBRARY, but allows calling @VisibleForTesting API. Used for
+ * libraries that allow developers to test code that uses your library. Often provides test fakes.
+ * PUBLISHED_NATIVE_LIBRARY: PUBLISHED_LIBRARY, but uses native API tracking instead of Java
+ * INTERNAL_TEST_LIBRARY: unpublished, untracked, undocumented. Used in internal tests. Usually
+ * contains integration tests, but is _not_ an app. Runs device tests.
+ * INTERNAL_HOST_TEST_LIBRARY: as INTERNAL_TEST_LIBRARY, but runs host tests instead. Avoid mixing
+ * host tests and device tests in the same library, for performance / test-result-caching reasons.
+ * SAMPLES: a library containing sample code referenced in your library's documentation with
+ * @sampled, published as a documentation-related supplement to a conventional library.
  * LINT: a library of lint rules for using a conventional library. Published through lintPublish as
- *      part of an AAR, not published standalone.
- * COMPILER_PLUGIN: a tool that modifies the kotlin or java compiler. Used only while compiling.
+ * part of an AAR, not published standalone.
+ * COMPILER_DAEMON: a tool that modifies the kotlin or java compiler. Used only while compiling. Has
+ * no API and does not publish source jars, but does release to maven.
+ * COMPILER_DAEMON_TEST: a compiler plugin that is not published at all, for internal-only use.
+ * COMPILER_PLUGIN: as COMPILER_DAEMON, but is compatible with JDK 11.
  * GRADLE_PLUGIN: a library that is a gradle plugin.
  * ANNOTATION_PROCESSOR: a library consisting of an annotation processor. Used only while compiling.
+ * ANNOTATION_PROCESSOR_UTILS: contains reference code for understanding an annotation processor.
+ * Publishes source jars, but does not track API.
  * OTHER_CODE_PROCESSOR: a library that algorithmically generates and/or alters code
  *                      but not through hooking into custom annotations or the kotlin compiler.
  *                      For example, navigation:safe-args-generator or Jetifier.
+ * IDE_PLUGIN: a library that should only ever be downloaded by studio. Unfortunately, we don't
+ * yet have a good way to track API for these. b/281843422
  * UNSET: a library that has not yet been migrated to using LibraryType. Should never be used.
+ * APP: an app, such as an example app or integration testsapp. Should never be used; apps should
+ * not apply the AndroidX plugin or have an androidx block in their build.gradle files.
  *
  * TODO: potential future LibraryTypes:
  * KOTLIN_ONLY_LIBRARY: like PUBLISHED_LIBRARY, but not intended for use from java. ktx and compose.
@@ -82,7 +98,10 @@
         val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
         val IDE_PLUGIN = IdePlugin()
         val UNSET = Unset()
+        @Deprecated("Do not use an androidx block for apps/testapps, only for libraries")
+        val APP = UNSET
 
+        @Suppress("DEPRECATION")
         private val allTypes = mapOf(
             "PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
             "PUBLISHED_TEST_LIBRARY" to PUBLISHED_TEST_LIBRARY,
@@ -99,7 +118,8 @@
             "ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
             "OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
             "IDE_PLUGIN" to IDE_PLUGIN,
-            "UNSET" to UNSET
+            "UNSET" to UNSET,
+            "APP" to APP
         )
         fun valueOf(name: String): LibraryType {
             val result = allTypes[name]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 91d0350..c247a1d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -342,22 +342,17 @@
 
     var end = visibleItems.last().index
 
-    fun addItem(index: Int) {
-        if (list == null) list = mutableListOf()
-        requireNotNull(list).add(
-            measuredItemProvider.getAndMeasure(index)
-        )
-    }
-
     end = minOf(end + beyondBoundsItemCount, itemsCount - 1)
 
     for (i in visibleItems.last().index + 1..end) {
-        addItem(i)
+        if (list == null) list = mutableListOf()
+        list.add(measuredItemProvider.getAndMeasure(i))
     }
 
     pinnedItems.fastForEach { index ->
         if (index > end) {
-            addItem(index)
+            if (list == null) list = mutableListOf()
+            list?.add(measuredItemProvider.getAndMeasure(index))
         }
     }
 
@@ -374,22 +369,17 @@
 
     var start = currentFirstItemIndex
 
-    fun addItem(index: Int) {
-        if (list == null) list = mutableListOf()
-        requireNotNull(list).add(
-            measuredItemProvider.getAndMeasure(index)
-        )
-    }
-
     start = maxOf(0, start - beyondBoundsItemCount)
 
     for (i in currentFirstItemIndex - 1 downTo start) {
-        addItem(i)
+        if (list == null) list = mutableListOf()
+        list.add(measuredItemProvider.getAndMeasure(i))
     }
 
     pinnedItems.fastForEach { index ->
         if (index < start) {
-            addItem(index)
+            if (list == null) list = mutableListOf()
+            list?.add(measuredItemProvider.getAndMeasure(index))
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutBeyondBoundsState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutBeyondBoundsState.kt
index 2996035..7f5d9cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutBeyondBoundsState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutBeyondBoundsState.kt
@@ -58,4 +58,4 @@
         }
         return pinnedItems
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 2cc255c..4c6eb09 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -429,18 +429,15 @@
 
     val end = minOf(currentLastPage + beyondBoundsPageCount, pagesCount - 1)
 
-    fun addPage(index: Int) {
-        if (list == null) list = mutableListOf()
-        requireNotNull(list).add(getAndMeasure(index))
-    }
-
     for (i in currentLastPage + 1..end) {
-        addPage(i)
+        if (list == null) list = mutableListOf()
+        list.add(getAndMeasure(i))
     }
 
     pinnedPages.fastForEach { pageIndex ->
         if (pageIndex in (end + 1) until pagesCount) {
-            addPage(pageIndex)
+            if (list == null) list = mutableListOf()
+            list?.add(getAndMeasure(pageIndex))
         }
     }
 
@@ -457,20 +454,15 @@
 
     val start = maxOf(0, currentFirstPage - beyondBoundsPageCount)
 
-    fun addPage(index: Int) {
-        if (list == null) list = mutableListOf()
-        requireNotNull(list).add(
-            getAndMeasure(index)
-        )
-    }
-
     for (i in currentFirstPage - 1 downTo start) {
-        addPage(i)
+        if (list == null) list = mutableListOf()
+        list.add(getAndMeasure(i))
     }
 
     pinnedPages.fastForEach { pageIndex ->
         if (pageIndex < start) {
-            addPage(pageIndex)
+            if (list == null) list = mutableListOf()
+            list?.add(getAndMeasure(pageIndex))
         }
     }
 
diff --git a/compose/lint/OWNERS b/compose/lint/OWNERS
new file mode 100644
index 0000000..a973ce0
--- /dev/null
+++ b/compose/lint/OWNERS
@@ -0,0 +1 @@
+anbailey@google.com
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 23300d9..246125e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -54,8 +54,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -127,6 +130,38 @@
     }
 
     @Test
+    fun edm_doesNotCollapse_whenTypingOnSoftKeyboard() {
+        rule.setMaterialContent {
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).performClick()
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(TFTag).assertIsFocused()
+        rule.onNodeWithTag(EDMTag).assertIsDisplayed()
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+
+        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+        val zKey = device.findObject(By.desc("z")) ?: device.findObject(By.text("z"))
+        // Only run the test if we can find a key to type, which might fail for any number of
+        // reasons (keyboard doesn't appear, unexpected locale, etc.)
+        Assume.assumeNotNull(zKey)
+
+        repeat(3) {
+            zKey.click()
+            rule.waitForIdle()
+        }
+
+        rule.onNodeWithTag(TFTag).assertTextContains("zzz")
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
     fun expandedBehaviour_expandsAndFocusesTextFieldOnTrailingIconClick() {
         rule.setMaterialContent {
             var expanded by remember { mutableStateOf(false) }
@@ -310,6 +345,7 @@
         rule.onNodeWithTag(TFTag).assertTextContains(OptionName)
     }
 
+    @Ignore("b/266109857")
     @Test
     fun doesNotCrashWhenAnchorDetachedFirst() {
         var parent: FrameLayout? = null
@@ -319,9 +355,19 @@
                     FrameLayout(context).apply {
                         addView(ComposeView(context).apply {
                             setContent {
-                                Box {
-                                    ExposedDropdownMenuBox(expanded = true, onExpandedChange = {}) {
-                                        Box(Modifier.size(20.dp))
+                                ExposedDropdownMenuBox(expanded = true, onExpandedChange = {}) {
+                                    TextField(
+                                        value = "Text",
+                                        onValueChange = {},
+                                    )
+                                    ExposedDropdownMenu(
+                                        expanded = true,
+                                        onDismissRequest = {},
+                                    ) {
+                                        DropdownMenuItem(
+                                            content = { Text(OptionName) },
+                                            onClick = {},
+                                        )
                                     }
                                 }
                             }
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 d6f140d..e810b09 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
@@ -382,26 +382,15 @@
         // matter whether we return true or false as some upper layer decides on whether the
         // event is propagated to other windows or not. So for focusable the event is consumed but
         // for not focusable it is propagated to other windows.
-        if (
-            (
-                (event.action == MotionEvent.ACTION_DOWN) &&
-                    (
-                        (event.x < 0) ||
-                            (event.x >= width) ||
-                            (event.y < 0) ||
-                            (event.y >= height)
-                        )
-                ) ||
+        if ((event.action == MotionEvent.ACTION_DOWN &&
+                (event.x < 0 || event.x >= width || event.y < 0 || event.y >= height)) ||
             event.action == MotionEvent.ACTION_OUTSIDE
         ) {
             val parentBounds = parentBounds
             val shouldDismiss = parentBounds == null || dismissOnOutsideClick(
-                if (event.x != 0f || event.y != 0f) {
-                    Offset(
-                        params.x + event.x,
-                        params.y + event.y
-                    )
-                } else null,
+                // Keep menu open if ACTION_OUTSIDE event is reported as raw coordinates of (0, 0).
+                // This means it belongs to another owner, e.g., the soft keyboard or other window.
+                if (event.rawX != 0f && event.rawY != 0f) Offset(event.rawX, event.rawY) else null,
                 parentBounds
             )
             if (shouldDismiss) {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
index 95357cf..d6d9c74 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
@@ -363,7 +363,7 @@
                                     TextField(
                                         value = "Text",
                                         onValueChange = {},
-                                        modifier = Modifier.menuAnchor().size(20.dp),
+                                        modifier = Modifier.menuAnchor(),
                                     )
                                     ExposedDropdownMenu(
                                         expanded = true,
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 22a20e0..be1967c 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -168,6 +168,7 @@
   }
 
   public final class LocaleManagerCompat {
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales(android.content.Context);
     method @AnyThread public static androidx.core.os.LocaleListCompat getSystemLocales(android.content.Context);
   }
 
@@ -1077,6 +1078,7 @@
     method public static java.io.File getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
+    method public static android.content.Context getContextForLanguage(android.content.Context);
     method public static java.io.File? getDataDir(android.content.Context);
     method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
@@ -1085,6 +1087,7 @@
     method public static java.util.concurrent.Executor getMainExecutor(android.content.Context);
     method public static java.io.File? getNoBackupFilesDir(android.content.Context);
     method public static java.io.File![] getObbDirs(android.content.Context);
+    method public static String getString(android.content.Context, int);
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
@@ -1869,6 +1872,7 @@
 
   public final class ConfigurationCompat {
     method public static androidx.core.os.LocaleListCompat getLocales(android.content.res.Configuration);
+    method public static void setLocales(android.content.res.Configuration, androidx.core.os.LocaleListCompat);
   }
 
   public final class EnvironmentCompat {
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 83c2ad1..0c385e3 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -168,6 +168,7 @@
   }
 
   public final class LocaleManagerCompat {
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales(android.content.Context);
     method @AnyThread public static androidx.core.os.LocaleListCompat getSystemLocales(android.content.Context);
   }
 
@@ -1077,6 +1078,7 @@
     method public static java.io.File getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
+    method public static android.content.Context getContextForLanguage(android.content.Context);
     method public static java.io.File? getDataDir(android.content.Context);
     method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
@@ -1085,6 +1087,7 @@
     method public static java.util.concurrent.Executor getMainExecutor(android.content.Context);
     method public static java.io.File? getNoBackupFilesDir(android.content.Context);
     method public static java.io.File![] getObbDirs(android.content.Context);
+    method public static String getString(android.content.Context, int);
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
@@ -1876,6 +1879,7 @@
 
   public final class ConfigurationCompat {
     method public static androidx.core.os.LocaleListCompat getLocales(android.content.res.Configuration);
+    method public static void setLocales(android.content.res.Configuration, androidx.core.os.LocaleListCompat);
   }
 
   public final class EnvironmentCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 4e9425e..5f1829e 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -123,6 +123,11 @@
     method public static void onActivityCreate(android.app.Activity);
   }
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class AppLocalesStorageHelper {
+    method public static void persistLocales(android.content.Context, String);
+    method public static String readLocales(android.content.Context);
+  }
+
   public final class AppOpsManagerCompat {
     method public static int checkOrNoteProxyOp(android.content.Context, int, String, String);
     method public static int noteOp(android.content.Context, String, int, String);
@@ -210,6 +215,7 @@
   }
 
   public final class LocaleManagerCompat {
+    method @AnyThread public static androidx.core.os.LocaleListCompat getApplicationLocales(android.content.Context);
     method @AnyThread public static androidx.core.os.LocaleListCompat getSystemLocales(android.content.Context);
   }
 
@@ -1194,6 +1200,7 @@
     method public static java.io.File getCodeCacheDir(android.content.Context);
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
+    method public static android.content.Context getContextForLanguage(android.content.Context);
     method public static java.io.File? getDataDir(android.content.Context);
     method public static android.view.Display getDisplayOrDefault(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
@@ -1202,6 +1209,7 @@
     method public static java.util.concurrent.Executor getMainExecutor(android.content.Context);
     method public static java.io.File? getNoBackupFilesDir(android.content.Context);
     method public static java.io.File![] getObbDirs(android.content.Context);
+    method public static String getString(android.content.Context, int);
     method public static <T> T? getSystemService(android.content.Context, Class<T!>);
     method public static String? getSystemServiceName(android.content.Context, Class<?>);
     method public static boolean isDeviceProtectedStorage(android.content.Context);
@@ -2217,6 +2225,7 @@
 
   public final class ConfigurationCompat {
     method public static androidx.core.os.LocaleListCompat getLocales(android.content.res.Configuration);
+    method public static void setLocales(android.content.res.Configuration, androidx.core.os.LocaleListCompat);
   }
 
   public final class EnvironmentCompat {
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 03b73b5..c1e0eda 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -147,18 +147,23 @@
 import android.view.textservice.TextServicesManager;
 
 import androidx.annotation.OptIn;
+import androidx.core.app.AppLocalesStorageHelper;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.hardware.display.DisplayManagerCompat;
 import androidx.core.os.BuildCompat;
+import androidx.core.os.ConfigurationCompat;
+import androidx.core.os.LocaleListCompat;
 import androidx.core.test.R;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 @LargeTest
@@ -182,6 +187,11 @@
         mPermission = mContext.getPackageName() + ".DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION";
     }
 
+    @After
+    public void tearDown() {
+        setAppLocales(mContext, "");
+    }
+
     @Test
     public void getSystemServiceName() {
         assertEquals(ACCESSIBILITY_SERVICE,
@@ -659,4 +669,21 @@
                     actualDisplay.getDisplayId());
         }
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 17, maxSdkVersion = 32)
+    public void testGetContextForLanguage17() {
+        setAppLocales(mContext, LocaleListCompat.create(Locale.JAPAN).toLanguageTags());
+
+        // verify the context that respects the per-app locales
+        Context newContext = ContextCompat.getContextForLanguage(mContext);
+        LocaleListCompat locales = ConfigurationCompat.getLocales(
+                newContext.getResources().getConfiguration());
+        assertEquals(1, locales.size());
+        assertEquals(Locale.JAPAN, locales.get(0));
+    }
+
+    private void setAppLocales(Context context, String locales) {
+        AppLocalesStorageHelper.persistLocales(context, locales);
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java b/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java
new file mode 100644
index 0000000..0753e84
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/app/AppLocalesStorageHelper.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.app;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Helper class to manage storage of locales in app's persistent files.
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public class AppLocalesStorageHelper {
+    static final String APPLICATION_LOCALES_RECORD_FILE =
+            "androidx.appcompat.app.AppCompatDelegate.application_locales_record_file";
+    static final String LOCALE_RECORD_ATTRIBUTE_TAG = "application_locales";
+    static final String LOCALE_RECORD_FILE_TAG = "locales";
+
+    static final String TAG = "AppLocalesStorageHelper";
+    static final boolean DEBUG = false;
+
+    private AppLocalesStorageHelper() {}
+
+    /**
+     * Returns app locales after reading from storage, fetched using the application context.
+     */
+    @NonNull
+    public static String readLocales(@NonNull Context context) {
+        String appLocales = "";
+
+        FileInputStream fis;
+        try {
+            fis = context.openFileInput(APPLICATION_LOCALES_RECORD_FILE);
+        } catch (FileNotFoundException fnfe) {
+            if (DEBUG) {
+                Log.d(TAG, "Reading app Locales : Locales record file not found: "
+                        + APPLICATION_LOCALES_RECORD_FILE);
+            }
+            return appLocales;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, "UTF-8");
+            int type;
+            int outerDepth = parser.getDepth();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+
+                String tagName = parser.getName();
+                if (tagName.equals(LOCALE_RECORD_FILE_TAG)) {
+                    appLocales =  parser.getAttributeValue(/*namespace= */ null,
+                            LOCALE_RECORD_ATTRIBUTE_TAG);
+                    break;
+                }
+            }
+        } catch (XmlPullParserException | IOException e) {
+            Log.w(TAG,
+                    "Reading app Locales : Unable to parse through file :"
+                            + APPLICATION_LOCALES_RECORD_FILE);
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    /* ignore */
+                }
+            }
+        }
+
+        if (!appLocales.isEmpty()) {
+            if (DEBUG) {
+                Log.d(TAG,
+                        "Reading app Locales : Locales read from file: "
+                                + APPLICATION_LOCALES_RECORD_FILE + " ," + " appLocales: "
+                                + appLocales);
+            }
+        } else {
+            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+        }
+        return appLocales;
+    }
+
+    /**
+     * Stores the provided locales in internal app file, using the application context.
+     */
+    public static void persistLocales(@NonNull Context context, @NonNull String locales) {
+        if (locales.equals("")) {
+            context.deleteFile(APPLICATION_LOCALES_RECORD_FILE);
+            return;
+        }
+
+        FileOutputStream fos;
+        try {
+            fos = context.openFileOutput(APPLICATION_LOCALES_RECORD_FILE, Context.MODE_PRIVATE);
+        } catch (FileNotFoundException fnfe) {
+            Log.w(TAG, String.format("Storing App Locales : FileNotFoundException: Cannot open "
+                    + "file %s for writing ", APPLICATION_LOCALES_RECORD_FILE));
+            return;
+        }
+        XmlSerializer serializer = Xml.newSerializer();
+        try {
+            serializer.setOutput(fos, /* encoding= */ null);
+            serializer.startDocument("UTF-8", true);
+            serializer.startTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+            serializer.attribute(/* namespace= */ null, LOCALE_RECORD_ATTRIBUTE_TAG, locales);
+            serializer.endTag(/* namespace= */ null, LOCALE_RECORD_FILE_TAG);
+            serializer.endDocument();
+            if (DEBUG) {
+                Log.d(TAG, "Storing App Locales : app-locales: "
+                        + locales + " persisted successfully.");
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Storing App Locales : Failed to persist app-locales in storage ",
+                    e);
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException e) {
+                    /* ignore */
+                }
+            }
+        }
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java b/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
index 9aabc8c..4e29425 100644
--- a/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/LocaleManagerCompat.java
@@ -25,10 +25,8 @@
 import androidx.annotation.AnyThread;
 import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
-import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.os.BuildCompat;
 import androidx.core.os.LocaleListCompat;
 
 import java.util.Locale;
@@ -36,10 +34,6 @@
 /**
  * Helper for accessing features in {@link android.app.LocaleManager} in a backwards compatible
  * fashion.
- *
- * <p><b>Note:</b> Backwards compatibility for
- * {@link LocaleManager#setApplicationLocales(LocaleList)} and
- * {@link LocaleManager#getApplicationLocales()} is available via AppCompatDelegate.
  */
 public final class LocaleManagerCompat {
 
@@ -53,13 +47,12 @@
      * is set, this method helps cater to rare use-cases which might require specifically knowing
      * the system locale.
      */
-    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
     @NonNull
     @AnyThread
     public static LocaleListCompat getSystemLocales(@NonNull Context context) {
         LocaleListCompat systemLocales = LocaleListCompat.getEmptyLocaleList();
         // TODO: modify the check to Build.Version.SDK_INT >= 33.
-        if (BuildCompat.isAtLeastT()) {
+        if (Build.VERSION.SDK_INT >= 33) {
             // If the API version is 33 or above we want to redirect the call to the framework API.
             Object localeManager = getLocaleManagerForApplication(context);
             if (localeManager != null) {
@@ -77,6 +70,31 @@
     }
 
     /**
+     * Returns application locales for the calling app as a {@link LocaleListCompat}. This API
+     * for non-{@link androidx.appcompat.app.AppCompatDelegate} context to easily get the per-app
+     * locale on the prior API 33 devices.
+     *
+     * <p>Returns a {@link LocaleListCompat#getEmptyLocaleList()} if no app-specific locales are
+     * set.
+     */
+    @AnyThread
+    @NonNull
+    public static LocaleListCompat getApplicationLocales(@NonNull Context context) {
+        if (Build.VERSION.SDK_INT >= 33) {
+            // If the API version is 33 or above we want to redirect the call to the framework API.
+            Object localeManager = getLocaleManagerForApplication(context);
+            if (localeManager != null) {
+                return LocaleListCompat.wrap(Api33Impl.localeManagerGetApplicationLocales(
+                        localeManager));
+            } else {
+                return LocaleListCompat.getEmptyLocaleList();
+            }
+        } else {
+            return LocaleListCompat.forLanguageTags(AppLocalesStorageHelper.readLocales(context));
+        }
+    }
+
+    /**
      * Returns the localeManager for the current application.
      */
     @RequiresApi(33)
@@ -126,5 +144,11 @@
             LocaleManager mLocaleManager = (LocaleManager) localeManager;
             return mLocaleManager.getSystemLocales();
         }
+
+        @DoNotInline
+        static LocaleList localeManagerGetApplicationLocales(Object localeManager) {
+            LocaleManager mLocaleManager = (LocaleManager) localeManager;
+            return mLocaleManager.getApplicationLocales();
+        }
     }
 }
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 8cd98dd..d5d5f5a 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -96,6 +96,7 @@
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
+import android.content.res.Configuration;
 import android.graphics.drawable.Drawable;
 import android.hardware.ConsumerIrManager;
 import android.hardware.SensorManager;
@@ -152,11 +153,14 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.app.LocaleManagerCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.core.content.res.ResourcesCompat;
 import androidx.core.os.BuildCompat;
+import androidx.core.os.ConfigurationCompat;
 import androidx.core.os.EnvironmentCompat;
 import androidx.core.os.ExecutorCompat;
+import androidx.core.os.LocaleListCompat;
 import androidx.core.util.ObjectsCompat;
 
 import java.io.File;
@@ -900,6 +904,65 @@
     }
 
     /**
+     * Gets the resource string that also respects the per-app locales. If developers set the
+     * per-app locales via
+     * {@link androidx.appcompat.app.AppCompatDelegate#setApplicationLocales(LocaleListCompat)},
+     * this API returns localized strings even if the context is not
+     * {@link androidx.appcompat.app.AppCompatActivity}.
+     *
+     * <p>
+     * Compatibility behavior:
+     * <ul>
+     *     <li>API 17 and above, this method return the localized string that respects per-app
+     *     locales.</li>
+     *     <li>API 16 and earlier, this method directly return the result of
+     *     {@link Context#getString(int)}</li>
+     * </ul>
+     * </p>
+     */
+    @NonNull
+    public static String getString(@NonNull Context context, int resId) {
+        return getContextForLanguage(context).getString(resId);
+    }
+
+    /**
+     * Gets the context which respects the per-app locales locale. This API is specifically for
+     * developers who set the per-app locales via
+     * {@link androidx.appcompat.app.AppCompatDelegate#setApplicationLocales(LocaleListCompat)},
+     * but who needs to use the context out of {@link androidx.appcompat.app.AppCompatActivity}
+     * scope.
+     *
+     * <p>The developers can override the returned context in Application's
+     * {@link android.content.ContextWrapper#attachBaseContext(Context)}, so that developers can
+     * get the localized string via application's context.</p>
+     *
+     * <p>
+     * Compatibility behavior:
+     * <ul>
+     *     <li>API 17 and above, the locale in the context returned by this method will respect the
+     *     the per-app locale.</li>
+     *     <li>API 16 and earlier, this method directly return the {@link Context}</li>
+     * </ul>
+     * </p>
+     */
+    @NonNull
+    public static Context getContextForLanguage(@NonNull Context context) {
+        LocaleListCompat locales = LocaleManagerCompat.getApplicationLocales(context);
+
+        // The Android framework supports per-app locales on API 33, so we assume the
+        // configuration has been updated after API 32.
+        if (Build.VERSION.SDK_INT <= 32 && Build.VERSION.SDK_INT >= 17) {
+            if (!locales.isEmpty()) {
+                Configuration newConfig = new Configuration(
+                        context.getResources().getConfiguration());
+                ConfigurationCompat.setLocales(newConfig, locales);
+                return Api17Impl.createConfigurationContext(context, newConfig);
+            }
+        }
+        return context;
+    }
+
+    /**
      * Gets the name of the permission required to unexport receivers on pre Tiramisu versions of
      * Android, and then asserts that the app registering the receiver also has that permission
      * so it can receiver its own broadcasts.
@@ -1006,6 +1069,18 @@
         }
     }
 
+    @RequiresApi(17)
+    static class Api17Impl {
+        private Api17Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static Context createConfigurationContext(Context obj, Configuration config) {
+            return obj.createConfigurationContext(config);
+        }
+    }
+
     @RequiresApi(19)
     static class Api19Impl {
         private Api19Impl() {
diff --git a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
index ba35171..e5e80dc 100644
--- a/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ConfigurationCompat.java
@@ -19,11 +19,14 @@
 import static android.os.Build.VERSION.SDK_INT;
 
 import android.content.res.Configuration;
+import android.os.LocaleList;
 
 import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
+import java.util.Locale;
+
 /**
  * Helper class which allows access to properties of {@link Configuration} in
  * a backward compatible fashion.
@@ -47,6 +50,33 @@
         }
     }
 
+    /**
+     * Set the {@link Locale} into {@link Configuration}. This API is no-op on API 16 and earlier.
+     */
+    public static void setLocales(
+            @NonNull Configuration configuration, @NonNull LocaleListCompat locales) {
+        if (SDK_INT >= 24) {
+            Api24Impl.setLocales(configuration, locales);
+        } else if (SDK_INT >= 17) {
+            Api17Impl.setLocale(configuration, locales);
+        }
+    }
+
+    @RequiresApi(17)
+    static class Api17Impl {
+        private Api17Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void setLocale(
+                @NonNull Configuration configuration, @NonNull LocaleListCompat locales) {
+            if (!locales.isEmpty()) {
+                configuration.setLocale(locales.get(0));
+            }
+        }
+    }
+
     @RequiresApi(24)
     static class Api24Impl {
         private Api24Impl() {
@@ -57,5 +87,11 @@
         static android.os.LocaleList getLocales(Configuration configuration) {
             return configuration.getLocales();
         }
+
+        @DoNotInline
+        static void setLocales(
+                @NonNull Configuration configuration, @NonNull LocaleListCompat locales) {
+            configuration.setLocales((LocaleList) locales.unwrap());
+        }
     }
 }
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
index 786a9fa7..ff9a987 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/BufferTransformerTest.kt
@@ -52,6 +52,7 @@
         val expected = createMatrix()
         assertEquals(transform.transform.size, SIZE)
         assertIsEqual(transform.transform, expected)
+        assertEquals(BUFFER_TRANSFORM_IDENTITY, transform.computedTransform)
     }
 
     @Test
@@ -69,6 +70,7 @@
             }
         )
         assertIsEqual(transform.transform, expected)
+        assertEquals(BUFFER_TRANSFORM_ROTATE_90, transform.computedTransform)
     }
 
     @Test
@@ -86,6 +88,7 @@
             }
         )
         assertIsEqual(transform.transform, expected)
+        assertEquals(BUFFER_TRANSFORM_ROTATE_180, transform.computedTransform)
     }
 
     @Test
@@ -103,6 +106,7 @@
             }
         )
         assertIsEqual(transform.transform, expected)
+        assertEquals(BUFFER_TRANSFORM_ROTATE_270, transform.computedTransform)
     }
 
     @Test
@@ -115,6 +119,7 @@
         val expected = createMatrix()
         assertEquals(transform.transform.size, SIZE)
         assertIsEqual(transform.transform, expected)
+        assertEquals(BufferTransformHintResolver.UNKNOWN_TRANSFORM, transform.computedTransform)
     }
 
     private inline fun createMatrix(block: FloatArray.() -> Unit = {}): FloatArray =
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
index 87b7537..6367e09 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/BufferTransformer.kt
@@ -40,6 +40,9 @@
     var glHeight = 0
         private set
 
+    var computedTransform: Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        private set
+
     fun invertBufferTransform(transform: Int): Int =
         when (transform) {
             SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 ->
@@ -65,6 +68,7 @@
         val fHeight = height.toFloat()
         glWidth = width
         glHeight = height
+        computedTransform = transformHint
         when (transformHint) {
             SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 -> {
                 Matrix.setRotateM(mViewTransform, 0, -90f, 0f, 0f, 1f)
@@ -82,8 +86,12 @@
                 glWidth = height
                 glHeight = width
             }
+            SurfaceControlCompat.BUFFER_TRANSFORM_IDENTITY -> {
+                Matrix.setIdentityM(mViewTransform, 0)
+            }
             // Identity or unknown case, just set the identity matrix
             else -> {
+                computedTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
                 Matrix.setIdentityM(mViewTransform, 0)
             }
         }
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
index a933cfc..8f79640 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
@@ -18,6 +18,7 @@
 import android.os.Build
 import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
 import android.os.Build.VERSION_CODES.JELLY_BEAN
+import android.util.Log
 import android.view.Choreographer
 import androidx.annotation.RequiresApi
 import androidx.metrics.performance.FrameData
@@ -295,6 +296,8 @@
 
         frameInit.initFramePipeline()
 
+        resetFrameStateData()
+
         val state0 = StateInfo("Testing State 0", "sampleStateA")
         val state1 = StateInfo("Testing State 1", "sampleStateB")
         val state2 = StateInfo("Testing State 2", "sampleStateC")
@@ -332,6 +335,7 @@
         }
 
         // reset and clear states
+        resetFrameStateData()
         latchedListener.reset()
         metricsState.removeState(state0.key)
         metricsState.removeState(state1.key)
@@ -339,7 +343,7 @@
         runDelayTest(frameDelay, 1, latchedListener)
         item0 = latchedListener.jankData[0]
         assertEquals(
-            "States should be empty after being cleared",
+            "States should be empty after being cleared, but got ${item0.states}",
             0,
             item0.states.size
         )
@@ -350,15 +354,16 @@
         metricsState.putState(state4.first, state4.second)
         runDelayTest(frameDelay, 1, latchedListener)
         item0 = latchedListener.jankData[0]
-        assertEquals(2, item0.states.size)
+        assertEquals("states: ${item0.states}", 2, item0.states.size)
         latchedListener.reset()
 
         // Test removal of state3 and replacement of state4
+        resetFrameStateData()
         metricsState.removeState(state3.first)
         metricsState.putState(state4.first, "sampleStateF")
         runDelayTest(frameDelay, 1, latchedListener)
         item0 = latchedListener.jankData[0]
-        assertEquals(1, item0.states.size)
+        assertEquals("states: ${item0.states}", 1, item0.states.size)
         assertEquals(state4.first, item0.states[0].key)
         assertEquals("sampleStateF", item0.states[0].value)
         latchedListener.reset()
@@ -496,6 +501,7 @@
             mapOf("stateNameA" to "1"),
         )
 
+        resetFrameStateData()
         runDelayTest(frameDelay = 0, numFrames = perFrameStateData.size,
             latchedListener, perFrameStateData)
 
@@ -514,7 +520,8 @@
          */
         var expectedIndex = 0
         var resultIndex = 0
-        while (expectedIndex < expectedResults.size) {
+        while (expectedIndex < expectedResults.size &&
+            resultIndex < latchedListener.jankData.size) {
             val testResultStates = latchedListener.jankData[resultIndex].states
             // Test against this and next expected result, in case system skipped a frame
             var matched = checkFrameStates(expectedResults[expectedIndex], testResultStates)
@@ -522,7 +529,8 @@
                 expectedIndex++
                 matched = checkFrameStates(expectedResults[expectedIndex], testResultStates)
             }
-            assertTrue("States do not match at frame $expectedIndex", matched)
+            assertTrue("Expected states do not match $testResultStates at frame " +
+                "$expectedIndex", matched)
             expectedIndex++
             resultIndex++
         }
@@ -541,6 +549,38 @@
         return true
     }
 
+    /**
+     * We need to ensure that state data only gets set when the system is ready to send frameData
+     * for future frames. It is possible for some of the initial frames to have start times that
+     * pre-date the current time, which is when we might be setting/removing state.
+     *
+     * To ensure that the right thing happens, call this function prior to setting any frame state.
+     * It will run frames through the system until the frameData start timeis after the
+     * current time when this function is called.
+     */
+    private fun resetFrameStateData() {
+        val currentNanos = System.nanoTime()
+        // failsafe - limit the iterations, don't want to loop forever
+        var numAttempts = 0
+        try {
+            while (numAttempts < 100) {
+                runDelayTest(0, 1, latchedListener)
+                if (latchedListener.jankData.size > 0) {
+                    if (latchedListener.jankData[0].frameStartNanos > currentNanos) {
+                        return
+                    }
+                }
+                Log.d("JankStatsTest", "resetFrameStateData attempt $numAttempts:" +
+                    "frame start < currentTime: " +
+                    "${latchedListener.jankData[0].frameStartNanos}, $currentNanos")
+                latchedListener.reset()
+                numAttempts++
+            }
+        } finally {
+            latchedListener.reset()
+        }
+    }
+
     private fun runDelayTest(
         frameDelay: Int,
         numFrames: Int,
diff --git a/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt b/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
index 2a9c625..e816c7c 100644
--- a/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
+++ b/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
@@ -105,7 +105,7 @@
         try {
             while (true) {
                 processNextPageUpdateCh.trySend(Unit)
-                onPagesUpdatedEventsCh.receiveWithTimeoutMillis(10_000)
+                onPagesUpdatedEventsCh.receiveWithTimeoutMillis(1000)
                 pageUpdates++
             }
         } catch (e: TimeoutCancellationException) {
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
index 5f75646..d6670d7 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -1,19 +1,19 @@
-// /*
-// * 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.
-// */
-//
+/*
+ * 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.paging
 
 import androidx.paging.LoadState.Loading
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index 9ad57c2..2836ad7 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -124,7 +124,6 @@
 
         // Don't bother searching the subtree if it is not visible
         if (!node.isVisibleToUser()) {
-            Log.v(TAG, String.format("Skipping invisible child: %s", node));
             return ret;
         }
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index c353926..3880ae2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -126,6 +126,7 @@
     /** Returns whether there is a match for the given {@code selector} criteria. */
     @Override
     public boolean hasObject(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Searching for node with selector: %s.", selector));
         AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
         if (node != null) {
             node.recycle();
@@ -141,6 +142,7 @@
     @Override
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public UiObject2 findObject(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Retrieving node with selector: %s.", selector));
         AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
         if (node == null) {
             Log.d(TAG, String.format("Node not found with selector: %s.", selector));
@@ -153,6 +155,7 @@
     @Override
     @NonNull
     public List<UiObject2> findObjects(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Retrieving nodes with selector: %s.", selector));
         List<UiObject2> ret = new ArrayList<>();
         for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
             ret.add(new UiObject2(this, selector, node));
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 48f1e89..826130f 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -214,6 +214,7 @@
     /** Returns {@code true} if there is a nested element which matches the {@code selector}. */
     @Override
     public boolean hasObject(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Searching for node with selector: %s.", selector));
         AccessibilityNodeInfo node =
                 ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());
         if (node != null) {
@@ -230,6 +231,7 @@
     @Override
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public UiObject2 findObject(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Retrieving node with selector: %s.", selector));
         AccessibilityNodeInfo node =
                 ByMatcher.findMatch(getDevice(), selector, getAccessibilityNodeInfo());
         if (node == null) {
@@ -245,6 +247,7 @@
     @Override
     @NonNull
     public List<UiObject2> findObjects(@NonNull BySelector selector) {
+        Log.d(TAG, String.format("Retrieving nodes with selector: %s.", selector));
         List<UiObject2> ret = new ArrayList<>();
         for (AccessibilityNodeInfo node :
                 ByMatcher.findMatches(getDevice(), selector, getAccessibilityNodeInfo())) {
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
index c24141c..c5fdabd 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
@@ -28,4 +28,13 @@
             fail("Expected to be empty, but was $actual")
         }
     }
+
+    /** Fails if the map does not contain the given key. */
+    fun containsKey(key: Any?) {
+        requireNonNull(actual) { "Expected to contain $key, but was null" }
+
+        if (!actual.containsKey(key)) {
+            fail("Expected to contain $key, but was ${actual.keys}")
+        }
+    }
 }
diff --git a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
index 586efc9..98c6288 100644
--- a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
+++ b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/MapSubjectTest.kt
@@ -32,4 +32,57 @@
             assertThat(mapOf(1 to 5)).isEmpty()
         }
     }
+
+    @Test
+    fun containsKey() {
+        assertThat(mapOf("kurt" to "kluever")).containsKey("kurt")
+    }
+
+    @Test
+    fun containsKeyFailure() {
+        val actual = mapOf("kurt" to "kluever")
+        assertFailsWith<AssertionError> {
+            assertThat(actual).containsKey("greg")
+        }
+    }
+
+    @Test
+    fun containsKeyNullFailure() {
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf("kurt" to "kluever")).containsKey(null)
+        }
+    }
+
+    @Test
+    fun containsKey_failsWithSameToString() {
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf(1L to "value1", 2L to "value2", "1" to "value3")).containsKey(1)
+        }
+    }
+
+    @Test
+    fun containsKey_failsWithNullStringAndNull() {
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf("null" to "value1")).containsKey(null)
+        }
+    }
+
+    @Test
+    fun containsNullKey() {
+        assertThat(mapOf(null to "null")).containsKey(null)
+    }
+
+    @Test
+    fun failMapContainsKey() {
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf("a" to "A")).containsKey("b")
+        }
+    }
+
+    @Test
+    fun failMapContainsKeyWithNull() {
+        assertFailsWith<AssertionError> {
+            assertThat(mapOf("a" to "A")).containsKey(null)
+        }
+    }
 }
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
index 7503da4..4523dc5 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
@@ -803,8 +803,8 @@
     fun outlinedButtonBorder(
         enabled: Boolean,
         borderColor: Color = MaterialTheme.colorScheme.outline,
-        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
         borderWidth: Dp = 1.dp
     ): BorderStroke {
@@ -853,18 +853,13 @@
         contentColor: Color = contentColorFor(containerColor),
         secondaryContentColor: Color = contentColor,
         iconColor: Color = contentColor,
-        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
-        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
-        disabledSecondaryContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
-        disabledIconColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(),
+        disabledSecondaryContentColor: Color =
+            MaterialTheme.colorScheme.onSurface.toDisabledColor(),
+        disabledIconColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor()
     ): ButtonColors = ButtonColors(
         containerColor = containerColor,
         contentColor = contentColor,
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
index 1216db9..1ee0789 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
@@ -454,3 +454,11 @@
 }
 
 internal val LocalColors = staticCompositionLocalOf<ColorScheme> { ColorScheme() }
+
+/**
+ * Convert given color to disabled color.
+ * @param disabledAlpha Alpha used to represent disabled colors.
+ */
+@Composable
+internal fun Color.toDisabledColor(disabledAlpha: Float = ContentAlpha.disabled) =
+    this.copy(alpha = this.alpha * disabledAlpha)
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
index d4b2a43..75dd372 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
@@ -332,12 +332,10 @@
     fun iconButtonColors(
         containerColor: Color = Color.Transparent,
         contentColor: Color = MaterialTheme.colorScheme.onBackground,
-        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
-        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        )
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor()
     ): IconButtonColors = IconButtonColors(
         containerColor = containerColor,
         contentColor = contentColor,
@@ -359,8 +357,8 @@
     fun outlinedIconButtonBorder(
         enabled: Boolean,
         borderColor: Color = MaterialTheme.colorScheme.outline,
-        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
         borderWidth: Dp = 1.dp
     ): BorderStroke {
diff --git a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IListenableWorkerImpl.aidl b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IListenableWorkerImpl.aidl
index 3619cf2..9fa0b41 100644
--- a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IListenableWorkerImpl.aidl
+++ b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IListenableWorkerImpl.aidl
@@ -32,7 +32,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package androidx.work.multiprocess;
-/* @hide */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 interface IListenableWorkerImpl {
   oneway void startWork(in byte[] request, androidx.work.multiprocess.IWorkManagerImplCallback callback);
   oneway void interrupt(in byte[] request, androidx.work.multiprocess.IWorkManagerImplCallback callback);
diff --git a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImpl.aidl b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImpl.aidl
index e77921a..1a206cd 100644
--- a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImpl.aidl
+++ b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImpl.aidl
@@ -32,7 +32,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package androidx.work.multiprocess;
-/* @hide */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 interface IWorkManagerImpl {
   oneway void enqueueWorkRequests(in byte[] request, androidx.work.multiprocess.IWorkManagerImplCallback callback);
   oneway void updateUniquePeriodicWorkRequest(String name, in byte[] request, androidx.work.multiprocess.IWorkManagerImplCallback callback);
diff --git a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImplCallback.aidl b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
index bc20417..cb69433 100644
--- a/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
+++ b/work/work-runtime/api/aidlRelease/current/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
@@ -32,7 +32,7 @@
 // later when a module using the interface is updated, e.g., Mainline modules.
 
 package androidx.work.multiprocess;
-/* @hide */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 interface IWorkManagerImplCallback {
   oneway void onSuccess(in byte[] response);
   oneway void onFailure(String error);
diff --git a/work/work-runtime/lint-baseline.xml b/work/work-runtime/lint-baseline.xml
index e8a0ac0..8ab7d19 100644
--- a/work/work-runtime/lint-baseline.xml
+++ b/work/work-runtime/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
+<issues format="6" by="lint 8.1.0-beta02" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.0-beta02)" variant="all" version="8.1.0-beta02">
 
     <issue
         id="BanSynchronizedMethods"
@@ -263,33 +263,6 @@
     </issue>
 
     <issue
-        id="RequireUnstableAidlAnnotation"
-        message="Unstable AIDL files must be annotated with @RequiresOptIn marker"
-        errorLine1="oneway interface IListenableWorkerImpl {"
-        errorLine2="^">
-        <location
-            file="src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl"/>
-    </issue>
-
-    <issue
-        id="RequireUnstableAidlAnnotation"
-        message="Unstable AIDL files must be annotated with @RequiresOptIn marker"
-        errorLine1="oneway interface IWorkManagerImpl {"
-        errorLine2="^">
-        <location
-            file="src/main/aidl/androidx/work/multiprocess/IWorkManagerImpl.aidl"/>
-    </issue>
-
-    <issue
-        id="RequireUnstableAidlAnnotation"
-        message="Unstable AIDL files must be annotated with @RequiresOptIn marker"
-        errorLine1="oneway interface IWorkManagerImplCallback {"
-        errorLine2="^">
-        <location
-            file="src/main/aidl/androidx/work/multiprocess/IWorkManagerImplCallback.aidl"/>
-    </issue>
-
-    <issue
         id="UnknownNullness"
         message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://developer.android.com/kotlin/interop#nullability_annotations"
         errorLine1="    public final void addListener(Runnable listener, Executor executor) {"
diff --git a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
index 9ff1a3a..7d798ed 100644
--- a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
+++ b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
@@ -20,9 +20,8 @@
 
 /**
  * Implementation for a multi-process {@link ListenableWorker}.
- *
- * @hide
  */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 oneway interface IListenableWorkerImpl {
    // request is a ParcelablelRemoteRequest instance.
    // callback gets a parcelized representation of Result
diff --git a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImpl.aidl b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImpl.aidl
index 2e9c060..3688ab6 100644
--- a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImpl.aidl
+++ b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImpl.aidl
@@ -20,9 +20,8 @@
 
 /**
  * Implementation for {@link IWorkManager}.
- *
- * @hide
  */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 oneway interface IWorkManagerImpl {
     // Enqueues WorkRequests
     void enqueueWorkRequests(in byte[] request, IWorkManagerImplCallback callback);
diff --git a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImplCallback.aidl b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
index 82a5ecf..9bcd35b 100644
--- a/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
+++ b/work/work-runtime/src/main/stableAidl/androidx/work/multiprocess/IWorkManagerImplCallback.aidl
@@ -18,9 +18,8 @@
 
 /**
  * RPC Callbacks for {@link IWorkManagerImpl}.
- *
- * @hide
  */
+@JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 oneway interface IWorkManagerImplCallback {
     void onSuccess(in byte[] response);
     void onFailure(String error);