소스 검색

setup media player service with session

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
parneet-guraya 10 달 전
부모
커밋
47479d5745

+ 1 - 0
app/build.gradle

@@ -333,6 +333,7 @@ dependencies {
     implementation 'org.conscrypt:conscrypt-android:2.5.3'
 
     implementation "androidx.media3:media3-ui:$androidxMediaVersion"
+    implementation "androidx.media3:media3-session:$androidxMediaVersion"
     implementation "androidx.media3:media3-exoplayer:$androidxMediaVersion"
     implementation "androidx.media3:media3-datasource-okhttp:$androidxMediaVersion"
 

+ 8 - 1
app/src/main/AndroidManifest.xml

@@ -127,7 +127,14 @@
         android:usesCleartextTraffic="true"
         tools:ignore="UnusedAttribute"
         tools:replace="android:allowBackup">
-
+        <service
+            android:name="com.nextcloud.client.media.BackgroundPlayerService"
+            android:foregroundServiceType="mediaPlayback"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="androidx.media3.session.MediaSessionService"/>
+            </intent-filter>
+        </service>
         <meta-data android:name="android.content.APP_RESTRICTIONS"
             android:resource="@xml/app_config" />
 

+ 5 - 1
app/src/main/java/com/nextcloud/client/di/AppComponent.java

@@ -17,6 +17,8 @@ import com.nextcloud.client.integrations.IntegrationsModule;
 import com.nextcloud.client.jobs.JobsModule;
 import com.nextcloud.client.jobs.download.FileDownloadHelper;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
+import com.nextcloud.client.media.BackgroundPlayerService;
+import com.nextcloud.client.media.CustomExoPlayer;
 import com.nextcloud.client.network.NetworkModule;
 import com.nextcloud.client.onboarding.OnboardingModule;
 import com.nextcloud.client.preferences.PreferencesModule;
@@ -46,7 +48,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
     ThemeModule.class,
     DatabaseModule.class,
     DispatcherModule.class,
-    VariantModule.class
+    VariantModule.class,
 })
 @Singleton
 public interface AppComponent {
@@ -54,6 +56,8 @@ public interface AppComponent {
     void inject(MainApp app);
 
     void inject(MediaControlView mediaControlView);
+    void inject(CustomExoPlayer customExoPlayer);
+    void inject(BackgroundPlayerService backgroundPlayerService);
 
     void inject(ThemeableSwitchPreference switchPreference);
 

+ 9 - 1
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -17,6 +17,9 @@ import com.nextcloud.client.jobs.transfer.FileTransferService;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
 import com.nextcloud.client.logger.ui.LogsActivity;
 import com.nextcloud.client.logger.ui.LogsViewModel;
+import com.nextcloud.client.media.BackgroundPlayerService;
+import com.nextcloud.client.media.CustomExoPlayer;
+import com.nextcloud.client.media.NextcloudExoPlayer;
 import com.nextcloud.client.media.PlayerService;
 import com.nextcloud.client.migrations.Migrations;
 import com.nextcloud.client.onboarding.FirstRunActivity;
@@ -481,7 +484,12 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract TestJob testJob();
-    
+
     @ContributesAndroidInjector
     abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
+
+
+    @ContributesAndroidInjector
+    abstract BackgroundPlayerService backgroundPlayerService();
+
 }

+ 82 - 0
app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt

@@ -0,0 +1,82 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.media
+
+import android.content.Intent
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaSession
+import androidx.media3.session.MediaSessionService
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.common.NextcloudClient
+import com.owncloud.android.MainApp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class BackgroundPlayerService : MediaSessionService(), Injectable {
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    @Inject
+    lateinit var userAccountManager: UserAccountManager
+    lateinit var exoPlayer: ExoPlayer
+    private var mediaSession: MediaSession? = null
+
+    override fun onCreate() {
+        super.onCreate()
+        MainApp.getAppComponent().inject(this)
+        initNextcloudExoPlayer()
+    }
+
+    private fun initNextcloudExoPlayer() {
+        runBlocking {
+            var nextcloudClient: NextcloudClient
+            withContext(Dispatchers.IO) {
+                nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user)
+            }
+            nextcloudClient?.let {
+                exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
+                println(exoPlayer)
+                mediaSession =
+                    MediaSession.Builder(applicationContext, exoPlayer).setCallback(object : MediaSession.Callback {
+                        override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
+                            stopSelf()
+                        }
+                    }).build()
+            }
+            println("created client $nextcloudClient")
+        }
+    }
+
+    override fun onTaskRemoved(rootIntent: Intent?) {
+        val player = mediaSession?.player
+        if (player!!.playWhenReady) {
+            // Make sure the service is not in foreground.
+            player.pause()
+        }
+        stopSelf()
+    }
+
+    override fun onDestroy() {
+        mediaSession?.run {
+            player.release()
+            release()
+            mediaSession = null
+        }
+        super.onDestroy()
+    }
+
+    override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? {
+        return mediaSession
+    }
+}

+ 22 - 0
app/src/main/java/com/nextcloud/client/media/CustomExoPlayer.kt

@@ -0,0 +1,22 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.media
+
+import android.accounts.AccountManager
+import android.content.Context
+import androidx.media3.exoplayer.ExoPlayer
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.account.UserAccountManagerImpl
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.MainApp
+import javax.inject.Inject
+
+class CustomExoPlayer  {
+
+}

+ 4 - 0
app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt

@@ -15,8 +15,11 @@ import androidx.media3.datasource.DefaultDataSource
 import androidx.media3.datasource.okhttp.OkHttpDataSource
 import androidx.media3.exoplayer.ExoPlayer
 import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.network.ClientFactory
 import com.nextcloud.common.NextcloudClient
 import com.owncloud.android.MainApp
+import javax.inject.Inject
 
 object NextcloudExoPlayer {
     private const val FIVE_SECONDS_IN_MILLIS = 5000L
@@ -45,4 +48,5 @@ object NextcloudExoPlayer {
             .setSeekForwardIncrementMs(FIVE_SECONDS_IN_MILLIS)
             .build()
     }
+
 }

+ 34 - 24
app/src/main/java/com/owncloud/android/media/MediaControlView.kt

@@ -23,10 +23,10 @@ import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.LinearLayout
-import android.widget.MediaController.MediaPlayerControl
 import android.widget.SeekBar
 import android.widget.SeekBar.OnSeekBarChangeListener
 import androidx.core.content.ContextCompat
+import androidx.media3.common.Player
 import com.owncloud.android.MainApp
 import com.owncloud.android.R
 import com.owncloud.android.databinding.MediaControlBinding
@@ -50,7 +50,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
     View.OnClickListener,
     OnSeekBarChangeListener {
 
-    private var playerControl: MediaPlayerControl? = null
+    private var playerControl: Player? = null
     private var binding: MediaControlBinding
     private var isDragging = false
 
@@ -62,7 +62,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
     }
 
     @Suppress("MagicNumber")
-    fun setMediaPlayer(player: MediaPlayerControl?) {
+    fun setMediaPlayer(player: Player?) {
         playerControl = player
         handler.sendEmptyMessage(SHOW_PROGRESS)
 
@@ -104,14 +104,16 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
      */
     private fun disableUnsupportedButtons() {
         try {
-            if (playerControl?.canPause() == false) {
-                binding.playBtn.setEnabled(false)
+            //TODO: should we check for nullability && see if we need try catch block
+            if (playerControl!!.isCommandAvailable(Player.COMMAND_PLAY_PAUSE).not()) {
+                binding.playBtn.isEnabled = false
             }
-            if (playerControl?.canSeekBackward() == false) {
-                binding.rewindBtn.setEnabled(false)
+
+            if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK).not()) {
+                binding.rewindBtn.isEnabled = false
             }
-            if (playerControl?.canSeekForward() == false) {
-                binding.forwardBtn.setEnabled(false)
+            if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD).not()) {
+                binding.forwardBtn.isEnabled = false
             }
         } catch (ex: IncompatibleClassChangeError) {
             // We were given an old version of the interface, that doesn't have
@@ -149,7 +151,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
     }
 
     @Suppress("MagicNumber")
-    private fun formatTime(timeMs: Int): String {
+    private fun formatTime(timeMs: Long): String {
         val totalSeconds = timeMs / 1000
         val seconds = totalSeconds % 60
         val minutes = totalSeconds / 60 % 60
@@ -164,8 +166,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
     }
 
     @Suppress("MagicNumber")
-    private fun setProgress(): Int {
-        var position = 0
+    private fun setProgress(): Long {
+        var position = 0L
         if (playerControl == null || isDragging) {
             position = 0
         }
@@ -178,7 +180,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
                 val pos = 1000L * position / duration
                 binding.progressBar.progress = pos.toInt()
             }
-            val percent = playerControl.bufferPercentage
+            val percent = playerControl.bufferedPercentage
             binding.progressBar.setSecondaryProgress(percent * 10)
             val endTime = if (duration > 0) formatTime(duration) else "--:--"
             binding.totalTimeText.text = endTime
@@ -202,22 +204,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
                 }
                 return true
             }
+
             KeyEvent.KEYCODE_MEDIA_PLAY -> {
-                if (uniqueDown && playerControl?.isPlaying == false) {
-                    playerControl?.start()
+                if (uniqueDown && playerControl?.playWhenReady == false) {
+                    playerControl?.play()
                     updatePausePlay()
                 }
                 return true
             }
+
             KeyEvent.KEYCODE_MEDIA_STOP,
             KeyEvent.KEYCODE_MEDIA_PAUSE
             -> {
-                if (uniqueDown && playerControl?.isPlaying == true) {
+                if (uniqueDown && playerControl?.playWhenReady == true) {
                     playerControl?.pause()
                     updatePausePlay()
                 }
                 return true
             }
+
             else -> return super.dispatchKeyEvent(event)
         }
     }
@@ -225,18 +230,21 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
     fun updatePausePlay() {
         binding.playBtn.icon = ContextCompat.getDrawable(
             context,
+            // isPlaying reflects if the playback is actually moving forward, If the media is buffering and it will play when ready
+            // it would still return that it is not playing. So, in case of buffering it will show the pause icon which would show that
+            // media is loading, when user has not paused but moved the progress to a different position this works as a buffering signal.
             if (playerControl?.isPlaying == true) {
                 R.drawable.ic_pause
             } else {
                 R.drawable.ic_play
             }
         )
-        binding.forwardBtn.visibility = if (playerControl?.canSeekForward() == true) {
+        binding.forwardBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) {
             VISIBLE
         } else {
             INVISIBLE
         }
-        binding.rewindBtn.visibility = if (playerControl?.canSeekBackward() == true) {
+        binding.rewindBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK)) {
             VISIBLE
         } else {
             INVISIBLE
@@ -245,10 +253,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
 
     private fun doPauseResume() {
         playerControl?.run {
-            if (isPlaying) {
+            if (playWhenReady) {
                 pause()
             } else {
-                start()
+                play()
             }
         }
         updatePausePlay()
@@ -267,16 +275,17 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
 
     @Suppress("MagicNumber")
     override fun onClick(v: View) {
-        var pos: Int
+        var pos: Long
 
         playerControl?.let { playerControl ->
-            val playing = playerControl.isPlaying
+            val playing = playerControl.playWhenReady
             val id = v.id
 
             when (id) {
                 R.id.playBtn -> {
                     doPauseResume()
                 }
+
                 R.id.rewindBtn -> {
                     pos = playerControl.currentPosition
                     pos -= 5000
@@ -286,6 +295,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
                     }
                     setProgress()
                 }
+
                 R.id.forwardBtn -> {
                     pos = playerControl.currentPosition
                     pos += 15000
@@ -315,8 +325,8 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
         playerControl?.let { playerControl ->
             val duration = playerControl.duration.toLong()
             val newPosition = duration * progress / 1000L
-            playerControl.seekTo(newPosition.toInt())
-            binding.currentTimeText.text = formatTime(newPosition.toInt())
+            playerControl.seekTo(newPosition)
+            binding.currentTimeText.text = formatTime(newPosition)
         }
     }
 

+ 1 - 0
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -1832,6 +1832,7 @@ public class FileDisplayActivity extends FileActivity
                     ((PreviewMediaFragment) fileFragment).updateFile(renamedFile);
                     if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
                         long position = ((PreviewMediaFragment) fileFragment).getPosition();
+                        System.out.println("Start Fragment Media Preview");
                         startMediaPreview(renamedFile, position, true, true, true, false);
                     } else {
                         getFileOperationsHelper().openFile(renamedFile);

+ 117 - 171
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt

@@ -13,15 +13,11 @@
 package com.owncloud.android.ui.preview
 
 import android.app.Activity
-import android.content.BroadcastReceiver
-import android.content.Context
+import android.content.ComponentName
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.res.Configuration
-import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.drawable.Drawable
-import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.os.AsyncTask
 import android.os.Build
@@ -45,20 +41,24 @@ import androidx.core.view.WindowInsetsControllerCompat
 import androidx.core.view.marginBottom
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updatePadding
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import androidx.media3.common.Player
 import androidx.media3.common.util.UnstableApi
 import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
 import androidx.media3.ui.DefaultTimeBar
 import androidx.media3.ui.PlayerView
+import com.google.common.util.concurrent.MoreExecutors
 import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.jobs.BackgroundJobManager
 import com.nextcloud.client.jobs.download.FileDownloadHelper
+import com.nextcloud.client.media.BackgroundPlayerService
 import com.nextcloud.client.media.ExoplayerListener
 import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
-import com.nextcloud.client.media.PlayerService
 import com.nextcloud.client.media.PlayerServiceConnection
 import com.nextcloud.client.network.ClientFactory
 import com.nextcloud.client.network.ClientFactory.CreationException
@@ -71,7 +71,6 @@ import com.nextcloud.utils.extensions.statusBarHeight
 import com.owncloud.android.R
 import com.owncloud.android.databinding.ActivityPreviewMediaBinding
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.datamodel.ThumbnailsCacheManager
 import com.owncloud.android.files.StreamMediaFileOperation
 import com.owncloud.android.lib.common.OwnCloudClient
 import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
@@ -88,7 +87,6 @@ import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
 import com.owncloud.android.ui.dialog.SendShareDialog
 import com.owncloud.android.ui.fragment.FileFragment
 import com.owncloud.android.ui.fragment.OCFileListFragment
-import com.owncloud.android.utils.BitmapUtils
 import com.owncloud.android.utils.DisplayUtils
 import com.owncloud.android.utils.ErrorMessageAdapter
 import com.owncloud.android.utils.MimeTypeUtil
@@ -119,7 +117,7 @@ class PreviewMediaActivity :
     private var autoplay = true
     private val prepared = false
     private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
-    private var videoUri: Uri? = null
+    private var streamUri: Uri? = null
 
     @Inject
     lateinit var clientFactory: ClientFactory
@@ -132,13 +130,15 @@ class PreviewMediaActivity :
 
     private lateinit var binding: ActivityPreviewMediaBinding
     private var emptyListView: ViewGroup? = null
-    private var exoPlayer: ExoPlayer? = null
+    private var videoPlayer: ExoPlayer? = null
+    private var audioMediaController: MediaController? = null
     private var nextcloudClient: NextcloudClient? = null
     private lateinit var windowInsetsController: WindowInsetsControllerCompat
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
+
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
             setTheme(R.style.Theme_ownCloud_Toolbar)
         }
@@ -156,6 +156,13 @@ class PreviewMediaActivity :
         emptyListView = binding.emptyView.emptyListView
         showProgressLayout()
         addMarginForEmptyView()
+        if (file == null) {
+            return
+        }
+        if (MimeTypeUtil.isAudio(file)) {
+            setGenericThumbnail()
+            initializeAudioPlayer()
+        }
     }
 
     private fun addMarginForEmptyView() {
@@ -173,25 +180,6 @@ class PreviewMediaActivity :
         emptyListView?.layoutParams = layoutParams
     }
 
-    private fun registerMediaControlReceiver() {
-        val filter = IntentFilter(MEDIA_CONTROL_READY_RECEIVER)
-        LocalBroadcastManager.getInstance(this).registerReceiver(mediaControlReceiver, filter)
-    }
-
-    private val mediaControlReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            intent.getBooleanExtra(PlayerService.IS_MEDIA_CONTROL_LAYOUT_READY, false).run {
-                if (this) {
-                    hideProgressLayout()
-                    mediaPlayerServiceConnection?.bind()
-                    setupAudioPlayerServiceConnection()
-                } else {
-                    showProgressLayout()
-                }
-            }
-        }
-    }
-
     private fun initArguments(savedInstanceState: Bundle?) {
         intent?.let {
             initWithIntent(it)
@@ -203,20 +191,6 @@ class PreviewMediaActivity :
         } else {
             initWithBundle(savedInstanceState)
         }
-
-        if (MimeTypeUtil.isAudio(file)) {
-            preparePreviewForAudioFile()
-        }
-    }
-
-    private fun preparePreviewForAudioFile() {
-        registerMediaControlReceiver()
-
-        if (file.isDown) {
-            return
-        }
-
-        requestForDownload(file)
     }
 
     private fun initWithIntent(intent: Intent) {
@@ -245,8 +219,6 @@ class PreviewMediaActivity :
 
         if (isFileVideo) {
             binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
-        } else {
-            extractAndSetCoverArt(file)
         }
     }
 
@@ -265,13 +237,13 @@ class PreviewMediaActivity :
 
     private fun showProgressLayout() {
         binding.progress.visibility = View.VISIBLE
-        binding.mediaController.visibility = View.GONE
+        binding.audioControllerView.visibility = View.GONE
         binding.emptyView.emptyListView.visibility = View.GONE
     }
 
     private fun hideProgressLayout() {
         binding.progress.visibility = View.GONE
-        binding.mediaController.visibility = View.VISIBLE
+        binding.audioControllerView.visibility = View.VISIBLE
         binding.emptyView.emptyListView.visibility = View.VISIBLE
     }
 
@@ -287,48 +259,6 @@ class PreviewMediaActivity :
         }
     }
 
-    /**
-     * tries to read the cover art from the audio file and sets it as cover art.
-     *
-     * @param file audio file with potential cover art
-     */
-    @Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
-    private fun extractAndSetCoverArt(file: OCFile) {
-        if (!MimeTypeUtil.isAudio(file)) {
-            return
-        }
-
-        val bitmap = if (file.storagePath == null) {
-            getAudioThumbnail(file)
-        } else {
-            getThumbnail(file.storagePath) ?: getAudioThumbnail(file)
-        }
-
-        if (bitmap != null) {
-            binding.imagePreview.setImageBitmap(bitmap)
-        } else {
-            setGenericThumbnail()
-        }
-    }
-
-    @Suppress("TooGenericExceptionCaught")
-    private fun getThumbnail(storagePath: String?): Bitmap? {
-        return try {
-            MediaMetadataRetriever().run {
-                setDataSource(storagePath)
-                BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture?.size ?: 0)
-            }
-        } catch (t: Throwable) {
-            BitmapUtils.drawableToBitmap(genericThumbnail())
-        }
-    }
-
-    private fun getAudioThumbnail(file: OCFile): Bitmap? {
-        return ThumbnailsCacheManager.getBitmapFromDiskCache(
-            ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
-        )
-    }
-
     private fun setGenericThumbnail() {
         binding.imagePreview.setImageDrawable(genericThumbnail())
     }
@@ -356,8 +286,8 @@ class PreviewMediaActivity :
 
     private fun saveMediaInstanceState(bundle: Bundle) {
         bundle.run {
-            if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
-                exoPlayer?.let {
+            if (MimeTypeUtil.isVideo(file) && audioMediaController != null) {
+                audioMediaController?.let {
                     savedPlaybackPosition = it.currentPosition
                     autoplay = it.isPlaying
                 }
@@ -375,42 +305,13 @@ class PreviewMediaActivity :
 
         Log_OC.v(TAG, "onStart")
 
-        if (file == null) {
-            return
-        }
-
-        mediaPlayerServiceConnection?.bind()
-
-        if (MimeTypeUtil.isAudio(file)) {
-            setupAudioPlayerServiceConnection()
-        } else if (MimeTypeUtil.isVideo(file)) {
-            if (mediaPlayerServiceConnection?.isConnected == true) {
-                stopAudio()
-            }
-
-            if (exoPlayer != null) {
-                playVideo()
-            } else {
-                initNextcloudExoPlayer()
-            }
-        }
-    }
-
-    private fun setupAudioPlayerServiceConnection() {
-        binding.mediaController.run {
-            setMediaPlayer(mediaPlayerServiceConnection)
-            visibility = View.VISIBLE
-        }
-
-        user?.let {
-            mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
+        if (MimeTypeUtil.isVideo(file)) {
+            //TODO: should we somehow release any previous audio sessions?
+            initializeVideoPlayer()
         }
-
-        binding.emptyView.emptyListView.visibility = View.GONE
-        binding.progress.visibility = View.GONE
     }
 
-    private fun initNextcloudExoPlayer() {
+    private fun initializeVideoPlayer() {
         val handler = Handler(Looper.getMainLooper())
         Executors.newSingleThreadExecutor().execute {
             try {
@@ -418,9 +319,10 @@ class PreviewMediaActivity :
 
                 nextcloudClient?.let { client ->
                     handler.post {
-                        exoPlayer = createNextcloudExoplayer(this, client)
+                        videoPlayer = createNextcloudExoplayer(this, client)
 
-                        exoPlayer?.let { player ->
+
+                        videoPlayer?.let { player ->
                             player.addListener(
                                 ExoplayerListener(
                                     this,
@@ -439,6 +341,72 @@ class PreviewMediaActivity :
         }
     }
 
+    private fun releaseVideoPlayer() {
+        videoPlayer?.let {
+            savedPlaybackPosition = it.currentPosition
+            autoplay = it.playWhenReady
+            it.release()
+        }
+        videoPlayer = null
+    }
+
+    private fun initializeAudioPlayer() {
+        val sessionToken = SessionToken(this, ComponentName(this, BackgroundPlayerService::class.java))
+        val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
+        controllerFuture.addListener(
+            {
+                try {
+                    audioMediaController = controllerFuture.get()
+                    playAudio()
+                    binding.audioControllerView.setMediaPlayer(audioMediaController)
+                } catch (e: Exception) {
+                    println("exception raised while getting the media controller ${e.message}")
+                }
+            },
+            MoreExecutors.directExecutor()
+        )
+    }
+
+    private fun playAudio() {
+        if (file.isDown) {
+            prepareAudioPlayer(file.storageUri)
+        } else {
+            try {
+                LoadStreamUrl(this, user, clientFactory).execute(file.localId)
+            } catch (e: Exception) {
+                Log_OC.e(TAG, "Loading stream url not possible: $e")
+            }
+        }
+    }
+
+    private fun prepareAudioPlayer(uri: Uri) {
+        hideProgressLayout()
+        audioMediaController?.let { audioPlayer ->
+            audioPlayer.addListener(object : Player.Listener {
+                override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
+                    val artworkBitmap = mediaMetadata.artworkData?.let { bytes: ByteArray ->
+                        BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+                    }
+                    if (artworkBitmap != null) {
+                        binding.imagePreview.setImageBitmap(artworkBitmap)
+                    }
+                }
+            })
+            audioPlayer.setMediaItem(MediaItem.fromUri(uri))
+            audioPlayer.playWhenReady = autoplay
+            audioPlayer.seekTo(savedPlaybackPosition)
+            audioPlayer.prepare()
+        }
+    }
+
+    private fun releaseAudioPlayer() {
+        audioMediaController?.let { audioPlayer ->
+            audioPlayer.stop()
+            audioPlayer.release()
+        }
+        audioMediaController = null
+    }
+
     private fun initWindowInsetsController() {
         windowInsetsController = WindowCompat.getInsetsController(
             window,
@@ -495,7 +463,7 @@ class PreviewMediaActivity :
                     }
                 }
             )
-            it.player = exoPlayer
+            it.player = videoPlayer
         }
     }
 
@@ -567,7 +535,7 @@ class PreviewMediaActivity :
             }
 
             R.id.action_remove_file -> {
-                exoPlayer?.stop()
+                videoPlayer?.pause()
                 val dialog = RemoveFilesDialogFragment.newInstance(file)
                 dialog.show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
             }
@@ -614,6 +582,7 @@ class PreviewMediaActivity :
             val removedFile = operation.file
             val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
             if (!fileAvailable && removedFile == file) {
+                releaseAudioPlayer()
                 finish()
             }
         } else if (operation is SynchronizeFileOperation) {
@@ -670,7 +639,7 @@ class PreviewMediaActivity :
         setupVideoView()
 
         if (file.isDown) {
-            playVideoUri(file.storageUri)
+            prepareVideoPlayer(file.storageUri)
         } else {
             try {
                 LoadStreamUrl(this, user, clientFactory).execute(file.localId)
@@ -680,20 +649,15 @@ class PreviewMediaActivity :
         }
     }
 
-    private fun playVideoUri(uri: Uri) {
+    private fun prepareVideoPlayer(uri: Uri) {
         binding.progress.visibility = View.GONE
-
-        exoPlayer?.run {
-            setMediaItem(MediaItem.fromUri(uri))
+        val videoMediaItem = MediaItem.fromUri(uri)
+        videoPlayer?.run {
+            setMediaItem(videoMediaItem)
             playWhenReady = autoplay
+            seekTo(savedPlaybackPosition)
             prepare()
-
-            if (savedPlaybackPosition >= 0) {
-                seekTo(savedPlaybackPosition)
-            }
         }
-
-        autoplay = false
     }
 
     private class LoadStreamUrl(
@@ -728,8 +692,12 @@ class PreviewMediaActivity :
             val weakReference = previewMediaActivityWeakReference.get()
             weakReference?.apply {
                 if (uri != null) {
-                    videoUri = uri
-                    playVideoUri(uri)
+                    streamUri = uri
+                    if (MimeTypeUtil.isVideo(file)) {
+                        prepareVideoPlayer(uri)
+                    } else if (MimeTypeUtil.isAudio(file)) {
+                        prepareAudioPlayer(uri)
+                    }
                 } else {
                     emptyListView?.visibility = View.VISIBLE
                     setVideoErrorMessage(
@@ -754,29 +722,15 @@ class PreviewMediaActivity :
     }
 
     override fun onDestroy() {
-        Log_OC.v(TAG, "onDestroy")
-
-        LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaControlReceiver)
-
         super.onDestroy()
-        exoPlayer?.run {
-            stop()
-            release()
-        }
+
+        Log_OC.v(TAG, "onDestroy")
     }
 
     override fun onStop() {
         Log_OC.v(TAG, "onStop")
 
-        file?.let {
-            if (MimeTypeUtil.isVideo(it) && exoPlayer != null && exoPlayer?.isPlaying == true) {
-                savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
-            }
-        }
-
-        exoPlayer?.pause()
-        stopAudio()
-        mediaPlayerServiceConnection?.unbind()
+        releaseVideoPlayer()
         super.onStop()
     }
 
@@ -829,23 +783,15 @@ class PreviewMediaActivity :
     }
 
     private fun stopPreview(stopAudio: Boolean) {
+        //TODO: stop removes the media item attached but not release the player
+        // do we want to keep this behaviour or release the player too just like in onStop?
         if (MimeTypeUtil.isAudio(file) && stopAudio) {
-            mediaPlayerServiceConnection?.pause()
+            audioMediaController?.pause()
         } else if (MimeTypeUtil.isVideo(file)) {
-            savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
-            exoPlayer?.stop()
+            releaseVideoPlayer()
         }
     }
 
-    val position: Long
-        get() {
-            if (prepared) {
-                savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
-            }
-            Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
-            return savedPlaybackPosition
-        }
-
     companion object {
         private val TAG = PreviewMediaActivity::class.java.simpleName
 

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt

@@ -330,7 +330,7 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
     }
 
     private fun prepareForAudio() {
-        binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
+        binding.mediaController.setMediaPlayer(null)
         binding.mediaController.visibility = View.VISIBLE
         mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
         binding.emptyView.emptyListView.visibility = View.GONE

+ 4 - 2
app/src/main/res/layout/activity_preview_media.xml

@@ -42,7 +42,7 @@
         app:show_buffering="always" />
 
     <com.owncloud.android.media.MediaControlView
-        android:id="@+id/media_controller"
+        android:id="@+id/audio_controller_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
@@ -65,7 +65,9 @@
 
     <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        >
 
         <include
             android:id="@+id/empty_view"

+ 28 - 0
gradle/verification-metadata.xml

@@ -2548,6 +2548,14 @@
             <sha256 value="3670ba201f837bdce5ffaf4adc766a0d21cfd08db74efed5657513544c054eba" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.media3" name="media3-session" version="1.4.0">
+         <artifact name="media3-session-1.4.0.aar">
+            <sha256 value="a5daaaea8fc9a87ebb4411f1d97bcf887069132068b3af15374205cfd458bb7c" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="media3-session-1.4.0.module">
+            <sha256 value="6bccbca5b01eaa3fd0502300f3530ba1f1cdc952927a0a0f3fb1b1ae39860ed6" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.media3" name="media3-ui" version="1.2.0">
          <artifact name="media3-ui-1.2.0.aar">
             <sha256 value="fee39edbf615f9432f53af1cc9b20dd5706bfbc5dbd7fe581253a59eedd91482" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -9718,6 +9726,11 @@
             <sha256 value="16e05e9f49621b87c53e69350140f3c46d42d966c67a933bdf4b063a2b1c8fc5" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.jacoco" name="org.jacoco.agent" version="0.8.12">
+         <artifact name="org.jacoco.agent-0.8.12.pom">
+            <sha256 value="0f9da994abd9827f957fc1ba7c5bad3fe918f62601c1d743f216b0615efe480e" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="org.jacoco" name="org.jacoco.ant" version="0.8.11">
          <artifact name="org.jacoco.ant-0.8.11.jar">
             <sha256 value="81d7eb8890d9be30a939612c295603541063529cdd03a53265aba74474b70b7c" origin="Generated by Gradle"/>
@@ -11041,6 +11054,11 @@
             <sha256 value="92eee24bc3c843e4881d46c1dd6505471ee3142facfb466b428cfea5a56c6b60" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.ow2.asm" name="asm" version="9.7">
+         <artifact name="asm-9.7.pom">
+            <sha256 value="de00115f1d84f3a0b2ee3a4b6f6192d066f86d185d67b9d1522f2c80feac5f00" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="org.ow2.asm" name="asm-analysis" version="9.2">
          <artifact name="asm-analysis-9.2.jar">
             <sha256 value="878fbe521731c072d14d2d65b983b1beae6ad06fda0007b6a8bae81f73f433c4" origin="Generated by Gradle"/>
@@ -11091,6 +11109,11 @@
             <sha256 value="a98ae4895334baf8ff86bd66516210dbd9a03f1a6e15e47dda82afcf6b53d77c" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.ow2.asm" name="asm-commons" version="9.7">
+         <artifact name="asm-commons-9.7.pom">
+            <sha256 value="5acee3ee7252ed90b8074c755d022787499a95fafff98ac4a685107c4da409b4" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="org.ow2.asm" name="asm-tree" version="9.2">
          <artifact name="asm-tree-9.2.jar">
             <sha256 value="aabf9bd23091a4ebfc109c1f3ee7cf3e4b89f6ba2d3f51c5243f16b3cffae011" origin="Generated by Gradle"/>
@@ -11115,6 +11138,11 @@
             <sha256 value="1bcb481d7fc16b955bb60ca07c8cfa2424bcee78bdc405bba31c7d6f5dc2d113" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.ow2.asm" name="asm-tree" version="9.7">
+         <artifact name="asm-tree-9.7.pom">
+            <sha256 value="a34ea1e3e4128c01038db43c6976e88c779cf5af84b0505da266dfe6965668ec" origin="Generated by Gradle"/>
+         </artifact>
+      </component>
       <component group="org.ow2.asm" name="asm-util" version="9.2">
          <artifact name="asm-util-9.2.jar">
             <sha256 value="ff5b3cd331ae8a9a804768280da98f50f424fef23dd3c788bb320e08c94ee598" origin="Generated by Gradle"/>