diff --git a/build.gradle b/build.gradle index 693d9fd5..f6fabfa8 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { ext.wearableVersion = '0.1.1' ext.kotlinVersion = '1.4.32' - ext.coroutineVersion = '1.3.8' + ext.coroutineVersion = '1.5.2' ext.annotationVersion = '1.2.0' ext.appcompatVersion = '1.2.0' diff --git a/play-services-tasks-ktx/build.gradle b/play-services-tasks-ktx/build.gradle new file mode 100644 index 00000000..f39554a0 --- /dev/null +++ b/play-services-tasks-ktx/build.gradle @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2021, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +dependencies { + api project(":play-services-tasks") + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" +} + +android { + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG kotlin extensions for play-services-tasks' diff --git a/play-services-tasks-ktx/src/main/AndroidManifest.xml b/play-services-tasks-ktx/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6a32b511 --- /dev/null +++ b/play-services-tasks-ktx/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/play-services-tasks-ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt b/play-services-tasks-ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt new file mode 100644 index 00000000..3171dd6e --- /dev/null +++ b/play-services-tasks-ktx/src/main/kotlin/com/google/android/gms/tasks/Tasks.kt @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2016, JetBrains s.r.o. + * SPDX-FileCopyrightText: 2021, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.tasks + +import com.google.android.gms.tasks.* +import kotlinx.coroutines.* +import kotlin.coroutines.* + +/** + * Converts this deferred to the instance of [Task]. + * If deferred is cancelled then resulting task will be cancelled as well. + */ +fun Deferred.asTask(): Task { + val cancellation = CancellationTokenSource() + val source = TaskCompletionSource(cancellation.token) + + invokeOnCompletion callback@{ + if (it is CancellationException) { + cancellation.cancel() + return@callback + } + + val t = getCompletionExceptionOrNull() + if (t == null) { + source.setResult(getCompleted()) + } else { + source.setException(t as? Exception ?: RuntimeExecutionException(t)) + } + } + + return source.task +} + +/** + * Converts this task to an instance of [Deferred]. + * If task is cancelled then resulting deferred will be cancelled as well. + * However, the opposite is not true: if the deferred is cancelled, the [Task] will not be cancelled. + * For bi-directional cancellation, an overload that accepts [CancellationTokenSource] can be used. + */ +fun Task.asDeferred(): Deferred = asDeferredImpl(null) + +/** + * Converts this task to an instance of [Deferred] with a [CancellationTokenSource] to control cancellation. + * The cancellation of this function is bi-directional: + * * If the given task is cancelled, the resulting deferred will be cancelled. + * * If the resulting deferred is cancelled, the provided [cancellationTokenSource] will be cancelled. + * + * Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and + * leads to an unspecified behaviour. + */ +@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0 +fun Task.asDeferred(cancellationTokenSource: CancellationTokenSource): Deferred = + asDeferredImpl(cancellationTokenSource) + +private fun Task.asDeferredImpl(cancellationTokenSource: CancellationTokenSource?): Deferred { + val deferred = CompletableDeferred() + if (isComplete) { + val e = exception + if (e == null) { + if (isCanceled) { + deferred.cancel() + } else { + @Suppress("UNCHECKED_CAST") + deferred.complete(result as T) + } + } else { + deferred.completeExceptionally(e) + } + } else { + addOnCompleteListener { + val e = it.exception + if (e == null) { + @Suppress("UNCHECKED_CAST") + if (it.isCanceled) deferred.cancel() else deferred.complete(it.result as T) + } else { + deferred.completeExceptionally(e) + } + } + } + + if (cancellationTokenSource != null) { + deferred.invokeOnCompletion { + cancellationTokenSource.cancel() + } + } + // Prevent casting to CompletableDeferred and manual completion. + return object : Deferred by deferred {} +} + +/** + * Awaits the completion of the task without blocking a thread. + * + * This suspending function is cancellable. + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * stops waiting for the completion stage and immediately resumes with [CancellationException]. + * + * For bi-directional cancellation, an overload that accepts [CancellationTokenSource] can be used. + */ +suspend fun Task.await(): T = awaitImpl(null) + +/** + * Awaits the completion of the task that is linked to the given [CancellationTokenSource] to control cancellation. + * + * This suspending function is cancellable and cancellation is bi-directional: + * * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * cancels the [cancellationTokenSource] and throws a [CancellationException]. + * * If the task is cancelled, then this function will throw a [CancellationException]. + * + * Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and + * leads to an unspecified behaviour. + */ +@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0 +suspend fun Task.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource) + +private suspend fun Task.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T { + // fast path + if (isComplete) { + val e = exception + return if (e == null) { + if (isCanceled) { + throw CancellationException("Task $this was cancelled normally.") + } else { + @Suppress("UNCHECKED_CAST") + result as T + } + } else { + throw e + } + } + + return suspendCancellableCoroutine { cont -> + addOnCompleteListener { + val e = it.exception + if (e == null) { + @Suppress("UNCHECKED_CAST") + if (it.isCanceled) cont.cancel() else cont.resume(it.result as T) + } else { + cont.resumeWithException(e) + } + } + + if (cancellationTokenSource != null) { + cont.invokeOnCancellation { + cancellationTokenSource.cancel() + } + } + } +} diff --git a/settings.gradle b/settings.gradle index dff2bb69..1d7ba5c6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,6 +40,7 @@ include ':play-services-nearby-core-proto' include ':play-services-wearable-proto' include ':play-services-basement-ktx' +include ':play-services-tasks-ktx' include ':play-services-base-core' include ':play-services-conscrypt-provider-core'