Skip to content

Commit c0e1cfc

Browse files
fix: open course in offline mode (#414)
1 parent 46c32a2 commit c0e1cfc

File tree

2 files changed

+96
-45
lines changed

2 files changed

+96
-45
lines changed

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

Lines changed: 95 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import android.os.Build
66
import androidx.lifecycle.LiveData
77
import androidx.lifecycle.MutableLiveData
88
import androidx.lifecycle.viewModelScope
9-
import kotlinx.coroutines.SupervisorJob
109
import kotlinx.coroutines.async
11-
import kotlinx.coroutines.awaitAll
1210
import kotlinx.coroutines.delay
1311
import kotlinx.coroutines.flow.MutableSharedFlow
1412
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,11 +16,13 @@ import kotlinx.coroutines.flow.asSharedFlow
1816
import kotlinx.coroutines.flow.asStateFlow
1917
import kotlinx.coroutines.flow.update
2018
import kotlinx.coroutines.launch
19+
import kotlinx.coroutines.supervisorScope
2120
import org.openedx.core.config.Config
2221
import org.openedx.core.data.storage.CorePreferences
2322
import org.openedx.core.domain.model.CourseAccessError
2423
import org.openedx.core.domain.model.CourseDatesCalendarSync
2524
import org.openedx.core.domain.model.CourseEnrollmentDetails
25+
import org.openedx.core.domain.model.CourseStructure
2626
import org.openedx.core.exception.NoCachedDataException
2727
import org.openedx.core.extension.isFalse
2828
import org.openedx.core.extension.isTrue
@@ -163,62 +163,113 @@ class CourseContainerViewModel(
163163

164164
fun fetchCourseDetails() {
165165
courseDashboardViewed()
166-
if (_dataReady.value != null) {
167-
return
168-
}
166+
167+
// If data is already loaded, do nothing
168+
if (_dataReady.value != null) return
169169

170170
_showProgress.value = true
171+
171172
viewModelScope.launch {
172173
try {
173-
val deferredCourse = async(SupervisorJob()) {
174-
interactor.getCourseStructure(courseId, isNeedRefresh = true)
175-
}
176-
val deferredEnrollment = async(SupervisorJob()) {
177-
interactor.getEnrollmentDetails(courseId)
178-
}
179-
val (_, enrollment) = awaitAll(deferredCourse, deferredEnrollment)
180-
_courseDetails = enrollment as? CourseEnrollmentDetails
174+
val (courseStructure, courseEnrollmentDetails) = fetchCourseData(courseId)
181175
_showProgress.value = false
182-
_courseDetails?.let { courseDetails ->
183-
courseName = courseDetails.courseInfoOverview.name
184-
loadCourseImage(courseDetails.courseInfoOverview.media?.image?.large)
185-
if (courseDetails.hasAccess.isFalse()) {
186-
_dataReady.value = false
187-
if (courseDetails.isAuditAccessExpired) {
188-
_courseAccessStatus.value =
189-
CourseAccessError.AUDIT_EXPIRED_NOT_UPGRADABLE
190-
} else if (courseDetails.courseInfoOverview.isStarted.not()) {
191-
_courseAccessStatus.value = CourseAccessError.NOT_YET_STARTED
192-
} else {
193-
_courseAccessStatus.value = CourseAccessError.UNKNOWN
194-
}
195-
} else {
196-
_courseAccessStatus.value = CourseAccessError.NONE
197-
_isNavigationEnabled.value = true
198-
_calendarSyncUIState.update { state ->
199-
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
200-
}
201-
if (resumeBlockId.isNotEmpty()) {
202-
delay(500L)
203-
courseNotifier.send(CourseOpenBlock(resumeBlockId))
204-
}
205-
_dataReady.value = true
176+
when {
177+
courseEnrollmentDetails != null -> {
178+
handleCourseEnrollment(courseEnrollmentDetails)
179+
}
180+
181+
courseStructure != null -> {
182+
handleCourseStructureOnly(courseStructure)
183+
}
184+
185+
else -> {
186+
_courseAccessStatus.value = CourseAccessError.UNKNOWN
206187
}
207-
} ?: run {
208-
_courseAccessStatus.value = CourseAccessError.UNKNOWN
209188
}
210189
} catch (e: Exception) {
211190
e.printStackTrace()
212-
if (isNetworkRelatedError(e)) {
213-
_errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection)
214-
} else {
215-
_courseAccessStatus.value = CourseAccessError.UNKNOWN
216-
}
191+
handleFetchError(e)
217192
_showProgress.value = false
218193
}
219194
}
220195
}
221196

197+
private suspend fun fetchCourseData(
198+
courseId: String
199+
): Pair<CourseStructure?, CourseEnrollmentDetails?> = supervisorScope {
200+
val deferredCourse = async {
201+
runCatching {
202+
interactor.getCourseStructure(courseId, isNeedRefresh = true)
203+
}.getOrNull()
204+
}
205+
val deferredEnrollment = async {
206+
runCatching {
207+
interactor.getEnrollmentDetails(courseId)
208+
}.getOrNull()
209+
}
210+
211+
Pair(deferredCourse.await(), deferredEnrollment.await())
212+
}
213+
214+
/**
215+
* Handles the scenario where [CourseEnrollmentDetails] is successfully fetched.
216+
*/
217+
private fun handleCourseEnrollment(courseDetails: CourseEnrollmentDetails) {
218+
_courseDetails = courseDetails
219+
courseName = courseDetails.courseInfoOverview.name
220+
loadCourseImage(courseDetails.courseInfoOverview.media?.image?.large)
221+
222+
if (courseDetails.hasAccess.isFalse()) {
223+
_dataReady.value = false
224+
_courseAccessStatus.value = when {
225+
courseDetails.isAuditAccessExpired -> CourseAccessError.AUDIT_EXPIRED_NOT_UPGRADABLE
226+
courseDetails.courseInfoOverview.isStarted.not() -> CourseAccessError.NOT_YET_STARTED
227+
else -> CourseAccessError.UNKNOWN
228+
}
229+
} else {
230+
_courseAccessStatus.value = CourseAccessError.NONE
231+
_isNavigationEnabled.value = true
232+
_calendarSyncUIState.update { state ->
233+
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
234+
}
235+
if (resumeBlockId.isNotEmpty()) {
236+
// Small delay before sending block open event
237+
viewModelScope.launch {
238+
delay(500L)
239+
courseNotifier.send(CourseOpenBlock(resumeBlockId))
240+
}
241+
}
242+
_dataReady.value = true
243+
}
244+
}
245+
246+
/**
247+
* Handles the scenario where we only have [CourseStructure] but no enrollment details.
248+
*/
249+
private fun handleCourseStructureOnly(courseStructure: CourseStructure) {
250+
loadCourseImage(courseStructure.media?.image?.large)
251+
_courseAccessStatus.value = CourseAccessError.NONE
252+
_isNavigationEnabled.value = true
253+
_calendarSyncUIState.update { state ->
254+
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
255+
}
256+
if (resumeBlockId.isNotEmpty()) {
257+
viewModelScope.launch {
258+
delay(500L)
259+
courseNotifier.send(CourseOpenBlock(resumeBlockId))
260+
}
261+
}
262+
_dataReady.value = true
263+
}
264+
265+
private fun handleFetchError(e: Exception) {
266+
if (isNetworkRelatedError(e)) {
267+
_errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection)
268+
} else {
269+
_courseAccessStatus.value = CourseAccessError.UNKNOWN
270+
}
271+
}
272+
222273
private fun isNetworkRelatedError(e: Exception): Boolean {
223274
return e.isInternetError() || e is NoCachedDataException
224275
}

course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ class CourseContainerViewModelTest {
233233
courseRouter
234234
)
235235
every { networkConnection.isOnline() } returns true
236-
coEvery { interactor.getCourseStructure(any(), any()) } returns courseStructure
236+
coEvery { interactor.getCourseStructure(any(), any()) } throws Exception()
237237
coEvery { interactor.getEnrollmentDetails(any()) } throws Exception()
238238
every {
239239
analytics.logScreenEvent(

0 commit comments

Comments
 (0)