Skip to content

Instantly share code, notes, and snippets.

@cbeyls
Created August 6, 2017 11:30
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cbeyls/e7f874a480934d2a802873f8f8d91549 to your computer and use it in GitHub Desktop.
Save cbeyls/e7f874a480934d2a802873f8f8d91549 to your computer and use it in GitHub Desktop.
An Intent Service processing work in order from a Kotlin coroutine running on the main thread.
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)
}
@cbeyls
Copy link
Author

cbeyls commented Jan 26, 2018

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment