Skip to content

Commit 8cf0f36

Browse files
qwwdfsadalexvanyodkhalanskyjb
authored andcommitted
Introduce Task.await and Task.asDeferred with CancellationTokenSource (Kotlin#2786)
* Support bi-directional cancellation for Task.asDeferred and Task.await via passed in CancellationTokenSource Fixes Kotlin#2527 Co-authored-by: Alex Vanyo <[email protected]> Co-authored-by: dkhalanskyjb <[email protected]>
1 parent f661918 commit 8cf0f36

File tree

4 files changed

+349
-27
lines changed

4 files changed

+349
-27
lines changed

integration/kotlinx-coroutines-play-services/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Extension functions:
66

77
| **Name** | **Description**
88
| -------- | ---------------
9+
| [Task.asDeferred][asDeferred] | Converts a Task into a Deferred
910
| [Task.await][await] | Awaits for completion of the Task (cancellable)
1011
| [Deferred.asTask][asTask] | Converts a deferred value to a Task
1112

@@ -25,5 +26,14 @@ val snapshot = try {
2526
// Do stuff
2627
```
2728

29+
If the `Task` supports cancellation via passing a `CancellationToken`, pass the corresponding `CancellationTokenSource` to `asDeferred` or `await` to support bi-directional cancellation:
30+
31+
```kotlin
32+
val cancellationTokenSource = CancellationTokenSource()
33+
val currentLocationTask = fusedLocationProviderClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, cancellationTokenSource.token)
34+
val currentLocation = currentLocationTask.await(cancellationTokenSource) // cancelling `await` also cancels `currentLocationTask`, and vice versa
35+
```
36+
37+
[asDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/as-deferred.html
2838
[await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/await.html
2939
[asTask]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/kotlinx.coroutines.-deferred/as-task.html
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
public final class kotlinx/coroutines/tasks/TasksKt {
22
public static final fun asDeferred (Lcom/google/android/gms/tasks/Task;)Lkotlinx/coroutines/Deferred;
3+
public static final fun asDeferred (Lcom/google/android/gms/tasks/Task;Lcom/google/android/gms/tasks/CancellationTokenSource;)Lkotlinx/coroutines/Deferred;
34
public static final fun asTask (Lkotlinx/coroutines/Deferred;)Lcom/google/android/gms/tasks/Task;
5+
public static final fun await (Lcom/google/android/gms/tasks/Task;Lcom/google/android/gms/tasks/CancellationTokenSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
46
public static final fun await (Lcom/google/android/gms/tasks/Task;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
57
}
68

integration/kotlinx-coroutines-play-services/src/Tasks.kt

+72-27
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,8 @@
66

77
package kotlinx.coroutines.tasks
88

9-
import com.google.android.gms.tasks.CancellationTokenSource
10-
import com.google.android.gms.tasks.RuntimeExecutionException
11-
import com.google.android.gms.tasks.Task
12-
import com.google.android.gms.tasks.TaskCompletionSource
13-
import kotlinx.coroutines.CancellationException
14-
import kotlinx.coroutines.CompletableDeferred
15-
import kotlinx.coroutines.Deferred
16-
import kotlinx.coroutines.Job
17-
import kotlinx.coroutines.suspendCancellableCoroutine
9+
import com.google.android.gms.tasks.*
10+
import kotlinx.coroutines.*
1811
import kotlin.coroutines.*
1912

2013
/**
@@ -45,39 +38,85 @@ public fun <T> Deferred<T>.asTask(): Task<T> {
4538
/**
4639
* Converts this task to an instance of [Deferred].
4740
* If task is cancelled then resulting deferred will be cancelled as well.
41+
* However, the opposite is not true: if the deferred is cancelled, the [Task] will not be cancelled.
42+
* For bi-directional cancellation, an overload that accepts [CancellationTokenSource] can be used.
4843
*/
49-
public fun <T> Task<T>.asDeferred(): Deferred<T> {
44+
public fun <T> Task<T>.asDeferred(): Deferred<T> = asDeferredImpl(null)
45+
46+
/**
47+
* Converts this task to an instance of [Deferred] with a [CancellationTokenSource] to control cancellation.
48+
* The cancellation of this function is bi-directional:
49+
* * If the given task is cancelled, the resulting deferred will be cancelled.
50+
* * If the resulting deferred is cancelled, the provided [cancellationTokenSource] will be cancelled.
51+
*
52+
* Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and
53+
* leads to an unspecified behaviour.
54+
*/
55+
@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0
56+
public fun <T> Task<T>.asDeferred(cancellationTokenSource: CancellationTokenSource): Deferred<T> =
57+
asDeferredImpl(cancellationTokenSource)
58+
59+
private fun <T> Task<T>.asDeferredImpl(cancellationTokenSource: CancellationTokenSource?): Deferred<T> {
60+
val deferred = CompletableDeferred<T>()
5061
if (isComplete) {
5162
val e = exception
52-
return if (e == null) {
53-
@Suppress("UNCHECKED_CAST")
54-
CompletableDeferred<T>().apply { if (isCanceled) cancel() else complete(result as T) }
63+
if (e == null) {
64+
if (isCanceled) {
65+
deferred.cancel()
66+
} else {
67+
@Suppress("UNCHECKED_CAST")
68+
deferred.complete(result as T)
69+
}
5570
} else {
56-
CompletableDeferred<T>().apply { completeExceptionally(e) }
71+
deferred.completeExceptionally(e)
72+
}
73+
} else {
74+
addOnCompleteListener {
75+
val e = it.exception
76+
if (e == null) {
77+
@Suppress("UNCHECKED_CAST")
78+
if (it.isCanceled) deferred.cancel() else deferred.complete(it.result as T)
79+
} else {
80+
deferred.completeExceptionally(e)
81+
}
5782
}
5883
}
5984

60-
val result = CompletableDeferred<T>()
61-
addOnCompleteListener {
62-
val e = it.exception
63-
if (e == null) {
64-
@Suppress("UNCHECKED_CAST")
65-
if (isCanceled) result.cancel() else result.complete(it.result as T)
66-
} else {
67-
result.completeExceptionally(e)
85+
if (cancellationTokenSource != null) {
86+
deferred.invokeOnCompletion {
87+
cancellationTokenSource.cancel()
6888
}
6989
}
70-
return result
90+
// Prevent casting to CompletableDeferred and manual completion.
91+
return object : Deferred<T> by deferred {}
7192
}
7293

7394
/**
74-
* Awaits for completion of the task without blocking a thread.
95+
* Awaits the completion of the task without blocking a thread.
7596
*
7697
* This suspending function is cancellable.
7798
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
7899
* stops waiting for the completion stage and immediately resumes with [CancellationException].
100+
*
101+
* For bi-directional cancellation, an overload that accepts [CancellationTokenSource] can be used.
102+
*/
103+
public suspend fun <T> Task<T>.await(): T = awaitImpl(null)
104+
105+
/**
106+
* Awaits the completion of the task that is linked to the given [CancellationTokenSource] to control cancellation.
107+
*
108+
* This suspending function is cancellable and cancellation is bi-directional:
109+
* * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
110+
* cancels the [cancellationTokenSource] and throws a [CancellationException].
111+
* * If the task is cancelled, then this function will throw a [CancellationException].
112+
*
113+
* Providing a [CancellationTokenSource] that is unrelated to the receiving [Task] is not supported and
114+
* leads to an unspecified behaviour.
79115
*/
80-
public suspend fun <T> Task<T>.await(): T {
116+
@ExperimentalCoroutinesApi // Since 1.5.1, tentatively until 1.6.0
117+
public suspend fun <T> Task<T>.await(cancellationTokenSource: CancellationTokenSource): T = awaitImpl(cancellationTokenSource)
118+
119+
private suspend fun <T> Task<T>.awaitImpl(cancellationTokenSource: CancellationTokenSource?): T {
81120
// fast path
82121
if (isComplete) {
83122
val e = exception
@@ -95,13 +134,19 @@ public suspend fun <T> Task<T>.await(): T {
95134

96135
return suspendCancellableCoroutine { cont ->
97136
addOnCompleteListener {
98-
val e = exception
137+
val e = it.exception
99138
if (e == null) {
100139
@Suppress("UNCHECKED_CAST")
101-
if (isCanceled) cont.cancel() else cont.resume(result as T)
140+
if (it.isCanceled) cont.cancel() else cont.resume(it.result as T)
102141
} else {
103142
cont.resumeWithException(e)
104143
}
105144
}
145+
146+
if (cancellationTokenSource != null) {
147+
cont.invokeOnCancellation {
148+
cancellationTokenSource.cancel()
149+
}
150+
}
106151
}
107152
}

0 commit comments

Comments
 (0)