Skip to content

Instantly share code, notes, and snippets.

@cbeyls
Created August 6, 2017 11:30
Show Gist options
  • 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)
}
@LouisCAD
Copy link

@cbeyls I agree that JobIntentService code is quite complex, but a coroutines implementation that would similarly use JobServiceEngine to use JobScheduler on Android O would probably be implemented more easily. I'll look into it.

BTW, in your snippet, you use LinkedListChannel but you could be using Channel() without any buffer, using RendezVousChannel under the hood , without breaking anything, right?

@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