Skip to content

Commit 5e1b11f

Browse files
feat: performance improvements
1 parent 14b9923 commit 5e1b11f

File tree

10 files changed

+116
-55
lines changed

10 files changed

+116
-55
lines changed

core/src/main/java/org/openedx/core/utils/PreviewHelper.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import android.media.MediaMetadataRetriever
77
import java.io.File
88
import java.io.FileOutputStream
99
import java.security.MessageDigest
10+
import java.util.concurrent.Executors
11+
import java.util.concurrent.TimeUnit
12+
import java.util.concurrent.TimeoutException
1013

1114
data class VideoPreview(
1215
val link: String? = null,
@@ -25,6 +28,9 @@ data class VideoPreview(
2528

2629
object PreviewHelper {
2730

31+
private const val TIMEOUT_MS = 5000L // 5 seconds
32+
private val executor = Executors.newSingleThreadExecutor()
33+
2834
fun getYouTubeThumbnailUrl(url: String): String {
2935
val videoId = extractYouTubeVideoId(url)
3036
return "https://img.youtube.com/vi/$videoId/0.jpg"
@@ -49,15 +55,34 @@ object PreviewHelper {
4955
try {
5056
BitmapFactory.decodeFile(cacheFile.absolutePath)
5157
} catch (_: Exception) {
52-
extractBitmapFromVideo(videoUrl, context)
58+
// If cache file is corrupted, try to extract from video with timeout
59+
extractBitmapFromVideoWithTimeout(videoUrl, context)
5360
}
5461
} else {
55-
extractBitmapFromVideo(videoUrl, context)
62+
// Extract from video with timeout
63+
extractBitmapFromVideoWithTimeout(videoUrl, context)
5664
}
5765
}
5866
return result
5967
}
6068

69+
private fun extractBitmapFromVideoWithTimeout(videoUrl: String, context: Context): Bitmap? {
70+
return try {
71+
val future = executor.submit<Bitmap?> {
72+
extractBitmapFromVideo(videoUrl, context)
73+
}
74+
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
75+
} catch (e: TimeoutException) {
76+
// Server didn't respond within timeout, return null immediately
77+
e.printStackTrace()
78+
null
79+
} catch (e: Exception) {
80+
// Any other exception, return null immediately
81+
e.printStackTrace()
82+
null
83+
}
84+
}
85+
6186
private fun extractBitmapFromVideo(videoUrl: String, context: Context): Bitmap? {
6287
val retriever = MediaMetadataRetriever()
6388
try {

course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import okhttp3.MultipartBody
66
import org.openedx.core.ApiConstants
77
import org.openedx.core.data.api.CourseApi
88
import org.openedx.core.data.model.BlocksCompletionBody
9+
import org.openedx.core.data.model.room.CourseProgressEntity
910
import org.openedx.core.data.model.room.OfflineXBlockProgress
1011
import org.openedx.core.data.model.room.VideoProgressEntity
1112
import org.openedx.core.data.model.room.XBlockProgressData
@@ -256,15 +257,20 @@ class CourseRepository(
256257
?: VideoProgressEntity(blockId, "", 0L, 0L)
257258
}
258259

259-
fun getCourseProgress(courseId: String, isRefresh: Boolean): Flow<CourseProgress> =
260+
fun getCourseProgress(
261+
courseId: String,
262+
isRefresh: Boolean,
263+
getOnlyCacheIfExist: Boolean
264+
): Flow<CourseProgress> =
260265
channelFlowWithAwait {
266+
var courseProgress: CourseProgressEntity? = null
261267
if (!isRefresh) {
262-
val cached = courseDao.getCourseProgressById(courseId)
263-
if (cached != null) {
264-
trySend(cached.mapToDomain())
268+
courseProgress = courseDao.getCourseProgressById(courseId)
269+
if (courseProgress != null) {
270+
trySend(courseProgress.mapToDomain())
265271
}
266272
}
267-
if (networkConnection.isOnline()) {
273+
if (networkConnection.isOnline() && (!getOnlyCacheIfExist || courseProgress == null)) {
268274
val response = api.getCourseProgress(courseId)
269275
courseDao.insertCourseProgressEntity(response.mapToRoomEntity(courseId))
270276
trySend(response.mapToDomain())

course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ class CourseInteractor(
120120
suspend fun submitOfflineXBlockProgress(blockId: String, courseId: String) =
121121
repository.submitOfflineXBlockProgress(blockId, courseId)
122122

123-
fun getCourseProgress(courseId: String, isRefresh: Boolean) =
124-
repository.getCourseProgress(courseId, isRefresh)
123+
fun getCourseProgress(courseId: String, isRefresh: Boolean, getOnlyCacheIfExist: Boolean) =
124+
repository.getCourseProgress(courseId, isRefresh, getOnlyCacheIfExist)
125125

126126
suspend fun getVideoProgress(blockId: String) = repository.getVideoProgress(blockId)
127127
}

course/src/main/java/org/openedx/course/presentation/assignments/CourseAssignmentViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class CourseAssignmentViewModel(
3838

3939
private fun collectData() {
4040
viewModelScope.launch {
41-
val courseProgressFlow = interactor.getCourseProgress(courseId, false)
41+
val courseProgressFlow = interactor.getCourseProgress(courseId, false, true)
4242
val courseStructureFlow = interactor.getCourseStructureFlow(courseId)
4343

4444
combine(

course/src/main/java/org/openedx/course/presentation/assignments/CourseContentAssignmentScreen.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ private fun CourseContentAssignmentScreen(
148148
) {
149149
val progress = uiState.progress
150150
val description = stringResource(
151-
id = R.string.course_completed,
151+
id = R.string.course_completed_of,
152152
progress.completed,
153153
progress.total
154154
)
@@ -277,11 +277,7 @@ private fun AssignmentGroupSection(
277277
assignment = assignment,
278278
isSelected = assignment.id == selectedId,
279279
onClick = {
280-
selectedId = if (selectedId == assignment.id) {
281-
null
282-
} else {
283-
assignment.id
284-
}
280+
selectedId = assignment.id
285281
}
286282
)
287283
}

course/src/main/java/org/openedx/course/presentation/home/CourseHomeViewModel.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.openedx.course.presentation.home
33
import android.content.Context
44
import androidx.fragment.app.FragmentManager
55
import androidx.lifecycle.viewModelScope
6+
import kotlinx.coroutines.Dispatchers
67
import kotlinx.coroutines.flow.MutableSharedFlow
78
import kotlinx.coroutines.flow.MutableStateFlow
89
import kotlinx.coroutines.flow.SharedFlow
@@ -175,7 +176,7 @@ class CourseHomeViewModel(
175176
.catch { emit(null) }
176177
val courseStatusFlow = interactor.getCourseStatusFlow(courseId)
177178
val courseDatesFlow = interactor.getCourseDatesFlow(courseId)
178-
val courseProgressFlow = interactor.getCourseProgress(courseId, false)
179+
val courseProgressFlow = interactor.getCourseProgress(courseId, false, true)
179180
combine(
180181
courseStructureFlow,
181182
courseStatusFlow,
@@ -229,11 +230,6 @@ class CourseHomeViewModel(
229230
// Get video data
230231
val allVideos = courseVideos.values.flatten()
231232
val firstIncompleteVideo = allVideos.find { !it.isCompleted() }
232-
val videoPreview = firstIncompleteVideo?.getVideoPreview(
233-
context,
234-
networkConnection.isOnline(),
235-
null
236-
)
237233
val videoProgress = if (firstIncompleteVideo != null) {
238234
try {
239235
val videoProgressEntity = interactor.getVideoProgress(firstIncompleteVideo.id)
@@ -260,9 +256,24 @@ class CourseHomeViewModel(
260256
courseProgress = courseProgress,
261257
courseVideos = courseVideos,
262258
courseAssignments = courseAssignments,
263-
videoPreview = videoPreview,
259+
videoPreview = (_uiState.value as? CourseHomeUIState.CourseData)?.videoPreview,
264260
videoProgress = videoProgress
265261
)
262+
getVideoPreview(firstIncompleteVideo)
263+
}
264+
265+
private fun getVideoPreview(videoBlock: Block?) {
266+
viewModelScope.launch(Dispatchers.IO) {
267+
val videoPreview = videoBlock?.getVideoPreview(
268+
context,
269+
networkConnection.isOnline(),
270+
null
271+
)
272+
_uiState.value = (_uiState.value as? CourseHomeUIState.CourseData)
273+
?.copy(
274+
videoPreview = videoPreview
275+
) ?: return@launch
276+
}
266277
}
267278

268279
private suspend fun handleCourseDataError(e: Throwable?) {
@@ -485,7 +496,7 @@ class CourseHomeViewModel(
485496
if (_uiState.value !is CourseHomeUIState.CourseData) {
486497
_uiState.value = CourseHomeUIState.Loading
487498
}
488-
interactor.getCourseProgress(courseId, false)
499+
interactor.getCourseProgress(courseId, false, true)
489500
.catch { e ->
490501
if (_uiState.value !is CourseHomeUIState.CourseData) {
491502
_uiState.value = CourseHomeUIState.Error

course/src/main/java/org/openedx/course/presentation/progress/CourseProgressScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ private fun AssignmentTypeRow(
356356
color: Color
357357
) {
358358
val assignments = progress.getAssignmentSections(policy.type)
359-
val earned = assignments.filter { it.numPointsEarned > 0f }.size //Is it correct?
359+
val earned = assignments.filter { it.numPointsEarned > 0f }.size // Is it correct?
360360
val possible = assignments.size
361361
Column(
362362
modifier = Modifier

course/src/main/java/org/openedx/course/presentation/progress/CourseProgressViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class CourseProgressViewModel(
4747
if (!isRefresh) {
4848
_uiState.value = CourseProgressUIState.Loading
4949
}
50-
interactor.getCourseProgress(courseId, isRefresh)
50+
interactor.getCourseProgress(courseId, isRefresh, getOnlyCacheIfExist = false)
5151
.catch { e ->
5252
if (_uiState.value !is CourseProgressUIState.Data) {
5353
_uiState.value = CourseProgressUIState.Error

course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.StateFlow
1212
import kotlinx.coroutines.flow.asSharedFlow
1313
import kotlinx.coroutines.flow.asStateFlow
1414
import kotlinx.coroutines.launch
15-
import kotlinx.coroutines.withContext
1615
import org.openedx.core.BlockType
1716
import org.openedx.core.config.Config
1817
import org.openedx.core.data.storage.CorePreferences
@@ -148,16 +147,6 @@ class CourseVideoViewModel(
148147
courseSubSectionUnit.clear()
149148
courseStructure = courseStructure.copy(blockData = sortBlocks(blocks))
150149
initDownloadModelsStatus()
151-
val downloadingModels = getDownloadModelList()
152-
val videoPreview = withContext(Dispatchers.IO) {
153-
courseVideos.values.flatten().associate { block ->
154-
block.id to block.getVideoPreview(
155-
context,
156-
networkConnection.isOnline(),
157-
downloadingModels.find { block.id == it.id }?.path
158-
)
159-
}
160-
}
161150
val videoProgress = courseVideos.values.flatten().associate { block ->
162151
val videoProgressEntity = interactor.getVideoProgress(block.id)
163152
val progress = videoProgressEntity.videoTime.toFloat()
@@ -176,18 +165,38 @@ class CourseVideoViewModel(
176165
subSectionsDownloadsCount = subSectionsDownloadsCount,
177166
downloadModelsSize = getDownloadModelsSize(),
178167
isCompletedSectionsShown = isCompletedSectionsShown,
179-
videoPreview = videoPreview,
168+
videoPreview = (_uiState.value as? CourseVideoUIState.CourseData)?.videoPreview
169+
?: emptyMap(),
180170
videoProgress = videoProgress,
181171
)
182172
}
183173
courseNotifier.send(CourseLoading(false))
174+
getVideoPreviews()
184175
} catch (e: Exception) {
185176
e.printStackTrace()
186177
_uiState.value = CourseVideoUIState.Empty
187178
}
188179
}
189180
}
190181

182+
private fun getVideoPreviews() {
183+
viewModelScope.launch(Dispatchers.IO) {
184+
val downloadingModels = getDownloadModelList()
185+
courseVideos.values.flatten().forEach { block ->
186+
val previewMap = block.id to block.getVideoPreview(
187+
context,
188+
networkConnection.isOnline(),
189+
downloadingModels.find { block.id == it.id }?.path
190+
)
191+
val currentUiState =
192+
(_uiState.value as? CourseVideoUIState.CourseData) ?: return@forEach
193+
_uiState.value = currentUiState.copy(
194+
videoPreview = currentUiState.videoPreview + previewMap
195+
)
196+
}
197+
}
198+
}
199+
191200
private fun sortBlocks(blocks: List<Block>): List<Block> {
192201
if (blocks.isEmpty()) return emptyList()
193202

0 commit comments

Comments
 (0)