Edited at

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


ターゲットを28以上にする場合(Pie以降)

Pie(API Level 28)から更にForeground Serviceについて変更が発生しています。

ターゲットのAPI Levelが28以降の場合は、Manifestファイルに下記を追記する必要があります。

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


まとめ

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