diff --git a/WORKSPACE b/WORKSPACE index d67051567..0e6e71845 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -55,7 +55,6 @@ load( "ANDROIDX_CURSOR_ADAPTER_VERSION", "ANDROIDX_DRAWER_LAYOUT_VERSION", "ANDROIDX_FRAGMENT_VERSION", - "ANDROIDX_JUNIT_VERSION", "ANDROIDX_LEGACY_SUPPORT_VERSION", "ANDROIDX_LIFECYCLE_VERSION", "ANDROIDX_MULTIDEX_VERSION", @@ -63,12 +62,10 @@ load( "ANDROIDX_TRACING_VERSION", "ANDROIDX_VIEWPAGER_VERSION", "ANDROIDX_WINDOW_VERSION", - "CORE_VERSION", "GOOGLE_MATERIAL_VERSION", "GUAVA_LISTENABLEFUTURE_VERSION", "GUAVA_VERSION", "JUNIT_VERSION", - "RUNNER_VERSION", "UIAUTOMATOR_VERSION", ) diff --git a/build_extensions/axt_deps_versions.bzl b/build_extensions/axt_deps_versions.bzl index 08804ea76..d33b16aff 100644 --- a/build_extensions/axt_deps_versions.bzl +++ b/build_extensions/axt_deps_versions.bzl @@ -1,30 +1,12 @@ -"""Defines current AXT versions and dependencies.""" - -# AXT versions listed as next # last published, stable -RUNNER_VERSION = "1.6.0-alpha06" # 1.6.0-alpha05, 1.5.1 -RULES_VERSION = "1.6.0-alpha03" # 1.6.0-alpha02, 1.5.0 -MONITOR_VERSION = "1.7.0-alpha04" # 1.7.0-alpha03, 1.6.0 -ESPRESSO_VERSION = "3.6.0-alpha03" # 3.6.0-alpha02, 3.5.0 -CORE_VERSION = "1.6.0-alpha05" # 1.6.0-alpha04, 1.5.0 -ESPRESSO_DEVICE_VERSION = "1.0.0-alpha08" # 1.0.0-alpha07 -ANDROIDX_JUNIT_VERSION = "1.2.0-alpha03" # 1.2.0-alpha02, 1.1.4 -ANDROIDX_TRUTH_VERSION = "1.6.0-alpha03" # 1.6.0-alpha02, 1.5.0 -ANNOTATION_VERSION = "1.1.0-alpha03" # 1.1.0-alpha02, 1.0.1 -ORCHESTRATOR_VERSION = "1.5.0-alpha03" # 1.5.0-alpha02, 1.4.2 - -SERVICES_VERSION = "1.5.0-alpha03" # 1.5.0-alpha02, 1.4.2 - -# Full maven artifact strings for apks. -SERVICES_APK_ARTIFACT = "androidx.test.services:test-services:%s" % SERVICES_VERSION -ORCHESTRATOR_ARTIFACT = "androidx.test:orchestrator:%s" % ORCHESTRATOR_VERSION +"""Defines versions of androidx.test dependencies.""" # Maven dependency versions ANDROIDX_ANNOTATION_VERSION = "1.7.0-beta01" ANDROIDX_ANNOTATION_EXPERIMENTAL_VERSION = "1.1.0" ANDROIDX_COMPAT_VERSION = "1.3.1" -# TODO(i336855276): update to beta/stable release when available -ANDROIDX_CONCURRENT_VERSION = "1.2.0-alpha03" +# TODO(i336855276): update to 1.2.0 release when available +ANDROIDX_CONCURRENT_VERSION = "1.1.0" ANDROIDX_CORE_VERSION = "1.6.0" ANDROIDX_FRAGMENT_VERSION = "1.3.6" ANDROIDX_CURSOR_ADAPTER_VERSION = "1.0.0" diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 71b977e10..e87808af7 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -7,6 +7,7 @@ **Bug Fixes** * Remove unused androidx.test.annotation dependency +* Revert back to androidx.concurrent 1.1.0 **New Features** diff --git a/core/java/androidx/test/core/view/SuspendToFutureAdapter.kt b/core/java/androidx/test/core/view/SuspendToFutureAdapter.kt new file mode 100644 index 000000000..bf1a7e27e --- /dev/null +++ b/core/java/androidx/test/core/view/SuspendToFutureAdapter.kt @@ -0,0 +1,159 @@ +/* + * 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.test.core.view + +import androidx.concurrent.futures.ResolvableFuture +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.createCoroutine +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +/** + * A utility for launching suspending calls scoped and managed by a returned [ListenableFuture], + * used for adapting Kotlin suspending APIs to be callable from the Java programming language. + * + * TODO(b/336855276): Forked from androidx.concurrent. Remove in favor of just using androidx.concurrent + * 1.2.0 when available and toolchain compatibility issues have been addressed + */ +internal object SuspendToFutureAdapter { + + // the CoroutineScope() factory function is not used here as it adds a Job by default; + // we don't want one as a failed task shouldn't fail a root Job. + // To make SuspendToFutureAdapter behave as much like a "regular" ListenableFuture-returning + // task as possible we don't want to hold additional references to child jobs from a global/root + // scope, hence no SupervisorJob either. + private val GlobalListenableFutureScope = object : CoroutineScope { + override val coroutineContext: CoroutineContext = Dispatchers.Main + } + private val GlobalListenableFutureAwaitContext = Dispatchers.Unconfined + + /** + * Launch [block] in [context], returning a [ListenableFuture] to manage the launched operation. + * [block] will run **synchronously** to its first suspend point, behaving as + * [CoroutineStart.UNDISPATCHED] by default; set [launchUndispatched] to false to override + * and behave as [CoroutineStart.DEFAULT]. + * + * [launchFuture] can be used to write adapters for calling suspending functions from the + * Java programming language, e.g. + * + * ``` + * @file:JvmName("FancyServices") + * + * fun FancyService.requestAsync( + * args: FancyServiceArgs + * ): ListenableFuture = SuspendToFutureAdapter.launchFuture { + * request(args) + * } + * ``` + * + * which can be called from Java language source code as follows: + * ``` + * final ListenableFuture result = FancyServices.requestAsync(service, args); + * ``` + * + * If no [kotlinx.coroutines.CoroutineDispatcher] is provided in [context], [Dispatchers.Main] + * is used as the default. [ListenableFuture.get] should not be called from the main thread + * prior to the future's completion (whether it was obtained from [SuspendToFutureAdapter] + * or not) as any operation performed in the process of completing the future may require + * main thread event processing in order to proceed, leading to potential main thread deadlock. + * + * If the operation performed by [block] is known to be safe for potentially reentrant + * continuation resumption, immediate dispatchers such as [Dispatchers.Unconfined] may be used + * as part of [context] to avoid additional thread dispatch latency. This should not be used + * as a means of supporting clients blocking the main thread using [ListenableFuture.get]; + * this support can be broken by valid internal implementation changes to any transitive + * dependencies of the operation performed by [block]. + */ + @Suppress("AsyncSuffixFuture") + public fun launchFuture( + context: CoroutineContext = EmptyCoroutineContext, + launchUndispatched: Boolean = true, + block: suspend CoroutineScope.() -> T, + ): ListenableFuture { + val resultDeferred = GlobalListenableFutureScope.async( + context = context, + start = if (launchUndispatched) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT, + block = block + ) + return DeferredFuture(resultDeferred).also { future -> + // Deferred.getCompleted is marked experimental, so external libraries can't rely on it. + // Instead, use await in a raw coroutine that will invoke [resumeWith] when it returns + // using the Unconfined dispatcher. + resultDeferred::await.createCoroutine(future).resume(Unit) + } + } + + private class DeferredFuture( + private val resultDeferred: Deferred + ) : ListenableFuture, Continuation { + + private val delegateFuture = ResolvableFuture.create() + + // Implements external cancellation, propagating the cancel request to resultDeferred. + // delegateFuture will be cancelled if resultDeferred becomes cancelled for + // internal cancellation. + override fun cancel(shouldInterrupt: Boolean): Boolean = + delegateFuture.cancel(shouldInterrupt).also { didCancel -> + if (didCancel) { + resultDeferred.cancel() + } + } + + override fun isCancelled(): Boolean = delegateFuture.isCancelled + + override fun isDone(): Boolean = delegateFuture.isDone + + override fun get(): T = delegateFuture.get() + + override fun get(timeout: Long, unit: TimeUnit): T = delegateFuture.get(timeout, unit) + + override fun addListener(listener: Runnable, executor: Executor) = + delegateFuture.addListener(listener, executor) + + override val context: CoroutineContext + get() = GlobalListenableFutureAwaitContext + + /** + * Implementation of [Continuation] that will resume for the raw call to await + * to resolve the [delegateFuture] + */ + override fun resumeWith(result: Result) { + val unused = result.fold( + onSuccess = { + delegateFuture.set(it) + }, + onFailure = { + if (it is CancellationException) { + delegateFuture.cancel(false) + } else { + delegateFuture.setException(it) + } + } + ) + } + } +} diff --git a/core/java/androidx/test/core/view/ViewCapture.kt b/core/java/androidx/test/core/view/ViewCapture.kt index 0e0dda56a..535e83bc5 100644 --- a/core/java/androidx/test/core/view/ViewCapture.kt +++ b/core/java/androidx/test/core/view/ViewCapture.kt @@ -33,7 +33,6 @@ import android.view.ViewTreeObserver.OnDrawListener import android.view.WindowManager import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo -import androidx.concurrent.futures.SuspendToFutureAdapter import androidx.test.core.internal.os.HandlerExecutor import androidx.test.internal.platform.ServiceLoaderWrapper import androidx.test.internal.platform.os.ControlledLooper diff --git a/core/java/androidx/test/core/view/WindowCapture.kt b/core/java/androidx/test/core/view/WindowCapture.kt index 8bbc9206b..83c7b27b9 100644 --- a/core/java/androidx/test/core/view/WindowCapture.kt +++ b/core/java/androidx/test/core/view/WindowCapture.kt @@ -25,7 +25,6 @@ import android.os.Looper import android.view.PixelCopy import android.view.Window import androidx.annotation.RequiresApi -import androidx.concurrent.futures.SuspendToFutureAdapter import androidx.test.platform.graphics.HardwareRendererCompat import com.google.common.util.concurrent.ListenableFuture import kotlin.coroutines.resumeWithException