-
-
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) | |
} |
Probably yes, but the code of JobIntentService
is quite complex. Also, JobIntentService
uses a thread pool which is already more efficient than IntentService
which creates a new thread everytime it's created.
@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?
@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.
Hi @cbeyls,
A JobIntentService could be made using coroutines too, right?