概要
フォアグラウンドサービスとは、フォアグラウンドで実行できるサービスのことで、タスクの終了などによりアプリを終了させても動作させ続けることができます。
フォアグラウンドサービスが動いている間はサービスがシステムのリソースを消費し続けている事実をユーザーに認識させるために、専用の通知を通知領域に表示し続ける必要があります。
なお、サービスそのものについては以下の記事を参考にしていただければと思います。
環境
Android Studio:2021.1.1 Patch 2
kotlin:1.6.10
targetSdkVersion:31
minSdkVersion:27
実装
今回は音楽を再生するサービスをフォアグラウンドで動かすサービスを作ります。
まずは Manifest でフォアグラウンドでサービスを実行する権限を付与します。(APIレベル28以降では必須)
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
次にサービスを起動する Activity を作成します。
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 を起動します。
レイアウトファイルも記載しておきます。
<?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>
次にサービスを作成します。
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プロジェクト 齊藤 新三、監修:山田 祥寛)