Skip to content

Commit

Permalink
fix: Use only one instance of MediaController
Browse files Browse the repository at this point in the history
  • Loading branch information
tevincent committed Feb 6, 2025
1 parent d40e978 commit 4e08fb9
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 98 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ android {

namespace 'com.infomaniak.drive'

compileSdk 34
compileSdk 35

ndkVersion "25.2.9519653"

Expand Down Expand Up @@ -142,7 +142,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'androidx.webkit:webkit:1.12.1'

def exoplayer_version = '1.4.1'
def exoplayer_version = '1.5.1'
implementation "androidx.media3:media3-exoplayer:$exoplayer_version"
implementation "androidx.media3:media3-ui:$exoplayer_version"
implementation "androidx.media3:media3-datasource-okhttp:$exoplayer_version"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@

<activity
android:name=".ui.MainActivity"
android:configChanges="orientation|screenSize|layoutDirection|screenLayout" />
android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|layoutDirection|screenLayout|smallestScreenSize" />

<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
Expand Down
136 changes: 108 additions & 28 deletions app/src/main/java/com/infomaniak/drive/ui/BasePreviewSliderFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,31 @@
package com.infomaniak.drive.ui

import android.annotation.SuppressLint
import android.app.PictureInPictureParams
import android.content.ComponentName
import android.os.Build
import android.os.Bundle
import android.util.Rational
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withResumed
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.common.util.concurrent.ListenableFuture
import com.infomaniak.drive.R
import com.infomaniak.drive.data.models.File
import com.infomaniak.drive.data.models.UserDrive
Expand All @@ -43,6 +52,7 @@ import com.infomaniak.drive.ui.fileList.preview.PreviewPDFActivity
import com.infomaniak.drive.ui.fileList.preview.PreviewPDFHandler
import com.infomaniak.drive.ui.fileList.preview.PreviewSliderAdapter
import com.infomaniak.drive.ui.fileList.preview.PreviewSliderViewModel
import com.infomaniak.drive.ui.fileList.preview.playback.PlaybackService
import com.infomaniak.drive.ui.fileList.preview.playback.PreviewPlaybackFragment
import com.infomaniak.drive.utils.*
import com.infomaniak.drive.utils.Utils.openWith
Expand Down Expand Up @@ -74,6 +84,13 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
override val currentContext by lazy { requireContext() }
override lateinit var currentFile: File

private val pipParams: PictureInPictureParams? by lazy { getPictureInPictureParams() }

private val mainExecutor by lazy { ContextCompat.getMainExecutor(requireContext()) }

private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private var mediaController: MediaController? = null

// This is not protected, otherwise it won't build because PublicSharePreviewSliderFragment needs it public for the interface
// it implements
val drivePermissions: DrivePermissions = DrivePermissions()
Expand Down Expand Up @@ -120,22 +137,61 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
userDrive = previewSliderViewModel.userDrive,
)

initViewPager()

previewSliderViewModel.pdfIsDownloading.observe(viewLifecycleOwner) { isDownloading ->
if (!currentFile.isOnlyOfficePreview()) header.toggleOpenWithVisibility(isVisible = !isDownloading)
}

mainViewModel.currentPreviewFileList.let { files ->
previewSliderAdapter.setFiles(ArrayList(files.values))
val position = previewSliderAdapter.getPosition(currentFile)
runCatching {
viewPager.setCurrentItem(position, false)
}.onFailure {
Sentry.withScope { scope ->
scope.setExtra("currentFile", "id: ${currentFile.id}")
scope.setExtra("files.values", files.values.joinToString { "id: ${it.id}" })
Sentry.captureException(it)
}
currentFile = files.values.first()
viewPager.setCurrentItem(0, false)
}
}
}

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (isInPictureInPictureMode) {
toggleBottomSheet(false)
}
}

@OptIn(UnstableApi::class)
fun getMediaController(callback: (MediaController) -> Unit) {
if (mediaController == null) {
val context = requireContext()
val playbackSessionToken = SessionToken(context, ComponentName(context, PlaybackService::class.java))
mediaControllerFuture = MediaController.Builder(context, playbackSessionToken).buildAsync().apply {
addListener(
getRunnable(callback),
mainExecutor,
)
}
} else {
callback(mediaController!!)
}
}

private fun initViewPager() = with(binding) {
viewPager.apply {
adapter = previewSliderAdapter
offscreenPageLimit = 1

registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
@RequiresApi(Build.VERSION_CODES.N)
@OptIn(UnstableApi::class)
override fun onPageSelected(position: Int) {
val selectedFragment = childFragmentManager.findFragmentByTag("f${previewSliderAdapter.getItemId(position)}")

// Implementation of onFragmentUnselected to handle resume of media to the same position, only
// for PreviewVideoFragment.
childFragmentManager.fragments.filter {
it is PreviewPlaybackFragment && it != selectedFragment
}.forEach { unselectedFragment ->
(unselectedFragment as? PreviewPlaybackFragment)?.onFragmentUnselected()
}

currentFile = previewSliderAdapter.getFile(position)
previewSliderViewModel.currentPreview = currentFile
Expand All @@ -147,28 +203,18 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
setPrintButtonVisibility(isGone = !currentFile.isPDF())
(bottomSheetView as? FileInfoActionsView)?.openWith?.isGone = isPublicShare
updateBottomSheetWithCurrentFile()
}
})
}

previewSliderViewModel.pdfIsDownloading.observe(viewLifecycleOwner) { isDownloading ->
if (!currentFile.isOnlyOfficePreview()) header.toggleOpenWithVisibility(isVisible = !isDownloading)
}
val selectedFragment = childFragmentManager.findFragmentByTag("f${previewSliderAdapter.getItemId(position)}")

mainViewModel.currentPreviewFileList.let { files ->
previewSliderAdapter.setFiles(ArrayList(files.values))
val position = previewSliderAdapter.getPosition(currentFile)
runCatching {
viewPager.setCurrentItem(position, false)
}.onFailure {
Sentry.withScope { scope ->
scope.setExtra("currentFile", "id: ${currentFile.id}")
scope.setExtra("files.values", files.values.joinToString { "id: ${it.id}" })
Sentry.captureException(it)
// Implementation of onFragmentUnselected to handle resume of media to the same position, only
// for PreviewPlaybackFragment.
childFragmentManager.fragments.filterIsInstance<PreviewPlaybackFragment>().forEach { fragment ->
if (fragment != selectedFragment) {
(fragment as? PreviewPlaybackFragment)?.onFragmentUnselected()
}
}
}
currentFile = files.values.first()
viewPager.setCurrentItem(0, false)
}
})
}
}

Expand All @@ -181,8 +227,13 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
super.onPause()
if (noPreviewList()) return
previewSliderViewModel.currentPreview = currentFile

if (mediaController?.isPlaying == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pipParams?.let { requireActivity().enterPictureInPictureMode(it) }
}
}

@RequiresApi(Build.VERSION_CODES.N)
override fun onStop() {
clearEdgeToEdge()
super.onStop()
Expand All @@ -200,9 +251,27 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
mainViewModel.currentPreviewFileList = LinkedHashMap()
}

// Release Player
mediaController?.apply {
release()
mediaController = null
mediaControllerFuture?.let { MediaController.releaseFuture(it) }
mediaControllerFuture = null
}

super.onDestroy()
}

private fun getPictureInPictureParams(): PictureInPictureParams? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
} else {
null
}
}

protected fun noPreviewList() = mainViewModel.currentPreviewFileList.isEmpty()

protected open fun setBackActionHandlers() {
Expand Down Expand Up @@ -231,6 +300,17 @@ abstract class BasePreviewSliderFragment : Fragment(), FileInfoActionsView.OnIte
}
}

@OptIn(UnstableApi::class)
private fun getRunnable(callback: (MediaController) -> Unit): Runnable {
return Runnable {
if (mediaController == null) {
mediaController = mediaControllerFuture?.get()?.apply {
callback(this)
}
}
}
}

private fun updateBottomSheetWithCurrentFile() = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
lifecycle.withResumed {
when (val fileActionBottomSheet = bottomSheetView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ class PlaybackService : MediaSessionService() {

private val mediaSessionCallback = object : MediaSession.Callback {

// When the user returns from the PreviewVideoFragment, we want to stop
// When the user returns from the PreviewPlaybackFragment, we want to stop
// the service because it does not make sense to have the media notification
// when the user willingly quits the PreviewVideoFragment.
// when the user willingly quits the PreviewPlaybackFragment.
override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
super.onDisconnected(session, controller)
stopSelf()
Expand Down
Loading

0 comments on commit 4e08fb9

Please sign in to comment.