1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:JvmName("PausingDispatcherKt")
18 
19 package androidx.lifecycle
20 
21 import kotlin.coroutines.CoroutineContext
22 import kotlinx.coroutines.CoroutineDispatcher
23 import kotlinx.coroutines.CoroutineScope
24 import kotlinx.coroutines.Dispatchers
25 import kotlinx.coroutines.Job
26 import kotlinx.coroutines.Runnable
27 import kotlinx.coroutines.withContext
28 
29 /**
30  * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
31  * [Lifecycle.State.CREATED] state.
32  *
33  * @see Lifecycle.whenStateAtLeast for details
34  */
35 @Deprecated(
36     "whenCreated has been deprecated because it runs the block on a " +
37         "pausing dispatcher that suspends, rather than cancels work when the " +
38         "lifecycle state goes below the given state. Use withCreated for " +
39         "non-suspending work that needs to run only once when the Lifecycle changes."
40 )
41 @Suppress("DEPRECATION")
whenCreatednull42 public suspend fun <T> LifecycleOwner.whenCreated(block: suspend CoroutineScope.() -> T): T =
43     lifecycle.whenCreated(block)
44 
45 /**
46  * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.CREATED] state.
47  *
48  * @see Lifecycle.whenStateAtLeast for details
49  */
50 @Deprecated(
51     "whenCreated has been deprecated because it runs the block on a " +
52         "pausing dispatcher that suspends, rather than cancels work when the " +
53         "lifecycle state goes below the given state. Use withCreated for " +
54         "non-suspending work that needs to run only once when the Lifecycle changes."
55 )
56 @Suppress("DEPRECATION")
57 public suspend fun <T> Lifecycle.whenCreated(block: suspend CoroutineScope.() -> T): T {
58     return whenStateAtLeast(Lifecycle.State.CREATED, block)
59 }
60 
61 /**
62  * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
63  * [Lifecycle.State.STARTED] state.
64  *
65  * @see Lifecycle.whenStateAtLeast for details
66  */
67 @Deprecated(
68     "whenStarted has been deprecated because it runs the block on a " +
69         "pausing dispatcher that suspends, rather than cancels work when the " +
70         "lifecycle state goes below the given state. Use withStarted for " +
71         "non-suspending work that needs to run only once when the Lifecycle changes."
72 )
73 @Suppress("DEPRECATION")
whenStartednull74 public suspend fun <T> LifecycleOwner.whenStarted(block: suspend CoroutineScope.() -> T): T =
75     lifecycle.whenStarted(block)
76 
77 /**
78  * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.STARTED] state.
79  *
80  * @see Lifecycle.whenStateAtLeast for details
81  */
82 @Deprecated(
83     "whenStarted has been deprecated because it runs the block on a " +
84         "pausing dispatcher that suspends, rather than cancels work when the " +
85         "lifecycle state goes below the given state. Use withStarted for " +
86         "non-suspending work that needs to run only once when the Lifecycle changes."
87 )
88 @Suppress("DEPRECATION")
89 public suspend fun <T> Lifecycle.whenStarted(block: suspend CoroutineScope.() -> T): T {
90     return whenStateAtLeast(Lifecycle.State.STARTED, block)
91 }
92 
93 /**
94  * Runs the given block when the [LifecycleOwner]'s [Lifecycle] is at least in
95  * [Lifecycle.State.RESUMED] state.
96  *
97  * @see Lifecycle.whenStateAtLeast for details
98  */
99 @Deprecated(
100     "whenResumed has been deprecated because it runs the block on a " +
101         "pausing dispatcher that suspends, rather than cancels work when the " +
102         "lifecycle state goes below the given state. Use withResumed for " +
103         "non-suspending work that needs to run only once when the Lifecycle changes."
104 )
105 @Suppress("DEPRECATION")
whenResumednull106 public suspend fun <T> LifecycleOwner.whenResumed(block: suspend CoroutineScope.() -> T): T =
107     lifecycle.whenResumed(block)
108 
109 /**
110  * Runs the given block when the [Lifecycle] is at least in [Lifecycle.State.RESUMED] state.
111  *
112  * @see Lifecycle.whenStateAtLeast for details
113  */
114 @Deprecated(
115     "whenResumed has been deprecated because it runs the block on a " +
116         "pausing dispatcher that suspends, rather than cancels work when the " +
117         "lifecycle state goes below the given state. Use withResumed for " +
118         "non-suspending work that needs to run only once when the Lifecycle changes."
119 )
120 @Suppress("DEPRECATION")
121 public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {
122     return whenStateAtLeast(Lifecycle.State.RESUMED, block)
123 }
124 
125 /**
126  * Runs the given [block] on a [CoroutineDispatcher] that executes the [block] on the main thread
127  * and suspends the execution unless the [Lifecycle]'s state is at least [minState].
128  *
129  * If the [Lifecycle] moves to a lesser state while the [block] is running, the [block] will be
130  * suspended until the [Lifecycle] reaches to a state greater or equal to [minState].
131  *
132  * Note that this won't effect any sub coroutine if they use a different [CoroutineDispatcher].
133  * However, the [block] will not resume execution when the sub coroutine finishes unless the
134  * [Lifecycle] is at least in [minState].
135  *
136  * If the [Lifecycle] is destroyed while the [block] is suspended, the [block] will be cancelled
137  * which will also cancel any child coroutine launched inside the [block].
138  *
139  * If you have a `try finally` block in your code, the `finally` might run after the [Lifecycle]
140  * moves outside the desired state. It is recommended to check the [Lifecycle.getCurrentState]
141  * before accessing the UI. Similarly, if you have a `catch` statement that might catch
142  * `CancellationException`, you should check the [Lifecycle.getCurrentState] before accessing the
143  * UI. See the sample below for more details.
144  *
145  * ```
146  * // running a block of code only if lifecycle is STARTED
147  * viewLifecycle.whenStateAtLeast(Lifecycle.State.STARTED) {
148  *     // here, we are on the main thread and view lifecycle is guaranteed to be STARTED or RESUMED.
149  *     // We can safely access our views.
150  *     loadingBar.visibility = View.VISIBLE
151  *     try {
152  *         // we can call any suspend function
153  *         val data = withContext(Dispatchers.IO) {
154  *             // this will run in IO thread pool. It will keep running as long as Lifecycle
155  *             // is not DESTROYED. If it is destroyed, this coroutine will be cancelled as well.
156  *             // However, we CANNOT access Views here.
157  *
158  *             // We are using withContext(Dispatchers.IO) here just for demonstration purposes.
159  *             // Such code should live in your business logic classes and your UI should use a
160  *             // ViewModel (or similar) to access it.
161  *             api.getUser()
162  *         }
163  *         // this line will execute on the main thread and only if the lifecycle is in at least
164  *         // STARTED state (STARTED is the parameter we've passed to whenStateAtLeast)
165  *         // Because of this guarantee, we can safely access the UI again.
166  *         loadingBar.visibility = View.GONE
167  *         nameTextView.text = user.name
168  *         lastNameTextView.text = user.lastName
169  *     } catch(ex : UserNotFoundException) {
170  *         // same as above, this code can safely access UI elements because it only runs if
171  *         // view lifecycle is at least STARTED
172  *         loadingBar.visibility = View.GONE
173  *         showErrorDialog(ex)
174  *     } catch(th : Throwable) {
175  *          // Unlike the catch statement above, this catch statements it too generic and might
176  *          // also catch the CancellationException. Before accessing UI, you should check isActive
177  *          // or lifecycle state
178  *          if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
179  *              // here you can access the view because you've checked the coroutine is active
180  *          }
181  *     } finally {
182  *         // in case of cancellation, this line might run even if the Lifecycle is not DESTROYED.
183  *         // You cannot access Views here unless you check `isActive` or lifecycle state
184  *         if (viewLifecycle.currentState >= Lifecycle.State.STARTED) {
185  *             // safe to access views
186  *         } else {
187  *             // not safe to access views
188  *         }
189  *     }
190  * }
191  * ```
192  *
193  * @param minState The desired minimum state to run the [block].
194  * @param block The block to run when the lifecycle is at least in [minState].
195  * @return <T> The return value of the [block]
196  */
197 @Deprecated(
198     "whenStateAtLeast has been deprecated because it runs the block on a " +
199         "pausing dispatcher that suspends, rather than cancels work when the " +
200         "lifecycle state goes below the given state. Use withStateAtLeast for " +
201         "non-suspending work that needs to run only once when the Lifecycle changes."
202 )
whenStateAtLeastnull203 public suspend fun <T> Lifecycle.whenStateAtLeast(
204     minState: Lifecycle.State,
205     block: suspend CoroutineScope.() -> T
206 ): T =
207     withContext(Dispatchers.Main.immediate) {
208         val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")
209         val dispatcher = PausingDispatcher()
210         val controller =
211             LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)
212         try {
213             withContext(dispatcher, block)
214         } finally {
215             controller.finish()
216         }
217     }
218 
219 /**
220  * A [CoroutineDispatcher] implementation that maintains a dispatch queue to be able to pause
221  * execution of coroutines.
222  *
223  * @see [DispatchQueue] and [Lifecycle.whenStateAtLeast] for details.
224  */
225 internal class PausingDispatcher : CoroutineDispatcher() {
226     /** helper class to maintain state and enqueued continuations. */
227     @JvmField internal val dispatchQueue = DispatchQueue()
228 
isDispatchNeedednull229     override fun isDispatchNeeded(context: CoroutineContext): Boolean {
230         if (Dispatchers.Main.immediate.isDispatchNeeded(context)) {
231             return true
232         }
233         // It's safe to call dispatchQueue.canRun() here because
234         // Dispatchers.Main.immediate.isDispatchNeeded returns true if we're not on the main thread
235         // If the queue is paused right now we need to dispatch so that the block is added to the
236         // the queue
237         return !dispatchQueue.canRun()
238     }
239 
dispatchnull240     override fun dispatch(context: CoroutineContext, block: Runnable) {
241         dispatchQueue.dispatchAndEnqueue(context, block)
242     }
243 }
244