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);