From 0a56e130601d78513a4a37a6cbafa69d903d92b1 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 13 Nov 2024 20:31:53 +0200 Subject: [PATCH] refactor: Address NestedBlockDepth warnings by reducing block nesting --- .../data/networking/HandleErrorInterceptor.kt | 54 +++---- .../openedx/app/deeplink/DeepLinkRouter.kt | 7 +- config/detekt.yml | 3 - .../org/openedx/core/domain/model/Block.kt | 35 ++--- .../module/download/AbstractDownloader.kt | 56 ++++---- .../module/download/BaseDownloadViewModel.kt | 87 ++++++++---- .../core/module/download/DownloadHelper.kt | 22 +-- .../domain/interactor/CourseInteractor.kt | 60 ++++---- .../outline/CourseOutlineViewModel.kt | 31 ++-- .../unit/video/VideoFullScreenFragment.kt | 133 ++++++++++-------- .../videos/CourseVideoViewModel.kt | 37 +++-- 11 files changed, 280 insertions(+), 245 deletions(-) diff --git a/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt index 0588d5695..9e4459554 100644 --- a/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt @@ -14,37 +14,39 @@ class HandleErrorInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) - val responseCode = response.code - if (responseCode in 400..500 && response.body != null) { - val jsonStr = response.body!!.string() - - try { - val errorResponse = gson.fromJson(jsonStr, ErrorResponse::class.java) - if (errorResponse?.error != null) { - when (errorResponse.error) { - ERROR_INVALID_GRANT -> { - throw EdxError.InvalidGrantException() - } - - ERROR_USER_NOT_ACTIVE -> { - throw EdxError.UserNotActiveException() - } - - else -> { - return response - } - } - } else if (errorResponse?.errorDescription != null) { - throw EdxError.ValidationException(errorResponse.errorDescription ?: "") - } - } catch (e: JsonSyntaxException) { - throw IOException("JsonSyntaxException $jsonStr", e) - } + if (isErrorResponse(response)) { + val jsonStr = response.body?.string() ?: return response + return handleErrorResponse(response, jsonStr) } return response } + private fun isErrorResponse(response: Response): Boolean { + return response.code in 400..500 && response.body != null + } + + private fun handleErrorResponse(response: Response, jsonStr: String): Response { + return try { + val errorResponse = gson.fromJson(jsonStr, ErrorResponse::class.java) + handleParsedErrorResponse(errorResponse) ?: response + } catch (e: JsonSyntaxException) { + throw IOException("JsonSyntaxException $jsonStr", e) + } + } + + private fun handleParsedErrorResponse(errorResponse: ErrorResponse?): Response? { + val exception = when { + errorResponse?.error == ERROR_INVALID_GRANT -> EdxError.InvalidGrantException() + errorResponse?.error == ERROR_USER_NOT_ACTIVE -> EdxError.UserNotActiveException() + errorResponse?.errorDescription != null -> + EdxError.ValidationException(errorResponse.errorDescription.orEmpty()) + + else -> return null + } + throw exception + } + companion object { const val ERROR_INVALID_GRANT = "invalid_grant" const val ERROR_USER_NOT_ACTIVE = "user_not_active" diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt index 9244e7519..6061eb6b1 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt @@ -78,9 +78,7 @@ class DeepLinkRouter( navigateToCourseDashboard(fm, deepLink, courseTitle) } - DeepLinkType.UNENROLL, DeepLinkType.REMOVE_BETA_TESTER -> { /* Just navigate to dashboard */ - } - + DeepLinkType.UNENROLL, DeepLinkType.REMOVE_BETA_TESTER -> {} // Just navigate to dashboard DeepLinkType.COURSE_VIDEOS -> navigateToCourseVideos(fm, deepLink) DeepLinkType.COURSE_DATES -> navigateToCourseDates(fm, deepLink) DeepLinkType.COURSE_DISCUSSION -> navigateToCourseDiscussion(fm, deepLink) @@ -94,8 +92,7 @@ class DeepLinkRouter( } DeepLinkType.FORUM_COMMENT -> navigateToDiscussionCommentWithDiscussion(fm, deepLink) - else -> { /* ignore */ - } + else -> {} // ignore } } diff --git a/config/detekt.yml b/config/detekt.yml index 6f848bd80..8ef51403a 100644 --- a/config/detekt.yml +++ b/config/detekt.yml @@ -76,9 +76,6 @@ complexity: ignoreAnnotatedFunctions: [ 'Composable' ] ignoreOverridden: true ignorePrivate: true - NestedBlockDepth: - active: true - threshold: 6 CyclomaticComplexMethod: active: true ignoreAnnotated: [ 'Composable' ] 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 ad3c519cd..670535b7a 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 @@ -158,33 +158,16 @@ data class EncodedVideos( } private fun getDefaultVideoInfoForDownloading(): VideoInfo? { - var result: VideoInfo? = null - - if (isPreferredVideoInfo(mobileLow)) { - result = mobileLow - } else if (isPreferredVideoInfo(mobileHigh)) { - result = mobileHigh - } else if (isPreferredVideoInfo(desktopMp4)) { - result = desktopMp4 - } else { - fallback?.let { - if (isPreferredVideoInfo(it) && - !VideoUtil.videoHasFormat(it.url, AppDataConstants.VIDEO_FORMAT_M3U8) - ) { - result = fallback - } - } - - if (result == null) { - hls?.let { - if (isPreferredVideoInfo(it)) { - result = hls - } - } - } + return when { + isPreferredVideoInfo(mobileLow) -> mobileLow + isPreferredVideoInfo(mobileHigh) -> mobileHigh + isPreferredVideoInfo(desktopMp4) -> desktopMp4 + fallback != null && isPreferredVideoInfo(fallback) && + !VideoUtil.videoHasFormat(fallback!!.url, AppDataConstants.VIDEO_FORMAT_M3U8) -> fallback + + hls != null && isPreferredVideoInfo(hls) -> hls + else -> null } - - return result } private fun isPreferredVideoInfo(videoInfo: VideoInfo?): Boolean { diff --git a/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt b/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt index f9c888c64..d2c6d8c74 100644 --- a/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt +++ b/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt @@ -38,41 +38,43 @@ abstract class AbstractDownloader : KoinComponent { ): DownloadResult { isCanceled = false return try { - val response = downloadApi.downloadFile(url).body() - if (response != null) { - val file = File(path) - if (file.exists()) { - file.delete() + val responseBody = downloadApi.downloadFile(url).body() ?: return DownloadResult.ERROR + initializeFile(path) + responseBody.byteStream().use { inputStream -> + FileOutputStream(File(path)).use { outputStream -> + writeToFile(inputStream, outputStream) } - file.createNewFile() - input = response.byteStream() - currentDownloadingFilePath = path - fos = FileOutputStream(file) - fos.use { output -> - val buffer = ByteArray(BUFFER_SIZE) - var read: Int - while (input!!.read(buffer).also { read = it } != -1) { - output?.write(buffer, 0, read) - } - output?.flush() - } - DownloadResult.SUCCESS - } else { - DownloadResult.ERROR } + DownloadResult.SUCCESS } catch (e: Exception) { e.printStackTrace() - if (isCanceled) { - DownloadResult.CANCELED - } else { - DownloadResult.ERROR - } + if (isCanceled) DownloadResult.CANCELED else DownloadResult.ERROR } finally { - fos?.close() - input?.close() + closeResources() } } + private fun initializeFile(path: String) { + val file = File(path) + if (file.exists()) file.delete() + file.createNewFile() + currentDownloadingFilePath = path + } + + private fun writeToFile(inputStream: InputStream, outputStream: FileOutputStream) { + val buffer = ByteArray(BUFFER_SIZE) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + outputStream.flush() + } + + private fun closeResources() { + fos?.close() + input?.close() + } + suspend fun cancelDownloading() { isCanceled = true withContext(Dispatchers.IO) { diff --git a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt index 10ddc8173..0fcf962a3 100644 --- a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt +++ b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt @@ -60,33 +60,54 @@ abstract class BaseDownloadViewModel( private suspend fun updateDownloadModelsStatus(models: List) { val downloadModelMap = models.associateBy { it.id } - for (item in downloadableChildrenMap) { - var downloadingCount = 0 - var downloadedCount = 0 - item.value.forEach { blockId -> - val downloadModel = downloadModelMap[blockId] - if (downloadModel != null) { - if (downloadModel.downloadedState.isWaitingOrDownloading) { - downloadModelsStatus[blockId] = DownloadedState.DOWNLOADING - downloadingCount++ - } else if (downloadModel.downloadedState.isDownloaded) { - downloadModelsStatus[blockId] = DownloadedState.DOWNLOADED - downloadedCount++ - } - } else { - downloadModelsStatus[blockId] = DownloadedState.NOT_DOWNLOADED + + downloadableChildrenMap.forEach { (parentId, children) -> + val (downloadingCount, downloadedCount) = updateChildrenStatus(children, downloadModelMap) + updateParentStatus(parentId, children.size, downloadingCount, downloadedCount) + } + + downloadingModelsList = models.filter { it.downloadedState.isWaitingOrDownloading } + _downloadingModelsFlow.emit(downloadingModelsList) + } + + private fun updateChildrenStatus( + children: List, + downloadModelMap: Map + ): Pair { + var downloadingCount = 0 + var downloadedCount = 0 + + children.forEach { blockId -> + val downloadModel = downloadModelMap[blockId] + downloadModelsStatus[blockId] = when { + downloadModel?.downloadedState?.isWaitingOrDownloading == true -> { + downloadingCount++ + DownloadedState.DOWNLOADING + } + + downloadModel?.downloadedState?.isDownloaded == true -> { + downloadedCount++ + DownloadedState.DOWNLOADED } - } - downloadModelsStatus[item.key] = when { - downloadingCount > 0 -> DownloadedState.DOWNLOADING - downloadedCount == item.value.size -> DownloadedState.DOWNLOADED else -> DownloadedState.NOT_DOWNLOADED } } - downloadingModelsList = models.filter { it.downloadedState.isWaitingOrDownloading } - _downloadingModelsFlow.emit(downloadingModelsList) + return downloadingCount to downloadedCount + } + + private fun updateParentStatus( + parentId: String, + childrenSize: Int, + downloadingCount: Int, + downloadedCount: Int + ) { + downloadModelsStatus[parentId] = when { + downloadingCount > 0 -> DownloadedState.DOWNLOADING + downloadedCount == childrenSize -> DownloadedState.DOWNLOADED + else -> DownloadedState.NOT_DOWNLOADED + } } protected fun setBlocks(list: List) { @@ -200,23 +221,27 @@ abstract class BaseDownloadViewModel( } } + @Suppress("NestedBlockDepth") protected fun addDownloadableChildrenForSequentialBlock(sequentialBlock: Block) { - for (item in sequentialBlock.descendants) { - allBlocks[item]?.let { blockDescendant -> - if (blockDescendant.type == BlockType.VERTICAL) { - for (unitBlockId in blockDescendant.descendants) { - val block = allBlocks[unitBlockId] - if (block?.isDownloadable == true) { - val id = sequentialBlock.id - val children = downloadableChildrenMap[id] ?: listOf() - downloadableChildrenMap[id] = children + block.id - } + sequentialBlock.descendants.forEach { descendantId -> + val blockDescendant = allBlocks[descendantId] ?: return@forEach + + if (blockDescendant.type == BlockType.VERTICAL) { + blockDescendant.descendants.forEach { unitBlockId -> + val block = allBlocks[unitBlockId] + if (block?.isDownloadable == true) { + addDownloadableChild(sequentialBlock.id, block.id) } } } } } + private fun addDownloadableChild(parentId: String, childId: String) { + val children = downloadableChildrenMap[parentId] ?: listOf() + downloadableChildrenMap[parentId] = children + childId + } + fun logBulkDownloadToggleEvent(toggle: Boolean) { logEvent( CoreAnalyticsEvent.VIDEO_BULK_DOWNLOAD_TOGGLE, diff --git a/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt b/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt index 79e44ab3c..327d4814e 100644 --- a/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt +++ b/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt @@ -93,22 +93,14 @@ class DownloadHelper( } private fun calculateDirectorySize(directory: File): Long { - var size: Long = 0 + if (!directory.exists()) return 0 - if (directory.exists()) { - val files = directory.listFiles() - - if (files != null) { - for (file in files) { - size += if (file.isDirectory) { - calculateDirectorySize(file) - } else { - file.length() - } - } + return directory.listFiles()?.sumOf { file -> + if (file.isDirectory) { + calculateDirectorySize(file) + } else { + file.length() } - } - - return size + } ?: 0 } } diff --git a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt index 88e736ae0..fdbcdd204 100644 --- a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt @@ -32,36 +32,40 @@ class CourseInteractor( val courseStructure = repository.getCourseStructure(courseId, isNeedRefresh) val blocks = courseStructure.blockData val videoBlocks = blocks.filter { it.type == BlockType.VIDEO } - val resultBlocks = ArrayList() + val resultBlocks = mutableListOf() + videoBlocks.forEach { videoBlock -> - val verticalBlock = blocks.firstOrNull { it.descendants.contains(videoBlock.id) } - if (verticalBlock != null) { - val sequentialBlock = - blocks.firstOrNull { it.descendants.contains(verticalBlock.id) } - if (sequentialBlock != null) { - val chapterBlock = - blocks.firstOrNull { it.descendants.contains(sequentialBlock.id) } - if (chapterBlock != null) { - resultBlocks.add(videoBlock) - val verticalIndex = resultBlocks.indexOfFirst { it.id == verticalBlock.id } - if (verticalIndex == -1) { - resultBlocks.add(verticalBlock.copy(descendants = listOf(videoBlock.id))) - } else { - val block = resultBlocks[verticalIndex] - resultBlocks[verticalIndex] = - block.copy(descendants = block.descendants + videoBlock.id) - } - if (!resultBlocks.contains(sequentialBlock)) { - resultBlocks.add(sequentialBlock) - } - if (!resultBlocks.contains(chapterBlock)) { - resultBlocks.add(chapterBlock) - } - } - } - } + val verticalBlock = findParentBlock(videoBlock.id, blocks) ?: return@forEach + val sequentialBlock = findParentBlock(verticalBlock.id, blocks) ?: return@forEach + val chapterBlock = findParentBlock(sequentialBlock.id, blocks) ?: return@forEach + + addToResultBlocks(videoBlock, verticalBlock, resultBlocks) + addIfAbsent(resultBlocks, sequentialBlock) + addIfAbsent(resultBlocks, chapterBlock) + } + + return courseStructure.copy(blockData = resultBlocks) + } + + private fun findParentBlock(childId: String, blocks: List): Block? { + return blocks.firstOrNull { it.descendants.contains(childId) } + } + + private fun addToResultBlocks(videoBlock: Block, verticalBlock: Block, resultBlocks: MutableList) { + resultBlocks.add(videoBlock) + val verticalIndex = resultBlocks.indexOfFirst { it.id == verticalBlock.id } + if (verticalIndex == -1) { + resultBlocks.add(verticalBlock.copy(descendants = listOf(videoBlock.id))) + } else { + val block = resultBlocks[verticalIndex] + resultBlocks[verticalIndex] = block.copy(descendants = block.descendants + videoBlock.id) + } + } + + private fun addIfAbsent(resultBlocks: MutableList, block: Block) { + if (!resultBlocks.contains(block)) { + resultBlocks.add(block) } - return courseStructure.copy(blockData = resultBlocks.toList()) } suspend fun getCourseStatus(courseId: String) = repository.getCourseStatus(courseId) diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt index a37423cf8..e0f9267cc 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineViewModel.kt @@ -250,25 +250,30 @@ class CourseOutlineViewModel( } private fun sortBlocks(blocks: List): List { - val resultBlocks = mutableListOf() if (blocks.isEmpty()) return emptyList() + + val resultBlocks = mutableListOf() blocks.forEach { block -> if (block.type == BlockType.CHAPTER) { resultBlocks.add(block) - block.descendants.forEach { descendant -> - blocks.find { it.id == descendant }?.let { sequentialBlock -> - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(sequentialBlock) - courseSubSectionUnit[sequentialBlock.id] = - sequentialBlock.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[sequentialBlock.id] = - sequentialBlock.getDownloadsCount(blocks) - addDownloadableChildrenForSequentialBlock(sequentialBlock) - } - } + processDescendants(block, blocks) } } - return resultBlocks.toList() + return resultBlocks + } + + private fun processDescendants(block: Block, blocks: List) { + block.descendants.forEach { descendantId -> + val sequentialBlock = blocks.find { it.id == descendantId } ?: return@forEach + addSequentialBlockToSubSections(block, sequentialBlock) + courseSubSectionUnit[sequentialBlock.id] = sequentialBlock.getFirstDescendantBlock(blocks) + subSectionsDownloadsCount[sequentialBlock.id] = sequentialBlock.getDownloadsCount(blocks) + addDownloadableChildrenForSequentialBlock(sequentialBlock) + } + } + + private fun addSequentialBlockToSubSections(block: Block, sequentialBlock: Block) { + courseSubSections.getOrPut(block.id) { mutableListOf() }.add(sequentialBlock) } private fun getResumeBlock( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt index bab1c13f6..9c8340897 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.core.os.bundleOf import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment @@ -12,6 +13,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.Clock +import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory @@ -93,73 +95,84 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) private fun initPlayer() { - with(binding) { - if (exoPlayer == null) { - val videoQuality = viewModel.getVideoQuality() - val params = DefaultTrackSelector.Parameters.Builder(requireContext()) - .apply { - if (videoQuality != VideoQuality.AUTO) { - setMaxVideoSize(videoQuality.width, videoQuality.height) - setViewportSize(videoQuality.width, videoQuality.height, false) - } - } - .build() - - val factory = AdaptiveTrackSelection.Factory() - val selector = DefaultTrackSelector(requireContext(), factory) - selector.parameters = params - - exoPlayer = ExoPlayer.Builder( - requireContext(), - DefaultRenderersFactory(requireContext()), - DefaultMediaSourceFactory(requireContext(), DefaultExtractorsFactory()), - selector, - DefaultLoadControl(), - DefaultBandwidthMeter.getSingletonInstance(requireContext()), - DefaultAnalyticsCollector(Clock.DEFAULT) - ).build() + if (exoPlayer == null) { + exoPlayer = buildExoPlayer() + } + setupPlayerView() + setupMediaItem() + setupPlayerListeners() + } + + @OptIn(UnstableApi::class) + private fun buildExoPlayer(): ExoPlayer { + val videoQuality = viewModel.getVideoQuality() + val trackSelector = DefaultTrackSelector(requireContext(), AdaptiveTrackSelection.Factory()) + trackSelector.parameters = DefaultTrackSelector.Parameters.Builder(requireContext()).apply { + if (videoQuality != VideoQuality.AUTO) { + setMaxVideoSize(videoQuality.width, videoQuality.height) + setViewportSize(videoQuality.width, videoQuality.height, false) } - playerView.player = exoPlayer - playerView.setShowNextButton(false) - playerView.setShowPreviousButton(false) - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - setPlayerMedia(mediaItem) - exoPlayer?.prepare() - exoPlayer?.playWhenReady = viewModel.isPlaying ?: false - - playerView.setFullscreenButtonClickListener { _ -> + }.build() + + return ExoPlayer.Builder( + requireContext(), + DefaultRenderersFactory(requireContext()), + DefaultMediaSourceFactory(requireContext(), DefaultExtractorsFactory()), + trackSelector, + DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(requireContext()), + DefaultAnalyticsCollector(Clock.DEFAULT) + ).build() + } + + @OptIn(UnstableApi::class) + private fun setupPlayerView() { + with(binding.playerView) { + player = exoPlayer + setShowNextButton(false) + setShowPreviousButton(false) + setFullscreenButtonClickListener { requireActivity().supportFragmentManager.popBackStackImmediate() } + } + } - exoPlayer?.addListener(object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - viewModel.logPlayPauseEvent( - viewModel.videoUrl, - isPlaying, - viewModel.currentVideoTime, - CourseAnalyticsKey.NATIVE.key - ) - } + private fun setupMediaItem() { + val mediaItem = MediaItem.fromUri(viewModel.videoUrl) + setPlayerMedia(mediaItem) + exoPlayer?.prepare() + exoPlayer?.playWhenReady = viewModel.isPlaying ?: false + } - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - if (playbackState == Player.STATE_ENDED) { - viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.NATIVE.key) - } - } + private fun setupPlayerListeners() { + exoPlayer?.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + viewModel.logPlayPauseEvent( + viewModel.videoUrl, + isPlaying, + viewModel.currentVideoTime, + CourseAnalyticsKey.NATIVE.key + ) + } - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - super.onPlaybackParametersChanged(playbackParameters) - viewModel.logVideoSpeedEvent( - viewModel.videoUrl, - playbackParameters.speed, - viewModel.currentVideoTime, - CourseAnalyticsKey.NATIVE.key - ) + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == Player.STATE_ENDED) { + viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.NATIVE.key) } - }) - } + } + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + super.onPlaybackParametersChanged(playbackParameters) + viewModel.logVideoSpeedEvent( + viewModel.videoUrl, + playbackParameters.speed, + viewModel.currentVideoTime, + CourseAnalyticsKey.NATIVE.key + ) + } + }) } @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index 672c98d4b..809a399eb 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -223,23 +223,38 @@ class CourseVideoViewModel( } private fun sortBlocks(blocks: List): List { - val resultBlocks = mutableListOf() if (blocks.isEmpty()) return emptyList() + + val resultBlocks = mutableListOf() blocks.forEach { block -> if (block.type == BlockType.CHAPTER) { resultBlocks.add(block) - block.descendants.forEach { descendant -> - blocks.find { it.id == descendant }?.let { - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(it) - courseSubSectionUnit[it.id] = it.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[it.id] = it.getDownloadsCount(blocks) - addDownloadableChildrenForSequentialBlock(it) - } - } + processDescendants(block, blocks) } } - return resultBlocks.toList() + return resultBlocks + } + + private fun processDescendants(chapterBlock: Block, blocks: List) { + chapterBlock.descendants.forEach { descendantId -> + val sequentialBlock = blocks.find { it.id == descendantId } ?: return@forEach + addToSubSections(chapterBlock, sequentialBlock) + updateSubSectionUnit(sequentialBlock, blocks) + updateDownloadsCount(sequentialBlock, blocks) + addDownloadableChildrenForSequentialBlock(sequentialBlock) + } + } + + private fun addToSubSections(chapterBlock: Block, sequentialBlock: Block) { + courseSubSections.getOrPut(chapterBlock.id) { mutableListOf() }.add(sequentialBlock) + } + + private fun updateSubSectionUnit(sequentialBlock: Block, blocks: List) { + courseSubSectionUnit[sequentialBlock.id] = sequentialBlock.getFirstDescendantBlock(blocks) + } + + private fun updateDownloadsCount(sequentialBlock: Block, blocks: List) { + subSectionsDownloadsCount[sequentialBlock.id] = sequentialBlock.getDownloadsCount(blocks) } fun downloadBlocks(blocksIds: List, fragmentManager: FragmentManager) {