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
}
}