diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f5edc6ce..dd18430b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,7 +10,7 @@ ktlintGradle = "10.2.0"
detekt = "1.19.0"
preferences = "1.1.1"
media = "1.3.1"
-media3 = "1.0.0-alpha03"
+media3 = "1.0.0-beta01"
androidSupport = "28.0.0"
androidLegacySupport = "1.0.0"
@@ -101,4 +101,3 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
-
diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml
index 521d62a7..4b9c04b2 100644
--- a/ultrasonic/lint-baseline.xml
+++ b/ultrasonic/lint-baseline.xml
@@ -55,7 +55,7 @@
errorLine2=" ~~~~~~~~">
@@ -66,7 +66,7 @@
errorLine2=" ~~~~~~~~">
@@ -77,18 +77,7 @@
errorLine2=" ~~~~~~~">
-
-
-
-
@@ -180,6 +169,61 @@
column="1"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml
index d8ed9a5f..ab6384c3 100644
--- a/ultrasonic/src/main/AndroidManifest.xml
+++ b/ultrasonic/src/main/AndroidManifest.xml
@@ -67,6 +67,7 @@
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
index e2abb257..1f1b8e85 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt
@@ -21,7 +21,6 @@ import androidx.media3.common.Player
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
-import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@@ -81,15 +80,12 @@ private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
private const val DISPLAY_LIMIT = 100
private const val SEARCH_LIMIT = 10
-private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
-private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
-
/**
* MediaBrowserService implementation for e.g. Android Auto
*/
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
class AutoMediaBrowserCallback(var player: Player) :
- MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent {
+ MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
private val mediaPlayerController by inject()
private val activeServerProvider: ActiveServerProvider by inject()
@@ -181,39 +177,23 @@ class AutoMediaBrowserCallback(var player: Player) :
return onLoadChildren(parentId)
}
- private fun setMediaItemFromSearchQuery(query: String) {
- // Only accept query with pattern "play [Title]" or "[Title]"
- // Where [Title]: must be exactly matched
- // If no media with exact name found, play a random media instead
- val mediaTitle =
- if (query.startsWith("play ", ignoreCase = true)) {
- query.drop(5)
- } else {
- query
- }
-
- playFromMediaId(mediaTitle)
- }
-
- override fun onSetMediaUri(
- session: MediaSession,
+ /*
+ * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
+ * and thereby customarily it is required to rebuild it..
+ * See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error
+ */
+ override fun onAddMediaItems(
+ mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
- uri: Uri,
- extras: Bundle
- ): Int {
+ mediaItems: MutableList
+ ): ListenableFuture> {
- if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
- uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
- ) {
- val searchQuery =
- uri.getQueryParameter("query")
- ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
- setMediaItemFromSearchQuery(searchQuery)
-
- return SessionResult.RESULT_SUCCESS
- } else {
- return SessionResult.RESULT_ERROR_NOT_SUPPORTED
+ val updatedMediaItems = mediaItems.map { mediaItem ->
+ mediaItem.buildUpon()
+ .setUri(mediaItem.requestMetadata.mediaUri)
+ .build()
}
+ return Futures.immediateFuture(updatedMediaItems.toMutableList())
}
@Suppress("ReturnCount", "ComplexMethod")
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt
index 88d5dc13..77013e45 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/LegacyPlaylistManager.kt
@@ -50,7 +50,7 @@ class LegacyPlaylistManager : KoinComponent {
for (i in 0 until n) {
val item = controller.getMediaItemAt(i)
- val file = mediaItemCache[item.mediaMetadata.mediaUri.toString()]
+ val file = mediaItemCache[item.requestMetadata.toString()]
if (file != null)
_playlist.add(file)
}
@@ -59,11 +59,11 @@ class LegacyPlaylistManager : KoinComponent {
}
fun addToCache(item: MediaItem, file: DownloadFile) {
- mediaItemCache.put(item.mediaMetadata.mediaUri.toString(), file)
+ mediaItemCache.put(item.requestMetadata.toString(), file)
}
fun updateCurrentPlaying(item: MediaItem?) {
- currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()]
+ currentPlaying = mediaItemCache[item?.requestMetadata.toString()]
}
@Synchronized
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt
index 5ecb4fe4..d9cd7c36 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/MediaNotificationProvider.kt
@@ -7,144 +7,38 @@
package org.moire.ultrasonic.playback
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
import android.content.Context
-import android.graphics.BitmapFactory
-import android.os.Build
-import android.os.Bundle
import androidx.core.app.NotificationCompat
-import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.Player
-import androidx.media3.common.util.Assertions
import androidx.media3.common.util.UnstableApi
-import androidx.media3.common.util.Util
-import androidx.media3.session.MediaController
+import androidx.media3.session.CommandButton
+import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification
-import androidx.media3.session.MediaNotification.ActionFactory
-import org.moire.ultrasonic.R
+import androidx.media3.session.MediaSession
-/*
-* This is a copy of DefaultMediaNotificationProvider.java with some small changes
-* I have opened a bug https://github.com/androidx/media/issues/65 to make it easier to customize
-* the icons and actions without creating our own copy of this class..
- */
@UnstableApi
-/* package */
-internal class MediaNotificationProvider(context: Context) :
- MediaNotification.Provider {
- private val context: Context = context.applicationContext
- private val notificationManager: NotificationManager = Assertions.checkStateNotNull(
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- )
+class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
- @Suppress("LongMethod")
- override fun createNotification(
- mediaController: MediaController,
- actionFactory: ActionFactory,
- onNotificationChangedCallback: MediaNotification.Provider.Callback
- ): MediaNotification {
- ensureNotificationChannel()
- val builder: NotificationCompat.Builder = NotificationCompat.Builder(
- context,
- NOTIFICATION_CHANNEL_ID
- )
- // Skip to previous action.
- builder.addAction(
- actionFactory.createMediaAction(
- IconCompat.createWithResource(
- context,
- R.drawable.media3_notification_seek_to_previous
- ),
- context.getString(R.string.media3_controls_seek_to_previous_description),
- ActionFactory.COMMAND_SKIP_TO_PREVIOUS
- )
- )
- if (mediaController.playbackState == Player.STATE_ENDED ||
- !mediaController.playWhenReady
- ) {
- // Play action.
- builder.addAction(
- actionFactory.createMediaAction(
- IconCompat.createWithResource(context, R.drawable.media3_notification_play),
- context.getString(R.string.media3_controls_play_description),
- ActionFactory.COMMAND_PLAY
- )
- )
- } else {
- // Pause action.
- builder.addAction(
- actionFactory.createMediaAction(
- IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
- context.getString(R.string.media3_controls_pause_description),
- ActionFactory.COMMAND_PAUSE
- )
- )
- }
- // Skip to next action.
- builder.addAction(
- actionFactory.createMediaAction(
- IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next),
- context.getString(R.string.media3_controls_seek_to_next_description),
- ActionFactory.COMMAND_SKIP_TO_NEXT
- )
- )
-
- // Set metadata info in the notification.
- val metadata = mediaController.mediaMetadata
- builder.setContentTitle(metadata.title).setContentText(metadata.artist)
- if (metadata.artworkData != null) {
- val artworkBitmap =
- BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData!!.size)
- builder.setLargeIcon(artworkBitmap)
- }
- val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
- .setShowActionsInCompactView(0, 1, 2)
- val notification: Notification = builder
- .setContentIntent(mediaController.sessionActivity)
- .setOnlyAlertOnce(true)
- .setSmallIcon(getSmallIconResId())
- .setStyle(mediaStyle)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setOngoing(false)
- .build()
- return MediaNotification(
- NOTIFICATION_ID,
- notification
- )
+ override fun addNotificationActions(
+ mediaSession: MediaSession,
+ mediaButtons: MutableList,
+ builder: NotificationCompat.Builder,
+ actionFactory: MediaNotification.ActionFactory
+ ): IntArray {
+ return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory)
}
- override fun handleCustomAction(
- mediaController: MediaController,
- action: String,
- extras: Bundle
- ) {
- // We don't handle custom commands.
- }
+ override fun getMediaButtons(
+ playerCommands: Player.Commands,
+ customLayout: MutableList,
+ playWhenReady: Boolean
+ ): MutableList {
+ val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady)
- private fun ensureNotificationChannel() {
- if (Util.SDK_INT < Build.VERSION_CODES.O ||
- notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null
- ) {
- return
+ commands.forEachIndexed { index, command ->
+ command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
}
- val channel = NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- NOTIFICATION_CHANNEL_NAME,
- NotificationManager.IMPORTANCE_LOW
- )
- channel.setShowBadge(false)
- notificationManager.createNotificationChannel(channel)
- }
-
- companion object {
- private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
- private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
- private const val NOTIFICATION_ID = 3032
- private fun getSmallIconResId(): Int {
- return R.drawable.ic_stat_ultrasonic
- }
+ return commands
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
index 8404d0f9..1cc994d2 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
@@ -11,9 +11,7 @@ import android.content.Intent
import android.os.Build
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
-import androidx.media3.common.C.CONTENT_TYPE_MUSIC
import androidx.media3.common.C.USAGE_MEDIA
-import androidx.media3.common.MediaItem
import androidx.media3.datasource.DataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
@@ -38,29 +36,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var apiDataSource: APIDataSource.Factory
- private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
+ private lateinit var librarySessionCallback: MediaLibrarySession.Callback
private var rxBusSubscription = CompositeDisposable()
private var isStarted = false
- /*
- * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
- * and thereby customarily it is required to rebuild it..
- */
- private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
- override fun fillInLocalConfiguration(
- session: MediaSession,
- controller: MediaSession.ControllerInfo,
- mediaItem: MediaItem
- ): MediaItem {
- // Again, set the Uri, so that it will get a LocalConfiguration
- return mediaItem.buildUpon()
- .setUri(mediaItem.mediaMetadata.mediaUri)
- .build()
- }
- }
-
override fun onCreate() {
Timber.i("onCreate called")
super.onCreate()
@@ -134,7 +115,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
// This will need to use the AutoCalls
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
- .setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(getPendingIntentForContent())
.build()
@@ -171,7 +151,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
private fun getAudioAttributes(): AudioAttributes {
return AudioAttributes.Builder()
.setUsage(USAGE_MEDIA)
- .setContentType(CONTENT_TYPE_MUSIC)
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build()
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
index 749e30e0..73a5ef25 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt
@@ -659,16 +659,21 @@ fun Track.toMediaItem(): MediaItem {
val bitrate = Settings.maxBitRate
val uri = "$id|$bitrate|$filePath"
+ val rmd = MediaItem.RequestMetadata.Builder()
+ .setMediaUri(uri.toUri())
+ .build()
+
val metadata = MediaMetadata.Builder()
metadata.setTitle(title)
.setArtist(artist)
.setAlbumTitle(album)
- .setMediaUri(uri.toUri())
.setAlbumArtist(artist)
+ .build()
val mediaItem = MediaItem.Builder()
.setUri(uri)
.setMediaId(id)
+ .setRequestMetadata(rmd)
.setMediaMetadata(metadata.build())
return mediaItem.build()
diff --git a/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml
new file mode 100644
index 00000000..81e0ed96
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/media3_notification_small_icon.xml
@@ -0,0 +1,9 @@
+
+
+