Service

Background

  |   10 min czytania

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