Skip to content

Commit 9f0ae15

Browse files
feat: changes according PR feedback
1 parent bf564c8 commit 9f0ae15

File tree

20 files changed

+234
-92
lines changed

20 files changed

+234
-92
lines changed

app/src/main/java/org/openedx/app/di/ScreenModule.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,16 @@ val screenModule = module {
338338
get(),
339339
get(),
340340
get(),
341+
get(),
341342
)
342343
}
343344
viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) }
344345
viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get(), get()) }
345-
viewModel { (courseId: String, videoUrl: String) ->
346+
viewModel { (courseId: String, videoUrl: String, blockId: String) ->
346347
VideoUnitViewModel(
347348
courseId,
348349
videoUrl,
350+
blockId,
349351
get(),
350352
get(),
351353
get(),
@@ -539,14 +541,12 @@ val screenModule = module {
539541
router = get()
540542
)
541543
}
542-
viewModel { (courseId: String, courseName: String) ->
544+
viewModel { (courseId: String) ->
543545
CourseAssignmentViewModel(
544546
courseId = courseId,
545-
courseName = courseName,
546547
interactor = get(),
547548
courseRouter = get(),
548549
courseNotifier = get(),
549-
analytics = get()
550550
)
551551
}
552552
}

core/src/main/java/org/openedx/core/data/model/room/VideoProgressEntity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import androidx.room.PrimaryKey
77
@Entity(tableName = "video_progress_table")
88
data class VideoProgressEntity(
99
@PrimaryKey
10+
@ColumnInfo("block_id")
11+
val blockId: String,
1012
@ColumnInfo("video_url")
1113
val videoUrl: String,
1214
@ColumnInfo("video_time")

core/src/main/java/org/openedx/core/data/storage/CourseDao.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ interface CourseDao {
4848
@Insert(onConflict = OnConflictStrategy.REPLACE)
4949
suspend fun insertVideoProgressEntity(vararg videoProgressEntity: VideoProgressEntity)
5050

51-
@Query("SELECT * FROM video_progress_table WHERE video_url=:videoUrl")
52-
suspend fun getVideoProgressByVideoUrl(videoUrl: String): VideoProgressEntity?
51+
@Query("SELECT * FROM video_progress_table WHERE block_id=:blockId")
52+
suspend fun getVideoProgressByBlockId(blockId: String): VideoProgressEntity?
5353

5454
@Insert(onConflict = OnConflictStrategy.REPLACE)
5555
suspend fun insertCourseProgressEntity(vararg courseProgressEntity: CourseProgressEntity)

core/src/main/java/org/openedx/core/domain/model/Block.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.openedx.core.domain.model
22

3+
import android.content.Context
34
import android.os.Parcelable
45
import android.webkit.URLUtil
56
import kotlinx.parcelize.Parcelize
@@ -9,6 +10,7 @@ import org.openedx.core.BlockType
910
import org.openedx.core.module.db.DownloadModel
1011
import org.openedx.core.module.db.FileType
1112
import org.openedx.core.utils.PreviewHelper
13+
import org.openedx.core.utils.VideoPreview
1214
import org.openedx.core.utils.VideoUtil
1315
import java.util.Date
1416

@@ -82,21 +84,24 @@ data class Block(
8284
}
8385
}
8486

85-
fun getVideoPreview(isOnline: Boolean, offlineUrl: String?): Any? {
87+
fun getVideoPreview(context: Context, isOnline: Boolean, offlineUrl: String?): VideoPreview? {
8688
return if (studentViewData?.encodedVideos?.hasYoutubeUrl == true) {
87-
PreviewHelper.getYouTubeThumbnailUrl(
88-
studentViewData.encodedVideos.youtube?.url ?: ""
89+
val youtubeUrl = studentViewData.encodedVideos.youtube?.url ?: ""
90+
VideoPreview.createYoutubePreview(
91+
PreviewHelper.getYouTubeThumbnailUrl(youtubeUrl)
8992
)
9093
} else if (studentViewData?.encodedVideos?.hasVideoUrl == true) {
9194
val videoUrl = if (studentViewData.encodedVideos.videoUrl.isNotEmpty() && isOnline) {
9295
studentViewData.encodedVideos.videoUrl
9396
} else {
9497
offlineUrl ?: ""
9598
}
96-
PreviewHelper.getVideoFrameBitmap(
99+
val bitmap = PreviewHelper.getVideoFrameBitmap(
100+
context = context,
97101
isOnline = isOnline,
98102
videoUrl = videoUrl
99103
)
104+
bitmap?.let { VideoPreview.createEncodedVideoPreview(it) }
100105
} else {
101106
null
102107
}

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

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
11
package org.openedx.core.utils
22

3+
import android.content.Context
34
import android.graphics.Bitmap
5+
import android.graphics.BitmapFactory
46
import android.media.MediaMetadataRetriever
7+
import java.io.File
8+
import java.io.FileOutputStream
9+
import java.security.MessageDigest
10+
11+
data class VideoPreview(
12+
val link: String? = null,
13+
val bitmap: Bitmap? = null
14+
) {
15+
companion object {
16+
fun createYoutubePreview(link: String): VideoPreview {
17+
return VideoPreview(link = link)
18+
}
19+
20+
fun createEncodedVideoPreview(bitmap: Bitmap): VideoPreview {
21+
return VideoPreview(bitmap = bitmap)
22+
}
23+
}
24+
}
525

626
object PreviewHelper {
727

@@ -20,23 +40,44 @@ object PreviewHelper {
2040
return matchResult?.groups?.get(1)?.value ?: ""
2141
}
2242

23-
fun getVideoFrameBitmap(isOnline: Boolean, videoUrl: String): Bitmap? {
24-
if (!isOnline && !isLocalFile(videoUrl)) {
25-
return null
43+
fun getVideoFrameBitmap(context: Context, isOnline: Boolean, videoUrl: String): Bitmap? {
44+
var result: Bitmap? = null
45+
if (isOnline || isLocalFile(videoUrl)) {
46+
// Check cache first
47+
val cacheFile = getCacheFile(context, videoUrl)
48+
if (cacheFile.exists()) {
49+
try {
50+
result = BitmapFactory.decodeFile(cacheFile.absolutePath)
51+
} catch (e: Exception) {
52+
e.printStackTrace()
53+
}
54+
} else {
55+
result = extractBitmapFromVideo(videoUrl, context)
56+
}
2657
}
58+
return result
59+
}
2760

61+
private fun extractBitmapFromVideo(videoUrl: String, context: Context): Bitmap? {
2862
val retriever = MediaMetadataRetriever()
29-
return try {
63+
try {
3064
if (isLocalFile(videoUrl)) {
3165
retriever.setDataSource(videoUrl)
3266
} else {
3367
retriever.setDataSource(videoUrl, HashMap())
3468
}
35-
retriever.getFrameAtTime(0)
69+
val bitmap = retriever.getFrameAtTime(0)
70+
71+
// Save bitmap to cache if it was successfully retrieved
72+
bitmap?.let {
73+
saveBitmapToCache(context, videoUrl, it)
74+
}
75+
76+
return bitmap
3677
} catch (e: Exception) {
3778
// Log the exception for debugging but don't crash
3879
e.printStackTrace()
39-
null
80+
return null
4081
} finally {
4182
try {
4283
retriever.release()
@@ -50,4 +91,57 @@ object PreviewHelper {
5091
private fun isLocalFile(url: String): Boolean {
5192
return url.startsWith("/") || url.startsWith("file://")
5293
}
94+
95+
private fun getCacheFile(context: Context, videoUrl: String): File {
96+
val cacheDir = context.cacheDir
97+
val fileName = generateFileName(videoUrl)
98+
return File(cacheDir, "video_thumbnails/$fileName")
99+
}
100+
101+
private fun generateFileName(videoUrl: String): String {
102+
val md = MessageDigest.getInstance("MD5")
103+
val digest = md.digest(videoUrl.toByteArray())
104+
return digest.joinToString("") { "%02x".format(it) } + ".jpg"
105+
}
106+
107+
private fun saveBitmapToCache(context: Context, videoUrl: String, bitmap: Bitmap) {
108+
try {
109+
val cacheFile = getCacheFile(context, videoUrl)
110+
cacheFile.parentFile?.mkdirs() // Create directories if they don't exist
111+
112+
FileOutputStream(cacheFile).use { out ->
113+
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
114+
}
115+
} catch (e: Exception) {
116+
e.printStackTrace()
117+
}
118+
}
119+
120+
/**
121+
* Clear the bitmap cache to free storage
122+
*/
123+
fun clearCache(context: Context) {
124+
try {
125+
val cacheDir = File(context.cacheDir, "video_thumbnails")
126+
if (cacheDir.exists()) {
127+
cacheDir.deleteRecursively()
128+
}
129+
} catch (e: Exception) {
130+
e.printStackTrace()
131+
}
132+
}
133+
134+
/**
135+
* Remove a specific bitmap from cache
136+
*/
137+
fun removeFromCache(context: Context, videoUrl: String) {
138+
try {
139+
val cacheFile = getCacheFile(context, videoUrl)
140+
if (cacheFile.exists()) {
141+
cacheFile.delete()
142+
}
143+
} catch (e: Exception) {
144+
e.printStackTrace()
145+
}
146+
}
53147
}

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,19 @@ class CourseRepository(
254254
trySend(response.mapToDomain())
255255
}
256256

257-
suspend fun saveVideoProgress(videoUrl: String, videoTime: Long, duration: Long) {
258-
val videoProgressEntity = VideoProgressEntity(videoUrl, videoTime, duration)
257+
suspend fun saveVideoProgress(
258+
blockId: String,
259+
videoUrl: String,
260+
videoTime: Long,
261+
duration: Long
262+
) {
263+
val videoProgressEntity = VideoProgressEntity(blockId, videoUrl, videoTime, duration)
259264
courseDao.insertVideoProgressEntity(videoProgressEntity)
260265
}
261266

262-
suspend fun getVideoProgress(videoUrl: String): VideoProgressEntity {
263-
return courseDao.getVideoProgressByVideoUrl(videoUrl)
264-
?: VideoProgressEntity(videoUrl, 0L, 0L)
267+
suspend fun getVideoProgress(blockId: String): VideoProgressEntity {
268+
return courseDao.getVideoProgressByBlockId(blockId)
269+
?: VideoProgressEntity(blockId, "", 0L, 0L)
265270
}
266271

267272
fun getCourseProgress(courseId: String, isRefresh: Boolean): Flow<CourseProgress> =

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class CourseInteractor(
123123
fun getCourseProgress(courseId: String, isRefresh: Boolean) =
124124
repository.getCourseProgress(courseId, isRefresh)
125125

126-
suspend fun getVideoProgress(videoUrl: String) = repository.getVideoProgress(videoUrl)
126+
suspend fun getVideoProgress(blockId: String) = repository.getVideoProgress(blockId)
127127

128128
fun getCourseProgress(courseId: String, isRefresh: Boolean) =
129129
repository.getCourseProgress(courseId, isRefresh)

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,13 @@ import org.openedx.core.domain.model.Progress
1414
import org.openedx.core.system.notifier.CourseNotifier
1515
import org.openedx.core.system.notifier.CourseStructureUpdated
1616
import org.openedx.course.domain.interactor.CourseInteractor
17-
import org.openedx.course.presentation.CourseAnalytics
18-
import org.openedx.course.presentation.CourseAnalyticsEvent
19-
import org.openedx.course.presentation.CourseAnalyticsKey
2017
import org.openedx.course.presentation.CourseRouter
21-
import org.openedx.course.presentation.outline.CourseContentAllUIState
2218

2319
class CourseAssignmentViewModel(
2420
val courseId: String,
2521
val courseRouter: CourseRouter,
26-
private val courseName: String,
2722
private val interactor: CourseInteractor,
2823
private val courseNotifier: CourseNotifier,
29-
private val analytics: CourseAnalytics,
3024
) : ViewModel() {
3125
private val _uiState =
3226
MutableStateFlow<CourseAssignmentUIState>(CourseAssignmentUIState.Loading)
@@ -96,22 +90,4 @@ class CourseAssignmentViewModel(
9690
}
9791
}
9892
}
99-
100-
fun navigateToSequentialEvent(blockId: String) {
101-
val currentState = uiState.value
102-
if (currentState is CourseContentAllUIState.CourseData) {
103-
analytics.logEvent(
104-
CourseAnalyticsEvent.ASSIGNMENT_CLICKED.eventName,
105-
buildMap {
106-
put(
107-
CourseAnalyticsKey.NAME.key,
108-
CourseAnalyticsEvent.ASSIGNMENT_CLICKED.biValue
109-
)
110-
put(CourseAnalyticsKey.COURSE_ID.key, courseId)
111-
put(CourseAnalyticsKey.COURSE_NAME.key, courseName)
112-
put(CourseAnalyticsKey.BLOCK_ID.key, blockId)
113-
}
114-
)
115-
}
116-
}
11793
}

course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ import org.openedx.core.utils.TimeUtils
8686
import org.openedx.course.DatesShiftedSnackBar
8787
import org.openedx.course.R
8888
import org.openedx.course.databinding.FragmentCourseContainerBinding
89-
import org.openedx.course.presentation.contenttab.ContentScreen
89+
import org.openedx.course.presentation.contenttab.ContentTabScreen
9090
import org.openedx.course.presentation.dates.CourseDatesScreen
9191
import org.openedx.course.presentation.handouts.HandoutsScreen
9292
import org.openedx.course.presentation.handouts.HandoutsType
@@ -269,6 +269,10 @@ fun CourseDashboard(
269269
initialPage = CourseContainerTab.entries.indexOf(requiredTab),
270270
pageCount = { CourseContainerTab.entries.size }
271271
)
272+
val contentTabPagerState = rememberPagerState(
273+
initialPage = 0,
274+
pageCount = { CourseContentTab.entries.size }
275+
)
272276
val accessStatus = viewModel.courseAccessStatus.observeAsState()
273277
val tabState = rememberLazyListState()
274278
val snackState = remember { SnackbarHostState() }
@@ -407,6 +411,7 @@ fun CourseDashboard(
407411
windowSize = windowSize,
408412
viewModel = viewModel,
409413
pagerState = pagerState,
414+
contentTabPagerState = contentTabPagerState,
410415
isResumed = isResumed,
411416
fragmentManager = fragmentManager,
412417
onContentTabSelected = { tab ->
@@ -451,6 +456,7 @@ private fun DashboardPager(
451456
windowSize: WindowSize,
452457
viewModel: CourseContainerViewModel,
453458
pagerState: PagerState,
459+
contentTabPagerState: PagerState,
454460
isResumed: Boolean,
455461
fragmentManager: FragmentManager,
456462
onContentTabSelected: (CourseContentTab) -> Unit,
@@ -533,11 +539,12 @@ private fun DashboardPager(
533539
}
534540

535541
CourseContainerTab.CONTENT -> {
536-
ContentScreen(
542+
ContentTabScreen(
537543
windowSize = windowSize,
538544
fragmentManager = fragmentManager,
539545
courseId = viewModel.courseId,
540546
courseName = viewModel.courseName,
547+
pagerState = contentTabPagerState,
541548
onTabSelected = onContentTabSelected
542549
)
543550
}

0 commit comments

Comments
 (0)