Android
Kotlin
service
Oreo

Foreground Serviceの基本

検証環境

この記事の内容は、以下の環境で検証した。
* Java:open jdk 1.8.0_152
* Kotlin 1.2.10
* Android Studio 3.0.2
* CompileSdkVersion:26

はじめに

Android 8.0であるOreoからバックグラウンドに関して、制限がかかってきます。
その制限から既存のServiceを救う方法の記事です。

Foreground Serviceとは・・・

Foreground Serviceとは、通常のサービスと違い、通知(Notification)を表示し、バックグラウンドで実行している事をユーザに認識させた状態で実行するものです。
処理自体はMainスレッドとは別スレッドで実行されているが、ユーザが通知を通じ認識できるため、Foreground Serviceとなります。

完成イメージ

Foreground Serviceか開始してから終わるまで通知は消せません。
サービスの終了とともに通知を消すことも可能です。
このサービスは内部で5秒スリープして、スリープ後に通知を消すサンプルです。

Untitled (1).png

実装方法

実装方法がminSdkVersionによって異なりますが、
共通していけることは、サービスを実行するonStartCommandメソッド内で
通知(Notification)を表示させます。
そして、その通知をstartForegroundメソッドで呼び出すことで
ユーザが認識できる「Foreground Service」になります。

実装方法は以下の通りです。

レイアウトXML

レイアウト画面は共通です。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="jp.co.casareal.foregroundservicekotlin.MainActivity">

    <Button
        android:id="@+id/buttonServiceStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_start_service"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

minSdkVersionが26未満の場合

Activityクラス

サービスの起動方法は今まで通り。

MainActivity.kt
package jp.co.casareal.foregroundservicekotlin

import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.foregroundservicekotlin.service.ForegroundService
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        buttonServiceStart.setOnClickListener {
            val serviceIntent = Intent(this, ForegroundService::class.java)
            startService(serviceIntent)
        }
    }
}

Serviceクラス

サービスクラスの全体像は以下の通り。

ForegroundService.kt
package jp.co.casareal.foregroundservicekotlin.service

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.support.v4.app.NotificationCompat
import jp.co.casareal.foregroundservicekotlin.R

class ForegroundService : Service() {

    override fun onBind(intent: Intent): IBinder? {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = NotificationCompat.Builder(this).apply {
            mContentTitle = "通知のタイトル"
            mContentText = "通知の内容"
            setSmallIcon(R.mipmap.ic_launcher)
        }.build()

        Thread(
                Runnable {
                    (0..5).map {
                        Thread.sleep(1000)

                    }

                    stopForeground(true)
                    // もしくは
                    // stopSelf()

                }).start()

        startForeground(1, notification)

        return START_STICKY
    }
}

コードの説明

①Notificationの作成
サービスが起動している最中の通知を作成しています。

val notification = NotificationCompat.Builder(this).apply {
    mContentTitle = "通知のタイトル"
    mContentText = "通知の内容"
    setSmallIcon(R.mipmap.ic_launcher)
}.build()

②startForegroundメソッド
通知を表示させます。Foreground Serviceにするには、startForegroundメソッドを呼び出します。
第1引数には通知のIDを指定します。第2引数に、表示するNotificationのオブジェクトを指定します。
このことからわかるようにNotificationManagerは使用しません。
通知のIDに0を指定すると、通知が表示されないので注意してください。

startForeground(1, notification)

③ stopForegroundメソッド
サービスの処理が終了する際に、stopForegroundメソッドを呼び出します。
引数には、Boolean型をしていします。 true の場合、呼び出しと同時に通知を削除します。 falseの場合は、通知が残ります。

stopForeground(true)

④ stopSelfメソッド
stopSelfを呼び出すことでサービスを終了します。その場合、通知は削除されます。

minSdkVersionが26以上の場合

Activityクラス

サービスを呼び出す時のメソッドが異なります。

MainActivity.kt
package jp.co.casareal.foregroundserviceoreo

import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import jp.co.casareal.foregroundserviceoreo.service.ForegroundService
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        buttonServiceStart.setOnClickListener {
            val serviceIntent = Intent(this, ForegroundService::class.java)
            startForegroundService(serviceIntent)
        }
    }


}

コードの説明

①startForegroundServiceメソッド
Foreground Serviceとしてサービスを起動するには、startForegroundServiceメソッドでサービスを起動します。
5秒以内に起動したサービスクラスでstartForegroundメソッドを呼び出さないとANRになります。

startForegroundService(serviceIntent)

Serviceクラス

ForegroundService.kt
package jp.co.casareal.foregroundserviceoreo.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.support.v4.app.NotificationCompat
import jp.co.casareal.foregroundserviceoreo.R

class ForegroundService : Service() {

    override fun onBind(intent: Intent): IBinder? {
        throw UnsupportedOperationException("Not yet implemented")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val name = "通知のタイトル的情報を設定"
        val id = "casareal_foreground"
        val notifyDescription = "この通知の詳細情報を設定します"

        if (manager.getNotificationChannel(id) == null) {
            val mChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
            mChannel.apply {
                description = notifyDescription
            }
            manager.createNotificationChannel(mChannel)
        }

        val notification = NotificationCompat.Builder(this,id).apply {
            mContentTitle = "通知のタイトル"
            mContentText = "通知の内容"
            setSmallIcon(R.drawable.ic_launcher_background)
        }.build()

        Thread(
                Runnable {
                    (0..5).map {
                        Thread.sleep(1000)

                    }

                    stopForeground(Service.STOP_FOREGROUND_DETACH)

                }).start()

        startForeground(1, notification)

        return START_STICKY

    }
}

コードの説明

①stopForegroundメソッド
26未満との差分としては、引数に渡せる定数が追加されています。
引数の種類と詳細は以下の通りです。

定数 詳細
Service.STOP_FOREGROUND_REMOVE 26未満でtrueと渡した時と似たような動きをします。既に同じ通知がある場合は、通知を削除して新しい通知を表示します。
Service.STOP_FOREGROUND_DETACH 通知とサービスを切り離します。動きとしては26未満でfalseを渡した時と似たような動きをします。

②Notifaicaiton
詳細は下記の記事を参照してください。

https://qiita.com/naoi/items/367fc23e55292c50d459

まとめ

Foreground ServiceとしてServiceを起動すにはどうやらNotificationが必要です。Notificationの知識もしっかりしておかないといつか痛い目にあいそうです。