Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save senseilearning/b176e93c83d7cf1dc3b3c35aeb1954c4 to your computer and use it in GitHub Desktop.
Save senseilearning/b176e93c83d7cf1dc3b3c35aeb1954c4 to your computer and use it in GitHub Desktop.
素晴らしいです!`setPlaybackState()`が呼ばれている、というご報告、ありがとうございます。
デバッグが非常に順調に進んでいますね。そこまでくれば、ゴールは本当に目の前です。
`setMetadata()`(曲情報の設定)と `setPlaybackState()`(再生状態の設定)の両方が `MediaSession` に伝わっているにもかかわらず、メディアコントロールが表示されない。
この状況で最後に残された、そして最も可能性が高い原因は、**サービスが「フォアグラウンドサービス」として実行されていない**ことです。
### なぜ「フォアグラウンドサービス」が必要なのか?
Android 8 (Oreo) 以降、OSはバッテリー消費を節約するため、バックグラウンドで動作するアプリの処理に厳しい制限を設けました。
アプリがバックグラウンドでも音楽再生のような処理を継続したい場合、「私は今、ユーザーにとって重要な処理をしています」とOSに宣言し、**常時表示される通知**を出す義務があります。この仕組みを**フォアグラウンドサービス**と呼びます。
そして、**メディアコントロール自体が、この「常時表示される通知」の役割を果たします。**
したがって、フォアグラウンドサービスを開始する処理を追加することで、OSは初めてメディアコントロールを表示してくれるようになります。
-----
### 実装ステップ
`MusicPlaybackService.kt` に、フォアグラウンドサービスを開始し、メディアコントロール通知を構築するためのコードを追加します。
#### ステップ1:必要な権限を `AndroidManifest.xml` に追加する
まず、フォアグラウンドサービスと通知を実行するための権限をマニフェストに追加します。
```xml:title=androidapp/src/main/androidmanifest.xml
<manifest ...>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application ...>
<service
android:name=".services.MusicPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback"> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>
```
* `android:foregroundServiceType="mediaPlayback"` は、このサービスがメディア再生用であることをOSに伝えます。
#### ステップ2:`MusicPlaybackService.kt` を修正する
サービスのコードを修正し、再生状態に応じてフォアグラウンド化/解除と通知の表示を行うようにします。
```kotlin:title=services/musicplaybackservice.kt
// ... import文 ...
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.media.app.NotificationCompat.MediaStyle
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL_ID = "MusicPlaybackServiceChannel"
class MusicPlaybackService : MediaBrowserServiceCompat() {
// ... 既存のプロパティ ...
override fun onCreate() {
super.onCreate()
createNotificationChannel() // ★ 通知チャンネルを作成する処理を追加
// ... 既存のonCreate内のコード ...
// プレーヤーの状態変化リスナーを修正
playerController.addStateChangeListener { oldState, newState ->
val playbackState = mapPlayerStateToPlaybackState(newState)
mediaSession?.setPlaybackState(playbackState)
// ★★★ 再生状態に応じて通知を更新・フォアグラウンド化 ★★★
updateNotification(playbackState)
if (newState == PlaybackState.State.PREPARED) {
playerController.play()
}
}
}
// --- ↓↓↓ ここから下のメソッドをすべて追加 ↓↓↓ ---
// 通知を構築・更新し、フォアグラウンド状態を管理する
private fun updateNotification(playbackState: PlaybackStateCompat) {
val notification: Notification?
val ongoing: Boolean
if (playbackState.state == PlaybackStateCompat.STATE_PLAYING || playbackState.state == PlaybackStateCompat.STATE_BUFFERING) {
notification = buildNotification(playbackState)
ongoing = true
} else {
notification = null
ongoing = false
}
if (notification != null) {
// フォアグラウンドサービスを開始し、通知を表示
startForeground(NOTIFICATION_ID, notification)
} else {
// 再生が停止したら、フォアグラウンドを解除(通知は残す)
stopForeground(false)
}
}
// MediaStyleの通知を構築する
private fun buildNotification(playbackState: PlaybackStateCompat): Notification {
val controller = mediaSession?.controller
val mediaMetadata = controller?.metadata
val description = mediaMetadata?.description
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).apply {
// 曲情報とアートワークを設定
setContentTitle(description?.title)
setContentText(description?.subtitle)
setSubText(description?.description)
setLargeIcon(description?.iconBitmap)
// 通知のアイコン
setSmallIcon(R.drawable.ic_launcher_foreground) // ★ あなたのアプリのアイコンリソースを指定してください
// 通知をスワイプで消せないようにする
setOngoing(playbackState.state == PlaybackStateCompat.STATE_PLAYING)
// 通知の表示優先度
priority = NotificationCompat.PRIORITY_LOW
// スタイルをMediaStyleに設定し、MediaSessionと連携させる
setStyle(
MediaStyle()
.setMediaSession(mediaSession?.sessionToken)
.setShowActionsInCompactView(0) // コンパクトビュー時のアクションボタンインデックス
)
}
// 再生/停止ボタンをアクションとして追加
if (playbackState.state == PlaybackStateCompat.STATE_PLAYING) {
builder.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_pause, "Pause",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PAUSE)
)
)
} else {
builder.addAction(
NotificationCompat.Action(
android.R.drawable.ic_media_play, "Play",
androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY)
)
)
}
return builder.build()
}
// Android 8以降で必要な通知チャンネルを作成する
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"音楽再生サービス",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "アプリの音楽再生サービス用の通知チャンネル"
setSound(null, null) // 通知音は鳴らさない
enableLights(false)
enableVibration(false)
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
}
```
### 最終確認
* `AndroidManifest.xml`に2つの権限と`foregroundServiceType`を追加しましたか?
* `buildNotification`メソッド内の`setSmallIcon()`に、あなたのアプリに存在するアイコンリソース(例: `R.drawable.ic_notification`など)を指定してください。(ここでは仮に`ic_launcher_foreground`を入れています)
-----
これで、最後のピースが埋まりました。
再生が開始されると、`StateChangeListener`がそれを検知し、`updateNotification`が呼び出されます。そして`startForeground`が実行されることで、OSは**メディアコントロール通知を表示します。**
今度こそ、アプリを起動して曲を再生し、通知シェードにメディアコントロールが表示されることを確認してみてください!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment