Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.os.SystemClock
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.nextcloud.test.Flaky
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import org.junit.Assert.assertEquals
Expand All @@ -33,6 +34,7 @@ class LauncherActivityIT : AbstractIT() {
}

@Test
@Flaky(reason = "Opening FileDisplayActivity can keep the main thread busy past the launch timeout")
fun testSplashScreenWithTitlesShouldShowTitles() {
val activity = launchLauncherActivity()

Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,9 @@
android:launchMode="singleTask"
android:theme="@style/Theme.ownCloud.Overlay" />
<activity
android:name="com.nextcloud.client.player.ui.PlayerActivity"
android:name="com.nextcloud.client.player.ui.audio.AudioPlayerActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTask"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.ownCloud.NoActionBar" />

<service
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import androidx.lifecycle.ViewModelProvider
import com.nextcloud.client.documentscan.DocumentScanViewModel
import com.nextcloud.client.etm.EtmViewModel
import com.nextcloud.client.logger.ui.LogsViewModel
import com.nextcloud.client.player.ui.PlayerViewModel
import com.nextcloud.client.player.ui.audio.AudioPlayerViewModel
import com.nextcloud.ui.fileactions.FileActionsViewModel
import com.nextcloud.ui.tags.TagManagementViewModel
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel
Expand Down Expand Up @@ -66,8 +66,8 @@ abstract class ViewModelModule {

@Binds
@IntoMap
@ViewModelKey(PlayerViewModel::class)
abstract fun playerViewModel(vm: PlayerViewModel): ViewModel
@ViewModelKey(AudioPlayerViewModel::class)
abstract fun audioPlayerViewModel(vm: AudioPlayerViewModel): ViewModel

@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
Expand Down
12 changes: 2 additions & 10 deletions app/src/main/java/com/nextcloud/client/player/PlayerModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import com.nextcloud.client.player.media3.PlaybackService
import com.nextcloud.client.player.ui.PlayerActivity
import com.nextcloud.client.player.ui.PlayerProgressIndicator
import com.nextcloud.client.player.ui.audio.AudioFileFragment
import com.nextcloud.client.player.ui.audio.AudioPlayerActivity
import com.nextcloud.client.player.ui.audio.AudioPlayerView
import com.nextcloud.client.player.ui.control.PlayerControlView
import com.nextcloud.client.player.ui.video.VideoFileFragment
import com.nextcloud.client.player.ui.video.VideoPlayerView
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
Expand Down Expand Up @@ -48,14 +46,11 @@ class PlayerModule {
abstract fun playbackService(): PlaybackService

@ContributesAndroidInjector
abstract fun playerActivity(): PlayerActivity
abstract fun audioPlayerActivity(): AudioPlayerActivity

@ContributesAndroidInjector
abstract fun audioPlayerView(): AudioPlayerView

@ContributesAndroidInjector
abstract fun videoPlayerView(): VideoPlayerView

@ContributesAndroidInjector
abstract fun playerControlView(): PlayerControlView

Expand All @@ -64,8 +59,5 @@ class PlayerModule {

@ContributesAndroidInjector
abstract fun audioFileFragment(): AudioFileFragment

@ContributesAndroidInjector
abstract fun videoFileFragment(): VideoFileFragment
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ class PlaybackModel @Inject constructor(

private var mediaSession: MediaSession? = null

var onPictureInPictureClose: (() -> Unit)? = null

private var videoSurfaceView: SurfaceView? = null

private var cachedCurrentFiles: List<PlaybackFile>? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading
Loading