diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 74c1b0761..94d175e04 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,6 +52,10 @@ android:authorities="${applicationId}.firebaseinitprovider" android:exported="false" tools:node="remove" /> + + \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index b074d3165..29181da88 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -17,7 +17,6 @@ import org.openedx.course.presentation.outline.CourseOutlineViewModel import org.openedx.discovery.presentation.search.CourseSearchViewModel import org.openedx.course.presentation.section.CourseSectionViewModel import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel -import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.dashboard.data.repository.DashboardRepository @@ -46,6 +45,8 @@ import org.openedx.profile.presentation.settings.video.VideoSettingsViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module +import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel +import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.profile.presentation.anothers_account.AnothersProfileViewModel val screenModule = module { @@ -86,6 +87,7 @@ val screenModule = module { viewModel { (courseId: String) -> CourseVideoViewModel(courseId, get(), get(), get(), get(), get(), get(), get()) } viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get()) } viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) } + viewModel { (courseId: String, blockId: String) -> EncodedVideoUnitViewModel(courseId, blockId, get(), get(), get(), get(), get()) } viewModel { (courseId:String, handoutsType: String) -> HandoutsViewModel(courseId, handoutsType, get()) } viewModel { CourseSearchViewModel(get(), get(), get()) } viewModel { SelectDialogViewModel(get()) } diff --git a/build.gradle b/build.gradle index b4ce2e34a..81792e08f 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ ext { fragment_version = "1.6.1" constraintlayout_version = "2.1.4" viewpager2_version = "1.0.0" - media3 = "1.1.1" + media3_version = "1.1.1" youtubeplayer_version = "11.1.0" firebase_version = "32.1.0" diff --git a/core/src/main/java/org/openedx/core/domain/model/Block.kt b/core/src/main/java/org/openedx/core/domain/model/Block.kt index 36163e1c1..81b030db0 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Block.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Block.kt @@ -24,7 +24,7 @@ data class Block( val descendants: List, val descendantsType: BlockType, val completion: Double, - val downloadModel: DownloadModel? = null + val downloadModel: DownloadModel? = null, ) { val isDownloadable: Boolean get() { @@ -36,6 +36,7 @@ data class Block( BlockType.VIDEO -> { FileType.VIDEO } + else -> { FileType.UNKNOWN } @@ -55,7 +56,7 @@ data class StudentViewData( val duration: Any, val transcripts: HashMap?, val encodedVideos: EncodedVideos?, - val topicId: String + val topicId: String, ) data class EncodedVideos( @@ -64,7 +65,7 @@ data class EncodedVideos( var fallback: VideoInfo?, var desktopMp4: VideoInfo?, var mobileHigh: VideoInfo?, - var mobileLow: VideoInfo? + var mobileLow: VideoInfo?, ) { val hasDownloadableVideo: Boolean get() = isPreferredVideoInfo(hls) || @@ -73,6 +74,21 @@ data class EncodedVideos( isPreferredVideoInfo(mobileHigh) || isPreferredVideoInfo(mobileLow) + val hasNonYoutubeVideo: Boolean + get() = mobileHigh?.url != null + || mobileLow?.url != null + || desktopMp4?.url != null + || hls?.url != null + || fallback?.url != null + + val videoUrl: String + get() = mobileHigh?.url + ?: mobileLow?.url + ?: desktopMp4?.url + ?: hls?.url + ?: fallback?.url + ?: "" + fun getPreferredVideoInfoForDownloading(preferredVideoQuality: VideoQuality): VideoInfo? { var preferredVideoInfo = when (preferredVideoQuality) { VideoQuality.OPTION_360P -> mobileLow @@ -126,9 +142,9 @@ data class EncodedVideos( data class VideoInfo( val url: String, - val fileSize: Int + val fileSize: Int, ) data class BlockCounts( - val video: Int + val video: Int, ) diff --git a/course/build.gradle b/course/build.gradle index f42807f81..d7b0f2cab 100644 --- a/course/build.gradle +++ b/course/build.gradle @@ -61,8 +61,9 @@ dependencies { implementation project(path: ':core') implementation project(path: ':discussion') implementation "com.pierfrancescosoffritti.androidyoutubeplayer:core:$youtubeplayer_version" - implementation "androidx.media3:media3-exoplayer:$media3" - implementation "androidx.media3:media3-ui:$media3" + implementation "androidx.media3:media3-exoplayer:$media3_version" + implementation "androidx.media3:media3-ui:$media3_version" + implementation "androidx.media3:media3-cast:$media3_version" androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt index b0ac5a952..ee7ea2363 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt @@ -14,8 +14,8 @@ import org.openedx.discussion.presentation.topics.DiscussionTopicsFragment class CourseUnitContainerAdapter( fragment: Fragment, + val blocks: List, private val viewModel: CourseUnitContainerViewModel, - private var blocks: List ) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = blocks.size diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt index 0b4391892..43aaa384d 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -3,8 +3,10 @@ package org.openedx.course.presentation.unit.container import android.content.res.Configuration import android.os.Bundle import android.os.SystemClock +import android.view.LayoutInflater import android.view.View import androidx.compose.foundation.layout.statusBarsPadding +import android.view.ViewGroup import androidx.compose.foundation.layout.width import androidx.compose.material.MaterialTheme import androidx.compose.runtime.getValue @@ -16,8 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.viewpager2.widget.ViewPager2 +import com.google.android.gms.cast.framework.CastButtonFactory import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -25,7 +29,6 @@ import org.openedx.core.BlockType import org.openedx.core.extension.serializable import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.InsetHolder -import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.BackBtn import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -41,7 +44,9 @@ import org.openedx.course.presentation.ui.VideoTitle class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_container) { - private val binding by viewBinding(FragmentCourseUnitContainerBinding::bind) + private val binding: FragmentCourseUnitContainerBinding + get() = _binding!! + private var _binding: FragmentCourseUnitContainerBinding? = null private val viewModel by viewModel { parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) @@ -55,6 +60,19 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta private var lastClickTime = 0L + private val onPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + val blocks = viewModel.getUnitBlocks() + blocks.getOrNull(position)?.let { currentBlock -> + val encodedVideo = currentBlock.studentViewData?.encodedVideos + binding.mediaRouteButton.isVisible = currentBlock.type == BlockType.VIDEO + && encodedVideo?.hasNonYoutubeVideo == true + && !encodedVideo.videoUrl.endsWith(".m3u8") + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) @@ -63,6 +81,15 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta viewModel.setupCurrentIndex(blockId) } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentCourseUnitContainerBinding.inflate(inflater, container, false) + return binding.root + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -77,6 +104,9 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta binding.cvCount.layoutParams = countParams } + binding.mediaRouteButton.setAlwaysVisible(true) + CastButtonFactory.setUpMediaRouteButton(requireContext(), binding.mediaRouteButton) + initViewPager() if (savedInstanceState == null) { val currentBlockIndex = viewModel.getUnitBlocks().indexOfFirst { @@ -166,7 +196,12 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta } } } + } + override fun onDestroyView() { + binding.viewPager.unregisterOnPageChangeCallback(onPageChangeCallback) + super.onDestroyView() + _binding = null } private fun updateNavigationButtons(updatedData: (String, Boolean, Boolean) -> Unit) { @@ -193,9 +228,10 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta private fun initViewPager() { binding.viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL binding.viewPager.offscreenPageLimit = 1 - adapter = CourseUnitContainerAdapter(this, viewModel, viewModel.getUnitBlocks()) + adapter = CourseUnitContainerAdapter(this, viewModel.getUnitBlocks(), viewModel) binding.viewPager.adapter = adapter binding.viewPager.isUserInputEnabled = false + binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback) } private fun handlePrevClick(buttonChanged: (String, Boolean, Boolean) -> Unit) { diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt new file mode 100644 index 000000000..dbb6b385e --- /dev/null +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt @@ -0,0 +1,99 @@ +package org.openedx.course.presentation.unit.video + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.media3.cast.CastPlayer +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import com.google.android.gms.cast.framework.CastContext +import org.openedx.core.module.TranscriptManager +import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.course.data.repository.CourseRepository +import java.util.concurrent.Executors + +class EncodedVideoUnitViewModel( + courseId: String, + val blockId: String, + courseRepository: CourseRepository, + notifier: CourseNotifier, + networkConnection: NetworkConnection, + transcriptManager: TranscriptManager, + private val context: Context, +) : VideoUnitViewModel( + courseId, + courseRepository, + notifier, + networkConnection, + transcriptManager +) { + + var exoPlayer: ExoPlayer? = null + private set + var castPlayer: CastPlayer? = null + private set + private var castContext: CastContext? = null + + var isCastActive = false + + var isPlayerSetUp = false + + private val exoPlayerListener = object : Player.Listener { + override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { + super.onPlayWhenReadyChanged(playWhenReady, reason) + isPlaying = playWhenReady + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == Player.STATE_ENDED) { + markBlockCompleted(blockId) + } + } + } + + @androidx.media3.common.util.UnstableApi + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + + if (exoPlayer != null) { + return + } + + exoPlayer = ExoPlayer.Builder(context) + .build() + + val executor = Executors.newSingleThreadExecutor() + castContext = CastContext.getSharedInstance(context, executor).result + castContext?.let { + castPlayer = CastPlayer(it) + } + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + exoPlayer?.addListener(exoPlayerListener) + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + exoPlayer?.removeListener(exoPlayerListener) + exoPlayer?.pause() + } + + fun getActivePlayer(): Player? { + return if (isCastActive) { + castPlayer + } else { + exoPlayer + } + } + + @androidx.media3.common.util.UnstableApi + fun releasePlayers() { + exoPlayer?.release() + castPlayer?.release() + exoPlayer = null + castPlayer = null + } +} \ No newline at end of file diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt index 65f94eba0..2d82c3f5b 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt @@ -17,10 +17,15 @@ import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.media3.cast.CastPlayer +import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.window.layout.WindowMetricsCalculator +import com.google.android.gms.cast.framework.CastContext import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -40,50 +45,38 @@ import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.ConnectionErrorView import org.openedx.course.presentation.ui.VideoSubtitles import org.openedx.course.presentation.ui.VideoTitle +import java.util.concurrent.Executors import kotlin.math.roundToInt class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { private val binding by viewBinding(FragmentVideoUnitBinding::bind) - private val viewModel by viewModel { - parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) + private val viewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_COURSE_ID, ""), + requireArguments().getString(ARG_BLOCK_ID, ""), + ) } private val router by inject() - private var exoPlayer: ExoPlayer? = null private var windowSize: WindowSize? = null - private var blockId = "" - private val handler = Handler(Looper.getMainLooper()) private var videoTimeRunnable: Runnable = object : Runnable { override fun run() { - exoPlayer?.let { + viewModel.getActivePlayer()?.let { if (it.isPlaying) { viewModel.setCurrentVideoTime(it.currentPosition) } val completePercentage = it.currentPosition.toDouble() / it.duration.toDouble() if (completePercentage >= 0.8f) { - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(viewModel.blockId) } } handler.postDelayed(this, 200) } } - private val exoPlayerListener = object : Player.Listener { - override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { - super.onPlayWhenReadyChanged(playWhenReady, reason) - viewModel.isPlaying = playWhenReady - } - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - if (playbackState == Player.STATE_ENDED) { - viewModel.markBlockCompleted(blockId) - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) windowSize = computeWindowSizeClasses() @@ -91,12 +84,10 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { handler.post(videoTimeRunnable) requireArguments().apply { viewModel.videoUrl = getString(ARG_VIDEO_URL, "") - viewModel.transcripts = - stringToObject>( - getString(ARG_TRANSCRIPT_URL, "") - ) ?: emptyMap() + viewModel.transcripts = stringToObject>( + getString(ARG_TRANSCRIPT_URL, "") + ) ?: emptyMap() viewModel.isDownloaded = getBoolean(ARG_DOWNLOADED) - blockId = getString(ARG_BLOCK_ID, "") } viewModel.downloadSubtitles() } @@ -135,13 +126,13 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { showSubtitleLanguage = viewModel.transcripts.size > 1, currentIndex = currentIndex, onTranscriptClick = { - exoPlayer?.apply { + viewModel.getActivePlayer()?.apply { seekTo(it.start.mseconds.toLong()) play() } }, onSettingsClick = { - exoPlayer?.pause() + viewModel.getActivePlayer()?.pause() val dialog = SelectBottomDialogFragment.newInstance( LocaleUtils.getLanguages(viewModel.transcripts.keys.toList()) ) @@ -154,10 +145,12 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } - binding.connectionError.isVisible = !viewModel.hasInternetConnection && !viewModel.isDownloaded + binding.connectionError.isVisible = + !viewModel.hasInternetConnection && !viewModel.isDownloaded val orientation = resources.configuration.orientation - val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()) + val windowMetrics = + WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()) val currentBounds = windowMetrics.bounds val layoutParams = binding.playerView.layoutParams as FrameLayout.LayoutParams if (orientation == Configuration.ORIENTATION_PORTRAIT || windowSize?.isTablet == true) { @@ -182,29 +175,67 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - private fun initPlayer() { + @androidx.annotation.OptIn(UnstableApi::class) + private fun initPlayer() { with(binding) { - if (exoPlayer == null) { - exoPlayer = ExoPlayer.Builder(requireContext()) - .build() - } - playerView.player = exoPlayer + playerView.player = viewModel.getActivePlayer() playerView.setShowNextButton(false) playerView.setShowPreviousButton(false) - playerView.controllerAutoShow = true - playerView.controllerShowTimeoutMs = 2000 - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - exoPlayer?.setMediaItem(mediaItem, viewModel.getCurrentVideoTime()) - exoPlayer?.prepare() - exoPlayer?.playWhenReady = viewModel.isPlaying + showVideoControllerIndefinitely(false) + + val movieMetadata = MediaMetadata.Builder() + .setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE) + .build() + val mediaItem = MediaItem.Builder().setMediaMetadata(movieMetadata) + .setUri(viewModel.videoUrl) + .setMimeType("video/*") + .build() + + if (!viewModel.isPlayerSetUp) { + viewModel.getActivePlayer()?.setMediaItem( + mediaItem, + viewModel.getCurrentVideoTime() + ) + viewModel.getActivePlayer()?.prepare() + viewModel.getActivePlayer()?.playWhenReady = viewModel.isPlaying + + viewModel.isPlayerSetUp = true + } + + viewModel.castPlayer?.setSessionAvailabilityListener( + object : SessionAvailabilityListener { + override fun onCastSessionAvailable() { + viewModel.isCastActive = true + viewModel.exoPlayer?.pause() + playerView.player = viewModel.castPlayer + viewModel.castPlayer?.setMediaItem( + mediaItem, + viewModel.exoPlayer?.currentPosition ?: 0L + ) + viewModel.castPlayer?.playWhenReady = false + showVideoControllerIndefinitely(true) + } + + override fun onCastSessionUnavailable() { + viewModel.isCastActive = false + playerView.player = viewModel.exoPlayer + viewModel.exoPlayer?.seekTo(viewModel.castPlayer?.currentPosition ?: 0L) + viewModel.castPlayer?.stop() + viewModel.exoPlayer?.play() + showVideoControllerIndefinitely(false) + } + } + ) + + playerView.setFullscreenButtonClickListener { + if (viewModel.isCastActive) + return@setFullscreenButtonClickListener - playerView.setFullscreenButtonClickListener { isFullScreen -> router.navigateToFullScreenVideo( requireActivity().supportFragmentManager, viewModel.videoUrl, - exoPlayer?.currentPosition ?: 0L, - blockId, + viewModel.exoPlayer?.currentPosition ?: 0L, + viewModel.blockId, viewModel.courseId, viewModel.isPlaying ) @@ -212,21 +243,12 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } - override fun onResume() { - super.onResume() - exoPlayer?.addListener(exoPlayerListener) - } - - override fun onPause() { - super.onPause() - exoPlayer?.removeListener(exoPlayerListener) - exoPlayer?.pause() - } - + @UnstableApi override fun onDestroyView() { - exoPlayer?.release() - exoPlayer = null super.onDestroyView() + if (!requireActivity().isChangingConfigurations) { + viewModel.releasePlayers() + } } override fun onDestroy() { @@ -234,6 +256,18 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { super.onDestroy() } + @UnstableApi + private fun showVideoControllerIndefinitely(show: Boolean) { + if (show) { + binding.playerView.controllerAutoShow = false + binding.playerView.controllerShowTimeoutMs = 0 + binding.playerView.showController() + } else { + binding.playerView.controllerAutoShow = true + binding.playerView.controllerShowTimeoutMs = 2000 + } + } + companion object { private const val ARG_BLOCK_ID = "blockId" private const val ARG_VIDEO_URL = "videoUrl" @@ -241,13 +275,14 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" private const val ARG_DOWNLOADED = "isDownloaded" + fun newInstance( blockId: String, courseId: String, videoUrl: String, transcriptsUrl: Map, title: String, - isDownloaded: Boolean + isDownloaded: Boolean, ): VideoUnitFragment { val fragment = VideoUnitFragment() fragment.arguments = bundleOf( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt index b5d949082..1ceb8f8c6 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt @@ -17,7 +17,7 @@ import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository import subtitleFile.TimedTextObject -class VideoUnitViewModel( +open class VideoUnitViewModel( val courseId: String, private val courseRepository: CourseRepository, private val notifier: CourseNotifier, diff --git a/course/src/main/res/layout-land/fragment_course_unit_container.xml b/course/src/main/res/layout-land/fragment_course_unit_container.xml index ec2b65e5b..5ea93aa1d 100644 --- a/course/src/main/res/layout-land/fragment_course_unit_container.xml +++ b/course/src/main/res/layout-land/fragment_course_unit_container.xml @@ -13,24 +13,35 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + - - + + + +