From d90c162cc858b691834c6e7baf4e1928fe18a1d5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Feb 2026 10:03:29 +0100 Subject: [PATCH 1/5] fix(exo-player): service not start in time Signed-off-by: alperozturk96 --- .../client/media/BackgroundPlayerService.kt | 258 ++++++++++-------- .../nextcloud/client/media/PlayerService.kt | 31 ++- app/src/main/res/values/strings.xml | 6 + 3 files changed, 181 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt index 7e1adcaedc48..fecf0f427e78 100644 --- a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2024 Parneet Singh * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -41,9 +42,14 @@ import com.nextcloud.client.network.ClientFactory import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.extensions.registerBroadcastReceiver import com.owncloud.android.MainApp +import com.owncloud.android.R import com.owncloud.android.datamodel.ReceiverFlag +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -52,21 +58,21 @@ class BackgroundPlayerService : MediaSessionService(), Injectable { + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val seekBackSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_BACK, Bundle.EMPTY) private val seekForwardSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_FORWARD, Bundle.EMPTY) - val seekForward = - CommandButton.Builder() - .setDisplayName("Seek Forward") - .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) + private val seekForward = + CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) + .setDisplayName(getString(R.string.media_player_seek_forward)) .setSessionCommand(seekForwardSessionCommand) .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) }) .build() - val seekBackward = - CommandButton.Builder() - .setDisplayName("Seek Backward") - .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_5)) + private val seekBackward = + CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_15)) + .setDisplayName(getString(R.string.media_player_seek_backward)) .setSessionCommand(seekBackSessionCommand) .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) }) .build() @@ -76,14 +82,23 @@ class BackgroundPlayerService : @Inject lateinit var userAccountManager: UserAccountManager - lateinit var exoPlayer: ExoPlayer + + private lateinit var exoPlayer: ExoPlayer private var mediaSession: MediaSession? = null + private var isPlayerReady = false + private val stopReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { RELEASE_MEDIA_SESSION_BROADCAST_ACTION -> release() - STOP_MEDIA_SESSION_BROADCAST_ACTION -> exoPlayer.stop() + STOP_MEDIA_SESSION_BROADCAST_ACTION -> { + if (isPlayerReady) { + exoPlayer.stop() + } else { + stopSelf() + } + } } } } @@ -91,6 +106,13 @@ class BackgroundPlayerService : override fun onCreate() { super.onCreate() + MainApp.getAppComponent().inject(this) + + exoPlayer = ExoPlayer.Builder(this).build() + mediaSession = buildMediaSession(exoPlayer) + + setMediaNotificationProvider(buildNotificationProvider()) + registerBroadcastReceiver( stopReceiver, IntentFilter().apply { @@ -100,109 +122,121 @@ class BackgroundPlayerService : ReceiverFlag.NotExported ) - MainApp.getAppComponent().inject(this) - initNextcloudExoPlayer() - - setMediaNotificationProvider(object : DefaultMediaNotificationProvider(this) { - override fun getMediaButtons( - session: MediaSession, - playerCommands: Player.Commands, - customLayout: ImmutableList, - showPauseButton: Boolean - ): ImmutableList { - val playPauseButton = - CommandButton.Builder() - .setDisplayName("PlayPause") - .setIconResId( - CommandButton.getIconResIdForIconConstant( - if (mediaSession?.player?.isPlaying == true) { - CommandButton.ICON_PAUSE - } else { - CommandButton.ICON_PLAY - } - ) - ) - .setPlayerCommand(COMMAND_PLAY_PAUSE) - .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) - .build() - - val myCustomButtonsLayout = - ImmutableList.of(seekBackward, playPauseButton, seekForward) - return myCustomButtonsLayout - } - }) + initExoPlayer() } - private fun initNextcloudExoPlayer() { - runBlocking { - var nextcloudClient: NextcloudClient - withContext(Dispatchers.IO) { - nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user) - } - nextcloudClient.let { - exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient) - mediaSession = - MediaSession.Builder(applicationContext, exoPlayer) - // set id to distinct this session to avoid crash - // in case session release delayed a bit and - // we start another session for eg. video - .setId(BACKGROUND_MEDIA_SESSION_ID) - .setCustomLayout(listOf(seekBackward, seekForward)) - .setCallback(object : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): ConnectionResult = AcceptedResultBuilder(mediaSession!!) - .setAvailablePlayerCommands( - ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() - .remove(COMMAND_SEEK_TO_NEXT) - .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) - .remove(COMMAND_SEEK_TO_PREVIOUS) - .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) - .build() - ) - .setAvailableSessionCommands( - ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - .addSessionCommands( - listOf(seekBackSessionCommand, seekForwardSessionCommand) - ).build() - ) - .build() - - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - session.setCustomLayout(listOf(seekBackward, seekForward)) - } - - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture = when (customCommand.customAction) { - SESSION_COMMAND_ACTION_SEEK_FORWARD -> { - session.player.seekForward() - Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - - SESSION_COMMAND_ACTION_SEEK_BACK -> { - session.player.seekBack() - Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - - else -> super.onCustomCommand(session, controller, customCommand, args) - } - }) - .build() + private fun initExoPlayer() { + serviceScope.launch { + try { + val nextcloudClient: NextcloudClient = withContext(Dispatchers.IO) { + clientFactory.createNextcloudClient(userAccountManager.user) + } + + val realPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient) + + exoPlayer.release() + exoPlayer = realPlayer + isPlayerReady = true + + // Update the session to use the real player + mediaSession?.player = realPlayer + } catch (e: Exception) { + Log_OC.e(TAG, "Failed to initialise Nextcloud ExoPlayer: ${e.message}") + stopSelf() } } } + private fun buildMediaSession(player: ExoPlayer): MediaSession = + MediaSession.Builder(applicationContext, player) + .setId(BACKGROUND_MEDIA_SESSION_ID) + .setCustomLayout(listOf(seekBackward, seekForward)) + .setCallback(object : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult = AcceptedResultBuilder(mediaSession ?: session) + .setAvailablePlayerCommands( + ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() + .remove(COMMAND_SEEK_TO_NEXT) + .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .remove(COMMAND_SEEK_TO_PREVIOUS) + .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build() + ) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .addSessionCommands( + listOf(seekBackSessionCommand, seekForwardSessionCommand) + ).build() + ) + .build() + + override fun onPostConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ) { + session.setCustomLayout(listOf(seekBackward, seekForward)) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture = when (customCommand.customAction) { + SESSION_COMMAND_ACTION_SEEK_FORWARD -> { + session.player.seekForward() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + SESSION_COMMAND_ACTION_SEEK_BACK -> { + session.player.seekBack() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + else -> super.onCustomCommand(session, controller, customCommand, args) + } + }) + .build() + + private fun buildNotificationProvider() = object : DefaultMediaNotificationProvider(this) { + val icon = if (mediaSession?.player?.isPlaying == true) { + CommandButton.ICON_PAUSE + } else { + CommandButton.ICON_PLAY + } + + val displayName = if (mediaSession?.player?.isPlaying == true) { + getString(R.string.media_player_pause) + } else { + getString(R.string.media_player_play) + } + + override fun getMediaButtons( + session: MediaSession, + playerCommands: Player.Commands, + customLayout: ImmutableList, + showPauseButton: Boolean + ): ImmutableList { + val playPauseButton = + CommandButton.Builder(CommandButton.getIconResIdForIconConstant(icon)) + .setDisplayName(displayName) + .setPlayerCommand(COMMAND_PLAY_PAUSE) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) + .build() + + return ImmutableList.of(seekBackward, playPauseButton, seekForward) + } + } + override fun onTaskRemoved(rootIntent: Intent?) { release() } override fun onDestroy() { unregisterReceiver(stopReceiver) + serviceScope.cancel() mediaSession?.run { player.release() release() @@ -214,28 +248,26 @@ class BackgroundPlayerService : private fun release() { val player = mediaSession?.player if (player?.playWhenReady == true) { - // Make sure the service is not in foreground. player.pause() } - // Bug in Android 14, https://github.com/androidx/media/issues/805 - // that sometimes onTaskRemove() doesn't get called immediately - // eventually gets called so the service stops but the notification doesn't clear out. - // [WORKAROUND] So, explicitly removing the notification here. - // TODO revisit after bug solved! val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager nm.cancel(DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID) stopSelf() } - override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? = mediaSession + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession companion object { + private val TAG = BackgroundPlayerService::class.java.simpleName + private const val SESSION_COMMAND_ACTION_SEEK_BACK = "SESSION_COMMAND_ACTION_SEEK_BACK" private const val SESSION_COMMAND_ACTION_SEEK_FORWARD = "SESSION_COMMAND_ACTION_SEEK_FORWARD" + private const val BACKGROUND_MEDIA_SESSION_ID = + "com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID" - private const val BACKGROUND_MEDIA_SESSION_ID = "com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID" - - const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.RELEASE_MEDIA_SESSION" - const val STOP_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.STOP_MEDIA_SESSION" + const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION = + "com.nextcloud.client.media.RELEASE_MEDIA_SESSION" + const val STOP_MEDIA_SESSION_BROADCAST_ACTION = + "com.nextcloud.client.media.STOP_MEDIA_SESSION" } } diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt index e7c67a7a6ccc..15e6398351c0 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt @@ -64,7 +64,6 @@ class PlayerService : Service() { putExtra(IS_MEDIA_CONTROL_LAYOUT_READY, false) } LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) - startForeground(file) } override fun onStart() { @@ -133,6 +132,19 @@ class PlayerService : Service() { override fun onBind(intent: Intent?): IBinder? = Binder(this) override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + Log_OC.d(TAG, "player service started") + if (!isRunning) { + val file = intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + if (file != null) { + startForeground(file) + } else { + startForegroundWithPlaceholder() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + return START_NOT_STICKY + } + } + when (intent.action) { ACTION_PLAY -> onActionPlay(intent) ACTION_STOP -> onActionStop() @@ -142,6 +154,23 @@ class PlayerService : Service() { return START_NOT_STICKY } + private fun startForegroundWithPlaceholder() { + val ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name)) + notificationBuilder.run { + setSmallIcon(R.drawable.ic_play_arrow) + setWhen(System.currentTimeMillis()) + setOngoing(false) + setContentTitle(ticker) + setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA) + } + ForegroundServiceHelper.startService( + this, + R.string.media_notif_ticker, + notificationBuilder.build(), + ForegroundServiceType.MediaPlayback + ) + } + private fun onActionToggle() { player.run { if (isPlaying) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3912bf80aca3..23b534092dcd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -473,6 +473,12 @@ - The URL does not match the hostname in the certificate Do you want to trust this certificate anyway? Could not save certificate + Seek forward + Seek backward + Play + Pause + + Details Hide Issued to: From e2a45bb445b5dd26b13a9bd4407f867397a6fdfc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Feb 2026 10:19:16 +0100 Subject: [PATCH 2/5] fix(exo-player): service not start in time Signed-off-by: alperozturk96 --- .../client/media/BackgroundPlayerService.kt | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt index fecf0f427e78..180eb176a4b4 100644 --- a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -63,19 +63,8 @@ class BackgroundPlayerService : private val seekBackSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_BACK, Bundle.EMPTY) private val seekForwardSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_FORWARD, Bundle.EMPTY) - private val seekForward = - CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) - .setDisplayName(getString(R.string.media_player_seek_forward)) - .setSessionCommand(seekForwardSessionCommand) - .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) }) - .build() - - private val seekBackward = - CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_15)) - .setDisplayName(getString(R.string.media_player_seek_backward)) - .setSessionCommand(seekBackSessionCommand) - .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) }) - .build() + private lateinit var seekForward: CommandButton + private lateinit var seekBackward: CommandButton @Inject lateinit var clientFactory: ClientFactory @@ -103,11 +92,26 @@ class BackgroundPlayerService : } } + @Suppress("DEPRECATION") override fun onCreate() { super.onCreate() MainApp.getAppComponent().inject(this) + seekForward = CommandButton.Builder() + .setDisplayName(getString(R.string.media_player_seek_forward)) + .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) + .setSessionCommand(seekForwardSessionCommand) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) }) + .build() + + seekBackward = CommandButton.Builder() + .setDisplayName(getString(R.string.media_player_seek_backward)) + .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_15)) + .setSessionCommand(seekBackSessionCommand) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) }) + .build() + exoPlayer = ExoPlayer.Builder(this).build() mediaSession = buildMediaSession(exoPlayer) @@ -201,18 +205,9 @@ class BackgroundPlayerService : .build() private fun buildNotificationProvider() = object : DefaultMediaNotificationProvider(this) { - val icon = if (mediaSession?.player?.isPlaying == true) { - CommandButton.ICON_PAUSE - } else { - CommandButton.ICON_PLAY - } - - val displayName = if (mediaSession?.player?.isPlaying == true) { - getString(R.string.media_player_pause) - } else { - getString(R.string.media_player_play) - } + val isPlaying = mediaSession?.player?.isPlaying ?: false + @Suppress("DEPRECATION") override fun getMediaButtons( session: MediaSession, playerCommands: Player.Commands, @@ -220,8 +215,14 @@ class BackgroundPlayerService : showPauseButton: Boolean ): ImmutableList { val playPauseButton = - CommandButton.Builder(CommandButton.getIconResIdForIconConstant(icon)) - .setDisplayName(displayName) + CommandButton.Builder() + .setDisplayName( + if (isPlaying) getString(R.string.media_player_pause) + else getString(R.string.media_player_play) + ) + .setIconResId( + if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow + ) .setPlayerCommand(COMMAND_PLAY_PAUSE) .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) .build() From 1e805eda8ccbd11218d94b55112f03010ff32da6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Feb 2026 10:25:12 +0100 Subject: [PATCH 3/5] fix(exo-player): illegal state exception creation of exoplayer Signed-off-by: alperozturk96 --- .../ui/preview/PreviewMediaFragment.kt | 135 ++++++++---------- 1 file changed, 56 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt index 3f68ba5ded0a..b36e87cb3574 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt @@ -18,10 +18,7 @@ import android.content.Intent import android.content.res.Configuration import android.content.res.Resources import android.net.Uri -import android.os.AsyncTask import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -37,6 +34,7 @@ import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi @@ -58,7 +56,6 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.logFileSize -import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.FragmentPreviewMediaBinding import com.owncloud.android.datamodel.OCFile @@ -71,8 +68,9 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.utils.MimeTypeUtil -import java.lang.ref.WeakReference -import java.util.concurrent.Executors +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -222,27 +220,30 @@ class PreviewMediaFragment : Log_OC.d(TAG, "File is null or fragment not attached to a context.") return } - prepareForVideo(context ?: MainApp.getAppContext()) + prepareForVideo() } @Suppress("DEPRECATION", "TooGenericExceptionCaught") - private fun prepareForVideo(context: Context) { + private fun prepareForVideo() { if (exoPlayer != null) { playVideo() - } else { - val handler = Handler(Looper.getMainLooper()) - Executors.newSingleThreadExecutor().execute { - try { - nextcloudClient = clientFactory.createNextcloudClient(accountManager.user) - handler.post { - nextcloudClient?.let { client -> - createExoPlayer(context, client) - playVideo() - } - } - } catch (e: CreationException) { - handler.post { Log_OC.e(TAG, "error setting up ExoPlayer", e) } + return + } + + lifecycleScope.launch { + try { + val client = withContext(Dispatchers.IO) { + clientFactory.createNextcloudClient(accountManager.user) } + nextcloudClient = client + val ctx = this@PreviewMediaFragment.context ?: return@launch + + withContext(Dispatchers.Main) { + createExoPlayer(ctx, client) + playVideo() + } + } catch (e: CreationException) { + Log_OC.e(TAG, "error setting up ExoPlayer", e) } } } @@ -253,9 +254,8 @@ class PreviewMediaFragment : val listener = ExoplayerListener(context, binding.exoplayerView, it) { goBackToLivePhoto() } it.addListener(listener) } - // session id needs to be unique since this fragment is used in viewpager multiple fragments can exist at a time mediaSession = MediaSession.Builder( - requireContext(), + context, exoPlayer as Player ).setId(System.currentTimeMillis().toString()).build() } @@ -408,21 +408,48 @@ class PreviewMediaFragment : @Suppress("TooGenericExceptionCaught") private fun playVideo() { setupVideoView() - // load the video file in the video player - // when done, VideoHelper#onPrepared() will be called if (file.isDown) { playVideoUri(file.storageUri) - } else { + return + } + + lifecycleScope.launch { try { - LoadStreamUrl(this, user, clientFactory).execute( - file.localId - ) + val uri = withContext(Dispatchers.IO) { + loadStreamUrl(user, clientFactory, file.localId) + } + if (uri != null) { + videoUri = uri + playVideoUri(uri) + } else { + emptyListView?.visibility = View.VISIBLE + setVideoErrorMessage(getString(R.string.stream_not_possible_headline)) + } } catch (e: Exception) { Log_OC.e(TAG, "Loading stream url not possible: $e") } } } + + private fun loadStreamUrl(user: User?, clientFactory: ClientFactory?, fileId: Long): Uri? { + val client: OwnCloudClient? = try { + clientFactory?.create(user) + } catch (e: CreationException) { + Log_OC.e(TAG, "Loading stream url not possible: $e") + return null + } + + val sfo = StreamMediaFileOperation(fileId) + val result = sfo.execute(client) + + if (result?.isSuccess == false) { + return null + } + + return (result?.data?.get(0) as String).toUri() + } + private fun playVideoUri(uri: Uri) { binding.progress.visibility = View.GONE @@ -438,56 +465,6 @@ class PreviewMediaFragment : autoplay = false } - @Suppress("DEPRECATION", "ReturnCount") - private class LoadStreamUrl( - previewMediaFragment: PreviewMediaFragment, - private val user: User?, - private val clientFactory: ClientFactory? - ) : AsyncTask() { - private val previewMediaFragmentWeakReference = WeakReference(previewMediaFragment) - - @Deprecated("Deprecated in Java") - override fun doInBackground(vararg fileId: Long?): Uri? { - val client: OwnCloudClient? - try { - client = clientFactory?.create(user) - } catch (e: CreationException) { - Log_OC.e(TAG, "Loading stream url not possible: $e") - return null - } - - val sfo = fileId[0]?.let { StreamMediaFileOperation(it) } - val result = sfo?.execute(client) - - if (result?.isSuccess == false) { - return null - } - - return (result?.data?.get(0) as String).toUri() - } - - @Deprecated("Deprecated in Java") - override fun onPostExecute(uri: Uri?) { - val previewMediaFragment = previewMediaFragmentWeakReference.get() - val context = previewMediaFragment?.context - - if (previewMediaFragment?.binding == null || context == null) { - Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!") - return - } - - previewMediaFragment.run { - if (uri != null) { - videoUri = uri - playVideoUri(uri) - } else { - emptyListView?.visibility = View.VISIBLE - setVideoErrorMessage(getString(R.string.stream_not_possible_headline)) - } - } - } - } - override fun onStop() { releaseVideoPlayer() super.onStop() From a003a5c1f035cf841f409f4eb4809db3ad7656b2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Feb 2026 10:47:11 +0100 Subject: [PATCH 4/5] fix(exo-player): crash Signed-off-by: alperozturk96 --- .../client/media/BackgroundPlayerService.kt | 118 ++++++++++-------- .../ui/preview/PreviewMediaFragment.kt | 2 +- app/src/main/res/drawable/ic_skip_next.xml | 15 +++ .../main/res/drawable/ic_skip_previous.xml | 15 +++ app/src/main/res/values/strings.xml | 2 +- 5 files changed, 101 insertions(+), 51 deletions(-) create mode 100644 app/src/main/res/drawable/ic_skip_next.xml create mode 100644 app/src/main/res/drawable/ic_skip_previous.xml diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt index 180eb176a4b4..bd40043a803e 100644 --- a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -13,8 +13,12 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ServiceInfo +import android.os.Build import android.os.Bundle import androidx.annotation.OptIn +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat import androidx.media3.common.Player import androidx.media3.common.Player.COMMAND_PLAY_PAUSE import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT @@ -45,6 +49,7 @@ import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.ReceiverFlag import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.notifications.NotificationUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -81,6 +86,7 @@ class BackgroundPlayerService : override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { RELEASE_MEDIA_SESSION_BROADCAST_ACTION -> release() + STOP_MEDIA_SESSION_BROADCAST_ACTION -> { if (isPlayerReady) { exoPlayer.stop() @@ -92,6 +98,26 @@ class BackgroundPlayerService : } } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notification = NotificationCompat.Builder(this, NotificationUtils.NOTIFICATION_CHANNEL_MEDIA) + .setSmallIcon(R.drawable.logo) + .setContentTitle(getString(R.string.media_player_playing)) + .setSilent(true) + .build() + + ServiceCompat.startForeground( + this, + DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID, + notification, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + } else { + 0 + } + ) + return super.onStartCommand(intent, flags, startId) + } + @Suppress("DEPRECATION") override fun onCreate() { super.onCreate() @@ -100,14 +126,14 @@ class BackgroundPlayerService : seekForward = CommandButton.Builder() .setDisplayName(getString(R.string.media_player_seek_forward)) - .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) + .setIconResId(R.drawable.ic_skip_next) .setSessionCommand(seekForwardSessionCommand) .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) }) .build() seekBackward = CommandButton.Builder() .setDisplayName(getString(R.string.media_player_seek_backward)) - .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_15)) + .setIconResId(R.drawable.ic_skip_previous) .setSessionCommand(seekBackSessionCommand) .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) }) .build() @@ -129,6 +155,7 @@ class BackgroundPlayerService : initExoPlayer() } + @Suppress("TooGenericExceptionCaught") private fun initExoPlayer() { serviceScope.launch { try { @@ -151,15 +178,12 @@ class BackgroundPlayerService : } } - private fun buildMediaSession(player: ExoPlayer): MediaSession = - MediaSession.Builder(applicationContext, player) - .setId(BACKGROUND_MEDIA_SESSION_ID) - .setCustomLayout(listOf(seekBackward, seekForward)) - .setCallback(object : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): ConnectionResult = AcceptedResultBuilder(mediaSession ?: session) + private fun buildMediaSession(player: ExoPlayer): MediaSession = MediaSession.Builder(applicationContext, player) + .setId(BACKGROUND_MEDIA_SESSION_ID) + .setCustomLayout(listOf(seekBackward, seekForward)) + .setCallback(object : MediaSession.Callback { + override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): ConnectionResult = + AcceptedResultBuilder(mediaSession ?: session) .setAvailablePlayerCommands( ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_NEXT) @@ -176,37 +200,32 @@ class BackgroundPlayerService : ) .build() - override fun onPostConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ) { - session.setCustomLayout(listOf(seekBackward, seekForward)) - } - - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture = when (customCommand.customAction) { - SESSION_COMMAND_ACTION_SEEK_FORWARD -> { - session.player.seekForward() - Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + session.setCustomLayout(listOf(seekBackward, seekForward)) + } - SESSION_COMMAND_ACTION_SEEK_BACK -> { - session.player.seekBack() - Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture = when (customCommand.customAction) { + SESSION_COMMAND_ACTION_SEEK_FORWARD -> { + session.player.seekForward() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } - else -> super.onCustomCommand(session, controller, customCommand, args) + SESSION_COMMAND_ACTION_SEEK_BACK -> { + session.player.seekBack() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } - }) - .build() - private fun buildNotificationProvider() = object : DefaultMediaNotificationProvider(this) { - val isPlaying = mediaSession?.player?.isPlaying ?: false + else -> super.onCustomCommand(session, controller, customCommand, args) + } + }) + .build() + private fun buildNotificationProvider() = object : DefaultMediaNotificationProvider(this) { @Suppress("DEPRECATION") override fun getMediaButtons( session: MediaSession, @@ -214,18 +233,19 @@ class BackgroundPlayerService : customLayout: ImmutableList, showPauseButton: Boolean ): ImmutableList { - val playPauseButton = - CommandButton.Builder() - .setDisplayName( - if (isPlaying) getString(R.string.media_player_pause) - else getString(R.string.media_player_play) - ) - .setIconResId( - if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow - ) - .setPlayerCommand(COMMAND_PLAY_PAUSE) - .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) - .build() + val isPlaying = mediaSession?.player?.isPlaying == true + val playPauseButton = CommandButton.Builder() + .setDisplayName( + if (isPlaying) { + getString(R.string.media_player_pause) + } else { + getString(R.string.media_player_play) + } + ) + .setIconResId(if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play_arrow) + .setPlayerCommand(COMMAND_PLAY_PAUSE) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) + .build() return ImmutableList.of(seekBackward, playPauseButton, seekForward) } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt index b36e87cb3574..cfc9f506b8fe 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt @@ -431,7 +431,7 @@ class PreviewMediaFragment : } } - + @Suppress("ReturnCount") private fun loadStreamUrl(user: User?, clientFactory: ClientFactory?, fileId: Long): Uri? { val client: OwnCloudClient? = try { clientFactory?.create(user) diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100644 index 000000000000..a4705f2a34a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_skip_previous.xml b/app/src/main/res/drawable/ic_skip_previous.xml new file mode 100644 index 000000000000..2809243d7b08 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_previous.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23b534092dcd..0f2f859ecc32 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -477,7 +477,7 @@ Seek backward Play Pause - + Playing media Details Hide From 37967368d66899bca6f70019e0e30dc61126d435 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 27 Feb 2026 11:14:37 +0100 Subject: [PATCH 5/5] fix(exo-player): crash Signed-off-by: alperozturk96 --- .../com/nextcloud/client/media/BackgroundPlayerService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt index bd40043a803e..2c40303f9bcb 100644 --- a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -79,7 +79,6 @@ class BackgroundPlayerService : private lateinit var exoPlayer: ExoPlayer private var mediaSession: MediaSession? = null - private var isPlayerReady = false private val stopReceiver = object : BroadcastReceiver() { @@ -115,6 +114,7 @@ class BackgroundPlayerService : 0 } ) + return super.onStartCommand(intent, flags, startId) } @@ -164,7 +164,6 @@ class BackgroundPlayerService : } val realPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient) - exoPlayer.release() exoPlayer = realPlayer isPlayerReady = true @@ -273,6 +272,7 @@ class BackgroundPlayerService : } val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager nm.cancel(DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID) + stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() }