blob: 0613657050219a2510c8bca3dfedfe9ed8298633 [file] [log] [blame]
/*
* Copyright (C) 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 com.android.adblib.tools
import com.android.adblib.AdbSession
import com.android.adblib.AdbSessionHost
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import java.util.concurrent.Future
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Collection of utility functions for invoking `"suspend"` functions from Java code.
*/
object JavaBridge {
/**
* Allows invoking any suspending function from a Java [block] using a
* [CancellableContinuation], returning a [JavaDeferred] that completes when the
* suspending function completes (exceptionally or not).
*
* The returned [JavaDeferred] can be converted into a specific implementation
* of [Future], for example [ListenableFuture](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/as-listenable-future.html)
* or [CompletableFuture][java.util.concurrent.CompletableFuture] (see `Deferred.asCompletableFuture`).
*
* Example:
*
* AdbSession session = (...)
* JavaDeferred<Integer> version = JavaBridge.invokeAsync(session, continuation ->
* session.getHostServices().version(continuation)
* );
* System.out.println("ADB internal version number is " + version.awaitBlocking());
*/
@JvmStatic
@JvmOverloads
fun <T> invokeAsync(
session: AdbSession,
block: CheckedFunction<CancellableContinuation<T>, Any?>,
scope: CoroutineScope = session.scope
): JavaDeferred<T> {
val deferred = scope.async<T>(session.host.ioDispatcher) {
suspendCancellableCoroutine { continuation ->
try {
val result = block.accept(continuation)
if (result != COROUTINE_SUSPENDED) {
// Handle case where coroutine terminated right away with a value
// (which should be type "T" if caller did not use unsafe casts)
@Suppress("UNCHECKED_CAST")
continuation.resume(result as T)
}
} catch (t: Throwable) {
continuation.resumeWithException(t)
}
}
}
return JavaDeferred(session, scope, deferred)
}
/**
* Allows invoking any [suspending function][block] from Java, blocking the calling
* thread until the coroutine completes.
*
* Throws an [IllegalStateException] if the current thread is not allowed to issue
* blocking calls (see [AdbSessionHost.isEventDispatchThread]).
*
* **Note** Consider using [invokeAsync] as an alternative, as [runBlocking] blocks
* the calling thread and may lead to thread starvation if used too liberally.
*
* Example:
*
* int version = JavaBridge.runBlocking(continuation ->
* session.getHostServices().version(continuation)
* );
* System.out.println("ADB internal version number is " + version);
*
* @see invokeAsync
* @see kotlinx.coroutines.runBlocking
* @see AdbSessionHost.isEventDispatchThread
*/
@JvmStatic
@JvmOverloads
fun <T> runBlocking(
session: AdbSession,
block: CheckedFunction<CancellableContinuation<T>, Any?>,
scope: CoroutineScope = session.scope,
timeoutMillis: Long = Long.MAX_VALUE
): T {
return invokeAsync(session, block, scope).awaitBlocking(timeoutMillis)
}
fun throwIfEventDispatchThread(session: AdbSession) {
if (session.host.isEventDispatchThread) {
throw IllegalStateException("Running a blocking operation on a event dispatch thread is not allowed")
}
}
}
/**
* A Java friendly wrapper for a [Deferred].
*/
class JavaDeferred<T>(
val session: AdbSession,
val scope: CoroutineScope,
val deferred: Deferred<T>
) {
/**
* @see Deferred.isActive
*/
val isActive: Boolean
get() = deferred.isActive
/**
* @see Deferred.isCancelled
*/
val isCancelled: Boolean
get() = deferred.isCancelled
/**
* @see Deferred.isCompleted
*/
val isCompleted: Boolean
get() = deferred.isCompleted
/**
* Adds a callback that is invoked (on a [AdbSessionHost.ioDispatcher] thread) when
* this [Deferred] completes.
*/
fun addCallback(callback: CheckedBiConsumer<Throwable?, T?>) {
scope.launch(session.host.ioDispatcher) {
try {
val result = deferred.await()
callback.accept(null, result)
} catch (t: Throwable) {
callback.accept(t, null)
}
}
}
/**
* Waits for the [Deferred] to complete, blocking the calling thread.
*
* Throws an [IllegalStateException] if the current thread is not allowed to issue
* blocking calls (see [AdbSessionHost.isEventDispatchThread]).
*/
@JvmOverloads
fun awaitBlocking(timeoutMillis: Long = Long.MAX_VALUE): T {
JavaBridge.throwIfEventDispatchThread(session)
// Note: This could be optimized once "Deferred.getCompleted()" is finalized.
return runBlocking {
withTimeout(timeoutMillis) {
deferred.await()
}
}
}
}
/**
* A [java.util.function.Function] which may throw.
*
* @param T the type of value supplied to this function.
* @param R the type of result of this function.
*/
@FunctionalInterface
interface CheckedFunction<T, R> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
* @return the function result
* @throws Throwable if an error occurs
*/
@Throws(Throwable::class)
fun accept(t: T): R
}
/**
* A [java.util.function.BiConsumer] which may throw.
*
* @param T the type of the first argument to the operation
* @param U the type of the second argument to the operation
*/
@FunctionalInterface
interface CheckedBiConsumer<T, U> {
/**
* Performs this operation on the given arguments.
*
* @param t the first input argument
* @param u the second input argument
* @throws Throwable if an error occurs
*/
@Throws(Throwable::class)
fun accept(t: T, u: U)
}