diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 88d06c38d..3ca7aea24 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -93,8 +93,18 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { lifecycle.addObserver(viewModel) viewModel.logAppLaunchEvent() setContentView(binding.root) - val container = binding.rootLayout + setupWindowInsets(savedInstanceState) + setupWindowSettings() + setupInitialFragment(savedInstanceState) + observeLogoutEvent() + observeDownloadFailedDialog() + + calendarSyncScheduler.scheduleDailySync() + } + + private fun setupWindowInsets(savedInstanceState: Bundle?) { + val container = binding.rootLayout container.addView(object : View(this) { override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) @@ -103,20 +113,10 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { }) computeWindowSizeClasses() - if (savedInstanceState != null) { - _insetTop = savedInstanceState.getInt(TOP_INSET, 0) - _insetBottom = savedInstanceState.getInt(BOTTOM_INSET, 0) - _insetCutout = savedInstanceState.getInt(CUTOUT_INSET, 0) - } - - window.apply { - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - - WindowCompat.setDecorFitsSystemWindows(this, false) - - val insetsController = WindowInsetsControllerCompat(this, binding.root) - insetsController.isAppearanceLightStatusBars = !isUsingNightModeResources() - statusBarColor = Color.TRANSPARENT + savedInstanceState?.let { + _insetTop = it.getInt(TOP_INSET, 0) + _insetBottom = it.getInt(BOTTOM_INSET, 0) + _insetCutout = it.getInt(CUTOUT_INSET, 0) } binding.root.setOnApplyWindowInsetsListener { _, insets -> @@ -137,36 +137,48 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { insets } binding.root.requestApplyInsetsWhenAttached() + } + private fun setupWindowSettings() { + window.apply { + addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + WindowCompat.setDecorFitsSystemWindows(this, false) + + val insetsController = WindowInsetsControllerCompat(this, binding.root) + insetsController.isAppearanceLightStatusBars = !isUsingNightModeResources() + statusBarColor = Color.TRANSPARENT + } + } + + private fun setupInitialFragment(savedInstanceState: Bundle?) { if (savedInstanceState == null) { when { corePreferencesManager.user == null -> { - if (viewModel.isLogistrationEnabled) { - addFragment(LogistrationFragment()) + val fragment = if (viewModel.isLogistrationEnabled) { + LogistrationFragment() } else { - addFragment(SignInFragment()) + SignInFragment() } + addFragment(fragment) } - whatsNewManager.shouldShowWhatsNew() -> { - addFragment(WhatsNewFragment.newInstance()) - } - - corePreferencesManager.user != null -> { - addFragment(MainFragment.newInstance()) - } + whatsNewManager.shouldShowWhatsNew() -> addFragment(WhatsNewFragment.newInstance()) + else -> addFragment(MainFragment.newInstance()) } - val extras = intent.extras - if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) { - handlePushNotification(extras) + intent.extras?.takeIf { it.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) }?.let { + handlePushNotification(it) } } + } + private fun observeLogoutEvent() { viewModel.logoutUser.observe(this) { profileRouter.restartApp(supportFragmentManager, viewModel.isLogistrationEnabled) } + } + private fun observeDownloadFailedDialog() { lifecycleScope.launch { viewModel.downloadFailedDialog.collect { downloadDialogManager.showDownloadFailedPopup( @@ -175,8 +187,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { ) } } - - calendarSyncScheduler.scheduleDailySync() } override fun onStart() { 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 9e4459554..b2529e06c 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,12 +14,12 @@ class HandleErrorInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) - if (isErrorResponse(response)) { - val jsonStr = response.body?.string() ?: return response - return handleErrorResponse(response, jsonStr) + return if (isErrorResponse(response)) { + val jsonStr = response.body?.string() + if (jsonStr != null) handleErrorResponse(response, jsonStr) else response + } else { + response } - - return response } private fun isErrorResponse(response: Response): Boolean { diff --git a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt index 57d31f918..a60a3a988 100644 --- a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt +++ b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt @@ -66,6 +66,7 @@ class OauthRefreshTokenAuthenticator( .create(AuthApi::class.java) } + @Suppress("ReturnCount") @Synchronized override fun authenticate(route: Route?, response: Response): Request? { val accessToken = preferencesManager.accessToken @@ -85,16 +86,18 @@ class OauthRefreshTokenAuthenticator( } DISABLED_USER_ERROR_MESSAGE, JWT_DISABLED_USER_ERROR_MESSAGE, JWT_USER_EMAIL_MISMATCH -> { - runBlocking { - appNotifier.send(LogoutEvent(true)) - } - null + handleDisabledUser() } else -> null } } + private fun handleDisabledUser(): Request? { + runBlocking { appNotifier.send(LogoutEvent(true)) } + return null + } + // Helper function for handling token expiration logic private fun handleTokenExpired(response: Response, refreshToken: String, accessToken: String): Request? { return try { diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index 760378a5a..21e12029e 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -137,70 +137,89 @@ class SignUpViewModel( fun register() { logEvent(AuthAnalyticsEvent.CREATE_ACCOUNT_CLICKED) - val mapFields = uiState.value.allFields.associate { it.name to it.placeholder } + - mapOf(ApiConstants.RegistrationFields.HONOR_CODE to true.toString()) - val resultMap = mapFields.toMutableMap() - uiState.value.allFields.filter { !it.required }.forEach { (k, _) -> - if (mapFields[k].isNullOrEmpty()) { - resultMap.remove(k) - } - } + val mapFields = prepareMapFields() _uiState.update { it.copy(isButtonLoading = true, validationError = false) } + viewModelScope.launch { try { setErrorInstructions(emptyMap()) val validationFields = interactor.validateRegistrationFields(mapFields) setErrorInstructions(validationFields.validationResult) + if (validationFields.hasValidationError()) { _uiState.update { it.copy(validationError = true, isButtonLoading = false) } } else { - val socialAuth = uiState.value.socialAuth - if (socialAuth?.accessToken != null) { - resultMap[ApiConstants.ACCESS_TOKEN] = socialAuth.accessToken - resultMap[ApiConstants.PROVIDER] = socialAuth.authType.postfix - resultMap[ApiConstants.CLIENT_ID] = config.getOAuthClientId() - } - interactor.register(resultMap.toMap()) - logEvent( - event = AuthAnalyticsEvent.REGISTER_SUCCESS, - params = buildMap { - put( - AuthAnalyticsKey.METHOD.key, - (socialAuth?.authType?.methodName ?: AuthType.PASSWORD.methodName).lowercase() - ) - } - ) - if (socialAuth == null) { - interactor.login( - resultMap.getValue(ApiConstants.EMAIL), - resultMap.getValue(ApiConstants.PASSWORD) - ) - setUserId() - _uiState.update { it.copy(successLogin = true, isButtonLoading = false) } - appNotifier.send(SignInEvent()) - } else { - exchangeToken(socialAuth) - } + handleRegistration(mapFields) } } catch (e: Exception) { - _uiState.update { it.copy(isButtonLoading = false) } - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(coreR.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(coreR.string.core_error_unknown_error) - ) - ) + handleRegistrationError(e) + } + } + } + + private fun prepareMapFields(): MutableMap { + val mapFields = uiState.value.allFields.associate { it.name to it.placeholder } + + mapOf(ApiConstants.RegistrationFields.HONOR_CODE to true.toString()) + + return mapFields.toMutableMap().apply { + uiState.value.allFields.filter { !it.required }.forEach { (key, _) -> + if (mapFields[key].isNullOrEmpty()) { + remove(key) } } } } + private suspend fun handleRegistration(mapFields: MutableMap) { + val resultMap = mapFields.toMutableMap() + uiState.value.socialAuth?.let { socialAuth -> + resultMap[ApiConstants.ACCESS_TOKEN] = socialAuth.accessToken + resultMap[ApiConstants.PROVIDER] = socialAuth.authType.postfix + resultMap[ApiConstants.CLIENT_ID] = config.getOAuthClientId() + } + + interactor.register(resultMap) + logRegisterSuccess() + + if (uiState.value.socialAuth == null) { + loginWithCredentials(resultMap) + } else { + exchangeToken(uiState.value.socialAuth!!) + } + } + + private fun logRegisterSuccess() { + logEvent( + AuthAnalyticsEvent.REGISTER_SUCCESS, + buildMap { + put( + AuthAnalyticsKey.METHOD.key, + (uiState.value.socialAuth?.authType?.methodName ?: AuthType.PASSWORD.methodName).lowercase() + ) + } + ) + } + + private suspend fun loginWithCredentials(resultMap: Map) { + interactor.login( + resultMap.getValue(ApiConstants.EMAIL), + resultMap.getValue(ApiConstants.PASSWORD) + ) + setUserId() + _uiState.update { it.copy(successLogin = true, isButtonLoading = false) } + appNotifier.send(SignInEvent()) + } + + private suspend fun handleRegistrationError(e: Exception) { + _uiState.update { it.copy(isButtonLoading = false) } + val errorMessage = if (e.isInternetError()) { + coreR.string.core_error_no_connection + } else { + coreR.string.core_error_unknown_error + } + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) + } + fun socialAuth(fragment: Fragment, authType: AuthType) { _uiState.update { it.copy(isLoading = true) } viewModelScope.launch { diff --git a/config/detekt.yml b/config/detekt.yml index 8ef51403a..11c63e2fc 100644 --- a/config/detekt.yml +++ b/config/detekt.yml @@ -43,13 +43,8 @@ style: active: true ignoreAnnotated: - 'Preview' - MaxLineLength: - active: false ForbiddenComment: active: false - ReturnCount: - active: true - max: 3 SerialVersionUIDInSerializableClass: active: false @@ -57,7 +52,6 @@ complexity: active: true LongMethod: active: true - threshold: 80 ignoreAnnotated: [ 'Composable' ] ignoreFunction: [ 'onCreateView' ] LargeClass: @@ -71,7 +65,6 @@ complexity: TooManyFunctions: active: true thresholdInClasses: 30 - thresholdInFiles: 30 thresholdInInterfaces: 30 ignoreAnnotatedFunctions: [ 'Composable' ] ignoreOverridden: true 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 670535b7a..ba7b91a41 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 @@ -63,13 +63,11 @@ data class Block( fun isCompleted() = completion == 1.0 fun getFirstDescendantBlock(blocks: List): Block? { - if (blocks.isEmpty()) return null - descendants.forEach { descendant -> - blocks.find { it.id == descendant }?.let { descendantBlock -> - return descendantBlock - } + return descendants.firstOrNull { descendant -> + blocks.find { it.id == descendant } != null + }?.let { descendant -> + blocks.find { it.id == descendant } } - return null } fun getDownloadsCount(blocks: List): Int { diff --git a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt index 1833573af..b80500ad1 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -105,19 +105,14 @@ class TranscriptManager( } private fun fetchTranscriptResponse(url: String?): InputStream? { - if (url == null) { - return null - } - val response: InputStream? - try { - if (has(url)) { - response = getInputStream(url) - return response - } + if (url == null) return null + + return try { + if (has(url)) getInputStream(url) else null } catch (e: IOException) { e.printStackTrace() + null } - return null } private fun getTranscriptDir(): File? { diff --git a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt index 601342578..b30746fe3 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt @@ -3,8 +3,6 @@ package org.openedx.core.ui import android.content.res.Configuration import android.graphics.Rect import android.view.ViewTreeObserver -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding @@ -40,9 +38,6 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.launch import org.openedx.core.R import org.openedx.core.presentation.global.InsetHolder @@ -182,21 +177,6 @@ fun isImeVisibleState(): State { return keyboardState } -fun LazyListState.disableScrolling(scope: CoroutineScope) { - scope.launch { - scroll(scrollPriority = MutatePriority.PreventUserInput) { - awaitCancellation() - } - } -} - -fun LazyListState.reEnableScrolling(scope: CoroutineScope) { - scope.launch { - scroll(scrollPriority = MutatePriority.PreventUserInput) {} - } -} - -@OptIn(ExperimentalFoundationApi::class) fun PagerState.calculateCurrentOffsetForPage(page: Int): Float { return (currentPage - page) + currentPageOffsetFraction } 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 e0f9267cc..4b373b05f 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 @@ -184,71 +184,84 @@ class CourseOutlineViewModel( private fun getCourseDataInternal() { viewModelScope.launch { try { - var courseStructure = interactor.getCourseStructure(courseId) + val courseStructure = interactor.getCourseStructure(courseId) val blocks = courseStructure.blockData - - val courseStatus = if (networkConnection.isOnline()) { - interactor.getCourseStatus(courseId) - } else { - CourseComponentStatus("") - } - - val courseDatesResult = if (networkConnection.isOnline()) { - interactor.getCourseDates(courseId) - } else { - CourseDatesResult( - datesSection = linkedMapOf(), - courseBanner = CourseDatesBannerInfo( - missedDeadlines = false, - missedGatedContent = false, - verifiedUpgradeLink = "", - contentTypeGatingEnabled = false, - hasEnded = false - ) - ) - } + val courseStatus = fetchCourseStatus() + val courseDatesResult = fetchCourseDates() val datesBannerInfo = courseDatesResult.courseBanner checkIfCalendarOutOfDate(courseDatesResult.datesSection.values.flatten()) updateOutdatedOfflineXBlocks(courseStructure) - setBlocks(blocks) - courseSubSections.clear() - courseSubSectionUnit.clear() - courseStructure = courseStructure.copy(blockData = sortBlocks(blocks)) - initDownloadModelsStatus() - - val courseSectionsState = - (_uiState.value as? CourseOutlineUIState.CourseData)?.courseSectionsState.orEmpty() - - _uiState.value = CourseOutlineUIState.CourseData( - courseStructure = courseStructure, - downloadedState = getDownloadModelsStatus(), - resumeComponent = getResumeBlock(blocks, courseStatus.lastVisitedBlockId), - resumeUnitTitle = resumeVerticalBlock?.displayName ?: "", - courseSubSections = courseSubSections, - courseSectionsState = courseSectionsState, - subSectionsDownloadsCount = subSectionsDownloadsCount, - datesBannerInfo = datesBannerInfo, - useRelativeDates = preferencesManager.isRelativeDatesEnabled - ) + initializeCourseData(blocks, courseStructure, courseStatus, datesBannerInfo) } catch (e: Exception) { - _uiState.value = CourseOutlineUIState.Error - if (e.isInternetError()) { - _uiMessage.emit( - UIMessage.SnackBarMessage( - resourceManager.getString(R.string.core_error_no_connection) - ) - ) - } else { - _uiMessage.emit( - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - ) - } + handleCourseDataError(e) } } } + private suspend fun fetchCourseStatus(): CourseComponentStatus { + return if (networkConnection.isOnline()) { + interactor.getCourseStatus(courseId) + } else { + CourseComponentStatus("") + } + } + + private suspend fun fetchCourseDates(): CourseDatesResult { + return if (networkConnection.isOnline()) { + interactor.getCourseDates(courseId) + } else { + CourseDatesResult( + datesSection = linkedMapOf(), + courseBanner = CourseDatesBannerInfo( + missedDeadlines = false, + missedGatedContent = false, + verifiedUpgradeLink = "", + contentTypeGatingEnabled = false, + hasEnded = false + ) + ) + } + } + + private suspend fun initializeCourseData( + blocks: List, + courseStructure: CourseStructure, + courseStatus: CourseComponentStatus, + datesBannerInfo: CourseDatesBannerInfo + ) { + setBlocks(blocks) + courseSubSections.clear() + courseSubSectionUnit.clear() + val sortedStructure = courseStructure.copy(blockData = sortBlocks(blocks)) + initDownloadModelsStatus() + + val courseSectionsState = + (_uiState.value as? CourseOutlineUIState.CourseData)?.courseSectionsState.orEmpty() + + _uiState.value = CourseOutlineUIState.CourseData( + courseStructure = sortedStructure, + downloadedState = getDownloadModelsStatus(), + resumeComponent = getResumeBlock(blocks, courseStatus.lastVisitedBlockId), + resumeUnitTitle = resumeVerticalBlock?.displayName ?: "", + courseSubSections = courseSubSections, + courseSectionsState = courseSectionsState, + subSectionsDownloadsCount = subSectionsDownloadsCount, + datesBannerInfo = datesBannerInfo, + useRelativeDates = preferencesManager.isRelativeDatesEnabled + ) + } + + private suspend fun handleCourseDataError(e: Exception) { + _uiState.value = CourseOutlineUIState.Error + val errorMessage = when { + e.isInternetError() -> R.string.core_error_no_connection + else -> R.string.core_error_unknown_error + } + _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(errorMessage))) + } + private fun sortBlocks(blocks: List): List { if (blocks.isEmpty()) return emptyList() diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt index dad21511a..1bc9fd50d 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt @@ -88,14 +88,15 @@ open class VideoUnitViewModel( private fun getTranscriptUrl(): String { val defaultTranscripts = transcripts[transcriptLanguage] - if (!defaultTranscripts.isNullOrEmpty()) { - return defaultTranscripts - } - if (transcripts.values.isNotEmpty()) { - transcriptLanguage = transcripts.keys.toList().first() - return transcripts[transcriptLanguage] ?: "" + return when { + !defaultTranscripts.isNullOrEmpty() -> defaultTranscripts + transcripts.values.isNotEmpty() -> { + transcriptLanguage = transcripts.keys.first() + transcripts[transcriptLanguage] ?: "" + } + + else -> "" } - return "" } open fun markBlockCompleted(blockId: String, medium: String) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/WebViewLink.kt b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/WebViewLink.kt index 70bcd0383..7d1e7659f 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/WebViewLink.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/WebViewLink.kt @@ -29,24 +29,22 @@ class WebViewLink( companion object { fun parse(uriStr: String?, uriScheme: String): WebViewLink? { - if (uriStr.isNullOrEmpty()) { - return null - } + if (uriStr.isNullOrEmpty()) return null val sanitizedUriStr = uriStr.replace("+", "%2B") val uri = Uri.parse(sanitizedUriStr) // Validate URI scheme and authority - val isSchemeValid = (uriScheme == uri.scheme) + val isSchemeValid = uriScheme == uri.scheme val uriAuthority = Authority.entries.find { it.key == uri.authority } - if (!isSchemeValid || uriAuthority == null) { - return null + return if (isSchemeValid && uriAuthority != null) { + // Parse the URI params + val params = uri.getQueryParams() + WebViewLink(uriAuthority, params) + } else { + null } - - // Parse the Uri params - val params = uri.getQueryParams() - return WebViewLink(uriAuthority, params) } } } diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt index fa9a44b97..b68379afe 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsFragment.kt @@ -210,7 +210,7 @@ class DiscussionThreadsFragment : Fragment() { } } -@Suppress("MaximumLineLength") +@Suppress("MaximumLineLength", "MaxLineLength") @OptIn(ExperimentalMaterialApi::class) @Composable private fun DiscussionThreadsScreen(