Use MediaStyle Notifications for purposes other than Media

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

Notifications for Android have MediaStyle. It provides you to control the playback of media by placing a button in the Notification, but it is not required to be combined with MediaSession.
It can also be used to control the state of a service (start, stop or mode switching) other than media applications.

This time, my sample application receive the Intent issued by the action of the Notification created by MediaStyle to switch the state of the Service. Sample code is available at https://github.com/Gan0803/MediaNotoficationStudy

When the sample app is launched, it creates a Foreground Service and displays a Notification.
The Notification has a single button that can be tapped to switch between the two states (running and stopping).

state-stop.png state-run.png

Implementation

First, add implementation "androidx.media:media:1.1.0" in dependencies of build.gradle of app in order to use androidx.media.app.NotificationCompat.

And since androidx.core.app.NotificationCompat and androidx.media.app.NotificationCompat have duplicate names, add as MediaNotificationCompat and androidx.core.app.NotificationCompat at import. to distinguish between them.

When creating a Notification, set the MediaStyle() to setStyle(), and the Intent to set the Notification is the Action Set the

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)
            )
        }
    }
}

The BroadcastReceiver will receive the broadcast action issued when the Notification action is executed. This is described in Broadcast Reception in developers. Then, call the callback according to the received intent.

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()
    }
}

The service registers the BroadcastReceiver using application context, and also registers a callback that it wants to be called when it is received. This connects the Notification to the service.
Then, when we receive the callback, we call NotificationManagerCompat.notify() to update the notification (by changing the button icon and the Intent action to match the state).
https://developer.android.com/training/notify-user/build-notification#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())
        }
    }
}