Foreground service and bind of Android

公開日: 2020年11月20日最終更新日: 2022年01月28日

I will look at the foreground service of android services while running it.
Source code is available at https://github.com/Gan0803/ServiceBasicStudy

By the way, the documentation for the Service is available in the developers' [Service overview] (https://developer.android.com/guide/components/services).

A foreground service performs some operation that is noticeable to the user. For example, an audio app would use a foreground service to play an audio track. Foreground services must display a Notification. Foreground services continue running even when the user isn't interacting with the app.

Since Android 8.0, Background Execution Limits has been imposed to prevent the system load from increasing and degrading the user experience. This requires the use of foreground services to achieve a Service that keeps running for long periods of time while the app is not on the screen.

Service Lifecycle

The lifecycle of the service is as shown in the following figure(https://developer.android.com/images/service_lifecycle.png)。

Service Lifecycle

There are two patterns, and the callback method is different depending on how to start the Service.
Activity calls startService() and starts the Service, it calls onStartCommand(). On the other hand, calling bindService() to start a Service will call onBind().
Alternatively, you can start the Service with onStartCommand() and then call bindService() to bind the Service (since the Service is already started, there is no double start, but onBind() will be called).

To see how it works, I created a sample application to create and bind the Foreground Service. The source code can be found at https://github.com/Gan0803/ServiceBasicStudy. Let's try running the sample app to see how it works.

service-lifecycle.gif

Here are the operations in which the methods are called in the following order:

  1. service method calls before bind
  2. startForegroundService()
  3. bindService()
  4. service method calls
  5. unBind()
  6. bindService()
  7. unBind() + stopService()

If you check the output of logcat as well, you will also see that onCreate is called only once at the beginning, regardless of whether it is called by startForegroundService() or bindService().

You can check the activation status of the Service by running adb shell dumpsys activity s MyService. When stopService() is called, the ServiceRecord will disappear.

adb shell dumpsys activity s MyService
ACTIVITY MANAGER SERVICES (dumpsys activity services)
  User 0 active services:
  * ServiceRecord{b934ad7 u0 gan0803.pj.study.servicebasicstudy/.MyService}
    intent={cmp=gan0803.pj.study.servicebasicstudy/.MyService}
    packageName=gan0803.pj.study.servicebasicstudy
    processName=gan0803.pj.study.servicebasicstudy
    baseDir=/data/app/gan0803.pj.study.servicebasicstudy-cSLjH7gk7eTn1aU0g_KfNQ==/base.apk
    dataDir=/data/user/0/gan0803.pj.study.servicebasicstudy
    app=ProcessRecord{628ea18 16664:gan0803.pj.study.servicebasicstudy/u0a140}
    createTime=-1s956ms startingBgTimeout=--
    lastActivity=-1s956ms restartTime=-1s956ms createdFromFg=true
    Bindings:
    * IntentBindRecord{c18f5e2 CREATE}:
      intent={cmp=gan0803.pj.study.servicebasicstudy/.MyService}
      binder=android.os.BinderProxy@57bd73
      requested=true received=true hasBound=true doRebind=false
      * Client AppBindRecord{b06e430 ProcessRecord{628ea18 16664:gan0803.pj.study.servicebasicstudy/u0a140}}
        Per-process Connections:
          ConnectionRecord{aa68a56 u0 CR gan0803.pj.study.servicebasicstudy/.MyService:@d4f2c71}
    All Connections:
      ConnectionRecord{aa68a56 u0 CR gan0803.pj.study.servicebasicstudy/.MyService:@d4f2c71}

  Connection bindings to services:
  * ConnectionRecord{aa68a56 u0 CR gan0803.pj.study.servicebasicstudy/.MyService:@d4f2c71}
    binding=AppBindRecord{b06e430 gan0803.pj.study.servicebasicstudy/.MyService:gan0803.pj.study.servicebasicstudy}
    activity=ActivityRecord{91aabb u0 gan0803.pj.study.servicebasicstudy/.MainActivity t123}
    conn=android.os.BinderProxy@d4f2c71 flags=0x1

adb shell dumpsys activity s MyService
ACTIVITY MANAGER SERVICES (dumpsys activity services)
  (nothing)

When the Foreground Service is running you will see a notification like the following:

notification.png

Source Code Description

AndroidManifest.xml

The permission android.permission.FOREGROUND_SERVICE is required to use the foreground service. Use <uses-permission> in AndroidManifest to create the following Declare as:

AndroidManifest.xml

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Also, to declare the Service as a component of the app, the <service> tag has been added to AndroidManifest.xml. Because this service is not used by other apps, I have set android:exported="false".

AndroidManifest.xml

    <service
        android:name=".MyService"
        android:enabled="true"
        android:exported="false" />

startForegroundService

Service is created by the MainActivity calling startForegroundService() when the button is pressed.

: MainActivity.kt

    private fun startMyService() {
        val intent = Intent(this, MyService::class.java)
        if (Build.VERSION.SDK_INT >= 26) {
            startForegroundService(intent)
        } else {
            startService(intent)
        }
    }

Correspondingly, MyService's onStartCommand() is called. Once the Service is created by startForegroundService, the Service must call startForeground() within 5 seconds.

MyService.kt

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // The service is starting, due to a call to startService()
        Log.d(TAG, "onStartCommand")
        Toast.makeText(applicationContext, "onStartCommand", Toast.LENGTH_SHORT).show()

        val notification = buildNotification(this, channelId)
        startForeground(ONGOING_NOTIFICATION_ID, notification)
        return startMode
    }

The Foreground Service displays the notification. Therefore, it requires the notification as an argument to startForeground(). This requires the creation of a NotificationChannel and the building of a notification beforehand.

MyService.kt

    private val channelId by lazy {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel(this, CHANNEL_ID, CHANNEL_NAME)
        } else {
            // If earlier version channel ID is not used
            ""
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createNotificationChannel(
        context: Context,
        channelId: String,
        channelName: String
    ): String {
        val channel = NotificationChannel(
            channelId,
            channelName, NotificationManager.IMPORTANCE_NONE
        )
        channel.lightColor = Color.BLUE
        channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        val service = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        service.createNotificationChannel(channel)
        return channelId
    }

    private fun buildNotification(context: Context, channelId: String): Notification {
        val intent = Intent(context, MainActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
        return NotificationCompat.Builder(context, channelId)
            .setContentTitle("Service Basic Study")
            .setContentText("Application is active.")
            .setSmallIcon(R.drawable.ic_baseline_tag_faces_24)
            .setContentIntent(pendingIntent)
            .setTicker("Application is active")
            .build()
    }

By the way, https://developer.android.com/guide/components/services#ExtendingService, the return value of onStartCommand() will be

The integer is a value that describes how the system should continue the service in the event that the system kills it.

The following values are available, please refer to the reference document (https://developer.android.com/reference/android/app/Service#constants_1) for more information.

START_NOT_STICKY

If the system kills the service after onStartCommand() returns, do not recreate the service unless there are pending intents to deliver. This is the safest option to avoid running your service when not necessary and when your application can simply restart any unfinished jobs.

START_STICKY

If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand(), but do not redeliver the last intent. Instead, the system calls onStartCommand() with a null intent unless there are pending intents to start the service. In that case, those intents are delivered. This is suitable for media players (or similar services) that are not executing commands but are running indefinitely and waiting for a job.

START_REDELIVER_INTENT

If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand() with the last intent that was delivered to the service. Any pending intents are delivered in turn. This is suitable for services that are actively performing a job that should be immediately resumed, such as downloading a file.

bindService

You can also call bindService() from MainActivity.kt to create a Service. Alternatively, you can call bindService() after startForeground() and then bind the Service by calling bindService(). (You can also create a Service with bindService() and then call startForeground().

MainActivity.kt

    private fun bindMyService() {
        // We can also write like this using "also".
        Intent(this, MyService::class.java).also { intent ->
            bindService(intent, connection, Context.BIND_AUTO_CREATE)
        }
    }

For bindService, MyService.kt's onBind() is called. Here, Binder's getService() is called to get access to the Service from the Activity.

MyService.kt

    private var binder = MyBinder()

    override fun onBind(intent: Intent): IBinder? {
        // A client is binding to the service with bindService()
        Log.d(TAG, "onBind")
        Toast.makeText(applicationContext, "onBind", Toast.LENGTH_SHORT).show()
        return binder
    }

    inner class MyBinder : Binder() {
        fun getService(): MyService = this@MyService
    }

In MainActivity, we call binder.getService() when establishing a connection to a service. Once this is obtained, you can use the functionality of the Service in the manner of a method call.

Service method calls

By binding the service, you will be able to call MyService methods from MainActivity.

Just call the service method retrieved by binder.getService() (here the service is running in the same process, so it is not an IPC. (Here, the Service is running in the same process, so it's not an IPC.
By the way, the Service is running in a UI thread at this time.
If you want to do something in a separate thread, you need to create a thread in the Service.

MainActivity.kt

    private fun callMyServiceMethod() {
        if (isServiceBound) {
            service.awesomeMethod("Hello MyService!")
        }
    }

MyService.kt

    fun awesomeMethod(msg: String) {
        Log.d(TAG, "awesomeMethod")

        Toast.makeText(
            applicationContext,
            "isUiThread $isUiThread / $msg",
            Toast.LENGTH_SHORT
        ).show()
    }

Unbind, Rebind

Calling unbindService() will unbind it; on the Service side, onUnbind() is called.
When calling unbindService, make sure to set the flags and clear references to avoid calling the unbind Service.

MainActivity.kt

    private fun unbindMyService() {
        if (isServiceBound) {
            unbindService(connection)
            isServiceBound = false
        }
    }

MyService.kt

    override fun onUnbind(intent: Intent): Boolean {
        // All clients have unbound with unbindService()
        Log.d(TAG, "onUnbind")
        Toast.makeText(applicationContext, "onUnbind", Toast.LENGTH_SHORT).show()

        return allowRebind
    }

    override fun onRebind(intent: Intent) {
        // A client is binding to the service with bindService(),
        // after onUnbind() has already been called
        Log.d(TAG, "onRebind")
        Toast.makeText(applicationContext, "onRebind", Toast.LENGTH_SHORT).show()
    }

The return value of "onUnbind" allows you to specify whether or not to allow Rebind, and if true, then the Service onRebind() will be called on subsequent bindService calls. It would be used if it were resident.

stopService

Calling stopService() from the MainActivity will terminate the Service.
Don't forget to terminate the Service here.

MyActivity.kt

    private fun stopMyService() {
        unbindMyService()
        val intent = Intent(this, MyService::class.java)
        stopService(intent)
    }

MyService.kt

    override fun onDestroy() {
        // The service is no longer used and is being destroyed
        Log.d(TAG, "onDestroy")
        Toast.makeText(applicationContext, "onDestroy", Toast.LENGTH_SHORT).show()
    }

Misc

Can't swipe the notification and turn it off

Norification displayed by startForegroundService() cannot be swiped off.
If you call stopForeground(false) and no longer use Foreground, you will be able to swipe out the notification. The argument of stopForeground(false) is whether to get rid of the notification immediately. Calling stopForeground(false) does not stop the Service.

Background Processing

There is also an IntentService, but it is deprecated at API level 30. IntentService has been trapped by [Background Execution Limitation] (https://developer.android.com/about/versions/oreo/background), so you should use WorkManager or JobIntentService and use JobIntentService.
Using JobIntentService, Android 8.0 and later uses the Job instead of the Service inside JobIntentService.

Background processing is described in the Guide to background processing.

Impressions

Service is a basic part and there is a lot to know about it. What I've written in this article is only part of it. I'll need to learn it by touching the code from time to time and checking the official documentation.
As is true for Android in general, it's hard to remember the code and forget it, and some of it changes with the Android versions.