-
-
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
JobIntentService
code is quite complex, but a coroutines implementation that would similarly useJobServiceEngine
to useJobScheduler
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 usingChannel()
without any buffer, usingRendezVousChannel
under the hood , without breaking anything, right?