From febd3b8caeb6875083c86904af3c645c910934d7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 28 Sep 2026 10:13:49 +0200 Subject: [PATCH 1/6] refactor(player): remove unused execution flow Signed-off-by: alperozturk96 --- app/src/main/AndroidManifest.xml | 3 +- .../nextcloud/client/di/ViewModelModule.kt | 6 +- .../nextcloud/client/player/PlayerModule.kt | 12 +- .../client/player/media3/PlaybackModel.kt | 2 - .../PlaybackQueueLoader.kt} | 50 ++--- .../client/player/media3/PlaybackService.kt | 52 +++-- .../player/model/file/PlaybackCollection.kt | 7 + .../client/player/ui/PlayerActivity.kt | 207 ------------------ .../client/player/ui/PlayerScreenEvent.kt | 31 --- .../nextcloud/client/player/ui/PlayerView.kt | 173 --------------- .../client/player/ui/VideoPictureInPicture.kt | 10 +- .../player/ui/audio/AudioPlayerActivity.kt | 133 +++++++++++ .../player/ui/audio/AudioPlayerLauncher.kt | 27 +++ .../AudioPlayerPager.kt} | 30 +-- .../AudioPlayerPagerAdapter.kt} | 13 +- .../player/ui/audio/AudioPlayerScreenEvent.kt | 31 +++ .../audio/AudioPlayerSystemBars.kt} | 20 +- .../client/player/ui/audio/AudioPlayerView.kt | 139 ++++++++++-- .../AudioPlayerViewModel.kt} | 30 +-- .../player/ui/video/VideoFileFragment.kt | 143 ------------ .../client/player/ui/video/VideoPlayerView.kt | 54 ----- .../client/player/util/PlayerUtil.kt | 2 +- .../SystemBarBackgroundCallbacks.kt | 4 +- .../utils/extensions/ViewExtensions.kt | 12 + .../android/ui/activity/FileActivity.java | 4 +- .../ui/activity/FileDisplayActivity.kt | 33 ++- .../ui/fragment/OCFileListFragment.java | 4 +- .../ui/fragment/albums/AlbumItemsFragment.kt | 8 +- .../ui/preview/PreviewImageActivity.kt | 2 +- .../ui/preview/PreviewPlaybackFragment.kt | 45 ++-- .../res/drawable/player_video_top_scrim.xml | 15 -- ...r_top_bar.xml => player_audio_top_bar.xml} | 2 +- app/src/main/res/layout/player_audio_view.xml | 4 +- .../res/layout/player_video_file_fragment.xml | 36 --- app/src/main/res/layout/player_video_view.xml | 27 --- ...r_top_bar.xml => player_audio_top_bar.xml} | 0 app/src/main/res/values/colors.xml | 2 - app/src/main/res/values/ids.xml | 6 +- 38 files changed, 487 insertions(+), 892 deletions(-) rename app/src/main/java/com/nextcloud/client/player/{ui/PlayerLauncher.kt => media3/PlaybackQueueLoader.kt} (53%) delete mode 100644 app/src/main/java/com/nextcloud/client/player/ui/PlayerActivity.kt delete mode 100644 app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt delete mode 100644 app/src/main/java/com/nextcloud/client/player/ui/PlayerView.kt create mode 100644 app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerActivity.kt create mode 100644 app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerLauncher.kt rename app/src/main/java/com/nextcloud/client/player/ui/{pager/PlayerPager.kt => audio/AudioPlayerPager.kt} (82%) rename app/src/main/java/com/nextcloud/client/player/ui/{pager/PlayerPagerAdapter.kt => audio/AudioPlayerPagerAdapter.kt} (83%) create mode 100644 app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerScreenEvent.kt rename app/src/main/java/com/nextcloud/client/player/{util/WindowWrapper.kt => ui/audio/AudioPlayerSystemBars.kt} (80%) rename app/src/main/java/com/nextcloud/client/player/ui/{PlayerViewModel.kt => audio/AudioPlayerViewModel.kt} (71%) delete mode 100644 app/src/main/java/com/nextcloud/client/player/ui/video/VideoFileFragment.kt delete mode 100644 app/src/main/java/com/nextcloud/client/player/ui/video/VideoPlayerView.kt delete mode 100644 app/src/main/res/drawable/player_video_top_scrim.xml rename app/src/main/res/layout/{player_top_bar.xml => player_audio_top_bar.xml} (94%) delete mode 100644 app/src/main/res/layout/player_video_file_fragment.xml delete mode 100644 app/src/main/res/layout/player_video_view.xml rename app/src/main/res/menu/{player_top_bar.xml => player_audio_top_bar.xml} (100%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bef21720c2e5..9020a664b936 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -391,10 +391,9 @@ android:launchMode="singleTask" android:theme="@style/Theme.ownCloud.Overlay" /> Unit)? = null - private var videoSurfaceView: SurfaceView? = null private var cachedCurrentFiles: List? = null diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt b/app/src/main/java/com/nextcloud/client/player/media3/PlaybackQueueLoader.kt similarity index 53% rename from app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt rename to app/src/main/java/com/nextcloud/client/player/media3/PlaybackQueueLoader.kt index 9306da9035e1..d57e951f6fcf 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt +++ b/app/src/main/java/com/nextcloud/client/player/media3/PlaybackQueueLoader.kt @@ -5,13 +5,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.player.ui +package com.nextcloud.client.player.media3 -import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.nextcloud.client.logger.Logger -import com.nextcloud.client.player.media3.PlaybackModel import com.nextcloud.client.player.media3.resumption.PlaybackResumptionConfigStore import com.nextcloud.client.player.model.file.PlaybackCollection import com.nextcloud.client.player.model.file.PlaybackFileType @@ -26,55 +24,33 @@ import kotlinx.coroutines.launch import java.util.concurrent.CancellationException import javax.inject.Inject -class PlayerLauncher @Inject constructor( +class PlaybackQueueLoader @Inject constructor( private val playbackResumptionConfigStore: PlaybackResumptionConfigStore, private val playbackFilesRepository: PlaybackFilesRepository, private val playbackModel: PlaybackModel, private val logger: Logger ) { - private var currentLaunchJob: Job? = null - - /** - * Starts playback and opens [PlayerActivity] on top of [activity]. - */ - fun launch(activity: AppCompatActivity, file: OCFile, collection: PlaybackCollection) { - run(activity) { - val fileType = prepareQueue(file, collection) - playbackModel.play() - activity.startActivity(PlayerActivity.createIntent(activity, fileType)) - } - } - - /** - * Loads [file] and the media of its collection into the player without opening [PlayerActivity], so that a host - * screen can render the playback itself. - */ - fun prepare(owner: LifecycleOwner, file: OCFile, collection: PlaybackCollection, autoplay: Boolean = false) { - run(owner) { - prepareQueue(file, collection) - if (autoplay) { - playbackModel.play() - } - } - } - - private fun run(owner: LifecycleOwner, block: suspend () -> Unit) { - currentLaunchJob?.cancel() - currentLaunchJob = owner.lifecycleScope.launch { - runCatching { block() }.onFailure { + private var loadJob: Job? = null + + fun load(owner: LifecycleOwner, file: OCFile, collection: PlaybackCollection, onLoaded: () -> Unit = {}) { + loadJob?.cancel() + loadJob = owner.lifecycleScope.launch { + runCatching { + loadQueue(file, collection) + onLoaded() + }.onFailure { if (it is CancellationException) throw it - logger.e(PlayerLauncher::class.java.simpleName, "Error launching player", it) + logger.e(PlaybackQueueLoader::class.java.simpleName, "Error loading playback queue", it) } } } - private suspend fun prepareQueue(file: OCFile, collection: PlaybackCollection): PlaybackFileType { + private suspend fun loadQueue(file: OCFile, collection: PlaybackCollection) { val fileType = PlaybackFileType.ofMimeType(file.resolveMimeType()) playbackResumptionConfigStore.saveConfig(file.localId.toString(), file.parentId, fileType, collection) playbackModel.start() playbackModel.setFiles(PlaybackFiles(listOf(file.toPlaybackFile()), PlaybackFilesComparator.NONE)) playbackModel.setFilesFlow(playbackFilesRepository.observe(file.parentId, fileType, collection)) - return fileType } } diff --git a/app/src/main/java/com/nextcloud/client/player/media3/PlaybackService.kt b/app/src/main/java/com/nextcloud/client/player/media3/PlaybackService.kt index b84e1e2ba14f..184f8904bd7e 100644 --- a/app/src/main/java/com/nextcloud/client/player/media3/PlaybackService.kt +++ b/app/src/main/java/com/nextcloud/client/player/media3/PlaybackService.kt @@ -11,14 +11,19 @@ import android.app.PendingIntent import android.content.Intent import android.os.Build import android.os.IBinder -import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.ControllerInfo import androidx.media3.session.MediaSessionService +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.player.media3.resumption.PlaybackResumptionConfigStore +import com.nextcloud.client.player.model.file.PlaybackFile import com.nextcloud.client.player.model.file.PlaybackFileType -import com.nextcloud.client.player.ui.PlayerActivity +import com.nextcloud.client.player.model.file.toVirtualFolderType +import com.nextcloud.client.player.ui.audio.AudioPlayerActivity import com.nextcloud.client.player.util.PlayerUtil.playbackFile +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.ui.preview.PreviewImageActivity import dagger.android.AndroidInjection import javax.inject.Inject @@ -32,9 +37,15 @@ class PlaybackService : MediaSessionService() { @Inject lateinit var playbackModel: PlaybackModel + @Inject + lateinit var userAccountManager: UserAccountManager + + @Inject + lateinit var playbackResumptionConfigStore: PlaybackResumptionConfigStore + private var bindingCount: Int = 0 - private var sessionActivityFileType: PlaybackFileType? = null + private var sessionActivityMediaId: String? = null override fun onCreate() { super.onCreate() @@ -77,22 +88,37 @@ class PlaybackService : MediaSessionService() { } private fun updateSessionActivity(session: MediaSession) { - val fileType = session.player.currentMediaItem.playbackFileType() ?: return - if (fileType == sessionActivityFileType) { - return + val mediaItem = session.player.currentMediaItem?.takeIf { it.mediaId != sessionActivityMediaId } ?: return + val intent = mediaItem.mediaMetadata.playbackFile?.let(::createSessionIntent) ?: return + sessionActivityMediaId = mediaItem.mediaId + session.setSessionActivity(createSessionActivity(intent)) + } + + private fun createSessionIntent(playbackFile: PlaybackFile): Intent? { + val fileType = PlaybackFileType.entries.firstOrNull { + playbackFile.mimeType.startsWith(it.value, ignoreCase = true) } - sessionActivityFileType = fileType - session.setSessionActivity(createSessionActivity(fileType)) + return when (fileType) { + PlaybackFileType.AUDIO -> AudioPlayerActivity.createIntent(this) + PlaybackFileType.VIDEO -> createVideoPreviewIntent(playbackFile) + null -> null + } } - private fun MediaItem?.playbackFileType(): PlaybackFileType? { - val mimeType = this?.mediaMetadata?.playbackFile?.mimeType ?: return null - return PlaybackFileType.entries.firstOrNull { mimeType.startsWith(it.value, ignoreCase = true) } + private fun createVideoPreviewIntent(playbackFile: PlaybackFile): Intent? { + val user = userAccountManager.user + val file = playbackFile.id.toLongOrNull() + ?.let { FileDataStorageManager(user, contentResolver).getFileByLocalId(it) } + ?: return null + val virtualFolderType = playbackResumptionConfigStore.loadConfig()?.collection?.toVirtualFolderType() + + return PreviewImageActivity.previewFileIntent(this, user, file).apply { + putExtra(PreviewImageActivity.EXTRA_VIRTUAL_TYPE, virtualFolderType) + } } - private fun createSessionActivity(fileType: PlaybackFileType): PendingIntent { - val intent = PlayerActivity.createIntent(this, fileType) + private fun createSessionActivity(intent: Intent): PendingIntent { val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { diff --git a/app/src/main/java/com/nextcloud/client/player/model/file/PlaybackCollection.kt b/app/src/main/java/com/nextcloud/client/player/model/file/PlaybackCollection.kt index 7155f2f16e98..c7f525b0b77d 100644 --- a/app/src/main/java/com/nextcloud/client/player/model/file/PlaybackCollection.kt +++ b/app/src/main/java/com/nextcloud/client/player/model/file/PlaybackCollection.kt @@ -31,3 +31,10 @@ fun VirtualFolderType?.toPlaybackCollection(): PlaybackCollection = when (this) VirtualFolderType.ALBUM -> PlaybackCollection.ALBUM else -> PlaybackCollection.FOLDER } + +fun PlaybackCollection.toVirtualFolderType(): VirtualFolderType = when (this) { + PlaybackCollection.FAVORITES -> VirtualFolderType.FAVORITE + PlaybackCollection.GALLERY -> VirtualFolderType.GALLERY + PlaybackCollection.ALBUM -> VirtualFolderType.ALBUM + PlaybackCollection.FOLDER, PlaybackCollection.SHARED -> VirtualFolderType.NONE +} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerActivity.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerActivity.kt deleted file mode 100644 index 01e39248a9bc..000000000000 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerActivity.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 STRATO GmbH. - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.player.ui - -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.media.AudioManager -import android.os.Bundle -import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.core.view.ViewCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import com.nextcloud.client.di.Injectable -import com.nextcloud.client.di.ViewModelFactory -import com.nextcloud.client.player.model.file.PlaybackFileType -import com.nextcloud.client.player.ui.audio.AudioPlayerView -import com.nextcloud.client.player.ui.video.VideoPlayerView -import com.nextcloud.ui.fileactions.FileActionsBottomSheet -import com.nextcloud.utils.SnackbarUtil -import com.nextcloud.utils.extensions.getSerializableArgument -import com.owncloud.android.R -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.ui.activity.FileActivity -import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment -import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -class PlayerActivity : - FileActivity(), - Injectable { - - companion object { - private const val PLAYBACK_FILE_TYPE: String = "PLAYBACK_FILE_TYPE" - - fun createIntent(context: Context, playbackFileType: PlaybackFileType): Intent = - Intent(context, PlayerActivity::class.java).apply { - putExtra(PLAYBACK_FILE_TYPE, playbackFileType) - addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - } - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - - private val viewModel by viewModels { viewModelFactory } - - private lateinit var playbackFileType: PlaybackFileType - - private lateinit var playerView: PlayerView - - private val pictureInPicture by lazy { VideoPictureInPicture(this, playbackModel, autoEnter = true) } - - private var onBackPressedCallback: OnBackPressedCallback? = null - - private var keepPlaybackAliveOnFinish = false - - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - super.onCreate(savedInstanceState) - ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, windowInsets -> windowInsets } - - playbackFileType = intent.getPlaybackFileType() - createPlayerView() - - viewModel.eventFlow - .flowWithLifecycle(lifecycle) - .onEach { handleEvent(it) } - .launchIn(lifecycleScope) - - onBackPressedCallback = onBackPressedDispatcher.addCallback(this) { - if (canUsePictureInPictureMode() && pictureInPicture.enter(playerView)) { - return@addCallback - } - - file = file?.parentId?.let { storageManager.getFileById(it) } - finish() - } - - volumeControlStream = AudioManager.STREAM_MUSIC - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - playbackFileType = intent.getPlaybackFileType() - recreatePlayerView() - } - - private fun createPlayerView() { - playerView = when (playbackFileType) { - PlaybackFileType.AUDIO -> AudioPlayerView(this) - PlaybackFileType.VIDEO -> VideoPlayerView(this) - } - playerView.onMoreClick = { viewModel.onMoreButtonClick() } - setContentView(playerView) - } - - private fun recreatePlayerView() { - playerView.onStop() - playerView.release() - createPlayerView() - playerView.onStart() - } - - private fun Intent.getPlaybackFileType(): PlaybackFileType = - getSerializableArgument(PLAYBACK_FILE_TYPE, PlaybackFileType::class.java) - ?: throw IllegalStateException("Playback file type was not defined") - - override fun onStart() { - super.onStart() - playerView.onStart() - } - - override fun onStop() { - super.onStop() - playerView.onStop() - } - - override fun onDestroy() { - super.onDestroy() - playbackModel.onPictureInPictureClose = null - if (isFinishing && !keepPlaybackAliveOnFinish && playbackFileType == PlaybackFileType.VIDEO) { - playbackModel.release() - } - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() - if (canUsePictureInPictureMode()) { - pictureInPicture.enter(playerView) - } - } - - override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { - super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) - - playbackModel.onPictureInPictureClose = if (isInPictureInPictureMode) { - { - keepPlaybackAliveOnFinish = true - finish() - } - } else { - null - } - - // Finish the activity if the user closes the PIP window - if (!isInPictureInPictureMode && lifecycle.currentState == Lifecycle.State.CREATED) { - finish() - return - } - } - - private fun canUsePictureInPictureMode(): Boolean = - playbackFileType == PlaybackFileType.VIDEO && pictureInPicture.isAllowed - - private fun handleEvent(event: PlayerScreenEvent) { - when (event) { - is PlayerScreenEvent.ShowFileActions -> showFileActions(event.file, event.actionsToHide) - is PlayerScreenEvent.ShowFileDetails -> showFileDetails(event.file) - is PlayerScreenEvent.ShowFileExportStartedMessage -> showFileExportStartedMessage() - is PlayerScreenEvent.ShowShareFileDialog -> fileOperationsHelper.sendShareFile(event.file) - is PlayerScreenEvent.ShowRemoveFileDialog -> showRemoveFileDialog(event.file) - is PlayerScreenEvent.LaunchOpenFileIntent -> fileOperationsHelper.openFile(event.file) - is PlayerScreenEvent.LaunchStreamFileIntent -> fileOperationsHelper.streamMediaFile(event.file) - is PlayerScreenEvent.ToggleFileLock -> fileOperationsHelper.toggleFileLock(event.file, event.shouldBeLocked) - is PlayerScreenEvent.AddFileToAlbum -> fileOperationsHelper.addFileToAlbum(listOf(event.file)) - } - } - - private fun showFileActions(file: OCFile, actionsToHide: List) { - FileActionsBottomSheet.newInstance(file, false, actionsToHide) - .setResultListener(supportFragmentManager, this) { viewModel.onFileActionChosen(file, it) } - .show(supportFragmentManager, "actions") - } - - private fun showFileDetails(file: OCFile) { - val intent = Intent(this, FileDisplayActivity::class.java).apply { - action = FileDisplayActivity.ACTION_DETAILS - putExtra(EXTRA_FILE, file) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } - startActivity(intent) - finish() - } - - private fun showFileExportStartedMessage() { - val message = resources.getQuantityString(R.plurals.export_start, 1, 1) - SnackbarUtil.show(playerView, message) - } - - private fun showRemoveFileDialog(file: OCFile) { - RemoveFilesDialogFragment.newInstance(file) - .show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) - } -} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt deleted file mode 100644 index 7f8f56c57202..000000000000 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerScreenEvent.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 STRATO GmbH. - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.player.ui - -import com.owncloud.android.datamodel.OCFile - -sealed interface PlayerScreenEvent { - - data class ShowFileActions(val file: OCFile, val actionsToHide: List) : PlayerScreenEvent - - data class ShowFileDetails(val file: OCFile) : PlayerScreenEvent - - data object ShowFileExportStartedMessage : PlayerScreenEvent - - data class ShowShareFileDialog(val file: OCFile) : PlayerScreenEvent - - data class ShowRemoveFileDialog(val file: OCFile) : PlayerScreenEvent - - data class LaunchOpenFileIntent(val file: OCFile) : PlayerScreenEvent - - data class LaunchStreamFileIntent(val file: OCFile) : PlayerScreenEvent - - data class ToggleFileLock(val file: OCFile, val shouldBeLocked: Boolean) : PlayerScreenEvent - - data class AddFileToAlbum(val file: OCFile) : PlayerScreenEvent -} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerView.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerView.kt deleted file mode 100644 index db771fe61b37..000000000000 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerView.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 STRATO GmbH. - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.player.ui - -import android.content.Context -import android.util.AttributeSet -import android.widget.LinearLayout -import androidx.annotation.CallSuper -import androidx.annotation.LayoutRes -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.Insets -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.google.android.material.appbar.MaterialToolbar -import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.jobs.download.FileDownloadHelper -import com.nextcloud.client.player.media3.PlaybackModel -import com.nextcloud.client.player.model.error.SourceException -import com.nextcloud.client.player.model.file.PlaybackFile -import com.nextcloud.client.player.model.state.PlaybackState -import com.nextcloud.client.player.ui.control.PlayerControlView -import com.nextcloud.client.player.ui.pager.PlayerPager -import com.nextcloud.client.player.util.WindowWrapper -import com.nextcloud.utils.SnackbarUtil -import com.owncloud.android.R -import com.owncloud.android.datamodel.FileDataStorageManager -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.operations.DownloadFileOperation -import dagger.android.HasAndroidInjector -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject - -abstract class PlayerView @JvmOverloads constructor( - private val context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr), - PlaybackModel.Listener { - - companion object { - private const val TAG = "PlayerView" - } - - @Inject - lateinit var playbackModel: PlaybackModel - - @Inject - lateinit var userAccountManager: UserAccountManager - - @get:LayoutRes - protected abstract val layoutRes: Int - - protected abstract val createFragment: (PlaybackFile) -> Fragment - - protected val activity: AppCompatActivity by lazy { context as AppCompatActivity } - protected val windowWrapper: WindowWrapper by lazy { WindowWrapper(activity.window) } - - protected val topBar: MaterialToolbar by lazy { findViewById(R.id.topBar) } - protected val playerPager: PlayerPager by lazy { findViewById(R.id.playerPager) } - protected val playerControlView: PlayerControlView by lazy { findViewById(R.id.playerControlView) } - - var onMoreClick: (() -> Unit)? = null - - init { - inflate(context, layoutRes, this) - if (!isInEditMode) { - (context.applicationContext as HasAndroidInjector).androidInjector().inject(this) - playerPager.initialize(activity.supportFragmentManager, activity.lifecycle, createFragment) - playerPager.onItemSelected = { playbackModel.switchToFile(it) } - playerControlView.navigator = playerPager - topBar.setNavigationOnClickListener { activity.onBackPressedDispatcher.onBackPressed() } - topBar.setOnMenuItemClickListener { menuItem -> - val isMore = menuItem.itemId == R.id.action_more - if (isMore) { - onMoreClick?.invoke() - } - isMore - } - } - } - - protected fun applyTopBarInsets(insets: Insets) { - val actionInset = resources.getDimensionPixelSize(R.dimen.standard_quarter_padding) - topBar.setPadding(insets.left, insets.top, insets.right + actionInset, 0) - } - - @CallSuper - open fun onStart() { - val state = playbackModel.state - if (state == null) { - activity.finish() - return - } - - render(state) - playbackModel.addListener(this) - playerControlView.onStart() - } - - @CallSuper - open fun onStop() { - playbackModel.removeListener(this) - playerControlView.onStop() - } - - @CallSuper - open fun release() { - playerPager.release() - } - - override fun onPlaybackUpdate(state: PlaybackState) { - render(state) - } - - override fun onPlaybackError(error: Throwable) { - if (error is SourceException) { - downloadFile() - } else { - SnackbarUtil.show(this, R.string.common_error_unknown) - } - } - - private fun downloadFile() { - val currentFile = playbackModel.state?.currentItemState?.file - val storageManager = FileDataStorageManager(userAccountManager.user, context.contentResolver) - val file = currentFile?.id?.toLong()?.let { storageManager.getFileByLocalId(it) } ?: return - - activity.lifecycleScope.launch(Dispatchers.IO) { - val operation = DownloadFileOperation(userAccountManager.user, file, context) - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getClientFor(userAccountManager.currentOwnCloudAccount, context) - val result = operation.execute(client) - if (result.isSuccess) { - Log_OC.d(TAG, "file is successfully downloaded") - val helper = FileDownloadHelper() - helper.saveFile(file, operation, storageManager) - } else { - Log_OC.e(TAG, "cannot download file") - withContext(Dispatchers.Main) { - SnackbarUtil.show(this@PlayerView, R.string.player_error_source_not_found) - } - } - } - } - - private fun render(state: PlaybackState) { - val currentFiles = state.currentFiles - if (state.currentFiles.isEmpty()) { - activity.finish() - return - } - - if (playerPager.getItems() != currentFiles) { - playerPager.setItems(currentFiles) - } - - if (state.currentItemState != null) { - val file = state.currentItemState.file - topBar.title = file.getNameWithoutExtension() - playerPager.setCurrentItem(file) - } else { - topBar.title = "" - } - } -} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt b/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt index 0946de7a6b19..8f7111cb8ebc 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/VideoPictureInPicture.kt @@ -10,7 +10,6 @@ package com.nextcloud.client.player.ui import android.app.Activity import android.app.PictureInPictureParams import android.graphics.Rect -import android.os.Build import android.util.Rational import android.view.View import com.nextcloud.client.player.media3.PlaybackModel @@ -22,11 +21,7 @@ private const val ASPECT_RATIO_HEIGHT = 9 private const val MIN_ASPECT_RATIO = 0.42f private const val MAX_ASPECT_RATIO = 2.39f -class VideoPictureInPicture( - private val activity: Activity, - private val playbackModel: PlaybackModel, - private val autoEnter: Boolean -) { +class VideoPictureInPicture(private val activity: Activity, private val playbackModel: PlaybackModel) { private val defaultAspectRatio = Rational(ASPECT_RATIO_WIDTH, ASPECT_RATIO_HEIGHT) @@ -38,9 +33,6 @@ class VideoPictureInPicture( val aspectRatio = getAspectRatio(playbackModel.state?.currentItemState?.videoSize) return PictureInPictureParams.Builder().let { it.setAspectRatio(aspectRatio) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - it.setAutoEnterEnabled(autoEnter) - } it.setSourceRectHint(getSourceRectHint(sourceView, aspectRatio)) it.build() } diff --git a/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerActivity.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerActivity.kt new file mode 100644 index 000000000000..eb9b8e00888c --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerActivity.kt @@ -0,0 +1,133 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.player.ui.audio + +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.os.Bundle +import androidx.activity.addCallback +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.view.ViewCompat +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.di.ViewModelFactory +import com.nextcloud.ui.fileactions.FileActionsBottomSheet +import com.nextcloud.utils.SnackbarUtil +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class AudioPlayerActivity : + FileActivity(), + Injectable { + + companion object { + fun createIntent(context: Context): Intent = Intent(context, AudioPlayerActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + } + } + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel by viewModels { viewModelFactory } + + private lateinit var audioPlayerView: AudioPlayerView + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, windowInsets -> windowInsets } + + audioPlayerView = AudioPlayerView(this) + audioPlayerView.onMoreClick = { viewModel.onMoreButtonClick() } + setContentView(audioPlayerView) + + viewModel.eventFlow + .flowWithLifecycle(lifecycle) + .onEach { handleEvent(it) } + .launchIn(lifecycleScope) + + onBackPressedDispatcher.addCallback(this) { + file = file?.parentId?.let { storageManager.getFileById(it) } + finish() + } + + volumeControlStream = AudioManager.STREAM_MUSIC + } + + override fun onStart() { + super.onStart() + audioPlayerView.onStart() + } + + override fun onStop() { + super.onStop() + audioPlayerView.onStop() + } + + private fun handleEvent(event: AudioPlayerScreenEvent) { + when (event) { + is AudioPlayerScreenEvent.ShowFileActions -> showFileActions(event.file, event.actionsToHide) + + is AudioPlayerScreenEvent.ShowFileDetails -> showFileDetails(event.file) + + is AudioPlayerScreenEvent.ShowFileExportStartedMessage -> showFileExportStartedMessage() + + is AudioPlayerScreenEvent.ShowShareFileDialog -> fileOperationsHelper.sendShareFile(event.file) + + is AudioPlayerScreenEvent.ShowRemoveFileDialog -> showRemoveFileDialog(event.file) + + is AudioPlayerScreenEvent.LaunchOpenFileIntent -> fileOperationsHelper.openFile(event.file) + + is AudioPlayerScreenEvent.LaunchStreamFileIntent -> fileOperationsHelper.streamMediaFile(event.file) + + is AudioPlayerScreenEvent.ToggleFileLock -> fileOperationsHelper.toggleFileLock( + event.file, + event.shouldBeLocked + ) + + is AudioPlayerScreenEvent.AddFileToAlbum -> fileOperationsHelper.addFileToAlbum(listOf(event.file)) + } + } + + private fun showFileActions(file: OCFile, actionsToHide: List) { + FileActionsBottomSheet.newInstance(file, false, actionsToHide) + .setResultListener(supportFragmentManager, this) { viewModel.onFileActionChosen(file, it) } + .show(supportFragmentManager, "actions") + } + + private fun showFileDetails(file: OCFile) { + val intent = Intent(this, FileDisplayActivity::class.java).apply { + action = FileDisplayActivity.ACTION_DETAILS + putExtra(EXTRA_FILE, file) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + startActivity(intent) + finish() + } + + private fun showFileExportStartedMessage() { + val message = resources.getQuantityString(R.plurals.export_start, 1, 1) + SnackbarUtil.show(audioPlayerView, message) + } + + private fun showRemoveFileDialog(file: OCFile) { + RemoveFilesDialogFragment.newInstance(file) + .show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + } +} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerLauncher.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerLauncher.kt new file mode 100644 index 000000000000..b0ba5aa26993 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerLauncher.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.player.ui.audio + +import androidx.appcompat.app.AppCompatActivity +import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.client.player.media3.PlaybackQueueLoader +import com.nextcloud.client.player.model.file.PlaybackCollection +import com.owncloud.android.datamodel.OCFile +import javax.inject.Inject + +class AudioPlayerLauncher @Inject constructor( + private val playbackQueueLoader: PlaybackQueueLoader, + private val playbackModel: PlaybackModel +) { + fun launch(activity: AppCompatActivity, file: OCFile, collection: PlaybackCollection) { + playbackQueueLoader.load(activity, file, collection) { + playbackModel.play() + activity.startActivity(AudioPlayerActivity.createIntent(activity)) + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPager.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPager.kt similarity index 82% rename from app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPager.kt rename to app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPager.kt index 455a84e9f03b..cec309ca6661 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPager.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPager.kt @@ -6,7 +6,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.player.ui.pager +package com.nextcloud.client.player.ui.audio import android.annotation.SuppressLint import android.content.Context @@ -14,7 +14,6 @@ import android.os.Parcel import android.os.Parcelable import android.util.AttributeSet import android.widget.LinearLayout -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.viewpager2.widget.ViewPager2 @@ -28,16 +27,16 @@ private const val NO_POSITION = -1 private const val FIRST_ENTITY_POSITION = 1 private const val OFFSCREEN_PAGE_LIMIT = 1 -class PlayerPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : +class AudioPlayerPager @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs), MediaNavigator { @SuppressLint("WrongConstant") private val viewPager = ViewPager2(context).apply { - id = R.id.player_view_pager + id = R.id.player_audio_pager offscreenPageLimit = OFFSCREEN_PAGE_LIMIT } - private lateinit var adapter: PlayerPagerAdapter + private lateinit var adapter: AudioPlayerPagerAdapter private var shift = NO_SHIFT private var restoredShift = NO_SHIFT @@ -63,28 +62,23 @@ class PlayerPager @JvmOverloads constructor(context: Context, attrs: AttributeSe addView(viewPager, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) } - fun initialize(fragmentManager: FragmentManager, lifecycle: Lifecycle, createFragment: (PlaybackFile) -> Fragment) { - adapter = PlayerPagerAdapter(fragmentManager, lifecycle, createFragment) + fun initialize(fragmentManager: FragmentManager, lifecycle: Lifecycle) { + adapter = AudioPlayerPagerAdapter(fragmentManager, lifecycle) viewPager.adapter = adapter } override fun onSaveInstanceState(): Parcelable { - val state = PlayerPagerState(super.onSaveInstanceState()) + val state = AudioPlayerPagerState(super.onSaveInstanceState()) state.shiftedPosition = shift return state } override fun onRestoreInstanceState(state: Parcelable?) { - val restoredState = state as PlayerPagerState + val restoredState = state as AudioPlayerPagerState super.onRestoreInstanceState(restoredState.superState) restoredShift = restoredState.shiftedPosition } - fun release() { - viewPager.unregisterOnPageChangeCallback(onPageChangeCallback) - viewPager.adapter = null - } - fun getItems(): List = adapter.getEntities() fun setItems(items: List) { @@ -137,7 +131,7 @@ class PlayerPager @JvmOverloads constructor(context: Context, attrs: AttributeSe return items.rotate(shift) } - class PlayerPagerState : BaseSavedState { + class AudioPlayerPagerState : BaseSavedState { var shiftedPosition: Int = 0 constructor(superState: Parcelable?) : super(superState) @@ -153,11 +147,11 @@ class PlayerPager @JvmOverloads constructor(context: Context, attrs: AttributeSe companion object { @JvmField - val CREATOR = object : Parcelable.Creator { + val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): PlayerPagerState = PlayerPagerState(parcel) + override fun createFromParcel(parcel: Parcel): AudioPlayerPagerState = AudioPlayerPagerState(parcel) - override fun newArray(size: Int): Array = arrayOfNulls(size) + override fun newArray(size: Int): Array = arrayOfNulls(size) } } } diff --git a/app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPagerAdapter.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPagerAdapter.kt similarity index 83% rename from app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPagerAdapter.kt rename to app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPagerAdapter.kt index cdac8a4f7624..1689e83cf381 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/pager/PlayerPagerAdapter.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerPagerAdapter.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.player.ui.pager +package com.nextcloud.client.player.ui.audio import android.annotation.SuppressLint import androidx.fragment.app.Fragment @@ -16,13 +16,10 @@ import com.nextcloud.client.player.model.file.PlaybackFile /** * Pads the item list with a copy of the last item at the front and a copy of the first item at the end, so that - * [PlayerPager] can wrap around and give the impression of an endless pager. + * [AudioPlayerPager] can wrap around and give the impression of an endless pager. */ -class PlayerPagerAdapter( - fragmentManager: FragmentManager, - lifecycle: Lifecycle, - private val fragmentFactory: (PlaybackFile) -> Fragment -) : FragmentStateAdapter(fragmentManager, lifecycle) { +class AudioPlayerPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : + FragmentStateAdapter(fragmentManager, lifecycle) { private var paddedEntities = mutableListOf() @@ -45,7 +42,7 @@ class PlayerPagerAdapter( override fun getItemCount(): Int = paddedEntities.size - override fun createFragment(position: Int): Fragment = fragmentFactory(paddedEntities[position]) + override fun createFragment(position: Int): Fragment = AudioFileFragment.createInstance(paddedEntities[position]) override fun getItemId(position: Int): Long = itemIdAt(position) diff --git a/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerScreenEvent.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerScreenEvent.kt new file mode 100644 index 000000000000..d11f95c5fdcd --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerScreenEvent.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 STRATO GmbH. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.player.ui.audio + +import com.owncloud.android.datamodel.OCFile + +sealed interface AudioPlayerScreenEvent { + + data class ShowFileActions(val file: OCFile, val actionsToHide: List) : AudioPlayerScreenEvent + + data class ShowFileDetails(val file: OCFile) : AudioPlayerScreenEvent + + data object ShowFileExportStartedMessage : AudioPlayerScreenEvent + + data class ShowShareFileDialog(val file: OCFile) : AudioPlayerScreenEvent + + data class ShowRemoveFileDialog(val file: OCFile) : AudioPlayerScreenEvent + + data class LaunchOpenFileIntent(val file: OCFile) : AudioPlayerScreenEvent + + data class LaunchStreamFileIntent(val file: OCFile) : AudioPlayerScreenEvent + + data class ToggleFileLock(val file: OCFile, val shouldBeLocked: Boolean) : AudioPlayerScreenEvent + + data class AddFileToAlbum(val file: OCFile) : AudioPlayerScreenEvent +} diff --git a/app/src/main/java/com/nextcloud/client/player/util/WindowWrapper.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerSystemBars.kt similarity index 80% rename from app/src/main/java/com/nextcloud/client/player/util/WindowWrapper.kt rename to app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerSystemBars.kt index 0407bd97974f..9b6b00ab1314 100644 --- a/app/src/main/java/com/nextcloud/client/player/util/WindowWrapper.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerSystemBars.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.player.util +package com.nextcloud.client.player.ui.audio import android.view.Gravity import android.view.View @@ -21,41 +21,35 @@ import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.updateLayoutParams import com.owncloud.android.R private const val LUMINANCE_THRESHOLD = 0.5 -class WindowWrapper(private val window: Window) { +class AudioPlayerSystemBars(private val window: Window) { private val context = window.context private val insetsController = WindowCompat.getInsetsController(window, window.decorView) - fun showSystemBars() { + fun show() { insetsController.show(WindowInsetsCompat.Type.systemBars()) } - fun hideSystemBars() { - insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - insetsController.hide(WindowInsetsCompat.Type.systemBars()) - } - - fun setupStatusBar(@ColorRes backgroundColorRes: Int) { + fun setStatusBarBackground(@ColorRes backgroundColorRes: Int) { val backgroundColor = ContextCompat.getColor(context, backgroundColorRes) insetsController.isAppearanceLightStatusBars = isLightColor(backgroundColor) drawSystemBarBackground( - R.id.player_status_bar_background, + R.id.player_audio_status_bar_background, Gravity.TOP, backgroundColor, systemBarInsets(WindowInsetsCompat.Type.statusBars()).top ) } - fun setupNavigationBar(@ColorRes backgroundColorRes: Int) { + fun setNavigationBarBackground(@ColorRes backgroundColorRes: Int) { val backgroundColor = ContextCompat.getColor(context, backgroundColorRes) insetsController.isAppearanceLightNavigationBars = isLightColor(backgroundColor) drawSystemBarBackground( - R.id.player_navigation_bar_background, + R.id.player_audio_navigation_bar_background, Gravity.BOTTOM, backgroundColor, systemBarInsets(WindowInsetsCompat.Type.navigationBars()).bottom diff --git a/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerView.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerView.kt index 13f27a139a55..52c084b358c8 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerView.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerView.kt @@ -7,37 +7,150 @@ package com.nextcloud.client.player.ui.audio -import android.content.Context import android.view.WindowInsets +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type -import androidx.fragment.app.Fragment -import com.nextcloud.client.player.model.file.PlaybackFile -import com.nextcloud.client.player.ui.PlayerView +import androidx.lifecycle.lifecycleScope +import com.google.android.material.appbar.MaterialToolbar +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.client.player.model.error.SourceException +import com.nextcloud.client.player.model.state.PlaybackState +import com.nextcloud.client.player.ui.control.PlayerControlView +import com.nextcloud.utils.SnackbarUtil import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.DownloadFileOperation +import dagger.android.HasAndroidInjector +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject -class AudioPlayerView(context: Context) : PlayerView(context) { +class AudioPlayerView(private val activity: AppCompatActivity) : + LinearLayout(activity), + PlaybackModel.Listener { - override val layoutRes get() = R.layout.player_audio_view + companion object { + private const val TAG = "AudioPlayerView" + } + + @Inject + lateinit var playbackModel: PlaybackModel + + @Inject + lateinit var userAccountManager: UserAccountManager - override val createFragment: (PlaybackFile) -> Fragment get() = { AudioFileFragment.createInstance(it) } + private val systemBars by lazy { AudioPlayerSystemBars(activity.window) } - override fun onStart() { - super.onStart() - windowWrapper.showSystemBars() + private val topBar: MaterialToolbar by lazy { findViewById(R.id.topBar) } + private val playerPager: AudioPlayerPager by lazy { findViewById(R.id.playerPager) } + private val playerControlView: PlayerControlView by lazy { findViewById(R.id.playerControlView) } + + var onMoreClick: (() -> Unit)? = null + + init { + inflate(activity, R.layout.player_audio_view, this) + (activity.applicationContext as HasAndroidInjector).androidInjector().inject(this) + playerPager.initialize(activity.supportFragmentManager, activity.lifecycle) + playerPager.onItemSelected = { playbackModel.switchToFile(it) } + playerControlView.navigator = playerPager + topBar.setNavigationOnClickListener { activity.onBackPressedDispatcher.onBackPressed() } + topBar.setOnMenuItemClickListener { menuItem -> + val isMore = menuItem.itemId == R.id.action_more + if (isMore) { + onMoreClick?.invoke() + } + isMore + } } override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowInsets) val insets = windowInsetsCompat.getInsets(Type.systemBars() or Type.displayCutout()) - applyTopBarInsets(insets) + val actionInset = resources.getDimensionPixelSize(R.dimen.standard_quarter_padding) + topBar.setPadding(insets.left, insets.top, insets.right + actionInset, 0) playerPager.setPadding(insets.left, 0, insets.right, 0) playerControlView.setPadding(insets.left, 0, insets.right, insets.bottom) - windowWrapper.setupStatusBar(R.color.player_background_color) - windowWrapper.setupNavigationBar(R.color.player_background_color) + systemBars.setStatusBarBackground(R.color.player_background_color) + systemBars.setNavigationBarBackground(R.color.player_background_color) return WindowInsetsCompat.CONSUMED.toWindowInsets() } + + fun onStart() { + val state = playbackModel.state + if (state == null) { + activity.finish() + return + } + + systemBars.show() + render(state) + playbackModel.addListener(this) + playerControlView.onStart() + } + + fun onStop() { + playbackModel.removeListener(this) + playerControlView.onStop() + } + + override fun onPlaybackUpdate(state: PlaybackState) { + render(state) + } + + override fun onPlaybackError(error: Throwable) { + if (error is SourceException) { + downloadFile() + } else { + SnackbarUtil.show(this, R.string.common_error_unknown) + } + } + + private fun downloadFile() { + val currentFile = playbackModel.state?.currentItemState?.file + val storageManager = FileDataStorageManager(userAccountManager.user, context.contentResolver) + val file = currentFile?.id?.toLong()?.let { storageManager.getFileByLocalId(it) } ?: return + + activity.lifecycleScope.launch(Dispatchers.IO) { + val operation = DownloadFileOperation(userAccountManager.user, file, context) + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(userAccountManager.currentOwnCloudAccount, context) + val result = operation.execute(client) + if (result.isSuccess) { + Log_OC.d(TAG, "file is successfully downloaded") + val helper = FileDownloadHelper() + helper.saveFile(file, operation, storageManager) + } else { + Log_OC.e(TAG, "cannot download file") + withContext(Dispatchers.Main) { + SnackbarUtil.show(this@AudioPlayerView, R.string.player_error_source_not_found) + } + } + } + } + + private fun render(state: PlaybackState) { + val currentFiles = state.currentFiles + if (currentFiles.isEmpty()) { + activity.finish() + return + } + + if (playerPager.getItems() != currentFiles) { + playerPager.setItems(currentFiles) + } + + val file = state.currentItemState?.file + topBar.title = file?.getNameWithoutExtension().orEmpty() + file?.let(playerPager::setCurrentItem) + } } diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerViewModel.kt similarity index 71% rename from app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt rename to app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerViewModel.kt index 640a32f02e21..231ad3123f9f 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/audio/AudioPlayerViewModel.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.client.player.ui +package com.nextcloud.client.player.ui.audio import androidx.core.text.isDigitsOnly import androidx.lifecycle.ViewModel @@ -28,7 +28,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException -class PlayerViewModel @Inject constructor( +class AudioPlayerViewModel @Inject constructor( private val playbackModel: PlaybackModel, private val storageManager: FileDataStorageManager, private val userAccountManager: UserAccountManager, @@ -36,42 +36,42 @@ class PlayerViewModel @Inject constructor( private val logger: Logger ) : ViewModel() { - private val eventChannel = Channel(Channel.BUFFERED) - val eventFlow: Flow = eventChannel.receiveAsFlow() + private val eventChannel = Channel(Channel.BUFFERED) + val eventFlow: Flow = eventChannel.receiveAsFlow() fun onMoreButtonClick() { viewModelScope.launch { val file = getCurrentOCFile() ?: return@launch val actionsToHide = FileAction.getFilePreviewActions(file) - eventChannel.trySend(PlayerScreenEvent.ShowFileActions(file, actionsToHide)) + eventChannel.trySend(AudioPlayerScreenEvent.ShowFileActions(file, actionsToHide)) } } fun onFileActionChosen(file: OCFile, actionId: Int) { when (actionId) { - R.id.action_see_details -> eventChannel.trySend(PlayerScreenEvent.ShowFileDetails(file)) + R.id.action_see_details -> eventChannel.trySend(AudioPlayerScreenEvent.ShowFileDetails(file)) R.id.action_download_file -> startFileDownloading(file) R.id.action_export_file -> startFileExport(file) - R.id.action_send_share_file -> eventChannel.trySend(PlayerScreenEvent.ShowShareFileDialog(file)) + R.id.action_send_share_file -> eventChannel.trySend(AudioPlayerScreenEvent.ShowShareFileDialog(file)) - R.id.action_remove_file -> eventChannel.trySend(PlayerScreenEvent.ShowRemoveFileDialog(file)) + R.id.action_remove_file -> eventChannel.trySend(AudioPlayerScreenEvent.ShowRemoveFileDialog(file)) R.id.action_open_file_with -> onOpenFileWithClick(file) R.id.action_stream_media -> onStreamFileClick(file) R.id.action_lock_file -> eventChannel.trySend( - PlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = true) + AudioPlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = true) ) R.id.action_unlock_file -> eventChannel.trySend( - PlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = false) + AudioPlayerScreenEvent.ToggleFileLock(file, shouldBeLocked = false) ) - R.id.action_add_to_album -> eventChannel.trySend(PlayerScreenEvent.AddFileToAlbum(file)) + R.id.action_add_to_album -> eventChannel.trySend(AudioPlayerScreenEvent.AddFileToAlbum(file)) } } @@ -87,7 +87,7 @@ class PlayerViewModel @Inject constructor( storageManager.getFileByLocalId(localId) }.getOrElse { if (it is CancellationException) throw it - logger.e(PlayerViewModel::class.java.simpleName, "Failed to get file by localId: $localId", it) + logger.e(AudioPlayerViewModel::class.java.simpleName, "Failed to get file by localId: $localId", it) null } } @@ -99,16 +99,16 @@ class PlayerViewModel @Inject constructor( private fun startFileExport(file: OCFile) { backgroundJobManager.startImmediateFilesExportJob(listOf(file)) - eventChannel.trySend(PlayerScreenEvent.ShowFileExportStartedMessage) + eventChannel.trySend(AudioPlayerScreenEvent.ShowFileExportStartedMessage) } private fun onOpenFileWithClick(file: OCFile) { playbackModel.pause() - eventChannel.trySend(PlayerScreenEvent.LaunchOpenFileIntent(file)) + eventChannel.trySend(AudioPlayerScreenEvent.LaunchOpenFileIntent(file)) } private fun onStreamFileClick(file: OCFile) { playbackModel.pause() - eventChannel.trySend(PlayerScreenEvent.LaunchStreamFileIntent(file)) + eventChannel.trySend(AudioPlayerScreenEvent.LaunchStreamFileIntent(file)) } } diff --git a/app/src/main/java/com/nextcloud/client/player/ui/video/VideoFileFragment.kt b/app/src/main/java/com/nextcloud/client/player/ui/video/VideoFileFragment.kt deleted file mode 100644 index ee2860be96de..000000000000 --- a/app/src/main/java/com/nextcloud/client/player/ui/video/VideoFileFragment.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 STRATO GmbH. - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.player.ui.video - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.nextcloud.client.player.media3.PlaybackModel -import com.nextcloud.client.player.model.PlayerThumbnailLoader -import com.nextcloud.client.player.model.file.PlaybackFile -import com.nextcloud.client.player.model.state.PlaybackState -import com.nextcloud.client.player.model.state.VideoSize -import com.nextcloud.client.player.util.PlayerUtil.applyVideoSize -import com.nextcloud.client.player.util.PlayerUtil.getPlaybackFile -import com.nextcloud.client.player.util.PlayerUtil.ownsPlayback -import com.nextcloud.client.player.util.PlayerUtil.putPlaybackFile -import com.owncloud.android.R -import com.owncloud.android.databinding.PlayerVideoFileFragmentBinding -import dagger.android.support.AndroidSupportInjection -import kotlinx.coroutines.launch -import javax.inject.Inject - -class VideoFileFragment : - Fragment(), - PlaybackModel.Listener { - - companion object { - private const val ARGUMENT_FILE = "ARGUMENT_FILE" - private const val SURFACE_ALPHA_VISIBLE = 1f - private const val SURFACE_ALPHA_HIDDEN = 0f - - fun createInstance(file: PlaybackFile) = VideoFileFragment().apply { - arguments = Bundle().apply { putPlaybackFile(ARGUMENT_FILE, file) } - } - } - - @Inject - lateinit var playerModel: PlaybackModel - - @Inject - lateinit var playerThumbnailLoader: PlayerThumbnailLoader - - private var _binding: PlayerVideoFileFragmentBinding? = null - private val binding get() = checkNotNull(_binding) { "Binding accessed outside of the view lifecycle" } - - private var renderedVideoSize: VideoSize? = null - - private val file by lazy { - requireNotNull(arguments.getPlaybackFile(ARGUMENT_FILE)) { - "VideoFileFragment requires a $ARGUMENT_FILE argument" - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - AndroidSupportInjection.inject(this) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - PlayerVideoFileFragmentBinding.inflate(inflater, container, false).also { _binding = it }.root - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - loadFileThumbnail() - binding.root.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - render(playerModel.state) - } - } - - override fun onStart() { - super.onStart() - render(playerModel.state) - playerModel.addListener(this) - } - - override fun onResume() { - super.onResume() - render(playerModel.state) - } - - override fun onStop() { - playerModel.removeListener(this) - super.onStop() - } - - override fun onDestroyView() { - _binding?.let { playerModel.clearVideoSurfaceView(it.surfaceView) } - _binding = null - super.onDestroyView() - } - - override fun onPlaybackUpdate(state: PlaybackState) { - render(state) - } - - private fun loadFileThumbnail() = viewLifecycleOwner.lifecycleScope.launch { - val context = context ?: return@launch - val thumbnailSize = context.resources.getDimensionPixelSize(R.dimen.player_album_cover_size) - val thumbnail = playerThumbnailLoader.await(file, thumbnailSize, thumbnailSize) ?: return@launch - _binding?.thumbnail?.setImageBitmap(thumbnail) - } - - private fun render(state: PlaybackState?) { - val binding = _binding ?: return - val currentItemState = state?.currentItemState - - if (currentItemState?.file == file) { - showVideo(currentItemState.videoSize) - return - } - - renderedVideoSize = null - binding.surfaceView.isVisible = false - if (currentItemState == null) { - playerModel.setVideoSurfaceView(null) - } - } - - private fun showVideo(videoSize: VideoSize?) { - if (ownsPlayback(binding.surfaceView)) { - playerModel.setVideoSurfaceView(binding.surfaceView) - } - - val size = videoSize ?: renderedVideoSize - renderedVideoSize = size - - binding.surfaceView.isVisible = true - binding.surfaceView.alpha = if (size == null) SURFACE_ALPHA_HIDDEN else SURFACE_ALPHA_VISIBLE - - if (size == null) return - - binding.surfaceView.applyVideoSize(size) - } -} diff --git a/app/src/main/java/com/nextcloud/client/player/ui/video/VideoPlayerView.kt b/app/src/main/java/com/nextcloud/client/player/ui/video/VideoPlayerView.kt deleted file mode 100644 index 5bf1f4c34ee4..000000000000 --- a/app/src/main/java/com/nextcloud/client/player/ui/video/VideoPlayerView.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 STRATO GmbH. - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.player.ui.video - -import android.content.Context -import android.view.WindowInsets -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsCompat.Type -import androidx.fragment.app.Fragment -import com.nextcloud.client.player.model.file.PlaybackFile -import com.nextcloud.client.player.ui.PlayerView -import com.owncloud.android.R -import kotlinx.coroutines.Job - -class VideoPlayerView(context: Context) : PlayerView(context) { - override val layoutRes get() = R.layout.player_video_view - - override val createFragment: (PlaybackFile) -> Fragment get() = { VideoFileFragment.createInstance(it) } - - private var hideControlsTimerJob: Job? = null - - init { - topBar.setBackgroundResource(R.drawable.player_video_top_scrim) - } - - override fun onStop() { - super.onStop() - cancelHideControlsTimer() - playbackModel.setVideoSurfaceView(null) - } - - override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? { - val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowInsets) - val insets = windowInsetsCompat.getInsets(Type.systemBars() or Type.displayCutout()) - - applyTopBarInsets(insets) - playerControlView.setPadding(insets.left, 0, insets.right, insets.bottom) - - windowWrapper.setupStatusBar(R.color.player_video_toolbar_background_color) - windowWrapper.setupNavigationBar(R.color.player_video_control_view_background_color) - - return WindowInsetsCompat.CONSUMED.toWindowInsets() - } - - private fun cancelHideControlsTimer() { - hideControlsTimerJob?.cancel() - hideControlsTimerJob = null - } -} diff --git a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt index d7dbaa93864a..6f95188370eb 100644 --- a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt +++ b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt @@ -16,10 +16,10 @@ import android.database.ContentObserver import android.graphics.Rect import android.net.Uri import android.os.Bundle -import android.util.LruCache import android.os.Handler import android.os.Looper import android.os.Process +import android.util.LruCache import android.view.SurfaceView import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/com/nextcloud/client/systembars/SystemBarBackgroundCallbacks.kt b/app/src/main/java/com/nextcloud/client/systembars/SystemBarBackgroundCallbacks.kt index a0ebd8ee76ab..0695af21d86a 100644 --- a/app/src/main/java/com/nextcloud/client/systembars/SystemBarBackgroundCallbacks.kt +++ b/app/src/main/java/com/nextcloud/client/systembars/SystemBarBackgroundCallbacks.kt @@ -13,6 +13,7 @@ import android.widget.FrameLayout import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import com.nextcloud.client.onboarding.FirstRunActivity +import com.nextcloud.client.player.ui.audio.AudioPlayerActivity import com.nmc.android.ui.LauncherActivity import com.owncloud.android.R import com.owncloud.android.authentication.AuthenticatorActivity @@ -29,7 +30,8 @@ class SystemBarBackgroundCallbacks(private val viewThemeUtilsProvider: Provider< private val excludedActivities = listOf( LauncherActivity::class, AuthenticatorActivity::class, - FirstRunActivity::class + FirstRunActivity::class, + AudioPlayerActivity::class ) private fun isExcluded(activity: Activity): Boolean = excludedActivities.any { it.isInstance(activity) } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt index 36195f78c2d3..1d7199dd6844 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt @@ -16,6 +16,9 @@ import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import com.nextcloud.ui.behavior.OnScrollBehavior import com.owncloud.android.lib.common.utils.Log_OC @@ -103,3 +106,12 @@ fun T.slideHideBottomBehavior(visible: Boolean) { } } } + +fun View.addPaddingForNavBar() { + addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ -> + val insets = ViewCompat.getRootWindowInsets(view) + ?.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()) + ?: return@addOnLayoutChangeListener + view.updatePadding(left = insets.left, right = insets.right, bottom = insets.bottom) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index be00f4f53900..494b1a8ae6d4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -37,7 +37,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.network.NetworkChangeListener; -import com.nextcloud.client.player.ui.PlayerActivity; +import com.nextcloud.client.player.ui.audio.AudioPlayerActivity; import com.nextcloud.client.utils.IntentUtil; import com.nextcloud.utils.EditorUtils; import com.nextcloud.utils.SnackbarUtil; @@ -255,7 +255,7 @@ public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailab refreshList(); } } else { - if (this instanceof PlayerActivity) { + if (this instanceof AudioPlayerActivity) { hideInfoBox(); } else { showInfoBox(R.string.offline_mode); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 436eb2373016..858b4700cb69 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -73,7 +73,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.client.player.model.file.toPlaybackCollection -import com.nextcloud.client.player.ui.PlayerLauncher +import com.nextcloud.client.player.ui.audio.AudioPlayerLauncher import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.utils.IntentUtil import com.nextcloud.model.OCUploadLocalPathData @@ -242,7 +242,7 @@ class FileDisplayActivity : private var listFragmentJustCreated = false @Inject - lateinit var playerLauncher: PlayerLauncher + lateinit var audioPlayerLauncher: AudioPlayerLauncher @Inject lateinit var localBroadcastManager: LocalBroadcastManager @@ -882,8 +882,7 @@ class FileDisplayActivity : } } - fun canMediaPreviewed(file: OCFile?): Boolean = - file != null && (MimeTypeUtil.isAudio(file) || MimeTypeUtil.isVideo(file)) + fun canPreviewInAudioPlayer(file: OCFile?): Boolean = file != null && MimeTypeUtil.isAudio(file) private fun tryStartWaitingPreview(success: Boolean): Boolean { if (!success) return false @@ -897,8 +896,8 @@ class FileDisplayActivity : true } - canMediaPreviewed(file) -> { - startMediaPreview(file, true, true) + canPreviewInAudioPlayer(file) -> { + startAudioPreview(file, true, true) true } @@ -2096,9 +2095,9 @@ class FileDisplayActivity : } else if (PreviewTextFileFragment.canBePreviewed(file)) { setFabVisible?.onComplete(false) startTextPreview(file, false) - } else if (canMediaPreviewed(file)) { + } else if (canPreviewInAudioPlayer(file)) { setFabVisible?.onComplete(false) - startMediaPreview(file, true, false) + startAudioPreview(file, true, false) } else { fileOperationsHelper.openFile(file) } @@ -2741,28 +2740,28 @@ class FileDisplayActivity : } /** - * Starts the preview of a media [OCFile], synchronizing it first when it is not available yet. + * Starts the preview of an audio [OCFile], synchronizing it first when it is not available yet. */ - fun startMediaPreview(file: OCFile, showPreview: Boolean, streamMedia: Boolean) { + fun startAudioPreview(file: OCFile, showPreview: Boolean, streamMedia: Boolean) { val user = getUser() if (!user.isPresent) { return // not reachable under normal conditions } if ((showPreview && file.isDown && !file.isDownloading) || streamMedia) { - startMediaActivity(file) + startAudioPlayer(file) } else { val previewIntent = Intent() previewIntent.putExtra(EXTRA_FILE, file) - previewIntent.putExtra(MEDIA_PREVIEW, true) + previewIntent.putExtra(AUDIO_PREVIEW, true) val fileOperationsHelper = FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent) } } - private fun startMediaActivity(file: OCFile) { + private fun startAudioPlayer(file: OCFile) { val collection = listOfFilesFragment?.currentSearchType.toPlaybackCollection() - playerLauncher.launch(this, file, collection) + audioPlayerLauncher.launch(this, file, collection) } fun configureToolbarForPreview(file: OCFile?) { @@ -2959,8 +2958,8 @@ class FileDisplayActivity : if (event.intent.getBooleanExtra(TEXT_PREVIEW, false)) { startTextPreview(file, true) - } else if (event.intent.getBooleanExtra(MEDIA_PREVIEW, false)) { - startMediaPreview(file, true, true) + } else if (event.intent.getBooleanExtra(AUDIO_PREVIEW, false)) { + startAudioPreview(file, true, true) } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) { val virtualType = bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE) as VirtualFolderType? startImagePreview( @@ -3345,7 +3344,7 @@ class FileDisplayActivity : const val TAG_LIST_OF_FILES: String = "LIST_OF_FILES" const val TEXT_PREVIEW: String = "TEXT_PREVIEW" - const val MEDIA_PREVIEW: String = "MEDIA_PREVIEW" + const val AUDIO_PREVIEW: String = "AUDIO_PREVIEW" const val KEY_IS_SEARCH_OPEN: String = "IS_SEARCH_OPEN" const val KEY_SEARCH_QUERY: String = "SEARCH_QUERY" diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 202fb2157656..e2e517b34599 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1238,9 +1238,9 @@ private void handlePendingDownloadFile(OCFile file) { boolean webViewAvailable = WebViewUtil.available(context); - if (!file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda && fda.canMediaPreviewed(file)) { + if (!file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda && fda.canPreviewInAudioPlayer(file)) { setFabVisible(false); - fda.startMediaPreview(file, true, true); + fda.startAudioPreview(file, true, true); } else if (webViewAvailable && editorUtils.getEditor(accountManager.getUser(), file.getMimeType()) != null && !file.isEncrypted()) { TextEditorWebView.Companion.startTextEditor(file, context); } else if (supportsDirectEditing(file, webViewAvailable)) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt index f779a7d78e36..d620e785abcb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt @@ -564,13 +564,13 @@ class AlbumItemsFragment : MimeTypeUtil.isVideo(file) -> activity.startImagePreview(file, true, VirtualFolderType.ALBUM) - file.isDown && activity.canMediaPreviewed(file) -> - activity.startMediaPreview(file, showPreview = true, streamMedia = false) + file.isDown && activity.canPreviewInAudioPlayer(file) -> + activity.startAudioPreview(file, showPreview = true, streamMedia = false) file.isDown -> containerActivity?.fileOperationsHelper?.openFile(file) - activity.canMediaPreviewed(file) && !file.isEncrypted -> - activity.startMediaPreview(file, showPreview = true, streamMedia = true) + activity.canPreviewInAudioPlayer(file) && !file.isEncrypted -> + activity.startAudioPreview(file, showPreview = true, streamMedia = true) else -> Log_OC.d(TAG, "Couldn't handle item click") } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index af1cd78fc7a5..8577a049d695 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -108,7 +108,7 @@ class PreviewImageActivity : private var isDownloadWorkStarted = false private var screenState = PreviewImageActivityState.Idle - private val pictureInPicture by lazy { VideoPictureInPicture(this, playbackModel, autoEnter = false) } + private val pictureInPicture by lazy { VideoPictureInPicture(this, playbackModel) } private var wasSystemUiVisibleBeforePictureInPicture = true private var keepPlaybackOnFinish = false diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt index 37619429685c..a284058854f0 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewPlaybackFragment.kt @@ -27,13 +27,13 @@ import com.google.android.material.snackbar.Snackbar import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.player.media3.PlaybackModel +import com.nextcloud.client.player.media3.PlaybackQueueLoader import com.nextcloud.client.player.model.PlayerThumbnailLoader import com.nextcloud.client.player.model.file.PlaybackCollection import com.nextcloud.client.player.model.file.PlaybackFile import com.nextcloud.client.player.model.state.PlaybackState import com.nextcloud.client.player.model.state.VideoSize import com.nextcloud.client.player.ui.MediaNavigator -import com.nextcloud.client.player.ui.PlayerLauncher import com.nextcloud.client.player.util.PlayerUtil.applyVideoSize import com.nextcloud.client.player.util.PlayerUtil.isPictureInPictureAllowed import com.nextcloud.client.player.util.PlayerUtil.ownsPlayback @@ -43,6 +43,7 @@ import com.nextcloud.ui.fileactions.FileActionsBottomSheet import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getSerializableArgument +import com.nextcloud.utils.extensions.addPaddingForNavBar import com.nextcloud.utils.extensions.setVisibilityWithAnimation import com.nextcloud.utils.extensions.showNavigationBar import com.nextcloud.utils.extensions.showSystemBar @@ -61,11 +62,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -/** - * Plays an audio or video page of the preview pager in place, so that swiping between images and media keeps the - * user on the same screen. Playback itself is owned by the shared [PlaybackModel], the same one that drives - * [com.nextcloud.client.player.ui.PlayerActivity], notification and background playback. - */ class PreviewPlaybackFragment : Fragment(), PlaybackModel.Listener { @@ -75,31 +71,24 @@ class PreviewPlaybackFragment : private const val ARGUMENT_FILE = "ARGUMENT_FILE" private const val ARGUMENT_COLLECTION = "ARGUMENT_COLLECTION" private const val ARGUMENT_AUTOPLAY = "ARGUMENT_AUTOPLAY" - private const val ARGUMENT_PICTURE_IN_PICTURE_ON_BACK = "ARGUMENT_PICTURE_IN_PICTURE_ON_BACK" private const val SURFACE_ALPHA_VISIBLE = 1f private const val SURFACE_ALPHA_HIDDEN = 0f - @Suppress("LongParameterList") - fun newInstance( - file: OCFile, - collection: PlaybackCollection, - autoplay: Boolean = false, - pictureInPictureOnBack: Boolean = true - ) = PreviewPlaybackFragment().apply { - arguments = bundleOf( - ARGUMENT_FILE to file, - ARGUMENT_COLLECTION to collection, - ARGUMENT_AUTOPLAY to autoplay, - ARGUMENT_PICTURE_IN_PICTURE_ON_BACK to pictureInPictureOnBack - ) - } + fun newInstance(file: OCFile, collection: PlaybackCollection, autoplay: Boolean = false) = + PreviewPlaybackFragment().apply { + arguments = bundleOf( + ARGUMENT_FILE to file, + ARGUMENT_COLLECTION to collection, + ARGUMENT_AUTOPLAY to autoplay + ) + } } @Inject lateinit var playbackModel: PlaybackModel @Inject - lateinit var playerLauncher: PlayerLauncher + lateinit var playbackQueueLoader: PlaybackQueueLoader @Inject lateinit var playerThumbnailLoader: PlayerThumbnailLoader @@ -118,7 +107,6 @@ class PreviewPlaybackFragment : private lateinit var playbackFile: PlaybackFile private var playbackCollection = PlaybackCollection.FOLDER private var autoplay: Boolean = false - private var pictureInPictureOnBack: Boolean = true private var wasCurrentItem = false private var renderedVideoSize: VideoSize? = null @@ -134,7 +122,6 @@ class PreviewPlaybackFragment : playbackCollection = arguments.getSerializableArgument(ARGUMENT_COLLECTION, PlaybackCollection::class.java) ?: PlaybackCollection.FOLDER autoplay = arguments?.getBoolean(ARGUMENT_AUTOPLAY) == true - pictureInPictureOnBack = arguments?.getBoolean(ARGUMENT_PICTURE_IN_PICTURE_ON_BACK) != false } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -142,6 +129,7 @@ class PreviewPlaybackFragment : loadThumbnail() registerPictureInPictureOnBack() binding.playerControlView.navigator = activity as? MediaNavigator + binding.playerControlView.addPaddingForNavBar() isFullScreen = previewActivity()?.isSystemUIVisible == false binding.surfaceView.setOnClickListener { toggleFullScreen() } updatePlayerControlsVisibility() @@ -279,7 +267,7 @@ class PreviewPlaybackFragment : } private fun registerPictureInPictureOnBack() { - if (!pictureInPictureOnBack || !MimeTypeUtil.isVideo(file)) { + if (!MimeTypeUtil.isVideo(file)) { return } @@ -308,7 +296,6 @@ class PreviewPlaybackFragment : override fun onResume() { super.onResume() - playbackModel.onPictureInPictureClose?.invoke() pictureInPictureCallback?.isEnabled = true preparePlayback() binding.playerControlView.onStart() @@ -359,7 +346,11 @@ class PreviewPlaybackFragment : playbackModel.play() } } else { - playerLauncher.prepare(this, file, playbackCollection, autoplay) + playbackQueueLoader.load(this, file, playbackCollection) { + if (autoplay) { + playbackModel.play() + } + } } } diff --git a/app/src/main/res/drawable/player_video_top_scrim.xml b/app/src/main/res/drawable/player_video_top_scrim.xml deleted file mode 100644 index 28424508e791..000000000000 --- a/app/src/main/res/drawable/player_video_top_scrim.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/player_top_bar.xml b/app/src/main/res/layout/player_audio_top_bar.xml similarity index 94% rename from app/src/main/res/layout/player_top_bar.xml rename to app/src/main/res/layout/player_audio_top_bar.xml index b6de94155ea8..9c499858204c 100644 --- a/app/src/main/res/layout/player_top_bar.xml +++ b/app/src/main/res/layout/player_audio_top_bar.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="@dimen/player_toolbar_height" - app:menu="@menu/player_top_bar" + app:menu="@menu/player_audio_top_bar" app:navigationContentDescription="@string/common_back" app:navigationIcon="@drawable/ic_arrow_back" tools:title="Player title" /> diff --git a/app/src/main/res/layout/player_audio_view.xml b/app/src/main/res/layout/player_audio_view.xml index 158ade3fdc6e..97eaba2bee3b 100644 --- a/app/src/main/res/layout/player_audio_view.xml +++ b/app/src/main/res/layout/player_audio_view.xml @@ -11,9 +11,9 @@ android:background="@color/player_background_color" android:orientation="vertical"> - + - - - - - - - - - - - diff --git a/app/src/main/res/layout/player_video_view.xml b/app/src/main/res/layout/player_video_view.xml deleted file mode 100644 index dfa6d20b83a5..000000000000 --- a/app/src/main/res/layout/player_video_view.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/menu/player_top_bar.xml b/app/src/main/res/menu/player_audio_top_bar.xml similarity index 100% rename from app/src/main/res/menu/player_top_bar.xml rename to app/src/main/res/menu/player_audio_top_bar.xml diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5a05b4786e5f..09c99a01702f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -96,8 +96,6 @@ @color/white @color/white @color/white - #21000000 - #21000000 #B3000000 #979797 diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 952ca315f9c3..18ffcda69b58 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -43,7 +43,7 @@ - - - + + + From 522727536f10418fd3b2982f193a8c0a4882982c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 28 Sep 2026 14:04:53 +0200 Subject: [PATCH 2/6] fix lint Signed-off-by: alperozturk96 --- app/src/main/res/values/styles.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 0003449383bf..3b48bcb6524a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -545,8 +545,6 @@ @color/player_default_text_color -