Skip to content

Commit

Permalink
fix: open course in offline mode
Browse files Browse the repository at this point in the history
  • Loading branch information
PavloNetrebchuk committed Jan 13, 2025
1 parent 46c32a2 commit f790cc3
Showing 1 changed file with 95 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -18,11 +16,13 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import org.openedx.core.config.Config
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.domain.model.CourseAccessError
import org.openedx.core.domain.model.CourseDatesCalendarSync
import org.openedx.core.domain.model.CourseEnrollmentDetails
import org.openedx.core.domain.model.CourseStructure
import org.openedx.core.exception.NoCachedDataException
import org.openedx.core.extension.isFalse
import org.openedx.core.extension.isTrue
Expand Down Expand Up @@ -163,62 +163,113 @@ class CourseContainerViewModel(

fun fetchCourseDetails() {
courseDashboardViewed()
if (_dataReady.value != null) {
return
}

// If data is already loaded, do nothing
if (_dataReady.value != null) return

_showProgress.value = true

viewModelScope.launch {
try {
val deferredCourse = async(SupervisorJob()) {
interactor.getCourseStructure(courseId, isNeedRefresh = true)
}
val deferredEnrollment = async(SupervisorJob()) {
interactor.getEnrollmentDetails(courseId)
}
val (_, enrollment) = awaitAll(deferredCourse, deferredEnrollment)
_courseDetails = enrollment as? CourseEnrollmentDetails
val (courseStructure, courseEnrollmentDetails) = fetchCourseData(courseId)
_showProgress.value = false
_courseDetails?.let { courseDetails ->
courseName = courseDetails.courseInfoOverview.name
loadCourseImage(courseDetails.courseInfoOverview.media?.image?.large)
if (courseDetails.hasAccess.isFalse()) {
_dataReady.value = false
if (courseDetails.isAuditAccessExpired) {
_courseAccessStatus.value =
CourseAccessError.AUDIT_EXPIRED_NOT_UPGRADABLE
} else if (courseDetails.courseInfoOverview.isStarted.not()) {
_courseAccessStatus.value = CourseAccessError.NOT_YET_STARTED
} else {
_courseAccessStatus.value = CourseAccessError.UNKNOWN
}
} else {
_courseAccessStatus.value = CourseAccessError.NONE
_isNavigationEnabled.value = true
_calendarSyncUIState.update { state ->
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
}
if (resumeBlockId.isNotEmpty()) {
delay(500L)
courseNotifier.send(CourseOpenBlock(resumeBlockId))
}
_dataReady.value = true
when {
courseEnrollmentDetails != null -> {
handleCourseEnrollment(courseEnrollmentDetails)
}

courseStructure != null -> {
handleCourseStructureOnly(courseStructure)
}

else -> {
_courseAccessStatus.value = CourseAccessError.UNKNOWN
}
} ?: run {
_courseAccessStatus.value = CourseAccessError.UNKNOWN
}
} catch (e: Exception) {
e.printStackTrace()
if (isNetworkRelatedError(e)) {
_errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection)
} else {
_courseAccessStatus.value = CourseAccessError.UNKNOWN
}
handleFetchError(e)
_showProgress.value = false
}
}
}

private suspend fun fetchCourseData(
courseId: String
): Pair<CourseStructure?, CourseEnrollmentDetails?> = supervisorScope {
val deferredCourse = async {
runCatching {
interactor.getCourseStructure(courseId, isNeedRefresh = true)
}.getOrNull()
}
val deferredEnrollment = async {
runCatching {
interactor.getEnrollmentDetails(courseId)
}.getOrNull()
}

Pair(deferredCourse.await(), deferredEnrollment.await())
}

/**
* Handles the scenario where [CourseEnrollmentDetails] is successfully fetched.
*/
private fun handleCourseEnrollment(courseDetails: CourseEnrollmentDetails) {
_courseDetails = courseDetails
courseName = courseDetails.courseInfoOverview.name
loadCourseImage(courseDetails.courseInfoOverview.media?.image?.large)

if (courseDetails.hasAccess.isFalse()) {
_dataReady.value = false
_courseAccessStatus.value = when {
courseDetails.isAuditAccessExpired -> CourseAccessError.AUDIT_EXPIRED_NOT_UPGRADABLE
courseDetails.courseInfoOverview.isStarted.not() -> CourseAccessError.NOT_YET_STARTED
else -> CourseAccessError.UNKNOWN
}
} else {
_courseAccessStatus.value = CourseAccessError.NONE
_isNavigationEnabled.value = true
_calendarSyncUIState.update { state ->
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
}
if (resumeBlockId.isNotEmpty()) {
// Small delay before sending block open event
viewModelScope.launch {
delay(500L)
courseNotifier.send(CourseOpenBlock(resumeBlockId))
}
}
_dataReady.value = true
}
}

/**
* Handles the scenario where we only have [CourseStructure] but no enrollment details.
*/
private fun handleCourseStructureOnly(courseStructure: CourseStructure) {
loadCourseImage(courseStructure.media?.image?.large)
_courseAccessStatus.value = CourseAccessError.NONE
_isNavigationEnabled.value = true
_calendarSyncUIState.update { state ->
state.copy(isCalendarSyncEnabled = isCalendarSyncEnabled())
}
if (resumeBlockId.isNotEmpty()) {
viewModelScope.launch {
delay(500L)
courseNotifier.send(CourseOpenBlock(resumeBlockId))
}
}
_dataReady.value = true
}

private fun handleFetchError(e: Exception) {
if (isNetworkRelatedError(e)) {
_errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection)
} else {
_courseAccessStatus.value = CourseAccessError.UNKNOWN
}
}

private fun isNetworkRelatedError(e: Exception): Boolean {
return e.isInternetError() || e is NoCachedDataException
}
Expand Down

0 comments on commit f790cc3

Please sign in to comment.