Merge pull request #13467 from nextcloud/improve-media

Improve Media Player
This commit is contained in:
Alper Öztürk 2024-09-12 11:20:41 +02:00 committed by GitHub
commit 233d8a9567
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 553 additions and 384 deletions

View file

@ -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"

View file

@ -350,6 +350,16 @@
android:exported="false"
android:theme="@style/Theme.ownCloud.Media" />
<service
android:name="com.nextcloud.client.media.BackgroundPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
<service
android:name=".authentication.AccountAuthenticatorService"
android:exported="false">

View file

@ -17,6 +17,7 @@ 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.network.NetworkModule;
import com.nextcloud.client.onboarding.OnboardingModule;
import com.nextcloud.client.preferences.PreferencesModule;
@ -27,6 +28,8 @@ import com.owncloud.android.ui.whatsnew.ProgressIndicator;
import javax.inject.Singleton;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import dagger.BindsInstance;
import dagger.Component;
import dagger.android.support.AndroidSupportInjectionModule;
@ -46,7 +49,7 @@ import dagger.android.support.AndroidSupportInjectionModule;
ThemeModule.class,
DatabaseModule.class,
DispatcherModule.class,
VariantModule.class
VariantModule.class,
})
@Singleton
public interface AppComponent {
@ -55,6 +58,9 @@ public interface AppComponent {
void inject(MediaControlView mediaControlView);
@OptIn(markerClass = UnstableApi.class)
void inject(BackgroundPlayerService backgroundPlayerService);
void inject(ThemeableSwitchPreference switchPreference);
void inject(FileUploadHelper fileUploadHelper);

View file

@ -17,6 +17,7 @@ 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.PlayerService;
import com.nextcloud.client.migrations.Migrations;
import com.nextcloud.client.onboarding.FirstRunActivity;
@ -123,6 +124,8 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment;
import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
import com.owncloud.android.ui.trashbin.TrashbinActivity;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
@ -481,7 +484,13 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract TestJob testJob();
@ContributesAndroidInjector
abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
@OptIn(markerClass = UnstableApi.class)
@ContributesAndroidInjector
abstract BackgroundPlayerService backgroundPlayerService();
}

View file

@ -0,0 +1,245 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.media
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.Player.COMMAND_PLAY_PAUSE
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ConnectionResult
import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
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.nextcloud.utils.extensions.registerBroadcastReceiver
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.ReceiverFlag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import javax.inject.Inject
@OptIn(UnstableApi::class)
class BackgroundPlayerService : MediaSessionService(), Injectable {
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))
.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))
.setSessionCommand(seekBackSessionCommand)
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) })
.build()
@Inject
lateinit var clientFactory: ClientFactory
@Inject
lateinit var userAccountManager: UserAccountManager
lateinit var exoPlayer: ExoPlayer
private var mediaSession: MediaSession? = null
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()
}
}
}
override fun onCreate() {
super.onCreate()
registerBroadcastReceiver(
stopReceiver,
IntentFilter().apply {
addAction(RELEASE_MEDIA_SESSION_BROADCAST_ACTION)
addAction(STOP_MEDIA_SESSION_BROADCAST_ACTION)
},
ReceiverFlag.NotExported
)
MainApp.getAppComponent().inject(this)
initNextcloudExoPlayer()
setMediaNotificationProvider(object : DefaultMediaNotificationProvider(this) {
override fun getMediaButtons(
session: MediaSession,
playerCommands: Player.Commands,
customLayout: ImmutableList<CommandButton>,
showPauseButton: Boolean
): ImmutableList<CommandButton> {
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
}
})
}
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 {
return 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<SessionResult> {
return 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()
}
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
release()
}
override fun onDestroy() {
unregisterReceiver(stopReceiver)
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
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? {
return mediaSession
}
companion object {
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"
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"
}
}

View file

@ -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)
@ -72,10 +72,6 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}, 100)
}
fun stopMediaPlayerMessages() {
handler.removeMessages(SHOW_PROGRESS)
}
@Suppress("MagicNumber")
private fun initControllerView() {
binding.playBtn.requestFocus()
@ -104,14 +100,15 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
*/
private fun disableUnsupportedButtons() {
try {
if (playerControl?.canPause() == false) {
binding.playBtn.setEnabled(false)
if (playerControl?.isCommandAvailable(Player.COMMAND_PLAY_PAUSE)?.not() == true) {
binding.playBtn.isEnabled = false
}
if (playerControl?.canSeekBackward() == false) {
binding.rewindBtn.setEnabled(false)
if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK)?.not() == true) {
binding.rewindBtn.isEnabled = false
}
if (playerControl?.canSeekForward() == false) {
binding.forwardBtn.setEnabled(false)
if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)?.not() == true) {
binding.forwardBtn.isEnabled = false
}
} catch (ex: IncompatibleClassChangeError) {
// We were given an old version of the interface, that doesn't have
@ -130,7 +127,7 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
val pos = setProgress()
if (!isDragging) {
sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000).toLong())
sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000))
}
}
}
@ -149,7 +146,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 +161,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 +175,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 +199,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 +225,21 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
fun updatePausePlay() {
binding.playBtn.icon = ContextCompat.getDrawable(
context,
// use isPlaying instead of playWhenReady
// it represents only the play/pause state
// which is needed to show play/pause icons
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) == true) {
VISIBLE
} else {
INVISIBLE
}
binding.rewindBtn.visibility = if (playerControl?.canSeekBackward() == true) {
binding.rewindBtn.visibility = if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK) == true) {
VISIBLE
} else {
INVISIBLE
@ -245,10 +248,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
private fun doPauseResume() {
playerControl?.run {
if (isPlaying) {
if (playWhenReady) {
pause()
} else {
start()
play()
}
}
updatePausePlay()
@ -267,30 +270,25 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
@Suppress("MagicNumber")
override fun onClick(v: View) {
var pos: Int
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
playerControl.seekTo(pos)
playerControl.seekBack()
if (!playing) {
playerControl.pause() // necessary in some 2.3.x devices
}
setProgress()
}
R.id.forwardBtn -> {
pos = playerControl.currentPosition
pos += 15000
playerControl.seekTo(pos)
R.id.forwardBtn -> {
playerControl.seekForward()
if (!playing) {
playerControl.pause() // necessary in some 2.3.x devices
}
@ -313,10 +311,10 @@ class MediaControlView(context: Context, attrs: AttributeSet?) :
}
playerControl?.let { playerControl ->
val duration = playerControl.duration.toLong()
val duration = playerControl.duration
val newPosition = duration * progress / 1000L
playerControl.seekTo(newPosition.toInt())
binding.currentTimeText.text = formatTime(newPosition.toInt())
playerControl.seekTo(newPosition)
binding.currentTimeText.text = formatTime(newPosition)
}
}

View file

@ -13,15 +13,12 @@
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.DialogInterface
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,21 +42,29 @@ 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.PlaybackException
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.MediaSession
import androidx.media3.session.SessionToken
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.util.concurrent.ListenableFuture
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.ErrorFormat
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
import com.nextcloud.common.NextcloudClient
@ -71,7 +76,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 +92,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
@ -107,6 +110,7 @@ import javax.inject.Inject
* instantiation too.
*/
@Suppress("TooManyFunctions")
@OptIn(UnstableApi::class)
class PreviewMediaActivity :
FileActivity(),
FileFragment.ContainerActivity,
@ -117,9 +121,7 @@ class PreviewMediaActivity :
private var user: User? = null
private var savedPlaybackPosition: Long = 0
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,7 +134,10 @@ class PreviewMediaActivity :
private lateinit var binding: ActivityPreviewMediaBinding
private var emptyListView: ViewGroup? = null
private var exoPlayer: ExoPlayer? = null
private var videoPlayer: ExoPlayer? = null
private var videoMediaSession: MediaSession? = null
private var audioMediaController: MediaController? = null
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private var nextcloudClient: NextcloudClient? = null
private lateinit var windowInsetsController: WindowInsetsControllerCompat
@ -150,12 +155,36 @@ class PreviewMediaActivity :
WindowCompat.setDecorFitsSystemWindows(window, false)
applyWindowInsets()
initArguments(savedInstanceState)
mediaPlayerServiceConnection = PlayerServiceConnection(this)
if (MimeTypeUtil.isVideo(file)) {
// release any background media session if exists
sendAudioSessionReleaseBroadcast()
} else if (MimeTypeUtil.isAudio(file)) {
val stopPlayer = Intent(BackgroundPlayerService.STOP_MEDIA_SESSION_BROADCAST_ACTION).apply {
setPackage(packageName)
}
sendBroadcast(stopPlayer)
}
showMediaTypeViews()
configureSystemBars()
emptyListView = binding.emptyView.emptyListView
showProgressLayout()
addMarginForEmptyView()
if (file == null) {
return
}
if (MimeTypeUtil.isAudio(file)) {
setGenericThumbnail()
initializeAudioPlayer()
}
}
private fun sendAudioSessionReleaseBroadcast() {
val intent = Intent(BackgroundPlayerService.RELEASE_MEDIA_SESSION_BROADCAST_ACTION).apply {
setPackage(packageName)
}
sendBroadcast(intent)
}
private fun addMarginForEmptyView() {
@ -173,25 +202,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 +213,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 +241,6 @@ class PreviewMediaActivity :
if (isFileVideo) {
binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
} else {
extractAndSetCoverArt(file)
}
}
@ -265,17 +259,17 @@ 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
}
private fun setVideoErrorMessage(headline: String, @StringRes message: Int) {
private fun setErrorMessage(headline: String, @StringRes message: Int) {
binding.emptyView.run {
emptyListViewHeadline.text = headline
emptyListViewText.setText(message)
@ -287,48 +281,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,17 +308,19 @@ class PreviewMediaActivity :
private fun saveMediaInstanceState(bundle: Bundle) {
bundle.run {
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
exoPlayer?.let {
if (MimeTypeUtil.isVideo(file)) {
videoPlayer?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.isPlaying
autoplay = it.playWhenReady
}
} else {
audioMediaController?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.playWhenReady
}
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
putBoolean(EXTRA_PLAYING, autoplay)
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection!!.isConnected) {
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection!!.currentPosition)
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection!!.isPlaying)
}
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
putBoolean(EXTRA_PLAYING, autoplay)
}
}
@ -375,42 +329,12 @@ 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()
}
if (MimeTypeUtil.isVideo(file)) {
initializeVideoPlayer()
}
}
private fun setupAudioPlayerServiceConnection() {
binding.mediaController.run {
setMediaPlayer(mediaPlayerServiceConnection)
visibility = View.VISIBLE
}
user?.let {
mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
}
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 +342,10 @@ class PreviewMediaActivity :
nextcloudClient?.let { client ->
handler.post {
exoPlayer = createNextcloudExoplayer(this, client)
videoPlayer = createNextcloudExoplayer(this, client)
videoMediaSession = MediaSession.Builder(this, videoPlayer as Player).build()
exoPlayer?.let { player ->
videoPlayer?.let { player ->
player.addListener(
ExoplayerListener(
this,
@ -439,6 +364,102 @@ class PreviewMediaActivity :
}
}
private fun releaseVideoPlayer() {
videoPlayer?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.playWhenReady
it.release()
videoMediaSession?.release()
}
videoMediaSession = null
videoPlayer = null
}
@Suppress("TooGenericExceptionCaught")
private fun initializeAudioPlayer() {
val sessionToken = SessionToken(this, ComponentName(this, BackgroundPlayerService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaControllerFuture?.addListener(
{
try {
audioMediaController = mediaControllerFuture?.get()
playAudio()
binding.audioControllerView.setMediaPlayer(audioMediaController)
} catch (e: Exception) {
Log_OC.e(TAG, "exception raised while getting the media controller ${e.message}")
}
},
MoreExecutors.directExecutor()
)
}
@Suppress("TooGenericExceptionCaught")
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 for Audio not possible: $e")
}
}
}
private fun prepareAudioPlayer(uri: Uri) {
audioMediaController?.let { audioPlayer ->
audioPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_READY) {
hideProgressLayout()
binding.emptyView.emptyListView.visibility = View.GONE
}
}
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
super.onMediaMetadataChanged(mediaMetadata)
val artworkBitmap = mediaMetadata.artworkData?.let { bytes: ByteArray ->
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
if (artworkBitmap != null) {
binding.imagePreview.setImageBitmap(artworkBitmap)
}
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
Log_OC.e(TAG, "Exoplayer error", error)
val message = ErrorFormat.toString(this@PreviewMediaActivity, error)
MaterialAlertDialogBuilder(this@PreviewMediaActivity)
.setMessage(message)
.setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
audioPlayer.seekToDefaultPosition()
audioPlayer.pause()
}
.setCancelable(false)
.show()
}
})
val mediaItem = MediaItem.Builder()
.setUri(uri)
.setMediaMetadata(MediaMetadata.Builder().setTitle(file.fileName).build())
.build()
audioPlayer.setMediaItem(mediaItem)
audioPlayer.playWhenReady = autoplay
audioPlayer.seekTo(savedPlaybackPosition)
audioPlayer.prepare()
}
}
private fun releaseAudioPlayer() {
audioMediaController?.let { audioPlayer ->
audioPlayer.release()
}
audioMediaController = null
}
private fun initWindowInsetsController() {
windowInsetsController = WindowCompat.getInsetsController(
window,
@ -448,7 +469,6 @@ class PreviewMediaActivity :
}
}
@OptIn(markerClass = [UnstableApi::class])
private fun applyWindowInsets() {
val playerView = binding.exoplayerView
val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)
@ -477,7 +497,6 @@ class PreviewMediaActivity :
}
}
@OptIn(UnstableApi::class)
private fun setupVideoView() {
initWindowInsetsController()
val type = WindowInsetsCompat.Type.systemBars()
@ -495,14 +514,10 @@ class PreviewMediaActivity :
}
}
)
it.player = exoPlayer
it.player = videoPlayer
}
}
private fun stopAudio() {
mediaPlayerServiceConnection?.stop()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
return true
@ -567,7 +582,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 +629,7 @@ class PreviewMediaActivity :
val removedFile = operation.file
val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
if (!fileAvailable && removedFile == file) {
sendAudioSessionReleaseBroadcast()
finish()
}
} else if (operation is SynchronizeFileOperation) {
@ -670,30 +686,25 @@ class PreviewMediaActivity :
setupVideoView()
if (file.isDown) {
playVideoUri(file.storageUri)
prepareVideoPlayer(file.storageUri)
} else {
try {
LoadStreamUrl(this, user, clientFactory).execute(file.localId)
} catch (e: Exception) {
Log_OC.e(TAG, "Loading stream url not possible: $e")
Log_OC.e(TAG, "Loading stream url for Video not possible: $e")
}
}
}
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,11 +739,15 @@ 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(
setErrorMessage(
weakReference.getString(R.string.stream_not_possible_headline),
R.string.stream_not_possible_message
)
@ -754,29 +769,16 @@ class PreviewMediaActivity :
}
override fun onDestroy() {
Log_OC.v(TAG, "onDestroy")
LocalBroadcastManager.getInstance(this).unregisterReceiver(mediaControlReceiver)
mediaControllerFuture?.let { MediaController.releaseFuture(it) }
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()
}
@ -830,22 +832,12 @@ class PreviewMediaActivity :
private fun stopPreview(stopAudio: Boolean) {
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

View file

@ -17,8 +17,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
@ -34,23 +32,23 @@ import android.view.View.OnTouchListener
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
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.Companion.instance
import com.nextcloud.client.media.BackgroundPlayerService
import com.nextcloud.client.media.ExoplayerListener
import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
import com.nextcloud.client.media.PlayerServiceConnection
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.network.ClientFactory.CreationException
import com.nextcloud.common.NextcloudClient
@ -61,7 +59,6 @@ import com.owncloud.android.MainApp
import com.owncloud.android.R
import com.owncloud.android.databinding.FragmentPreviewMediaBinding
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.utils.Log_OC
@ -105,7 +102,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
private var autoplay = true
private var isLivePhoto = false
private val prepared = false
private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
private var videoUri: Uri? = null
@ -121,16 +117,22 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
lateinit var binding: FragmentPreviewMediaBinding
private var emptyListView: ViewGroup? = null
private var exoPlayer: ExoPlayer? = null
private var mediaSession: MediaSession? = null
private var nextcloudClient: NextcloudClient? = null
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// release any background media session if exists
val intent = Intent(BackgroundPlayerService.RELEASE_MEDIA_SESSION_BROADCAST_ACTION).apply {
setPackage(requireActivity().packageName)
}
requireActivity().sendBroadcast(intent)
arguments?.let {
initArguments(it)
}
mediaPlayerServiceConnection = PlayerServiceConnection(requireContext())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -150,10 +152,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
checkArgumentsAfterViewCreation(savedInstanceState)
if (file != null) {
prepareExoPlayerView()
}
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
addMenuHost()
}
@ -198,62 +196,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
binding.progress.visibility = View.GONE
}
/**
* 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")
private fun extractAndSetCoverArt(file: OCFile) {
if (!MimeTypeUtil.isAudio(file)) return
if (file.storagePath == null) {
setThumbnailForAudio(file)
} else {
try {
val mmr = MediaMetadataRetriever().apply {
setDataSource(file.storagePath)
}
val data = mmr.embeddedPicture
if (data != null) {
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
binding.imagePreview.setImageBitmap(bitmap) // associated cover art in bitmap
} else {
setThumbnailForAudio(file)
}
} catch (t: Throwable) {
setGenericThumbnail()
}
}
}
private fun setThumbnailForAudio(file: OCFile) {
val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
)
if (thumbnail != null) {
binding.imagePreview.setImageBitmap(thumbnail)
} else {
setGenericThumbnail()
}
}
/**
* Set generic icon (logo) as placeholder for thumbnail in preview.
*/
private fun setGenericThumbnail() {
AppCompatResources.getDrawable(requireContext(), R.drawable.logo)?.let { logo ->
if (!resources.getBoolean(R.bool.is_branded_client)) {
// only colour logo of non-branded client
DrawableCompat.setTint(logo, resources.getColor(R.color.primary, requireContext().theme))
}
binding.imagePreview.setImageDrawable(logo)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
file.logFileSize(TAG)
@ -263,15 +205,10 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
putParcelable(EXTRA_FILE, file)
putParcelable(EXTRA_USER, user)
if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
autoplay = exoPlayer?.isPlaying ?: false
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
putBoolean(EXTRA_PLAYING, autoplay)
} else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection?.isConnected == true) {
putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection?.currentPosition ?: 0)
putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection?.isPlaying ?: false)
}
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
autoplay = exoPlayer?.isPlaying ?: false
putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
putBoolean(EXTRA_PLAYING, autoplay)
}
}
@ -285,22 +222,11 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
Log_OC.d(TAG, "File is null or fragment not attached to a context.")
return
}
mediaPlayerServiceConnection?.bind()
if (MimeTypeUtil.isAudio(file)) {
prepareForAudio()
} else if (MimeTypeUtil.isVideo(file)) {
prepareForVideo(context ?: MainApp.getAppContext())
}
prepareForVideo(context ?: MainApp.getAppContext())
}
@Suppress("DEPRECATION", "TooGenericExceptionCaught")
private fun prepareForVideo(context: Context) {
if (mediaPlayerServiceConnection?.isConnected == true) {
// always stop player
stopAudio()
}
if (exoPlayer != null) {
playVideo()
} else {
@ -327,14 +253,22 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
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(),
exoPlayer as Player
).setId(System.currentTimeMillis().toString()).build()
}
private fun prepareForAudio() {
binding.mediaController.setMediaPlayer(mediaPlayerServiceConnection)
binding.mediaController.visibility = View.VISIBLE
mediaPlayerServiceConnection?.start(user!!, file, autoplay, savedPlaybackPosition)
binding.emptyView.emptyListView.visibility = View.GONE
binding.progress.visibility = View.GONE
private fun releaseVideoPlayer() {
exoPlayer?.let {
savedPlaybackPosition = it.currentPosition
autoplay = it.playWhenReady
it.release()
mediaSession?.release()
}
mediaSession = null
exoPlayer = null
}
private fun goBackToLivePhoto() {
@ -346,17 +280,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
requireActivity().supportFragmentManager.popBackStack()
}
private fun prepareExoPlayerView() {
if (MimeTypeUtil.isVideo(file)) {
binding.exoplayerView.visibility = View.VISIBLE
binding.imagePreview.visibility = View.GONE
} else {
binding.exoplayerView.visibility = View.GONE
binding.imagePreview.visibility = View.VISIBLE
extractAndSetCoverArt(file)
}
}
private fun showActionBar() {
val currentActivity: Activity = requireActivity()
if (currentActivity is PreviewImageActivity) {
@ -374,10 +297,6 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
}
}
private fun stopAudio() {
mediaPlayerServiceConnection?.stop()
}
private fun addMenuHost() {
val menuHost: MenuHost = requireActivity()
@ -490,12 +409,12 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
}
private fun seeDetails() {
stopPreview(false)
releaseVideoPlayer()
containerActivity.showDetails(file)
}
private fun sendShareFile() {
stopPreview(false)
releaseVideoPlayer()
containerActivity.fileOperationsHelper.sendShareFile(file)
}
@ -583,19 +502,7 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
}
override fun onStop() {
Log_OC.v(TAG, "onStop")
val file = file
if (MimeTypeUtil.isAudio(file) && mediaPlayerServiceConnection?.isPlaying == false) {
stopAudio()
} else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer?.isPlaying == true) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
exoPlayer?.pause()
}
mediaPlayerServiceConnection?.unbind()
toggleDrawerLockMode(containerActivity, DrawerLayout.LOCK_MODE_UNLOCKED)
releaseVideoPlayer()
super.onStop()
}
@ -641,19 +548,9 @@ class PreviewMediaFragment : FileFragment(), OnTouchListener, Injectable {
* Opens the previewed file with an external application.
*/
private fun openFile() {
stopPreview(true)
containerActivity.fileOperationsHelper.openFile(file)
}
private fun stopPreview(stopAudio: Boolean) {
if (stopAudio && mediaPlayerServiceConnection != null) {
mediaPlayerServiceConnection?.stop()
} else if (exoPlayer != null) {
savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
exoPlayer?.stop()
}
}
val position: Long
get() {
if (prepared) {

View file

@ -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"

View file

@ -18,15 +18,6 @@
android:gravity="center"
tools:context=".ui.preview.PreviewMediaFragment">
<ImageView
android:id="@+id/image_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="@dimen/standard_margin"
android:contentDescription="@string/preview_image_description"
android:src="@drawable/logo" />
<androidx.media3.ui.PlayerView
android:id="@+id/exoplayer_view"
android:layout_width="match_parent"
@ -34,14 +25,6 @@
android:layout_gravity="center"
app:show_buffering="always" />
<com.owncloud.android.media.MediaControlView
android:id="@+id/media_controller"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_margin="@dimen/standard_margin"
android:visibility="gone" />
<FrameLayout
android:id="@+id/progress"
android:layout_width="match_parent"

View file

@ -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"/>