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'