AndroidのForeground Serviceとbind

公開日: 2020年11月20日最終更新日: 2021年10月17日

androidのServiceのうちforeground serviceについて、動かしながら見ていきます。
ソースコードはhttps://github.com/Gan0803/ServiceBasicStudyにあります。

ちなみにServiceに関するドキュメントはdevelopersのサービスの概要にあります。

フォアグラウンド サービスは、ユーザーが認識できる操作を行います。たとえば、オーディオ アプリは、フォアグラウンド サービスを使用してオーディオ トラックを再生します。フォアグラウンド サービスは通知を表示する必要があります。フォアグラウンド サービスは、ユーザーがアプリを操作していない間も動作し続けます。

Android 8.0以降、システムの負荷が増えてユーザーエクスペリエンスが低下することを防ぐためにバックグラウンド実行制限が課されています。
これによって、アプリの画面を表示していない間、長時間動作を続けるServiceを実現するためにはフォアグラウンドサービスを使う必要があります。

Serviceのライフサイクル

Serviceのライフサイクルは次の図の通りです(https://developer.android.com/images/service_lifecycle.png?hl=ja より)。

Serviceのライフサイクル

2パターンありますが、Serviceの起動方法によって呼ばれるコールバックメソッドが変わります。
ActivityなどでstartService()を呼んでServiceを起動するとonStartCommand()が呼ばれます。一方でbindService()を呼んでServiceを起動するとonBind()が呼ばれます。
また、onStartCommand()でServiceを起動した後にbindService()を呼んでServiceにbindすることもできます(すでにServiceは起動しているため二重に起動はしませんが、onBind()が呼ばれます)。

動作を確認するために、Foreground Serviceを作ってbindするサンプルアプリを作りました。ソースコードはhttps://github.com/Gan0803/ServiceBasicStudyにあります。 実際にサンプルアプリを動かして確認してみます。

service-lifecycle.gif

ここでは以下の順番でメソッドが呼ばれるように操作しています。

  1. bind前にServiceのメソッド呼び出し
  2. startForegroundService()
  3. bindService()
  4. Serviceのメソッド呼び出し
  5. unBind()
  6. bindService()
  7. unBind() + stopService()

logcatの出力も合わせて確認すると、startForegroundService()、bindService()どちらから呼び出しても最初にonCreateが一度だけ呼ばれていることもわかります。

adb shell dumpsys activity s MyServiceを実行してServiceの起動状況を確認することができます。Serviceが起動していると以下のようにServiceRecordが表示されます。stopService()を呼ぶとServiceRecordが消えます。

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)

Foreground Service実行時には以下のようなNotificationが表示されます。

notification.png

ソースコードの解説

AndroidManifest.xml

foreground serviceを使うにはandroid.permission.FOREGROUND_SERVICEのパーミッションが必要です。 AndroidManifestで<uses-permission>を使って、以下のように宣言します。

AndroidManifest.xml

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

また、Serviceをアプリのコンポーネントとして宣言するため、<service>タグをAndroidManifest.xmlに追記します。今回のServiceは他のアプリから使用しないため、android:exported="false"としています。

AndroidManifest.xml

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

startForegroundService

ボタンの押下時にMainActivityがstartForegroundService()を呼ぶことでServiceを作成しています。

MainActivity.kt

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

これに対応してMyServiceのonStartCommand()が呼び出されます。 startForegroundServiceでServiceが作成されたら、Serviceは5秒以内startForeground()を呼び出す必要があります。

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
    }

Foreground Serviceは通知を表示します。そのためstartForeground()の引数にnotificationを要求します。 そのために事前にNotificationChannelの作成と、notificationのbuildが必要になります。

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

ちなみに、https://developer.android.com/guide/components/services?hl=ja#ExtendingServiceに記載がある通り、onStartCommand()の戻り値は、

システムがサービスを強制終了した場合に、サービスをどのように続行するかを示す値です。

以下のような値がありますが、詳しくはリファレンスドキュメントを参照してください。

START_NOT_STICKY

onStartCommand() から戻った後でシステムがサービスを強制終了した場合、配信が保留中のインテントがない限り、サービスを再作成しません。これは、サービスが不必要で、アプリが未完了のジョブを再開できる場合にサービスを実行してしまうのを回避できる最も安全な選択肢です。

START_STICKY

onStartCommand() から戻った後でシステムがサービスを強制終了した場合、サービスを再作成し、onStartCommand() を呼び出しますが、最後のインテントは再配信しません。代わりに、システムは null インテントで onStartCommand() を呼び出します。ただし、サービスを開始する保留中のインテントがある場合は除きます。その場合は、それらのインテントが配信されます。これは、コマンドは実行しないが、無期限に動作し、ジョブを待機するメディア プレーヤー(または同様のサービス)に適しています。

START_REDELIVER_INTENT

onStartCommand() から戻った後でシステムがサービスを強制終了した場合、サービスを再作成し、サービスに最後に配信されたインテントで onStartCommand() を呼び出します。保留中のすべてのインテントが順に配信されます。これは、ファイルのダウンロードなど、活発にジョブを実行し、直ちに再開する必要のあるサービスに適しています。

bindService

MainActivity.ktからbindService()を呼ぶことでServiceを作成することもできます。また、startForeground()した後にbindService()を呼び出してServiceをbindすることもできます。(bindService()でServiceを作成した後に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)
        }
    }

bindServiceに対してはMyService.ktのonBind()が呼び出されます。ここではBinderのgetService()を呼び出すことで、ActivityからServiceへのアクセスが得られるようにしています。

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
    }

MainActivityでは、サービスへの接続確立時にbinder.getService()しています。これが得られればメソッド呼び出しの要領でServiceの機能を利用することができます。

Serviceのメソッド呼び出し

Serviceをbindすることで、MainActivityからMyServiceのメソッドを呼び出すことができるようになります。

binder.getService()で取得したserviceのメソッドを呼び出すだけです。(ここではServiceが同一プロセスで動いているのでIPCにはなりません。)
ちなみに、このときServiceはUIスレッドで動いています。 別スレッドでなにか処理をしたい場合は、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

unbindServiceを呼び出すとbindを解除します。Service側ではonUnbind()が呼ばれます。
このとき、bindを解除した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()
    }

onUnbindの戻り値でRebindを許可するかしないかを指定することができます。trueにして許可した場合、以降のbindService呼び出しに対してはServiceのonRebind()が呼び出されます。ServiceがActivityとは切り離されて常駐している場合に使うのでしょう。

stopService

MainActivityからstopService()を呼び出すと、Serviceを終了します。Service側ではonDestroy()が呼ばれます。
ここではServiceの終了のための処理を忘れずに行うようにします。

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

その他

Notificationをスワイプして消せない

startForegroundService()で表示させているNorificationはスワイプして消すことはできません。
stopForeground(false)を呼んでForegroundでなくすれば、スワイプで通知を消せるようになります。 stopForeground(false)の引数はすぐに通知を消すかどうかです。 stopForeground(false)を呼んでもServiceが停止するわけではありません。

バックグラウンド処理

IntentServiceというものもありますが、API level 30でdeprecatedになっています。 IntentServiceはバックグラウンド実行制限に引っかかるのでWorkManagerかJobIntentServiceを使います。
JobIntentServiceを使うと、Android 8.0以降ではServiceではなくJobをJobIntentService内部で使用してくれるようです。

バックグラウンド処理を何で実現するとよいかは、バックグラウンド処理ガイドに記載があります。

所感

Serviceは基本的な部分なので知っておくべきことが多いです。今回書いたのはそのほんの一部に過ぎません……。随時コードを触りながら、公式ドキュメントを確認しながら覚えていく必要があります。
Android全般に言えるんですが、覚えても忘れてしまったり、Androidのバージョンアップで一部変わったりするので大変です。