-
-
Save cbeyls/e7f874a480934d2a802873f8f8d91549 to your computer and use it in GitHub Desktop.
| package be.digitalia.common.services | |
| import android.app.Service | |
| import android.content.Intent | |
| import android.os.Message | |
| import kotlinx.coroutines.experimental.android.UI | |
| import kotlinx.coroutines.experimental.channels.Channel | |
| import kotlinx.coroutines.experimental.channels.LinkedListChannel | |
| import kotlinx.coroutines.experimental.launch | |
| /** | |
| * An Intent Service processing work in order from a coroutine running on the main thread. | |
| * | |
| * @author Christophe Beyls | |
| */ | |
| abstract class SuspendIntentService : Service() { | |
| private val channel: Channel<Message> = LinkedListChannel() | |
| /** | |
| * Sets intent redelivery preferences. Usually called from the constructor | |
| * with your preferred semantics. | |
| * | |
| * <p>If enabled is true, | |
| * {@link #onStartCommand(Intent, int, int)} will return | |
| * {@link Service#START_REDELIVER_INTENT}, so if this process dies before | |
| * {@link #onHandleIntent(Intent)} returns, the process will be restarted | |
| * and the intent redelivered. If multiple Intents have been sent, only | |
| * the most recent one is guaranteed to be redelivered. | |
| * | |
| * <p>If enabled is false (the default), | |
| * {@link #onStartCommand(Intent, int, int)} will return | |
| * {@link Service#START_NOT_STICKY}, and if the process dies, the Intent | |
| * dies along with it. | |
| */ | |
| var intentRedelivery: Boolean = false | |
| override fun onCreate() { | |
| super.onCreate() | |
| launch(UI) { | |
| for (msg in channel) { | |
| try { | |
| onHandleIntent(msg.obj as Intent) | |
| stopSelf(msg.arg1) | |
| } finally { | |
| msg.recycle() | |
| } | |
| } | |
| } | |
| } | |
| override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { | |
| val msg = Message.obtain().apply { | |
| arg1 = startId | |
| obj = intent | |
| } | |
| channel.offer(msg) | |
| return if (intentRedelivery) Service.START_REDELIVER_INTENT | |
| else Service.START_NOT_STICKY | |
| } | |
| override fun onDestroy() { | |
| // Remove remaining messages | |
| var msg = channel.poll() | |
| while (msg != null) { | |
| msg.recycle() | |
| msg = channel.poll() | |
| } | |
| channel.close() | |
| } | |
| override fun onBind(intent: Intent) = null | |
| /** | |
| * This will be called from a coroutine running on the main thread. | |
| * Execution may be suspended and resumed in order to wait for long-running operations to complete. | |
| */ | |
| protected abstract suspend fun onHandleIntent(intent: Intent) | |
| } |
@LouisCAD This was mainly a proof-of-concept for Kotlin coroutines to be used as a replacement for IntentService when you don't need background threads. When jobs are not always launched by a foreground app, you should use JobIntentService instead which handles all the complexity for you and I don't see the benefit of reimplementing it using coroutines and having to maintain this code as the support library implementation will certainly be improved over time.
I use LinkedListChannel because it's the only channel that never suspends the sender. You can't suspend the sender because the onStartCommand() method must return immediately, it's not a suspending function. This is similar to the framework's IntentService which uses a LinkedList to queue the incoming Intents. A circular array like ArrayDeque would have been a bit more efficient but there is no equivalent for channels.
@cbeyls I agree that
JobIntentServicecode is quite complex, but a coroutines implementation that would similarly useJobServiceEngineto useJobScheduleron Android O would probably be implemented more easily. I'll look into it.BTW, in your snippet, you use
LinkedListChannelbut you could be usingChannel()without any buffer, usingRendezVousChannelunder the hood , without breaking anything, right?