diff --git a/android-lint.xml b/android-lint.xml
index e20f8e8f..e0983c2d 100644
--- a/android-lint.xml
+++ b/android-lint.xml
@@ -11,4 +11,8 @@
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d8e26eb3..75cb78bb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -147,17 +147,12 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.okio)
implementation(libs.coil)
- implementation(libs.cronet.embedded)
// Media
implementation(libs.androidx.media)
implementation(libs.androidx.mediarouter)
- implementation(libs.bundles.exoplayer) {
- // Exclude Play Services cronet provider library
- exclude("com.google.android.gms", "play-services-cronet")
- }
- implementation(libs.jellyfin.exoplayer.ffmpegextension)
- proprietaryImplementation(libs.exoplayer.cast)
+ implementation(libs.bundles.androidx.media3)
+ proprietaryImplementation(libs.androidx.media3.cast)
proprietaryImplementation(libs.bundles.playservices)
// Room
diff --git a/app/src/libre/java/org/jellyfin/mobile/player/cast/CastPlayerProvider.kt b/app/src/libre/java/org/jellyfin/mobile/player/cast/CastPlayerProvider.kt
deleted file mode 100644
index 70d84821..00000000
--- a/app/src/libre/java/org/jellyfin/mobile/player/cast/CastPlayerProvider.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.jellyfin.mobile.player.cast
-
-import com.google.android.exoplayer2.Player
-import org.jellyfin.mobile.player.audio.MediaService
-
-class CastPlayerProvider(@Suppress("UNUSED_PARAMETER") mediaService: MediaService) : ICastPlayerProvider {
- override val isCastSessionAvailable: Boolean = false
-
- override fun get(): Player? = null
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 58d4e85f..7f6734d8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -78,16 +78,6 @@
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
-
-
-
-
-
-
diff --git a/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt b/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt
index 4cf5df8e..52770847 100644
--- a/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt
+++ b/app/src/main/java/org/jellyfin/mobile/app/AppModule.kt
@@ -2,36 +2,32 @@ package org.jellyfin.mobile.app
import android.content.Context
import androidx.core.net.toUri
+import androidx.media3.common.util.Util
+import androidx.media3.database.DatabaseProvider
+import androidx.media3.database.StandaloneDatabaseProvider
+import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.DataSpec
+import androidx.media3.datasource.DefaultDataSource
+import androidx.media3.datasource.DefaultHttpDataSource
+import androidx.media3.datasource.ResolvingDataSource
+import androidx.media3.datasource.cache.Cache
+import androidx.media3.datasource.cache.CacheDataSource
+import androidx.media3.datasource.cache.NoOpCacheEvictor
+import androidx.media3.datasource.cache.SimpleCache
+import androidx.media3.exoplayer.hls.HlsMediaSource
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
+import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
+import androidx.media3.exoplayer.source.SingleSampleMediaSource
+import androidx.media3.extractor.DefaultExtractorsFactory
+import androidx.media3.extractor.ts.TsExtractor
import coil.ImageLoader
-import com.google.android.exoplayer2.database.DatabaseProvider
-import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
-import com.google.android.exoplayer2.ext.cronet.CronetDataSource
-import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
-import com.google.android.exoplayer2.extractor.ts.TsExtractor
-import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
-import com.google.android.exoplayer2.source.MediaSource
-import com.google.android.exoplayer2.source.ProgressiveMediaSource
-import com.google.android.exoplayer2.source.SingleSampleMediaSource
-import com.google.android.exoplayer2.source.hls.HlsMediaSource
-import com.google.android.exoplayer2.upstream.DataSource
-import com.google.android.exoplayer2.upstream.DataSpec
-import com.google.android.exoplayer2.upstream.DefaultDataSource
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
-import com.google.android.exoplayer2.upstream.ResolvingDataSource
-import com.google.android.exoplayer2.upstream.cache.Cache
-import com.google.android.exoplayer2.upstream.cache.CacheDataSource
-import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
-import com.google.android.exoplayer2.upstream.cache.SimpleCache
-import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.channels.Channel
import okhttp3.OkHttpClient
-import org.chromium.net.CronetEngine
-import org.chromium.net.CronetProvider
import org.jellyfin.mobile.MainViewModel
import org.jellyfin.mobile.bridge.NativePlayer
import org.jellyfin.mobile.downloads.DownloadsViewModel
import org.jellyfin.mobile.events.ActivityEventHandler
-import org.jellyfin.mobile.player.audio.car.LibraryBrowser
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
import org.jellyfin.mobile.player.interaction.PlayerEvent
import org.jellyfin.mobile.player.qualityoptions.QualityOptionsProvider
@@ -53,10 +49,8 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import java.io.File
-import java.util.concurrent.Executors
const val PLAYER_EVENT_CHANNEL = "PlayerEventChannel"
-private const val HTTP_CACHE_SIZE: Long = 16 * 1024 * 1024
private const val TS_SEARCH_PACKETS = 1800
val applicationModule = module {
@@ -111,24 +105,8 @@ val applicationModule = module {
val context: Context = get()
val apiClient: ApiClient = get()
- val provider = CronetProvider.getAllProviders(context).firstOrNull { provider: CronetProvider ->
- (provider.name == CronetProvider.PROVIDER_NAME_APP_PACKAGED) && provider.isEnabled
- }
-
- val baseDataSourceFactory = if (provider != null) {
- val cronetEngine = provider.createBuilder()
- .enableHttp2(true)
- .enableQuic(true)
- .enableBrotli(true)
- .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, HTTP_CACHE_SIZE)
- .build()
- CronetDataSource.Factory(cronetEngine, Executors.newCachedThreadPool()).apply {
- setUserAgent(Util.getUserAgent(context, Constants.APP_INFO_NAME))
- }
- } else {
- DefaultHttpDataSource.Factory().apply {
- setUserAgent(Util.getUserAgent(context, Constants.APP_INFO_NAME))
- }
+ val baseDataSourceFactory = DefaultHttpDataSource.Factory().apply {
+ setUserAgent(Util.getUserAgent(context, Constants.APP_INFO_NAME))
}
val dataSourceFactory = DefaultDataSource.Factory(context, baseDataSourceFactory)
@@ -183,7 +161,4 @@ val applicationModule = module {
single { ProgressiveMediaSource.Factory(get()) }
single { HlsMediaSource.Factory(get()) }
single { SingleSampleMediaSource.Factory(get()) }
-
- // Media components
- single { LibraryBrowser(get(), get()) }
}
diff --git a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadServiceUtil.kt b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadServiceUtil.kt
index 94f93b10..e65430ac 100644
--- a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadServiceUtil.kt
+++ b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadServiceUtil.kt
@@ -1,11 +1,11 @@
package org.jellyfin.mobile.downloads
import android.content.Context
-import com.google.android.exoplayer2.database.DatabaseProvider
-import com.google.android.exoplayer2.offline.DownloadManager
-import com.google.android.exoplayer2.ui.DownloadNotificationHelper
-import com.google.android.exoplayer2.upstream.DataSource
-import com.google.android.exoplayer2.upstream.cache.Cache
+import androidx.media3.database.DatabaseProvider
+import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.cache.Cache
+import androidx.media3.exoplayer.offline.DownloadManager
+import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import org.jellyfin.mobile.utils.Constants.DOWNLOAD_NOTIFICATION_CHANNEL_ID
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
diff --git a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadTracker.kt b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadTracker.kt
index b219cae9..decbb09f 100644
--- a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadTracker.kt
+++ b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadTracker.kt
@@ -1,9 +1,9 @@
package org.jellyfin.mobile.downloads
import android.net.Uri
-import com.google.android.exoplayer2.offline.Download
-import com.google.android.exoplayer2.offline.DownloadIndex
-import com.google.android.exoplayer2.offline.DownloadManager
+import androidx.media3.exoplayer.offline.Download
+import androidx.media3.exoplayer.offline.DownloadIndex
+import androidx.media3.exoplayer.offline.DownloadManager
import com.google.common.base.Preconditions
import timber.log.Timber
import java.io.IOException
diff --git a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadUtils.kt b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadUtils.kt
index acd52dae..0659834a 100644
--- a/app/src/main/java/org/jellyfin/mobile/downloads/DownloadUtils.kt
+++ b/app/src/main/java/org/jellyfin/mobile/downloads/DownloadUtils.kt
@@ -19,11 +19,11 @@ import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
+import androidx.media3.exoplayer.offline.DownloadRequest
+import androidx.media3.exoplayer.offline.DownloadService
+import androidx.media3.exoplayer.scheduler.Requirements
import coil.ImageLoader
import coil.request.ImageRequest
-import com.google.android.exoplayer2.offline.DownloadRequest
-import com.google.android.exoplayer2.offline.DownloadService
-import com.google.android.exoplayer2.scheduler.Requirements
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
diff --git a/app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt b/app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt
index 26404a3e..ceded11b 100644
--- a/app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt
+++ b/app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt
@@ -4,14 +4,14 @@ import android.app.Notification
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
-import com.google.android.exoplayer2.offline.Download
-import com.google.android.exoplayer2.offline.DownloadManager
-import com.google.android.exoplayer2.offline.DownloadService
-import com.google.android.exoplayer2.scheduler.PlatformScheduler
-import com.google.android.exoplayer2.scheduler.Scheduler
-import com.google.android.exoplayer2.ui.DownloadNotificationHelper
-import com.google.android.exoplayer2.util.NotificationUtil
-import com.google.android.exoplayer2.util.Util
+import androidx.media3.common.util.NotificationUtil
+import androidx.media3.common.util.Util
+import androidx.media3.exoplayer.offline.Download
+import androidx.media3.exoplayer.offline.DownloadManager
+import androidx.media3.exoplayer.offline.DownloadNotificationHelper
+import androidx.media3.exoplayer.offline.DownloadService
+import androidx.media3.exoplayer.scheduler.PlatformScheduler
+import androidx.media3.exoplayer.scheduler.Scheduler
import org.jellyfin.mobile.R
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.extensions.toFileSize
diff --git a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt
index 02acdb87..4ac47b76 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/PlayerViewModel.kt
@@ -12,20 +12,20 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.viewModelScope
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.DefaultRenderersFactory
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.PlaybackException
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector
-import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException
-import com.google.android.exoplayer2.mediacodec.MediaCodecInfo
-import com.google.android.exoplayer2.mediacodec.MediaCodecSelector
-import com.google.android.exoplayer2.source.MediaSource
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
-import com.google.android.exoplayer2.util.Clock
-import com.google.android.exoplayer2.util.EventLogger
-import com.google.android.exoplayer2.util.MimeTypes
+import androidx.media3.common.C
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
+import androidx.media3.common.util.Clock
+import androidx.media3.exoplayer.DefaultRenderersFactory
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector
+import androidx.media3.exoplayer.mediacodec.MediaCodecDecoderException
+import androidx.media3.exoplayer.mediacodec.MediaCodecInfo
+import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
+import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
+import androidx.media3.exoplayer.util.EventLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
diff --git a/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt
index 11e5be84..fa484431 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/TrackSelectionHelper.kt
@@ -1,7 +1,7 @@
package org.jellyfin.mobile.player
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
+import androidx.media3.common.C
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import org.jellyfin.mobile.player.source.ExternalSubtitleStream
import org.jellyfin.mobile.player.source.JellyfinMediaSource
import org.jellyfin.mobile.utils.clearSelectionAndDisableRendererByType
diff --git a/app/src/main/java/org/jellyfin/mobile/player/audio/AudioNotificationManager.kt b/app/src/main/java/org/jellyfin/mobile/player/audio/AudioNotificationManager.kt
deleted file mode 100644
index 928755b0..00000000
--- a/app/src/main/java/org/jellyfin/mobile/player/audio/AudioNotificationManager.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-// Taken and adapted from https://github.com/android/uamp/blob/main/common/src/main/java/com/example/android/uamp/media/UampNotificationManager.kt
-
-/*
- * Copyright 2020 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.jellyfin.mobile.player.audio
-
-import android.app.PendingIntent
-import android.content.Context
-import android.graphics.Bitmap
-import android.net.Uri
-import android.support.v4.media.session.MediaControllerCompat
-import android.support.v4.media.session.MediaSessionCompat
-import androidx.core.graphics.drawable.toBitmap
-import coil.ImageLoader
-import coil.request.ImageRequest
-import com.google.android.exoplayer2.ForwardingPlayer
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.ui.PlayerNotificationManager
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.jellyfin.mobile.R
-import org.jellyfin.mobile.utils.Constants.MEDIA_NOTIFICATION_CHANNEL_ID
-import org.jellyfin.mobile.utils.Constants.MEDIA_PLAYER_NOTIFICATION_ID
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
-
-/**
- * A wrapper class for ExoPlayer's PlayerNotificationManager. It sets up the notification shown to
- * the user during audio playback and provides track metadata, such as track title and icon image.
- */
-class AudioNotificationManager(
- private val context: Context,
- sessionToken: MediaSessionCompat.Token,
- notificationListener: PlayerNotificationManager.NotificationListener,
-) : KoinComponent {
- private val imageLoader: ImageLoader by inject()
-
- private val serviceJob = SupervisorJob()
- private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
- private val notificationManager: PlayerNotificationManager
-
- init {
- val mediaController = MediaControllerCompat(context, sessionToken)
-
- notificationManager = PlayerNotificationManager
- .Builder(context, MEDIA_PLAYER_NOTIFICATION_ID, MEDIA_NOTIFICATION_CHANNEL_ID)
- .setChannelNameResourceId(R.string.music_notification_channel)
- .setChannelDescriptionResourceId(R.string.music_notification_channel_description)
- .setMediaDescriptionAdapter(DescriptionAdapter(mediaController))
- .setNotificationListener(notificationListener)
- .build()
-
- notificationManager.apply {
- setMediaSessionToken(sessionToken)
- setSmallIcon(R.drawable.ic_notification)
- }
- }
-
- fun showNotificationForPlayer(player: Player) {
- notificationManager.setPlayer(NotificationForwardingPlayer(player))
- }
-
- fun hideNotification() {
- notificationManager.setPlayer(null)
- }
-
- /**
- * Removes rewind and fast-forward buttons from notification
- */
- private class NotificationForwardingPlayer(player: Player) : ForwardingPlayer(player) {
- override fun getAvailableCommands(): Player.Commands = super.getAvailableCommands().buildUpon().removeAll(
- COMMAND_SEEK_BACK,
- COMMAND_SEEK_FORWARD,
- ).build()
- }
-
- private inner class DescriptionAdapter(
- private val controller: MediaControllerCompat,
- ) : PlayerNotificationManager.MediaDescriptionAdapter {
-
- var currentIconUri: Uri? = null
- var currentBitmap: Bitmap? = null
-
- override fun createCurrentContentIntent(player: Player): PendingIntent? =
- controller.sessionActivity
-
- override fun getCurrentContentText(player: Player) =
- controller.metadata.description.subtitle.toString()
-
- override fun getCurrentContentTitle(player: Player) =
- controller.metadata.description.title.toString()
-
- override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback): Bitmap? {
- val iconUri = controller.metadata.description.iconUri
- return when {
- currentIconUri != iconUri || currentBitmap == null -> {
- // Cache the bitmap for the current song so that successive calls to
- // `getCurrentLargeIcon` don't cause the bitmap to be recreated.
- currentIconUri = iconUri
- serviceScope.launch {
- currentBitmap = iconUri?.let {
- resolveUriAsBitmap(it)
- }
- currentBitmap?.let { callback.onBitmap(it) }
- }
- null
- }
- else -> currentBitmap
- }
- }
-
- private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? = withContext(Dispatchers.IO) {
- imageLoader.execute(ImageRequest.Builder(context).data(uri).build()).drawable?.toBitmap()
- }
- }
-}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/audio/MediaService.kt b/app/src/main/java/org/jellyfin/mobile/player/audio/MediaService.kt
deleted file mode 100644
index da9bd6dc..00000000
--- a/app/src/main/java/org/jellyfin/mobile/player/audio/MediaService.kt
+++ /dev/null
@@ -1,432 +0,0 @@
-// Contains code adapted from https://github.com/android/uamp/blob/main/common/src/main/java/com/example/android/uamp/media/MediaService.kt
-
-package org.jellyfin.mobile.player.audio
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.os.ResultReceiver
-import android.support.v4.media.MediaBrowserCompat.MediaItem
-import android.support.v4.media.MediaDescriptionCompat
-import android.support.v4.media.MediaMetadataCompat
-import android.support.v4.media.session.MediaControllerCompat
-import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
-import android.widget.Toast
-import androidx.core.content.ContextCompat
-import androidx.media.MediaBrowserServiceCompat
-import androidx.mediarouter.media.MediaControlIntent
-import androidx.mediarouter.media.MediaRouteSelector
-import androidx.mediarouter.media.MediaRouter
-import androidx.mediarouter.media.MediaRouterParams
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.PlaybackException
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.audio.AudioAttributes
-import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
-import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
-import com.google.android.exoplayer2.source.MediaSource
-import com.google.android.exoplayer2.ui.PlayerNotificationManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
-import org.jellyfin.mobile.R
-import org.jellyfin.mobile.app.ApiClientController
-import org.jellyfin.mobile.player.audio.car.LibraryBrowser
-import org.jellyfin.mobile.player.audio.car.LibraryPage
-import org.jellyfin.mobile.player.cast.CastPlayerProvider
-import org.jellyfin.mobile.player.cast.ICastPlayerProvider
-import org.jellyfin.mobile.utils.Constants
-import org.jellyfin.mobile.utils.extensions.mediaUri
-import org.jellyfin.mobile.utils.toast
-import org.jellyfin.sdk.api.client.exception.ApiClientException
-import org.koin.android.ext.android.get
-import org.koin.android.ext.android.inject
-import timber.log.Timber
-import com.google.android.exoplayer2.MediaItem as ExoPlayerMediaItem
-
-class MediaService : MediaBrowserServiceCompat() {
- private val apiClientController: ApiClientController by inject()
- private val libraryBrowser: LibraryBrowser by inject()
-
- private val serviceScope = MainScope()
- private var isForegroundService = false
-
- private lateinit var loadingJob: Job
-
- // The current player will either be an ExoPlayer (for local playback) or a CastPlayer (for
- // remote playback through a Cast device).
- private lateinit var currentPlayer: Player
-
- private lateinit var notificationManager: AudioNotificationManager
- private lateinit var mediaController: MediaControllerCompat
- private lateinit var mediaSession: MediaSessionCompat
- private lateinit var mediaSessionConnector: MediaSessionConnector
- private lateinit var mediaRouteSelector: MediaRouteSelector
- private lateinit var mediaRouter: MediaRouter
- private val mediaRouterCallback = MediaRouterCallback()
-
- private var currentPlaylistItems: List = emptyList()
-
- private val playerAudioAttributes = AudioAttributes.Builder()
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
- .setUsage(C.USAGE_MEDIA)
- .build()
-
- @Suppress("MemberVisibilityCanBePrivate")
- val playerListener: Player.Listener = PlayerEventListener()
-
- private val exoPlayer: Player by lazy {
- ExoPlayer.Builder(this, get()).apply {
- setUsePlatformDiagnostics(false)
- }.build().apply {
- setAudioAttributes(playerAudioAttributes, true)
- setHandleAudioBecomingNoisy(true)
- addListener(playerListener)
- }
- }
-
- private val castPlayerProvider: ICastPlayerProvider by lazy {
- CastPlayerProvider(this)
- }
-
- override fun onCreate() {
- super.onCreate()
-
- loadingJob = serviceScope.launch {
- apiClientController.loadSavedServerUser()
- }
-
- val packageLaunchIntent = packageManager?.getLaunchIntentForPackage(packageName)
- val sessionActivityPendingIntent = packageLaunchIntent?.let { sessionIntent ->
- PendingIntent.getActivity(this, 0, sessionIntent, Constants.PENDING_INTENT_FLAGS)
- }
-
- mediaSession = MediaSessionCompat(this, "MediaService").apply {
- setSessionActivity(sessionActivityPendingIntent)
- isActive = true
- }
-
- sessionToken = mediaSession.sessionToken
-
- notificationManager = AudioNotificationManager(
- this,
- mediaSession.sessionToken,
- PlayerNotificationListener(),
- )
-
- mediaController = MediaControllerCompat(this, mediaSession)
-
- mediaSessionConnector = MediaSessionConnector(mediaSession).apply {
- setPlayer(exoPlayer)
- setPlaybackPreparer(MediaPlaybackPreparer())
- setQueueNavigator(MediaQueueNavigator(mediaSession))
- }
-
- mediaRouter = MediaRouter.getInstance(this)
- mediaRouter.setMediaSessionCompat(mediaSession)
- mediaRouteSelector = MediaRouteSelector.Builder().apply {
- addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
- }.build()
- mediaRouter.routerParams = MediaRouterParams.Builder().apply {
- setTransferToLocalEnabled(true)
- }.build()
- mediaRouter.addCallback(mediaRouteSelector, mediaRouterCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY)
-
- switchToPlayer(
- previousPlayer = null,
- newPlayer = if (castPlayerProvider.isCastSessionAvailable) castPlayerProvider.get()!! else exoPlayer,
- )
- notificationManager.showNotificationForPlayer(currentPlayer)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- mediaSession.run {
- isActive = false
- release()
- }
-
- // Cancel coroutines when the service is going away
- serviceScope.cancel()
-
- // Free ExoPlayer resources
- exoPlayer.removeListener(playerListener)
- exoPlayer.release()
-
- // Stop listening for route changes.
- mediaRouter.removeCallback(mediaRouterCallback)
- }
-
- override fun onGetRoot(
- clientPackageName: String,
- clientUid: Int,
- rootHints: Bundle?,
- ): BrowserRoot = libraryBrowser.getRoot(rootHints)
-
- override fun onLoadChildren(parentId: String, result: Result>) {
- result.detach()
-
- serviceScope.launch(Dispatchers.IO) {
- // Ensure that server and credentials are available
- loadingJob.join()
-
- val items = try {
- libraryBrowser.loadLibrary(parentId)
- } catch (e: ApiClientException) {
- Timber.e(e)
- null
- }
- result.sendResult(items ?: emptyList())
- }
- }
-
- /**
- * Load the supplied list of songs and the song to play into the current player.
- */
- private fun preparePlaylist(
- metadataList: List,
- initialPlaybackIndex: Int = 0,
- playWhenReady: Boolean,
- playbackStartPositionMs: Long = 0,
- ) {
- currentPlaylistItems = metadataList
-
- val mediaItems = metadataList.map { metadata ->
- ExoPlayerMediaItem.Builder().apply {
- setUri(metadata.mediaUri)
- setTag(metadata)
- }.build()
- }
-
- currentPlayer.playWhenReady = playWhenReady
- with(currentPlayer) {
- stop()
- clearMediaItems()
- }
- if (currentPlayer == exoPlayer) {
- with(exoPlayer) {
- setMediaItems(mediaItems)
- prepare()
- seekTo(initialPlaybackIndex, playbackStartPositionMs)
- }
- } else {
- val castPlayer = castPlayerProvider.get()
- if (currentPlayer == castPlayer) {
- castPlayer.setMediaItems(
- mediaItems,
- initialPlaybackIndex,
- playbackStartPositionMs,
- )
- }
- }
- }
-
- private fun switchToPlayer(previousPlayer: Player?, newPlayer: Player) {
- if (previousPlayer == newPlayer) {
- return
- }
- currentPlayer = newPlayer
- if (previousPlayer != null) {
- val playbackState = previousPlayer.playbackState
- if (currentPlaylistItems.isEmpty()) {
- // We are joining a playback session.
- // Loading the session from the new player is not supported, so we stop playback.
- with(currentPlayer) {
- stop()
- clearMediaItems()
- }
- } else if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
- preparePlaylist(
- metadataList = currentPlaylistItems,
- initialPlaybackIndex = previousPlayer.currentMediaItemIndex,
- playWhenReady = previousPlayer.playWhenReady,
- playbackStartPositionMs = previousPlayer.currentPosition,
- )
- }
- }
- mediaSessionConnector.setPlayer(newPlayer)
- previousPlayer?.run {
- stop()
- clearMediaItems()
- }
- }
-
- private fun setPlaybackError() {
- val errorState = PlaybackStateCompat.Builder()
- .setState(PlaybackStateCompat.STATE_ERROR, 0, 1f)
- .setErrorMessage(
- PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED,
- getString(R.string.media_service_item_not_found),
- )
- .build()
- mediaSession.setPlaybackState(errorState)
- }
-
- @Suppress("unused")
- fun onCastSessionAvailable() {
- val castPlayer = castPlayerProvider.get() ?: return
- switchToPlayer(currentPlayer, castPlayer)
- }
-
- @Suppress("unused")
- fun onCastSessionUnavailable() {
- switchToPlayer(currentPlayer, exoPlayer)
- }
-
- private inner class MediaQueueNavigator(mediaSession: MediaSessionCompat) : TimelineQueueNavigator(mediaSession) {
- override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat =
- currentPlaylistItems[windowIndex].description
- }
-
- private inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {
- override fun getSupportedPrepareActions(): Long = 0L or
- PlaybackStateCompat.ACTION_PREPARE or
- PlaybackStateCompat.ACTION_PLAY or
- PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
- PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
- PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
- PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
-
- override fun onPrepare(playWhenReady: Boolean) {
- serviceScope.launch {
- val recents = try {
- libraryBrowser.getDefaultRecents()
- } catch (e: ApiClientException) {
- Timber.e(e)
- null
- }
- if (recents != null) {
- preparePlaylist(recents, 0, playWhenReady)
- } else {
- setPlaybackError()
- }
- }
- }
-
- override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
- if (mediaId == LibraryPage.RESUME) {
- // Requested recents
- onPrepare(playWhenReady)
- } else {
- serviceScope.launch {
- val result = libraryBrowser.buildPlayQueue(mediaId)
- if (result != null) {
- val (playbackQueue, initialPlaybackIndex) = result
- preparePlaylist(playbackQueue, initialPlaybackIndex, playWhenReady)
- } else {
- setPlaybackError()
- }
- }
- }
- }
-
- override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
- if (query.isEmpty()) {
- // No search provided, fallback to recents
- onPrepare(playWhenReady)
- } else {
- serviceScope.launch {
- val results = try {
- libraryBrowser.getSearchResults(query, extras)
- } catch (e: ApiClientException) {
- Timber.e(e)
- null
- }
- if (results != null) {
- preparePlaylist(results, 0, playWhenReady)
- } else {
- setPlaybackError()
- }
- }
- }
- }
-
- override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit
-
- override fun onCommand(
- player: Player,
- command: String,
- extras: Bundle?,
- cb: ResultReceiver?,
- ): Boolean = false
- }
-
- /**
- * Listen for notification events.
- */
- private inner class PlayerNotificationListener : PlayerNotificationManager.NotificationListener {
- override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
- if (ongoing && !isForegroundService) {
- val serviceIntent = Intent(applicationContext, this@MediaService.javaClass)
- ContextCompat.startForegroundService(applicationContext, serviceIntent)
-
- startForeground(notificationId, notification)
- isForegroundService = true
- }
- }
-
- override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
- stopForeground(true)
- isForegroundService = false
- stopSelf()
- }
- }
-
- /**
- * Listen for events from ExoPlayer.
- */
- private inner class PlayerEventListener : Player.Listener {
- @Deprecated("Deprecated in Java")
- override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
- when (playbackState) {
- Player.STATE_BUFFERING,
- Player.STATE_READY,
- -> {
- notificationManager.showNotificationForPlayer(currentPlayer)
- if (playbackState == Player.STATE_READY) {
- // TODO: When playing/paused save the current media item in persistent storage
- // so that playback can be resumed between device reboots
-
- if (!playWhenReady) {
- // If playback is paused we remove the foreground state which allows the
- // notification to be dismissed. An alternative would be to provide a
- // "close" button in the notification which stops playback and clears
- // the notification.
- stopForeground(false)
- }
- }
- }
- else -> notificationManager.hideNotification()
- }
- }
-
- override fun onPlayerError(error: PlaybackException) {
- toast("${getString(R.string.media_service_generic_error)}: ${error.errorCodeName}", Toast.LENGTH_LONG)
- }
- }
-
- /**
- * Listen for MediaRoute changes
- */
- private inner class MediaRouterCallback : MediaRouter.Callback() {
- override fun onRouteSelected(router: MediaRouter, route: MediaRouter.RouteInfo, reason: Int) {
- if (reason == MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
- Timber.d("Unselected because route changed, continue playback")
- } else if (reason == MediaRouter.UNSELECT_REASON_STOPPED) {
- Timber.d("Unselected because route was stopped, stop playback")
- currentPlayer.stop()
- }
- }
- }
-
- companion object {
- /** Declares that content style is supported */
- const val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"
- }
-}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryBrowser.kt b/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryBrowser.kt
deleted file mode 100644
index 22eceac5..00000000
--- a/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryBrowser.kt
+++ /dev/null
@@ -1,457 +0,0 @@
-package org.jellyfin.mobile.player.audio.car
-
-import android.content.Context
-import android.net.Uri
-import android.os.Bundle
-import android.provider.MediaStore
-import android.support.v4.media.MediaBrowserCompat
-import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
-import android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
-import android.support.v4.media.MediaDescriptionCompat
-import android.support.v4.media.MediaMetadataCompat
-import androidx.media.MediaBrowserServiceCompat
-import androidx.media.utils.MediaConstants
-import org.jellyfin.mobile.R
-import org.jellyfin.mobile.player.audio.MediaService
-import org.jellyfin.mobile.utils.extensions.mediaId
-import org.jellyfin.mobile.utils.extensions.setAlbum
-import org.jellyfin.mobile.utils.extensions.setAlbumArtUri
-import org.jellyfin.mobile.utils.extensions.setAlbumArtist
-import org.jellyfin.mobile.utils.extensions.setArtist
-import org.jellyfin.mobile.utils.extensions.setDisplayIconUri
-import org.jellyfin.mobile.utils.extensions.setMediaId
-import org.jellyfin.mobile.utils.extensions.setMediaUri
-import org.jellyfin.mobile.utils.extensions.setTitle
-import org.jellyfin.mobile.utils.extensions.setTrackNumber
-import org.jellyfin.sdk.api.client.ApiClient
-import org.jellyfin.sdk.api.client.exception.ApiClientException
-import org.jellyfin.sdk.api.client.extensions.genresApi
-import org.jellyfin.sdk.api.client.extensions.imageApi
-import org.jellyfin.sdk.api.client.extensions.itemsApi
-import org.jellyfin.sdk.api.client.extensions.playlistsApi
-import org.jellyfin.sdk.api.client.extensions.universalAudioApi
-import org.jellyfin.sdk.api.client.extensions.userViewsApi
-import org.jellyfin.sdk.api.operations.GenresApi
-import org.jellyfin.sdk.api.operations.ImageApi
-import org.jellyfin.sdk.api.operations.ItemsApi
-import org.jellyfin.sdk.api.operations.PlaylistsApi
-import org.jellyfin.sdk.api.operations.UniversalAudioApi
-import org.jellyfin.sdk.api.operations.UserViewsApi
-import org.jellyfin.sdk.model.api.BaseItemDto
-import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
-import org.jellyfin.sdk.model.api.BaseItemKind
-import org.jellyfin.sdk.model.api.CollectionType
-import org.jellyfin.sdk.model.api.ImageType
-import org.jellyfin.sdk.model.api.ItemFilter
-import org.jellyfin.sdk.model.api.ItemSortBy
-import org.jellyfin.sdk.model.api.MediaStreamProtocol
-import org.jellyfin.sdk.model.api.SortOrder
-import org.jellyfin.sdk.model.serializer.toUUID
-import org.jellyfin.sdk.model.serializer.toUUIDOrNull
-import timber.log.Timber
-import java.util.*
-
-@Suppress("TooManyFunctions")
-class LibraryBrowser(
- private val context: Context,
- private val apiClient: ApiClient,
-) {
- private val itemsApi: ItemsApi = apiClient.itemsApi
- private val userViewsApi: UserViewsApi = apiClient.userViewsApi
- private val genresApi: GenresApi = apiClient.genresApi
- private val playlistsApi: PlaylistsApi = apiClient.playlistsApi
- private val imageApi: ImageApi = apiClient.imageApi
- private val universalAudioApi: UniversalAudioApi = apiClient.universalAudioApi
-
- fun getRoot(hints: Bundle?): MediaBrowserServiceCompat.BrowserRoot {
- /**
- * By default return the browsable root. Treat the EXTRA_RECENT flag as a special case
- * and return the recent root instead.
- */
- val isRecentRequest = hints?.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT) ?: false
- val browserRoot = if (isRecentRequest) LibraryPage.RESUME else LibraryPage.LIBRARIES
-
- val rootExtras = Bundle().apply {
- putBoolean(MediaService.CONTENT_STYLE_SUPPORTED, true)
- putInt(
- MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
- MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM,
- )
- putInt(
- MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
- MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM,
- )
- }
- return MediaBrowserServiceCompat.BrowserRoot(browserRoot, rootExtras)
- }
-
- suspend fun loadLibrary(parentId: String): List? {
- if (parentId == LibraryPage.RESUME) {
- return getDefaultRecents()?.browsable()
- }
-
- val split = parentId.split('|')
-
- @Suppress("MagicNumber")
- if (split.size !in 1..3) {
- Timber.e("Invalid libraryId format '$parentId'")
- return null
- }
-
- val type = split[0]
- val libraryId = split.getOrNull(1)?.toUUIDOrNull()
- val itemId = split.getOrNull(2)?.toUUIDOrNull()
-
- return when {
- libraryId != null -> {
- when {
- itemId != null -> when (type) {
- LibraryPage.ARTIST_ALBUMS -> getAlbums(libraryId, filterArtist = itemId)
- LibraryPage.GENRE_ALBUMS -> getAlbums(libraryId, filterGenre = itemId)
- else -> null
- }
- else -> when (type) {
- LibraryPage.LIBRARY -> getLibraryViews(context, libraryId)
- LibraryPage.RECENTS -> getRecents(libraryId)?.playable()
- LibraryPage.ALBUMS -> getAlbums(libraryId)
- LibraryPage.ARTISTS -> getArtists(libraryId)
- LibraryPage.GENRES -> getGenres(libraryId)
- LibraryPage.PLAYLISTS -> getPlaylists(libraryId)
- LibraryPage.ALBUM -> getAlbum(libraryId)?.playable()
- LibraryPage.PLAYLIST -> getPlaylist(libraryId)?.playable()
- else -> null
- }
- }
- }
- else -> when (type) {
- LibraryPage.LIBRARIES -> getLibraries()
- else -> null
- }
- }
- }
-
- suspend fun buildPlayQueue(mediaId: String): Pair, Int>? {
- val split = mediaId.split('|')
- @Suppress("MagicNumber")
- if (split.size != 3) {
- Timber.e("Invalid mediaId format '$mediaId'")
- return null
- }
-
- val type = split[0]
- val collectionId = split[1].toUUID()
-
- val playQueue = try {
- when (type) {
- LibraryPage.RECENTS -> getRecents(collectionId)
- LibraryPage.ALBUM -> getAlbum(collectionId)
- LibraryPage.PLAYLIST -> getPlaylist(collectionId)
- else -> null
- }
- } catch (e: ApiClientException) {
- Timber.e(e)
- null
- } ?: return null
-
- val playIndex = playQueue.indexOfFirst { item ->
- item.mediaId == mediaId
- }.coerceAtLeast(0)
-
- return playQueue to playIndex
- }
-
- suspend fun getSearchResults(searchQuery: String, extras: Bundle?): List? {
- when (extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS)) {
- MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE -> {
- // Search for specific album
- extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)?.let { albumQuery ->
- Timber.d("Searching for album $albumQuery")
- searchItems(albumQuery, BaseItemKind.MUSIC_ALBUM)
- }?.let { albumId ->
- getAlbum(albumId)
- }?.let { albumContent ->
- Timber.d("Got result, starting playback")
- return albumContent
- }
- }
- MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> {
- // Search for specific artist
- extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)?.let { artistQuery ->
- Timber.d("Searching for artist $artistQuery")
- searchItems(artistQuery, BaseItemKind.MUSIC_ARTIST)
- }?.let { artistId ->
- itemsApi.getItems(
- artistIds = listOf(artistId),
- includeItemTypes = listOf(BaseItemKind.AUDIO),
- sortBy = listOf(ItemSortBy.RANDOM),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- enableTotalRecordCount = false,
- limit = 50,
- ).content.extractItems()
- }?.let { artistTracks ->
- Timber.d("Got result, starting playback")
- return artistTracks
- }
- }
- }
-
- // Fallback to generic search
- Timber.d("Searching for '$searchQuery'")
- val result by itemsApi.getItems(
- searchTerm = searchQuery,
- includeItemTypes = listOf(BaseItemKind.AUDIO),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- enableTotalRecordCount = false,
- limit = 50,
- )
-
- return result.extractItems()
- }
-
- /**
- * Find a single specific item for the given [searchQuery] with a specific [type]
- */
- private suspend fun searchItems(searchQuery: String, type: BaseItemKind): UUID? {
- val result by itemsApi.getItems(
- searchTerm = searchQuery,
- includeItemTypes = listOf(type),
- recursive = true,
- enableImages = false,
- enableTotalRecordCount = false,
- limit = 1,
- )
-
- return result.items.firstOrNull()?.id
- }
-
- suspend fun getDefaultRecents(): List? = getLibraries().firstOrNull()?.mediaId?.let { defaultLibrary ->
- val libraryId = defaultLibrary.split('|').getOrNull(1) ?: return@let null
-
- getRecents(libraryId.toUUID())
- }
-
- private suspend fun getLibraries(): List {
- val userViews by userViewsApi.getUserViews()
-
- return userViews.items
- .filter { item -> item.collectionType == CollectionType.MUSIC }
- .map { item ->
- val itemImageUrl = imageApi.getItemImageUrl(
- itemId = item.id,
- imageType = ImageType.PRIMARY,
- )
-
- val description = MediaDescriptionCompat.Builder().apply {
- setMediaId(LibraryPage.LIBRARY + "|" + item.id)
- setTitle(item.name)
- setIconUri(Uri.parse(itemImageUrl))
- }.build()
- MediaBrowserCompat.MediaItem(description, FLAG_BROWSABLE)
- }
- .toList()
- }
-
- private fun getLibraryViews(context: Context, libraryId: UUID): List {
- val libraryViews = arrayOf(
- LibraryPage.RECENTS to R.string.media_service_car_section_recents,
- LibraryPage.ALBUMS to R.string.media_service_car_section_albums,
- LibraryPage.ARTISTS to R.string.media_service_car_section_artists,
- LibraryPage.GENRES to R.string.media_service_car_section_genres,
- LibraryPage.PLAYLISTS to R.string.media_service_car_section_playlists,
- )
- return libraryViews.map { item ->
- val description = MediaDescriptionCompat.Builder().apply {
- setMediaId(item.first + "|" + libraryId)
- setTitle(context.getString(item.second))
-
- if (item.first == LibraryPage.ALBUMS) {
- setExtras(
- Bundle().apply {
- putInt(
- MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
- MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
- )
- putInt(
- MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
- MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM,
- )
- },
- )
- }
- }.build()
- MediaBrowserCompat.MediaItem(description, FLAG_BROWSABLE)
- }
- }
-
- private suspend fun getRecents(libraryId: UUID): List? {
- val result by itemsApi.getItems(
- parentId = libraryId,
- includeItemTypes = listOf(BaseItemKind.AUDIO),
- filters = listOf(ItemFilter.IS_PLAYED),
- sortBy = listOf(ItemSortBy.DATE_PLAYED),
- sortOrder = listOf(SortOrder.DESCENDING),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- enableTotalRecordCount = false,
- limit = 50,
- )
-
- return result.extractItems("${LibraryPage.RECENTS}|$libraryId")
- }
-
- private suspend fun getAlbums(
- libraryId: UUID,
- filterArtist: UUID? = null,
- filterGenre: UUID? = null,
- ): List? {
- val result by itemsApi.getItems(
- parentId = libraryId,
- artistIds = filterArtist?.let(::listOf),
- genreIds = filterGenre?.let(::listOf),
- includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM),
- sortBy = listOf(ItemSortBy.SORT_NAME),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- limit = 400,
- )
-
- return result.extractItems()?.browsable()
- }
-
- private suspend fun getArtists(libraryId: UUID): List? {
- val result by itemsApi.getItems(
- parentId = libraryId,
- includeItemTypes = listOf(BaseItemKind.MUSIC_ARTIST),
- sortBy = listOf(ItemSortBy.SORT_NAME),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- limit = 200,
- )
-
- return result.extractItems(libraryId.toString())?.browsable()
- }
-
- private suspend fun getGenres(libraryId: UUID): List? {
- val result by genresApi.getGenres(
- parentId = libraryId,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- limit = 50,
- )
-
- return result.extractItems(libraryId.toString())?.browsable()
- }
-
- private suspend fun getPlaylists(libraryId: UUID): List? {
- val result by itemsApi.getItems(
- parentId = libraryId,
- includeItemTypes = listOf(BaseItemKind.PLAYLIST),
- sortBy = listOf(ItemSortBy.SORT_NAME),
- recursive = true,
- imageTypeLimit = 1,
- enableImageTypes = listOf(ImageType.PRIMARY),
- limit = 50,
- )
-
- return result.extractItems()?.browsable()
- }
-
- private suspend fun getAlbum(albumId: UUID): List? {
- val result by itemsApi.getItems(
- parentId = albumId,
- sortBy = listOf(ItemSortBy.SORT_NAME),
- )
-
- return result.extractItems("${LibraryPage.ALBUM}|$albumId")
- }
-
- private suspend fun getPlaylist(playlistId: UUID): List? {
- val result by playlistsApi.getPlaylistItems(
- playlistId = playlistId,
- )
-
- return result.extractItems("${LibraryPage.PLAYLIST}|$playlistId")
- }
-
- private fun BaseItemDtoQueryResult.extractItems(libraryId: String? = null): List? =
- items?.map { item -> buildMediaMetadata(item, libraryId) }?.toList()
-
- private fun buildMediaMetadata(item: BaseItemDto, libraryId: String?): MediaMetadataCompat {
- val builder = MediaMetadataCompat.Builder()
- builder.setMediaId(buildMediaId(item, libraryId))
- builder.setTitle(item.name ?: context.getString(R.string.media_service_car_item_no_title))
-
- val isAlbum = item.albumId != null
- val itemId = when {
- item.imageTags?.containsKey(ImageType.PRIMARY) == true -> item.id
- isAlbum -> item.albumId
- else -> null
- }
- val primaryImageUrl = itemId?.let {
- imageApi.getItemImageUrl(
- itemId = itemId,
- imageType = ImageType.PRIMARY,
- tag = if (isAlbum) item.albumPrimaryImageTag else item.imageTags?.get(ImageType.PRIMARY),
- )
- }
-
- if (item.type == BaseItemKind.AUDIO) {
- val uri = universalAudioApi.getUniversalAudioStreamUrl(
- itemId = item.id,
- deviceId = apiClient.deviceInfo.id,
- maxStreamingBitrate = 140000000,
- container = listOf(
- "opus",
- "mp3|mp3",
- "aac",
- "m4a",
- "m4b|aac",
- "flac",
- "webma",
- "webm",
- "wav",
- "ogg",
- ),
- transcodingProtocol = MediaStreamProtocol.HLS,
- transcodingContainer = "ts",
- audioCodec = "aac",
- enableRemoteMedia = true,
- )
-
- builder.setMediaUri(uri)
- item.album?.let(builder::setAlbum)
- item.artists?.let { builder.setArtist(it.joinToString()) }
- item.albumArtist?.let(builder::setAlbumArtist)
- primaryImageUrl?.let(builder::setAlbumArtUri)
- item.indexNumber?.toLong()?.let(builder::setTrackNumber)
- } else {
- primaryImageUrl?.let(builder::setDisplayIconUri)
- }
-
- return builder.build()
- }
-
- private fun buildMediaId(item: BaseItemDto, extra: String?) = when (item.type) {
- BaseItemKind.MUSIC_ARTIST -> "${LibraryPage.ARTIST_ALBUMS}|$extra|${item.id}"
- BaseItemKind.MUSIC_GENRE -> "${LibraryPage.GENRE_ALBUMS}|$extra|${item.id}"
- BaseItemKind.MUSIC_ALBUM -> "${LibraryPage.ALBUM}|${item.id}"
- BaseItemKind.PLAYLIST -> "${LibraryPage.PLAYLIST}|${item.id}"
- BaseItemKind.AUDIO -> "$extra|${item.id}"
- else -> throw IllegalArgumentException("Unhandled item type ${item.type}")
- }
-
- private fun List.browsable(): List = map { metadata ->
- MediaBrowserCompat.MediaItem(metadata.description, FLAG_BROWSABLE)
- }
-
- private fun List.playable(): List = map { metadata ->
- MediaBrowserCompat.MediaItem(metadata.description, FLAG_PLAYABLE)
- }
-}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryPage.kt b/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryPage.kt
deleted file mode 100644
index 99b72837..00000000
--- a/app/src/main/java/org/jellyfin/mobile/player/audio/car/LibraryPage.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.jellyfin.mobile.player.audio.car
-
-object LibraryPage {
- /**
- * List of music libraries that the user can access (referred to as "user views" in Jellyfin)
- */
- const val LIBRARIES = "libraries"
-
- /**
- * Special root id for use with [EXTRA_RECENT][androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT]
- */
- const val RESUME = "resume"
-
- /**
- * A single music library
- */
- const val LIBRARY = "library"
-
- /**
- * A list of recently added tracks
- */
- const val RECENTS = "recents"
-
- /**
- * A list of albums
- */
- const val ALBUMS = "albums"
-
- /**
- * A list of artists
- */
- const val ARTISTS = "artists"
-
- /**
- * A list of albums by a specific artist
- */
- const val ARTIST_ALBUMS = "artist_albums"
-
- /**
- * A list of genres
- */
- const val GENRES = "genres"
-
- /**
- * A list of albums with a specific genre
- */
- const val GENRE_ALBUMS = "genre_albums"
-
- /**
- * A list of playlists
- */
- const val PLAYLISTS = "playlists"
-
- /**
- * An individual album
- */
- const val ALBUM = "album"
-
- /**
- * An individual playlist
- */
- const val PLAYLIST = "playlist"
-}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/cast/ICastPlayerProvider.kt b/app/src/main/java/org/jellyfin/mobile/player/cast/ICastPlayerProvider.kt
deleted file mode 100644
index 3be6d01f..00000000
--- a/app/src/main/java/org/jellyfin/mobile/player/cast/ICastPlayerProvider.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.jellyfin.mobile.player.cast
-
-import com.google.android.exoplayer2.Player
-
-interface ICastPlayerProvider {
- val isCastSessionAvailable: Boolean
-
- fun get(): Player?
-}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/CodecHelpers.kt b/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/CodecHelpers.kt
index 3de90ef1..79fe28ea 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/CodecHelpers.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/deviceprofile/CodecHelpers.kt
@@ -2,7 +2,7 @@ package org.jellyfin.mobile.player.deviceprofile
import android.media.MediaCodecInfo.CodecProfileLevel
import android.media.MediaFormat
-import com.google.android.exoplayer2.util.MimeTypes
+import androidx.media3.common.MimeTypes
@Suppress("TooManyFunctions", "CyclomaticComplexMethod")
object CodecHelpers {
diff --git a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt
index d13c3a4e..ef4e8def 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/interaction/PlayerNotificationHelper.kt
@@ -13,9 +13,9 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.viewModelScope
+import androidx.media3.common.Player
import coil.ImageLoader
import coil.request.ImageRequest
-import com.google.android.exoplayer2.Player
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
diff --git a/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt b/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt
index a082bd65..01a3584f 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/queue/QueueManager.kt
@@ -5,12 +5,12 @@ import androidx.annotation.CheckResult
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import com.google.android.exoplayer2.MediaItem
-import com.google.android.exoplayer2.source.MediaSource
-import com.google.android.exoplayer2.source.MergingMediaSource
-import com.google.android.exoplayer2.source.ProgressiveMediaSource
-import com.google.android.exoplayer2.source.SingleSampleMediaSource
-import com.google.android.exoplayer2.source.hls.HlsMediaSource
+import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.hls.HlsMediaSource
+import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.exoplayer.source.MergingMediaSource
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
+import androidx.media3.exoplayer.source.SingleSampleMediaSource
import org.jellyfin.mobile.data.dao.DownloadDao
import org.jellyfin.mobile.player.PlayerException
import org.jellyfin.mobile.player.PlayerViewModel
diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt
index d6034da7..062abd06 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerFragment.kt
@@ -25,8 +25,8 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.ui.PlayerView
+import androidx.media3.common.Player
+import androidx.media3.ui.PlayerView
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jellyfin.mobile.R
@@ -51,7 +51,7 @@ import org.jellyfin.mobile.utils.extensions.keepScreenOn
import org.jellyfin.mobile.utils.toast
import org.jellyfin.sdk.model.api.MediaStream
import org.koin.android.ext.android.inject
-import com.google.android.exoplayer2.ui.R as ExoplayerR
+import androidx.media3.ui.R as Media3R
class PlayerFragment : Fragment(), BackPressInterceptor {
private val appPreferences: AppPreferences by inject()
@@ -199,6 +199,9 @@ class PlayerFragment : Fragment(), BackPressInterceptor {
// Set controller timeout
suppressControllerAutoHide(false)
+ // Disable controller animations
+ playerView.setControllerAnimationEnabled(false)
+
playerLockScreenHelper = PlayerLockScreenHelper(this, playerBinding, orientationListener)
playerGestureHelper = PlayerGestureHelper(this, playerBinding, playerLockScreenHelper)
@@ -358,7 +361,7 @@ class PlayerFragment : Fragment(), BackPressInterceptor {
}
}
setAspectRatio(aspectRational)
- val contentFrame: View = playerView.findViewById(ExoplayerR.id.exo_content_frame)
+ val contentFrame: View = playerView.findViewById(Media3R.id.exo_content_frame)
val contentRect = with(contentFrame) {
val (x, y) = intArrayOf(0, 0).also(::getLocationInWindow)
Rect(x, y, x + width, y + height)
diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerGestureHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerGestureHelper.kt
index 0bec40cf..463e9b1a 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerGestureHelper.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerGestureHelper.kt
@@ -14,8 +14,8 @@ import android.widget.ProgressBar
import androidx.core.content.getSystemService
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
-import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
-import com.google.android.exoplayer2.ui.PlayerView
+import androidx.media3.ui.AspectRatioFrameLayout
+import androidx.media3.ui.PlayerView
import org.jellyfin.mobile.R
import org.jellyfin.mobile.app.AppPreferences
import org.jellyfin.mobile.databinding.FragmentPlayerBinding
@@ -123,7 +123,7 @@ class PlayerGestureHelper(
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
playerView.apply {
- if (!isControllerVisible) showController() else hideController()
+ if (!isControllerFullyVisible) showController() else hideController()
}
return true
}
diff --git a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerLockScreenHelper.kt b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerLockScreenHelper.kt
index 71dbf14f..f10b061f 100644
--- a/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerLockScreenHelper.kt
+++ b/app/src/main/java/org/jellyfin/mobile/player/ui/PlayerLockScreenHelper.kt
@@ -4,7 +4,7 @@ import android.content.pm.ActivityInfo
import android.view.OrientationEventListener
import android.widget.ImageButton
import androidx.core.view.isVisible
-import com.google.android.exoplayer2.ui.PlayerView
+import androidx.media3.ui.PlayerView
import org.jellyfin.mobile.databinding.FragmentPlayerBinding
import org.jellyfin.mobile.utils.AndroidVersion
import org.jellyfin.mobile.utils.Constants
@@ -50,7 +50,7 @@ class PlayerLockScreenHelper(
if (!AndroidVersion.isAtLeastN || !activity.isInPictureInPictureMode) {
playerView.useController = true
playerView.apply {
- if (!isControllerVisible) showController()
+ if (!isControllerFullyVisible) showController()
}
}
}
diff --git a/app/src/main/java/org/jellyfin/mobile/utils/MediaExtensions.kt b/app/src/main/java/org/jellyfin/mobile/utils/MediaExtensions.kt
index cc75b2f4..e873f94a 100644
--- a/app/src/main/java/org/jellyfin/mobile/utils/MediaExtensions.kt
+++ b/app/src/main/java/org/jellyfin/mobile/utils/MediaExtensions.kt
@@ -7,15 +7,14 @@ import android.media.AudioManager
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.analytics.AnalyticsCollector
+import androidx.media3.common.C
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.analytics.AnalyticsCollector
import org.jellyfin.mobile.player.source.JellyfinMediaSource
import org.jellyfin.mobile.ui.content.ImageProvider
import org.jellyfin.mobile.utils.extensions.width
import org.jellyfin.sdk.model.api.ImageType
-import com.google.android.exoplayer2.audio.AudioAttributes as ExoPlayerAudioAttributes
+import androidx.media3.common.AudioAttributes as Media3AudioAttributes
inline fun MediaSession.applyDefaultLocalAudioAttributes(contentType: Int) {
val audioAttributes = AudioAttributes.Builder().apply {
@@ -79,10 +78,10 @@ fun AudioManager.getVolumeLevelPercent(): Int {
}
/**
- * Set ExoPlayer [ExoPlayerAudioAttributes], make ExoPlayer handle audio focus
+ * Configure [Media3AudioAttributes] to handle audio focus
*/
-inline fun ExoPlayer.applyDefaultAudioAttributes(@C.AudioContentType contentType: Int) {
- val audioAttributes = ExoPlayerAudioAttributes.Builder()
+inline fun Player.applyDefaultAudioAttributes(@C.AudioContentType contentType: Int) {
+ val audioAttributes = Media3AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(contentType)
.build()
diff --git a/app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt b/app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt
index b99b7ac9..0798a11d 100644
--- a/app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt
+++ b/app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt
@@ -17,7 +17,7 @@ import android.provider.Settings
import android.provider.Settings.System.ACCELEROMETER_ROTATION
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.getSystemService
-import com.google.android.exoplayer2.offline.DownloadService
+import androidx.media3.exoplayer.offline.DownloadService
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.suspendCancellableCoroutine
import org.jellyfin.mobile.BuildConfig
diff --git a/app/src/main/java/org/jellyfin/mobile/utils/TrackSelectionUtils.kt b/app/src/main/java/org/jellyfin/mobile/utils/TrackSelectionUtils.kt
index 76aa9db2..4ac7edd3 100644
--- a/app/src/main/java/org/jellyfin/mobile/utils/TrackSelectionUtils.kt
+++ b/app/src/main/java/org/jellyfin/mobile/utils/TrackSelectionUtils.kt
@@ -1,9 +1,8 @@
package org.jellyfin.mobile.utils
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.source.TrackGroup
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
-import com.google.android.exoplayer2.trackselection.TrackSelectionOverride
+import androidx.media3.common.TrackGroup
+import androidx.media3.common.TrackSelectionOverride
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
/**
* Select the [trackGroup] of the specified [type] and ensure the type is enabled.
diff --git a/app/src/main/java/org/jellyfin/mobile/utils/extensions/MediaMetadataCompat.kt b/app/src/main/java/org/jellyfin/mobile/utils/extensions/MediaMetadataCompat.kt
deleted file mode 100644
index 7e43eafb..00000000
--- a/app/src/main/java/org/jellyfin/mobile/utils/extensions/MediaMetadataCompat.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-@file:Suppress("NOTHING_TO_INLINE")
-
-package org.jellyfin.mobile.utils.extensions
-
-import android.graphics.Bitmap
-import android.net.Uri
-import android.support.v4.media.MediaMetadataCompat
-import androidx.core.net.toUri
-
-inline val MediaMetadataCompat.mediaId: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
-
-inline val MediaMetadataCompat.title: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_TITLE)
-
-inline val MediaMetadataCompat.artist: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
-
-inline val MediaMetadataCompat.duration
- get() = getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
-
-inline val MediaMetadataCompat.album: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_ALBUM)
-
-inline val MediaMetadataCompat.author: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_AUTHOR)
-
-inline val MediaMetadataCompat.writer: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_WRITER)
-
-inline val MediaMetadataCompat.composer: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_COMPOSER)
-
-inline val MediaMetadataCompat.compilation: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_COMPILATION)
-
-inline val MediaMetadataCompat.date: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_DATE)
-
-inline val MediaMetadataCompat.year: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_YEAR)
-
-inline val MediaMetadataCompat.genre: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_GENRE)
-
-inline val MediaMetadataCompat.trackNumber
- get() = getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)
-
-inline val MediaMetadataCompat.trackCount
- get() = getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS)
-
-inline val MediaMetadataCompat.discNumber
- get() = getLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)
-
-inline val MediaMetadataCompat.albumArtist: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)
-
-inline val MediaMetadataCompat.art: Bitmap
- get() = getBitmap(MediaMetadataCompat.METADATA_KEY_ART)
-
-inline val MediaMetadataCompat.artUri: Uri
- get() = this.getString(MediaMetadataCompat.METADATA_KEY_ART_URI).toUri()
-
-inline val MediaMetadataCompat.albumArt: Bitmap?
- get() = getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)
-
-inline val MediaMetadataCompat.albumArtUri: Uri
- get() = this.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI).toUri()
-
-inline val MediaMetadataCompat.userRating
- get() = getLong(MediaMetadataCompat.METADATA_KEY_USER_RATING)
-
-inline val MediaMetadataCompat.rating
- get() = getLong(MediaMetadataCompat.METADATA_KEY_RATING)
-
-inline val MediaMetadataCompat.displayTitle: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)
-
-inline val MediaMetadataCompat.displaySubtitle: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
-
-inline val MediaMetadataCompat.displayDescription: String?
- get() = getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION)
-
-inline val MediaMetadataCompat.displayIcon: Bitmap
- get() = getBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON)
-
-inline val MediaMetadataCompat.displayIconUri: Uri
- get() = this.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI).toUri()
-
-inline val MediaMetadataCompat.mediaUri: Uri
- get() = this.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI).toUri()
-
-inline val MediaMetadataCompat.downloadStatus
- get() = getLong(MediaMetadataCompat.METADATA_KEY_DOWNLOAD_STATUS)
-
-inline fun MediaMetadataCompat.Builder.setMediaId(id: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
-
-inline fun MediaMetadataCompat.Builder.setTitle(title: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
-
-inline fun MediaMetadataCompat.Builder.setAlbum(album: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
-
-inline fun MediaMetadataCompat.Builder.setArtist(artist: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
-
-inline fun MediaMetadataCompat.Builder.setAlbumArtist(artist: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artist)
-
-inline fun MediaMetadataCompat.Builder.setTrackNumber(number: Long): MediaMetadataCompat.Builder =
- putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, number)
-
-inline fun MediaMetadataCompat.Builder.setMediaUri(uri: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri)
-
-inline fun MediaMetadataCompat.Builder.setAlbumArtUri(uri: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, uri)
-
-inline fun MediaMetadataCompat.Builder.setDisplayIconUri(uri: String): MediaMetadataCompat.Builder =
- putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, uri)
diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml
index 24206ac9..0b66e88c 100644
--- a/app/src/main/res/layout/exo_player_control_view.xml
+++ b/app/src/main/res/layout/exo_player_control_view.xml
@@ -42,23 +42,12 @@
app:layout_constraintTop_toTopOf="parent">
-
-
-
-
+ android:foreground="@drawable/ripple_background"
+ app:pause_icon="@drawable/ic_pause_black_42dp"
+ app:play_icon="@drawable/ic_play_black_42dp" />