Help us understand the problem. What is going on with this article?

Foreground Serviceの基本

More than 1 year has passed since last update.

検証環境

この記事の内容は、以下の環境で検証した。
* 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の知識もしっかりしておかないといつか痛い目にあいそうです。

naoi
casareal
システム開発/評価・検証支援/品質改善支援サービスと現場に即した実践的なIT研修サービスを提供しています。
https://www.casareal.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした