diff --git a/app/build.gradle b/app/build.gradle index 063fae79d9..6a79df8219 100644 --- a/app/build.gradle +++ b/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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e16471d7ff..b8acfd40db 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,7 +127,14 @@ android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute" tools:replace="android:allowBackup"> - + + + + + diff --git a/app/src/main/java/com/nextcloud/client/di/AppComponent.java b/app/src/main/java/com/nextcloud/client/di/AppComponent.java index da9d7cb33d..1cdb220298 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppComponent.java +++ b/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); diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index 8225e60be2..82d2d32615 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/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(); + } diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt new file mode 100644 index 0000000000..8699a46a6a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -0,0 +1,82 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * 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 + } +} diff --git a/app/src/main/java/com/nextcloud/client/media/CustomExoPlayer.kt b/app/src/main/java/com/nextcloud/client/media/CustomExoPlayer.kt new file mode 100644 index 0000000000..57d1c1bc7a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/media/CustomExoPlayer.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * 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 { + +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt b/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt index 283f4e56ba..62523f9d0a 100644 --- a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt +++ b/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() } + } diff --git a/app/src/main/java/com/owncloud/android/media/MediaControlView.kt b/app/src/main/java/com/owncloud/android/media/MediaControlView.kt index 25747c3f1b..76cfb216b5 100644 --- a/app/src/main/java/com/owncloud/android/media/MediaControlView.kt +++ b/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) } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index 9f4726d402..07eb775c72 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/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); diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt index 2b9b793b87..b449bf57c9 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt +++ b/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() - } + if (MimeTypeUtil.isVideo(file)) { + //TODO: should we somehow release any previous audio sessions? + 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 +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 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 f939dbf8ec..51cac35b2c 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 @@ -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 diff --git a/app/src/main/res/layout/activity_preview_media.xml b/app/src/main/res/layout/activity_preview_media.xml index b61873086f..7cc161f133 100644 --- a/app/src/main/res/layout/activity_preview_media.xml +++ b/app/src/main/res/layout/activity_preview_media.xml @@ -42,7 +42,7 @@ app:show_buffering="always" /> + android:layout_height="match_parent" + android:visibility="gone" + > + + + + + + + + @@ -9718,6 +9726,11 @@ + + + + + @@ -11041,6 +11054,11 @@ + + + + + @@ -11091,6 +11109,11 @@ + + + + + @@ -11115,6 +11138,11 @@ + + + + +