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](https://qiita-image-store.s3.amazonaws.com/0/99960/aefe9ce7-fb6c-d4cd-c33b-bd21d9d0e8d4.png)



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

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

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

```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クラス
サービスの起動方法は今まで通り。

```kotlin: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の作成
サービスが起動している最中の通知を作成しています。

```kotlin
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を指定すると、通知が表示されないので注意してください。**

```kotlin
startForeground(1, notification)
```

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

```kotlin
stopForeground(true)
```

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

## minSdkVersionが26以上の場合

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

```kotlin: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になります。**

```kotlin
startForegroundService(serviceIntent)
```

### Serviceクラス

```kotlin: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

## ターゲットを28以上にする場合（Pie以降）

Pie(API Level 28)から更にForeground Serviceについて変更が発生しています。
ターゲットのAPI Levelが28以降の場合は、Manifestファイルに下記を追記する必要があります。

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


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