0
Help us understand the problem. What are the problem?

posted at

updated at

Organization

【Android】フォアグランドサービスを作成する

概要

フォアグラウンドサービスとは、フォアグラウンドで実行できるサービスのことで、タスクの終了などによりアプリを終了させても動作させ続けることができます。

フォアグラウンドサービスが動いている間はサービスがシステムのリソースを消費し続けている事実をユーザーに認識させるために、専用の通知を通知領域に表示し続ける必要があります。

なお、サービスそのものについては以下の記事を参考にしていただければと思います。

【Android】サービスの概要とサンプルコード

環境

Android Studio:2021.1.1 Patch 2
kotlin:1.6.10
targetSdkVersion:31
minSdkVersion:27

実装

今回は音楽を再生するサービスをフォアグラウンドで動かすサービスを作ります。

まずは Manifest でフォアグラウンドでサービスを実行する権限を付与します。(APIレベル28以降では必須)

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

次にサービスを起動する Activity を作成します。

SoundManagerActivity.kt
class SoundManagerActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_sound_manager)

        Log.d("isActivate", SoundManagerService.isActive.toString())
        if (SoundManagerService.isActive) {
            buttonStart.isEnabled = false
            buttonStop.isEnabled = true
        } else {
            buttonStart.isEnabled = true
            buttonStop.isEnabled = false
        }

        buttonStart.setOnClickListener {
            val intent = Intent(this, SoundManagerService::class.java)
            startForegroundService(intent)

            buttonStart.isEnabled = false
            buttonStop.isEnabled = true
        }

        buttonStop.setOnClickListener {
            val intent = Intent(this, SoundManagerService::class.java)
            stopService(intent)

            buttonStart.isEnabled = true
            buttonStop.isEnabled = false
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("SoundManagerActivity", "onDestroy")
    }
}

フォアグラウンドサービスを起動するときは startForegroundService() で Service を起動します。

レイアウトファイルも記載しておきます。

activity_sound_manager.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".SoundManagerActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/buttonStart"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="button1"
            android:enabled="true"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/buttonStop"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/buttonStop"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="buttonStop"
            android:enabled="false"
            android:layout_marginTop="20dp"
            app:layout_constraintTop_toBottomOf="@+id/buttonStart"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />
            
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

次にサービスを作成します。

SoundManagerService.kt
class SoundManagerService : Service() {
    private var player: MediaPlayer? = null

    companion object {
        var isActive = false
    }

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    override fun onCreate() {
        super.onCreate()
        Log.d("SoundManagerService", "onCreate")
        player = MediaPlayer()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val url = "android.resource://${packageName}/${R.raw.spring_mountain}"
        Log.d("SoundManagerService", "onStartCommand")

        // MediaPlayerを準備・再生
        try {
            player?.apply {
                setDataSource(this@SoundManagerService, Uri.parse(url))
                setOnPreparedListener {
                    Log.d("準備完了", "準備完了")
                    player?.start()
                }

                prepareAsync()
            }
        } catch (e: Exception) {
            Log.d("準備中", "エラー発生")
        }

        // 通知チャネルを作成
        val channelId = "sound_manager_service_notification_channel"

        val channel = NotificationChannel(channelId, "サウンド再生サービス", NotificationManager.IMPORTANCE_HIGH)

        val manager =  getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        manager.createNotificationChannel(channel)


        // 通知タップでActivityを起動できるようPendingIntentを作成
        val intent = Intent(this, MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        val pendingIntent = PendingIntent.getActivities(this, 0, arrayOf(intent), PendingIntent.FLAG_IMMUTABLE)

        // 通知を作成
        val notification = Notification.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("音楽を再生中")
            .setContentText("音楽を再生しています")
            .setContentIntent(pendingIntent)
            .build()

        // サービスをフォアグラウンドで実行
        startForeground(1, notification)
        isActive = true
        return START_REDELIVER_INTENT
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        super.onTaskRemoved(rootIntent)
        Log.d("SoundManagerService", "onTaskRemoved")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d("SoundManagerService", "onDestroy")
        // MediaPlayerの停止・解放
        player?.let { player ->

            if (player.isPlaying) {
                player.stop()
            }
            player.release()
        }
        player = null
        isActive = false
    }
}

サービスをフォアグラウンドで動かすためには通知を表示することが必須であるため、Notification クラスを使って通知を作成してstartForeground()の引数に代入します。
今回は PendingIntent クラスを使用して通知をタップした時にMainActiviyを起動するようにしています。
Activity を起動する必要がなければこの処理は不要です。
このメソッドの第一引数には通知を一意に識別できる整数を指定します。
0以外なら何でも構いません。

また、通知の優先度は PRIORITY_LOW 以上に設定する必要があります。

なお、通知の作り方の詳細はこちらの記事をご参考にしていただければと思います。
通知を作成する  |  Android デベロッパー  |  Android Developers

ここで作成した通知はサービス開始後に通知センターに表示され、 stopService() でサービスが停止するまで消えることはありません。(スワイプしても消せません)

companion object のisActivate 変数は Activity のボタン管理に用いています。

実行

Activityを起動して buttonStart をタップすると、サービスが起動して音楽が再生されるとともに通知領域に通知が表示されます。
この状態でアプリをタスクキルし、再生が止まらないことを確認してください。
この時 Activity の onDestory は呼ばれるものの Service の onDestory は呼ばれず代わりに onTaskRemoved が呼ばれます。

この状態で通知をタップすると Activity が起動します。
そして buttonStop をタップすることで再生が停止することを確認してください。
また、Activity 再起動時のログに「onCreate」「onStartCommand」 が表示されない(=呼ばれない)ことも確認してください。

参考

サービスの概要  |  Android デベロッパー  |  Android Developers
Foreground services  |  Android Developers
基礎&応用力をしっかり育成! Androidアプリ開発の教科書 第2版 Kotlin対応 なんちゃって開発者にならないための実践ハンズオン (CodeZine BOOKS)(著:WINGSプロジェクト 齊藤 新三、監修:山田 祥寛)

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?