Wstęp
Service jest usługą dziąłającą w tle na głównym wątku aplikacji. Nie posiada interfejsu użytkownika i jest niezależny względem cyklu życia aktywności (Activity). Komponenty mogą związać się do serwisu w celu interakacji z nim. Wykorzystywany przede wszystkim do natychmiastowego wykonywania długich, powtarzalnych lub ciągłych zadań w tle implementowanych na wątku roboczym (np. pobieranie, przetwarzanie danych, odtwarzanie muzyki). Może zostać uruchomiony przez różne komponenty oraz przez inne aplikacje. Jest zdolny do kontynuowania pracy w tle nawet po wyjściu z aplikacji. Może rozpocząć działanie zarówno poprzez wystartowanie jak i podpięcie. Wyróżnia się trzy tryby działania serwisu: background, foreground i bound.
Ograniczenia
Ze względu na wykonywanie pracy całego serwisu na wątku głównym należy zaimplementować kosztowne zadania usługi w oparciu o wątki robocze lub jako alternatywę rozważyć wykorzystanie IntentService. W zależności od wybranego trybu działania serwisu trzeba uwzględnić także konieczność dostarczenia mechanizmu odpowiedzi zwrotnej do klienta oraz restrykcję systemu dla procesów działających w tle. Jeśli wykonywanie zadania ma sens tylko w ramach aktywnego danego ekranu czy też powiązane jest z cyklem życia komponentu należy zrezygnować z wykorzystania Service i użyć innych mechanizmów takich jak np. AsyncTask czy HandlerThread.
Implementacja
Aby stworzyć klasyczny serwis należy rozszerzyć klasę Service oraz dostarczyć implementację podpięcia serwisu i opcjonalnie nadpisać metody cyklu życia. Metoda onStartCommand wywoływana jest w wyniku żądania wystartowania serwisu przez startService lub startForegroundService, a onBind w sytuacji podpięcia serwisu przez bindService. Obie metody mogą zostać wykonane wielokrotnie w ramach działania serwisu. Metody cyklu życia onCreate i onDestroy wywoływane są jeden raz w momencie inicjalizacji i niszczenia serwisu niezależnie od tego czy został on wystartowany (startService) czy podpięty (bindService). Ponadto należy zadeklarować komponent serwisu w pliku AndroidManifest.
class CustomService : Service() {
//use worker thread if long running operation to not block main UI thread
//or provide some multithreading if needed
private val handlerThread = HandlerThread("HandlerThread")
private lateinit var handler : Handler
override fun onBind(intent: Intent?): IBinder? {
//invokes when bindService called, retrieve intent and decide what to do
//if service is created by bindService and onStartCommand wasn't called then runs only as components are bound
return null //provide communication interface or return null when no bind needed
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
//invokes when startService or startForegroundService called, retrieve intent and decide what to do
//continues to run until stops itself or by another component
return START_STICKY //restart service strategy after destroyed by the system
}
override fun onCreate() {
//invokes one time setup before onStartCommand or onBind
//not called when service already running
super.onCreate()
handlerThread.start()
handler = Handler(handlerThread.looper)
}
override fun onDestroy() {
//invokes when stopSelf or stopService is called
//service is no longer used or is being destroyed by system or client
super.onDestroy()
handler.removeCallbacksAndMessages(null)
handlerThread.quitSafely()
handlerThread.interrupt()
}
//more lifecycle callbacks
}
//run startService or bindService and pass Intent to run Service<!-- don't declare intent filters, use explicit intent to start -->
<!-- enabled - can be instantiated by the system, exported - is available for other apps -->
<service
android:name=".CustomService"
android:enabled="true"
android:exported="false"/> Background
Serwis w trybie background przeznaczony jest do wykonywania operacji, których wynik nie jest odnotowany przez użytkownika (np. krótkie zapytanie sieciowe). Wykorzystywany do zadań typu fire and forget, startowany za pomocą metody startService. Ze względu na nałożone ograniczenia systemowe (które nie pozwalają na działanie serwisu w tle bez wiedzy użytkownika) od wersji Android O serwis może działać tylko gdy aplikacja jest w trybie foreground. W przypadku wyjścia z aplikacji lub przeniesienia jej do tła działanie serwisu zostanie po pewnym czasie zatrzymane, chyba że zostanie on przeniesiony do trybu foreground.
class BackgroundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(intent != null && intent.hasExtra("PARAM")) {
val data = intent.getStringExtra("PARAM")
//do something based on param
action()
}
return START_STICKY
}
override fun onDestroy() {
//will destroy on Android Oreo and above if app close
super.onDestroy()
}
private fun action() {
//some work
//inform about finish by Toast
Toast.makeText(this, "Some message", Toast.LENGTH_LONG).show()
//after that service is no longer need, so destroy manual
stopSelf()
}
}
class BackgroundActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, BackgroundService::class.java)
intent.putExtra("PARAM", "value")
startService(intent)
}
override fun onStop() {
super.onStop()
stopService(Intent(this, BackgroundService::class.java))
}
}Foreground
Tryb foreground wykorzystywany jest do wykonywania zadań zauważalnych przez użytkownika (np. odtwarzanie muzyki) i startowany jest za pomocą startForegroundService. Serwis działający jako foreground musi wyświetlać notyfikację (Notification) za pomocą metody startForeground, która informuje użytkownika o działaniu jakiegoś zadania w tle spełniając tym samym ograniczenia systemów od wersji Android O dla usług działających w tle (bez wiedzy użytkownika). Kontynuuje pracę także po wyjściu z aplikacji czy pomimo braku interakacji.
class ForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
//must run service in foreground immediately by show notification
showNotification()
//notification can't be dismissed unless the service is stopped or removed from foreground
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
//do some action
return START_STICKY
}
private fun showNotification() {
//create NotificationChannel if not exists for Android Oreo and above
val channelId = "channel_id"
//customize notification
val notification = NotificationCompat.Builder(this, channelId)
.setOngoing(true)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("title")
.build()
//start service in foreground
startForeground(100, notification)
}
}
class ForegroundActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, ForegroundService::class.java)
intent.putExtra("PARAM", "value")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//background restriction for Android Oreo and above
startForegroundService(intent)
}
else {
//just start as normal
startService(intent)
}
//remove from foreground by calling stopForeground
}
}Bound
W momemencie podpięcia komponentu aplikacji do serwisu za pomocą metody bindService przechodzi on w tryb bound. Taki serwis dostarcza interfejs komunikacji między komponentem, a nim samym oraz umożliwia wysyłanie żądanie i odbieranie wyników. Jeśli nie został wcześniej wystartowany w standardowy sposób przez startService wówczas działa tak długo dopóki jest podpięty przynajmniej przez jeden komponent.
class BoundService : Service() {
inner class CustomBinder : Binder() {
fun getService() : CustomService = this@CustomService
//allow to call public methods
}
private val binder = CustomBinder()
override fun onBind(intent: Intent?): IBinder? {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
//called when all clients disconnect, return true to allow call onRebind
return super.onUnbind(intent)
}
override fun onRebind(intent: Intent?) {
//called when new client connect after all had disconnected
super.onRebind(intent)
}
//can be used by Binder
fun action() {
//some work
}
}
class BoundActivity : AppCompatActivity() {
private var service : CustomService? = null
private var isBound = false
private val connection = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as CustomService.CustomBinder
this@MainActivity.service = binder.getService()
isBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
isBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(this, CustomService::class.java)
intent.putExtra("PARAM", "value")
bindService(intent, connection, Context.BIND_AUTO_CREATE)
//use service binder action to communicate
button.setOnClickListener { service?.action() }
}
override fun onStop() {
super.onStop()
unbindService(connection)
isBound = false
}
}