As Android documentation says:
A
MediaSessionService
will automatically create aMediaStyle
notification for you in the form of aMediaNotification
.
So I implemented a minimal MediaSessionService
which supports "playing" a song and retrieving its metadata. I can control it using a MediaController
from my activity, but there is no notification to control it outside of the application. What am I missing?
The service:
@OptIn(UnstableApi::class)
class PlaybackService : MediaSessionService() {
private val _logger = Logger.getLogger("PlaybackService")
private val _job = SupervisorJob()
private var _mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val scope = CoroutineScope(Dispatchers.IO + _job)
_mediaSession = MediaSession.Builder(this, Player(mainLooper, scope)).build()
setListener(object : Listener {
override fun onForegroundServiceStartNotAllowedException() {
_logger.warning("Foreground service start not allowed.")
}
})
}
override fun onDestroy() {
_mediaSession?.player?.release();
_mediaSession?.release();
_job.cancel()
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return _mediaSession
}
private class Player(looper: Looper, val scope: CoroutineScope) : SimpleBasePlayer(looper) {
private val _logger = Logger.getLogger("PlaybackService")
private val _availableCommands: Commands = Commands.Builder().addAll(
COMMAND_PLAY_PAUSE,
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_METADATA
).build();
private val _metadata = MediaMetadata.Builder()
.setArtist("Artist")
.setTitle("Title")
.build()
private val _audioAttributes = AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build()
override fun getState(): State {
return State.Builder()
.setAvailableCommands(_availableCommands)
.setPlaylist(listOf(MediaItemData.Builder("")
.setMediaMetadata(_metadata)
.build()))
.setAudioAttributes(_audioAttributes)
.build()
}
override fun handleSetPlayWhenReady(playWhenReady: Boolean) = scope.future {
onPlayPause(playWhenReady)
}
private suspend fun onPlayPause(play: Boolean) {
delay(500)
if (play)
_logger.info("Play command invoked.")
else
_logger.info("Pause command invoked.")
}
}
}
The MediaController
usage:
override fun onStart() {
super.onStart()
val context = requireContext()
if (ContextCompat.checkSelfPermission(context, "android.permission.POST_NOTIFICATIONS") != PERMISSION_GRANTED) {
_logger.info("Requesting permission for 'android.permission.POST_NOTIFICATIONS'...")
ActivityCompat.requestPermissions(requireActivity(), arrayOf("android.permission.POST_NOTIFICATIONS"), 0)
} else
_logger.info("Already has permission for 'android.permission.POST_NOTIFICATIONS'.")
val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val future = MediaController.Builder(context, sessionToken).buildAsync();
future.addListener({
val mediaController = future.get()
mediaController.play()
val metadata = mediaController.mediaMetadata
}, MoreExecutors.directExecutor())
}
The Android manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=";
xmlns:tools=";>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LayoutPreview"
tools:targetApi="31">
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
EDIT: my application now has the android.permission.POST_NOTIFICATIONS
permission and onForegroundServiceStartNotAllowedException
is not called, but it still does not show a notification.
As Android documentation says:
A
MediaSessionService
will automatically create aMediaStyle
notification for you in the form of aMediaNotification
.
So I implemented a minimal MediaSessionService
which supports "playing" a song and retrieving its metadata. I can control it using a MediaController
from my activity, but there is no notification to control it outside of the application. What am I missing?
The service:
@OptIn(UnstableApi::class)
class PlaybackService : MediaSessionService() {
private val _logger = Logger.getLogger("PlaybackService")
private val _job = SupervisorJob()
private var _mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val scope = CoroutineScope(Dispatchers.IO + _job)
_mediaSession = MediaSession.Builder(this, Player(mainLooper, scope)).build()
setListener(object : Listener {
override fun onForegroundServiceStartNotAllowedException() {
_logger.warning("Foreground service start not allowed.")
}
})
}
override fun onDestroy() {
_mediaSession?.player?.release();
_mediaSession?.release();
_job.cancel()
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return _mediaSession
}
private class Player(looper: Looper, val scope: CoroutineScope) : SimpleBasePlayer(looper) {
private val _logger = Logger.getLogger("PlaybackService")
private val _availableCommands: Commands = Commands.Builder().addAll(
COMMAND_PLAY_PAUSE,
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_METADATA
).build();
private val _metadata = MediaMetadata.Builder()
.setArtist("Artist")
.setTitle("Title")
.build()
private val _audioAttributes = AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build()
override fun getState(): State {
return State.Builder()
.setAvailableCommands(_availableCommands)
.setPlaylist(listOf(MediaItemData.Builder("")
.setMediaMetadata(_metadata)
.build()))
.setAudioAttributes(_audioAttributes)
.build()
}
override fun handleSetPlayWhenReady(playWhenReady: Boolean) = scope.future {
onPlayPause(playWhenReady)
}
private suspend fun onPlayPause(play: Boolean) {
delay(500)
if (play)
_logger.info("Play command invoked.")
else
_logger.info("Pause command invoked.")
}
}
}
The MediaController
usage:
override fun onStart() {
super.onStart()
val context = requireContext()
if (ContextCompat.checkSelfPermission(context, "android.permission.POST_NOTIFICATIONS") != PERMISSION_GRANTED) {
_logger.info("Requesting permission for 'android.permission.POST_NOTIFICATIONS'...")
ActivityCompat.requestPermissions(requireActivity(), arrayOf("android.permission.POST_NOTIFICATIONS"), 0)
} else
_logger.info("Already has permission for 'android.permission.POST_NOTIFICATIONS'.")
val sessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val future = MediaController.Builder(context, sessionToken).buildAsync();
future.addListener({
val mediaController = future.get()
mediaController.play()
val metadata = mediaController.mediaMetadata
}, MoreExecutors.directExecutor())
}
The Android manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android/apk/res/android"
xmlns:tools="http://schemas.android/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LayoutPreview"
tools:targetApi="31">
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
EDIT: my application now has the android.permission.POST_NOTIFICATIONS
permission and onForegroundServiceStartNotAllowedException
is not called, but it still does not show a notification.
- Also, show us the manifest file where you declared the service and the permissions. – sdex Commented Mar 17 at 11:32
- @sdex added the manifest. – Dark Daskin Commented Mar 18 at 19:53
1 Answer
Reset to default 0To show the foreground service notification the app must declare and request the runtime permission:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Apart from that, set the media session listener when the media session is created:
setListener(MediaSessionServiceListener())
Implement your listener:
private inner class MediaSessionServiceListener : Listener
and override this method to handle the exception:
override fun onForegroundServiceStartNotAllowedException()