AndroidのMediaStyle NotificationをMedia以外の目的で使う

公開日: 2020年11月22日最終更新日: 2021年10月17日

AndroidのNotificationにはMediaStyleがあります。
これはNotificationにボタンを置いてMediaの再生をコントロールすることができるものですが、別にMediaSessionと組み合わせなければいけないというわけではありません。
Media用途以外でもサービスの状態(スタート、ストップやモード切替など)をコントロールするために使うことができます。

今回はMediaStyleで作ったNotificationのactionで発行されるIntentを受信してServiceの状態を切り替えます。サンプルコードはhttps://github.com/Gan0803/MediaNotoficationStudyにあります。

サンプルのアプリは起動するとForeground Serviceを作成し、Notificationを表示します。
Notificationにはボタンが1つあり、タップすることで2つの状態(走る、止まる)を切り替えます。

state-stop.png state-run.png

Implementation

最初に、androidx.media.app.NotificationCompatを使うため、appのbuild.gradleのdependenciesにimplementation "androidx.media:media:1.1.0"を追加します。

そして、androidx.core.app.NotificationCompatandroidx.media.app.NotificationCompatは名前が重複しているため、import時にas MediaNotificationCompatと別名を付けて区別します。

Notificationを作るときはsetStyle()MediaStyle()を指定します。NotificationにセットするIntentにはMyReceiverのAction を設定します。

MyNotificationBuilder.kt

...
import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat as MediaNotificationCompat

class MyNotificationBuilder {

    fun build(context: Context, isRunning: Boolean, channelId: String): Notification {
        return NotificationCompat.Builder(context, channelId)
            .setContentTitle("Notification Study")
            .setContentText("Application is active.")
            .setSmallIcon(R.drawable.ic_baseline_notifications_24)
            .setContentIntent(createPendingIntent(context))
            .setTicker("Application is active")
            .addAction(createMyAction(context, isRunning))
            .setStyle(
                MediaNotificationCompat.MediaStyle()
                    .setShowActionsInCompactView(0)
            )
            .build()
    }

    private fun createPendingIntent(context: Context): PendingIntent {
        return Intent(context, MainActivity::class.java).let { notificationIntent ->
            PendingIntent.getActivity(context, 0, notificationIntent, 0)
        }
    }

    private fun createMyAction(context: Context, isRunning: Boolean): NotificationCompat.Action {
        return if (isRunning) {
            val pauseIntent = Intent().apply {
                action = MyReceiver.ACTION_STOP
            }
            NotificationCompat.Action(
                R.drawable.ic_baseline_directions_run_24,
                "Run",
                PendingIntent.getBroadcast(context, 0, pauseIntent, 0)
            )
        } else {
            val playIntent = Intent().apply {
                action = MyReceiver.ACTION_RUN
            }
            NotificationCompat.Action(
                R.drawable.ic_baseline_emoji_people_24,
                "Stop",
                PendingIntent.getBroadcast(context, 0, playIntent, 0)
            )
        }
    }
}

Notificationのactionを実行したときに発行されるBroadcast actionをBroadcastReceiverで受け取ります。 このあたりはdevelopersのブロードキャストの受信に説明があります。 そして、受け取ったインテントに応じてコールバックを呼び出します。

MyReceiver.kt

class MyReceiver : BroadcastReceiver() {
    companion object {
        val TAG: String = this::class.java.simpleName
        const val ACTION_STOP = "gan0803.pj.study.medianotificationstudy.action.ACTION_STOP"
        const val ACTION_RUN = "gan0803.pj.study.medianotificationstudy.action.ACTION_RUN"
    }

    private var callback: IMyCallback? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        val action = intent?.action
        Log.d(TAG, "onReceive, action: {$action}")

        when (action) {
            ACTION_RUN -> {
                callback?.onReceiveRun()
            }
            ACTION_STOP -> {
                callback?.onReceiveStop()
            }
        }
    }

    fun registerCallback(callback: IMyCallback) {
        this.callback = callback
    }

    interface IMyCallback {
        fun onReceiveRun()
        fun onReceiveStop()
    }
}

サービスではBroadcastReceiverをapplicationのContextで登録し、受け取ったときに呼び出してほしいコールバックも登録しています。これでNotificationからサービスまでが繋がります。
そしてコールバックを受けたときに、NotificationManagerCompat.notify() を呼んでNotificationを更新します(ボタンのアイコンとIntentのアクションを状態に合わせてt変更しています)。
https://developer.android.com/training/notify-user/build-notification?hl=ja#Updating

MyService.kt

class MyService : Service(), MyReceiver.IMyCallback {

...

    private lateinit var myReceiver: MyReceiver
    private var isRunning = true

...

    private fun init() {
        myReceiver = MyReceiver()
        myReceiver.registerCallback(this)
        val filter = IntentFilter().apply {
            addAction(MyReceiver.ACTION_RUN)
            addAction(MyReceiver.ACTION_STOP)
        }
        application.registerReceiver(myReceiver, filter)
    }

    override fun onCreate() {
        super.onCreate()
        init()
    }

...

    override fun onDestroy() {
        application.unregisterReceiver(myReceiver)
        super.onDestroy()
    }

...

    private fun buildNotification(): Notification {
        return MyNotificationBuilder().build(this, isRunning, channelId)
    }

    override fun onReceiveRun() {
        Log.d(TAG, "onReceivePlay")
        isRunning = true
        // notificationを更新
        with(NotificationManagerCompat.from(this)) {
            notify(ONGOING_NOTIFICATION_ID, buildNotification())
        }
    }

    override fun onReceiveStop() {
        Log.d(TAG, "onReceivePause")
        isRunning = false
        // notificationを更新
        with(NotificationManagerCompat.from(this)) {
            notify(ONGOING_NOTIFICATION_ID, buildNotification())
        }
    }
}