diff --git a/.github/workflows/detekt.yml b/.github/workflows/detekt.yml new file mode 100644 index 000000000..0cee02ffe --- /dev/null +++ b/.github/workflows/detekt.yml @@ -0,0 +1,33 @@ +name: Detekt + +on: + workflow_dispatch: + pull_request: { } + +env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + CI_GRADLE_ARG_PROPERTIES: --stacktrace + +jobs: + linting: + name: Run Detekt + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + + - name: Run Detekt + run: ./gradlew detektAll + + - name: Upload report + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: build/reports/detekt/detekt.sarif diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index aa78f8d04..138692348 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -11,9 +11,17 @@ import org.openedx.foundation.interfaces.Analytics import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.whatsnew.presentation.WhatsNewAnalytics -class AnalyticsManager : AppAnalytics, AppReviewAnalytics, AuthAnalytics, CoreAnalytics, - CourseAnalytics, DashboardAnalytics, DiscoveryAnalytics, DiscussionAnalytics, - ProfileAnalytics, WhatsNewAnalytics { +class AnalyticsManager : + AppAnalytics, + AppReviewAnalytics, + AuthAnalytics, + CoreAnalytics, + CourseAnalytics, + DashboardAnalytics, + DiscoveryAnalytics, + DiscussionAnalytics, + ProfileAnalytics, + WhatsNewAnalytics { private val analytics: MutableList = mutableListOf() @@ -45,17 +53,26 @@ class AnalyticsManager : AppAnalytics, AppReviewAnalytics, AuthAnalytics, CoreAn } } - override fun dashboardCourseClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DASHBOARD_COURSE_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) + override fun dashboardCourseClickedEvent( + courseId: String, + courseName: String + ) { + logEvent( + Event.DASHBOARD_COURSE_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + } + ) } override fun logoutEvent(force: Boolean) { - logEvent(Event.USER_LOGOUT, buildMap { - put(Key.FORCE.keyName, force) - }) + logEvent( + Event.USER_LOGOUT, + buildMap { + put(Key.FORCE.keyName, force) + } + ) } override fun setUserIdForSession(userId: Long) { @@ -67,104 +84,164 @@ class AnalyticsManager : AppAnalytics, AppReviewAnalytics, AuthAnalytics, CoreAn } override fun discoveryCourseSearchEvent(label: String, coursesCount: Int) { - logEvent(Event.DISCOVERY_COURSE_SEARCH, buildMap { - put(Key.LABEL.keyName, label) - put(Key.COURSE_COUNT.keyName, coursesCount) - }) + logEvent( + Event.DISCOVERY_COURSE_SEARCH, + buildMap { + put(Key.LABEL.keyName, label) + put(Key.COURSE_COUNT.keyName, coursesCount) + } + ) } override fun discoveryCourseClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DISCOVERY_COURSE_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) + logEvent( + Event.DISCOVERY_COURSE_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + } + ) } override fun sequentialClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String, + courseId: String, + courseName: String, + blockId: String, + blockName: String, ) { - logEvent(Event.SEQUENTIAL_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) + logEvent( + Event.SEQUENTIAL_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.BLOCK_ID.keyName, blockId) + put(Key.BLOCK_NAME.keyName, blockName) + } + ) } override fun nextBlockClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String, + courseId: String, + courseName: String, + blockId: String, + blockName: String, ) { - logEvent(Event.NEXT_BLOCK_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) + logEvent( + Event.NEXT_BLOCK_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.BLOCK_ID.keyName, blockId) + put(Key.BLOCK_NAME.keyName, blockName) + } + ) } override fun prevBlockClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String, + courseId: String, + courseName: String, + blockId: String, + blockName: String, ) { - logEvent(Event.PREV_BLOCK_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) + logEvent( + Event.PREV_BLOCK_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.BLOCK_ID.keyName, blockId) + put(Key.BLOCK_NAME.keyName, blockName) + } + ) } override fun finishVerticalClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String, + courseId: String, + courseName: String, + blockId: String, + blockName: String, ) { - logEvent(Event.FINISH_VERTICAL_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) + logEvent( + Event.FINISH_VERTICAL_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.BLOCK_ID.keyName, blockId) + put(Key.BLOCK_NAME.keyName, blockName) + } + ) } override fun finishVerticalNextClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String, + courseId: String, + courseName: String, + blockId: String, + blockName: String, ) { - logEvent(Event.FINISH_VERTICAL_NEXT_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) + logEvent( + Event.FINISH_VERTICAL_NEXT_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.BLOCK_ID.keyName, blockId) + put(Key.BLOCK_NAME.keyName, blockName) + } + ) } - override fun finishVerticalBackClickedEvent(courseId: String, courseName: String) { - logEvent(Event.FINISH_VERTICAL_BACK_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) + override fun finishVerticalBackClickedEvent( + courseId: String, + courseName: String + ) { + logEvent( + Event.FINISH_VERTICAL_BACK_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + } + ) } - override fun discussionAllPostsClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DISCUSSION_ALL_POSTS_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) + override fun discussionAllPostsClickedEvent( + courseId: String, + courseName: String + ) { + logEvent( + Event.DISCUSSION_ALL_POSTS_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + } + ) } - override fun discussionFollowingClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DISCUSSION_FOLLOWING_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) + override fun discussionFollowingClickedEvent( + courseId: String, + courseName: String + ) { + logEvent( + Event.DISCUSSION_FOLLOWING_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + } + ) } override fun discussionTopicClickedEvent( - courseId: String, courseName: String, topicId: String, topicName: String, + courseId: String, + courseName: String, + topicId: String, + topicName: String, ) { - logEvent(Event.DISCUSSION_TOPIC_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.TOPIC_ID.keyName, topicId) - put(Key.TOPIC_NAME.keyName, topicName) - }) + logEvent( + Event.DISCUSSION_TOPIC_CLICKED, + buildMap { + put(Key.COURSE_ID.keyName, courseId) + put(Key.COURSE_NAME.keyName, courseName) + put(Key.TOPIC_ID.keyName, topicId) + put(Key.TOPIC_NAME.keyName, topicName) + } + ) } } diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index b736e937c..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() { @@ -220,15 +230,15 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { val widthDp = metrics.bounds.width() / resources.displayMetrics.density val widthWindowSize = when { - widthDp < 600f -> WindowType.Compact - widthDp < 840f -> WindowType.Medium + widthDp < COMPACT_MAX_WIDTH -> WindowType.Compact + widthDp < MEDIUM_MAX_WIDTH -> WindowType.Medium else -> WindowType.Expanded } val heightDp = metrics.bounds.height() / resources.displayMetrics.density val heightWindowSize = when { - heightDp < 480f -> WindowType.Compact - heightDp < 900f -> WindowType.Medium + heightDp < COMPACT_MAX_HEIGHT -> WindowType.Compact + heightDp < MEDIUM_MAX_HEIGHT -> WindowType.Medium else -> WindowType.Expanded } _windowSize = WindowSize(widthWindowSize, heightWindowSize) @@ -254,5 +264,10 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { const val CUTOUT_INSET = "cutoutInset" const val BRANCH_TAG = "Branch" const val BRANCH_FORCE_NEW_SESSION = "branch_force_new_session" + + internal const val COMPACT_MAX_WIDTH = 600 + internal const val MEDIUM_MAX_WIDTH = 840 + internal const val COMPACT_MAX_HEIGHT = 480 + internal const val MEDIUM_MAX_HEIGHT = 900 } } diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 4d4d38182..0130d6b31 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -12,8 +12,8 @@ import org.openedx.auth.presentation.signup.SignUpFragment import org.openedx.core.CalendarRouter import org.openedx.core.FragmentViewType import org.openedx.core.presentation.course.CourseViewMode -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter -import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRouter +import org.openedx.core.presentation.global.appupgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.webview.WebContentFragment import org.openedx.core.presentation.settings.video.VideoQualityFragment import org.openedx.core.presentation.settings.video.VideoQualityType @@ -58,10 +58,18 @@ import org.openedx.profile.presentation.video.VideoSettingsFragment import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment -class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, - ProfileRouter, AppUpgradeRouter, WhatsNewRouter, CalendarRouter { - - //region AuthRouter +class AppRouter : + AuthRouter, + DiscoveryRouter, + DashboardRouter, + CourseRouter, + DiscussionRouter, + ProfileRouter, + AppUpgradeRouter, + WhatsNewRouter, + CalendarRouter { + + // region AuthRouter override fun navigateToMain( fm: FragmentManager, courseId: String?, @@ -129,9 +137,9 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di } } } - //endregion + // endregion - //region DiscoveryRouter + // region DiscoveryRouter override fun navigateToCourseDetail(fm: FragmentManager, courseId: String) { replaceFragmentWithBackStack(fm, CourseDetailsFragment.newInstance(courseId)) } @@ -170,9 +178,9 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di CourseContainerFragment.newInstance(courseId, courseTitle) ) } - //endregion + // endregion - //region DashboardRouter + // region DashboardRouter override fun navigateToCourseOutline( fm: FragmentManager, @@ -205,9 +213,9 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di ) { replaceFragment(fm, NoAccessCourseContainerFragment.newInstance(title)) } - //endregion + // endregion - //region CourseRouter + // region CourseRouter override fun navigateToCourseSubsections( fm: FragmentManager, @@ -310,9 +318,9 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di HandoutsWebViewFragment.newInstance(type.name, courseId) ) } - //endregion + // endregion - //region DiscussionRouter + // region DiscussionRouter override fun navigateToDiscussionThread( fm: FragmentManager, action: String, @@ -372,9 +380,9 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di AnothersProfileFragment.newInstance(username) ) } - //endregion + // endregion - //region ProfileRouter + // region ProfileRouter override fun navigateToEditProfile(fm: FragmentManager, account: Account) { replaceFragmentWithBackStack(fm, EditProfileFragment.newInstance(account)) } @@ -433,7 +441,7 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToCoursesToSync(fm: FragmentManager) { replaceFragmentWithBackStack(fm, CoursesToSyncFragment()) } - //endregion + // endregion fun getVisibleFragment(fm: FragmentManager): Fragment? { return fm.fragments.firstOrNull { it.isVisible } @@ -465,7 +473,7 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di } } - //App upgrade + // App upgrade override fun navigateToUserProfile(fm: FragmentManager) { try { fm.popBackStack() @@ -476,5 +484,5 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di e.printStackTrace() } } - //endregion + // endregion } diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index e191a49c6..e195a7940 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -53,7 +53,6 @@ class AppViewModel( val downloadFailedDialog: SharedFlow get() = _downloadFailedDialog.asSharedFlow() - val isLogistrationEnabled get() = config.isPreLoginExperienceEnabled() private var logoutHandledAt: Long = 0 @@ -119,7 +118,7 @@ class AppViewModel( } private suspend fun handleLogoutEvent(event: LogoutEvent) { - if (System.currentTimeMillis() - logoutHandledAt > 5000) { + if (System.currentTimeMillis() - logoutHandledAt > LOGOUT_EVENT_THRESHOLD) { if (event.isForced) { logoutHandledAt = System.currentTimeMillis() preferencesManager.clearCorePreferences() @@ -138,4 +137,8 @@ class AppViewModel( } } } + + companion object { + private const val LOGOUT_EVENT_THRESHOLD = 5000L + } } diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 4011b3a04..d6a28f926 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -14,7 +14,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.FragmentMainBinding import org.openedx.app.deeplink.HomeTab import org.openedx.core.adapter.NavigationFragmentAdapter -import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment +import org.openedx.core.presentation.global.appupgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.learn.presentation.LearnFragment @@ -107,6 +107,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { } } + @Suppress("MagicNumber") private fun initViewPager() { binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL binding.viewPager.offscreenPageLimit = 4 diff --git a/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt index abf90d7a2..e789ed52b 100644 --- a/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt @@ -28,7 +28,9 @@ class AppUpgradeInterceptor( appNotifier.send(AppUpgradeEvent.UpgradeRecommendedEvent(latestAppVersion)) } - latestAppVersion.isNotEmpty() && BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime < Date().time -> { + latestAppVersion.isNotEmpty() && + BuildConfig.VERSION_NAME != latestAppVersion && + lastSupportedDateTime < Date().time -> { appNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) } } @@ -41,4 +43,3 @@ class AppUpgradeInterceptor( const val HEADER_APP_VERSION_LAST_SUPPORTED_DATE = "EDX-APP-VERSION-LAST-SUPPORTED-DATE" } } - 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 bd4aa1920..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 @@ -2,11 +2,11 @@ package org.openedx.app.data.networking import com.google.gson.Gson import com.google.gson.JsonSyntaxException -import org.openedx.core.data.model.ErrorResponse -import org.openedx.core.system.EdxError import okhttp3.Interceptor import okhttp3.Response import okio.IOException +import org.openedx.core.data.model.ErrorResponse +import org.openedx.core.system.EdxError class HandleErrorInterceptor( private val gson: Gson @@ -14,37 +14,41 @@ class HandleErrorInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) - val responseCode = response.code - if (responseCode in 400..500 && response.body != null) { - val jsonStr = response.body!!.string() - - try { - val errorResponse = gson.fromJson(jsonStr, ErrorResponse::class.java) - if (errorResponse?.error != null) { - when (errorResponse.error) { - ERROR_INVALID_GRANT -> { - throw EdxError.InvalidGrantException() - } - ERROR_USER_NOT_ACTIVE -> { - throw EdxError.UserNotActiveException() - } - else -> { - return response - } - } - } else if (errorResponse?.errorDescription != null) { - throw EdxError.ValidationException(errorResponse.errorDescription ?: "") - } - } catch (e: JsonSyntaxException) { - throw IOException("JsonSyntaxException $jsonStr", e) - } + return if (isErrorResponse(response)) { + val jsonStr = response.body?.string() + if (jsonStr != null) handleErrorResponse(response, jsonStr) else response + } else { + response + } + } + + private fun isErrorResponse(response: Response): Boolean { + return response.code in 400..500 && response.body != null + } + + private fun handleErrorResponse(response: Response, jsonStr: String): Response { + return try { + val errorResponse = gson.fromJson(jsonStr, ErrorResponse::class.java) + handleParsedErrorResponse(errorResponse) ?: response + } catch (e: JsonSyntaxException) { + throw IOException("JsonSyntaxException $jsonStr", e) } + } + + private fun handleParsedErrorResponse(errorResponse: ErrorResponse?): Response? { + val exception = when { + errorResponse?.error == ERROR_INVALID_GRANT -> EdxError.InvalidGrantException() + errorResponse?.error == ERROR_USER_NOT_ACTIVE -> EdxError.UserNotActiveException() + errorResponse?.errorDescription != null -> + EdxError.ValidationException(errorResponse.errorDescription.orEmpty()) - return response + else -> return null + } + throw exception } companion object { const val ERROR_INVALID_GRANT = "invalid_grant" const val ERROR_USER_NOT_ACTIVE = "user_not_active" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt index c91b27184..bdc7c6284 100644 --- a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt @@ -36,4 +36,4 @@ class HeadersInterceptor( }.build() ) } -} \ No newline at end of file +} 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 38305c007..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 @@ -3,13 +3,19 @@ package org.openedx.app.data.networking import android.util.Log import com.google.gson.Gson import kotlinx.coroutines.runBlocking -import okhttp3.* +import okhttp3.Authenticator +import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody +import okhttp3.Route import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONException import org.json.JSONObject -import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.auth.data.api.AuthApi import org.openedx.auth.domain.model.AuthResponse import org.openedx.core.ApiConstants @@ -18,6 +24,7 @@ import org.openedx.core.BuildConfig import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.core.utils.TimeUtils import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -45,8 +52,8 @@ class OauthRefreshTokenAuthenticator( init { val okHttpClient = OkHttpClient.Builder().apply { - writeTimeout(60, TimeUnit.SECONDS) - readTimeout(60, TimeUnit.SECONDS) + writeTimeout(timeout = 60, TimeUnit.SECONDS) + readTimeout(timeout = 60, TimeUnit.SECONDS) if (BuildConfig.DEBUG) { addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) } @@ -59,81 +66,83 @@ class OauthRefreshTokenAuthenticator( .create(AuthApi::class.java) } + @Suppress("ReturnCount") @Synchronized override fun authenticate(route: Route?, response: Response): Request? { val accessToken = preferencesManager.accessToken val refreshToken = preferencesManager.refreshToken - if (refreshToken.isEmpty()) { - return null + if (refreshToken.isEmpty()) return null + + val errorCode = getErrorCode(response.peekBody(Long.MAX_VALUE).string()) ?: return null + + return when (errorCode) { + TOKEN_EXPIRED_ERROR_MESSAGE, JWT_TOKEN_EXPIRED -> { + handleTokenExpired(response, refreshToken, accessToken) + } + + TOKEN_NONEXISTENT_ERROR_MESSAGE, TOKEN_INVALID_GRANT_ERROR_MESSAGE, JWT_INVALID_TOKEN -> { + handleInvalidToken(response, accessToken) + } + + DISABLED_USER_ERROR_MESSAGE, JWT_DISABLED_USER_ERROR_MESSAGE, JWT_USER_EMAIL_MISMATCH -> { + handleDisabledUser() + } + + else -> null } + } - val errorCode = getErrorCode(response.peekBody(Long.MAX_VALUE).string()) - if (errorCode != null) { - when (errorCode) { - TOKEN_EXPIRED_ERROR_MESSAGE, - JWT_TOKEN_EXPIRED, - -> { - try { - val newAuth = refreshAccessToken(refreshToken) - if (newAuth != null) { - return response.request.newBuilder() - .header( - HEADER_AUTHORIZATION, - config.getAccessTokenType() + " " + newAuth.accessToken - ) - .build() - } else { - val actualToken = preferencesManager.accessToken - if (actualToken != accessToken) { - return response.request.newBuilder() - .header( - HEADER_AUTHORIZATION, - "${config.getAccessTokenType()} $actualToken" - ) - .build() - } - return null - } - } catch (e: Exception) { - return null - } - } + private fun handleDisabledUser(): Request? { + runBlocking { appNotifier.send(LogoutEvent(true)) } + return null + } - TOKEN_NONEXISTENT_ERROR_MESSAGE, - TOKEN_INVALID_GRANT_ERROR_MESSAGE, - JWT_INVALID_TOKEN, - -> { - // Retry request with the current access_token if the original access_token used in - // request does not match the current access_token. This case can occur when - // asynchronous calls are made and are attempting to refresh the access_token where - // one call succeeds but the other fails. https://github.com/edx/edx-app-android/pull/834 - val authHeaders = response.request.headers[HEADER_AUTHORIZATION] - ?.split(" ".toRegex()) - if (authHeaders?.toTypedArray()?.getOrNull(1) != accessToken) { - return response.request.newBuilder() - .header( - HEADER_AUTHORIZATION, - "${config.getAccessTokenType()} $accessToken" - ).build() - } - - runBlocking { - appNotifier.send(LogoutEvent(true)) - } + // Helper function for handling token expiration logic + private fun handleTokenExpired(response: Response, refreshToken: String, accessToken: String): Request? { + return try { + val newAuth = refreshAccessToken(refreshToken) + if (newAuth != null) { + response.request.newBuilder() + .header( + HEADER_AUTHORIZATION, + "${config.getAccessTokenType()} ${newAuth.accessToken}" + ) + .build() + } else { + val actualToken = preferencesManager.accessToken + if (actualToken != accessToken) { + response.request.newBuilder() + .header( + HEADER_AUTHORIZATION, + "${config.getAccessTokenType()} $actualToken" + ) + .build() + } else { + null } + } + } catch (e: Exception) { + e.printStackTrace() + null + } + } - DISABLED_USER_ERROR_MESSAGE, - JWT_DISABLED_USER_ERROR_MESSAGE, - JWT_USER_EMAIL_MISMATCH, - -> { - runBlocking { - appNotifier.send(LogoutEvent(true)) - } - } + // Helper function for handling invalid token logic + private fun handleInvalidToken(response: Response, accessToken: String): Request? { + val authHeaders = response.request.headers[HEADER_AUTHORIZATION]?.split(" ".toRegex()) + return if (authHeaders?.toTypedArray()?.getOrNull(1) != accessToken) { + response.request.newBuilder() + .header( + HEADER_AUTHORIZATION, + "${config.getAccessTokenType()} $accessToken" + ).build() + } else { + runBlocking { + appNotifier.send(LogoutEvent(true)) } + null } - return null } private fun isTokenExpired(): Boolean { @@ -169,8 +178,8 @@ class OauthRefreshTokenAuthenticator( lastTokenRefreshRequestTime = TimeUtils.getCurrentTime() } } else if (response.code() == 400) { - //another refresh already in progress - Thread.sleep(1500) + // another refresh already in progress + Thread.sleep(REFRESH_TOKEN_THREAD_SLEEP) } } @@ -178,29 +187,22 @@ class OauthRefreshTokenAuthenticator( } private fun getErrorCode(responseBody: String): String? { - try { + return try { val jsonObj = JSONObject(responseBody) + if (jsonObj.has(FIELD_ERROR_CODE)) { - return jsonObj.getString(FIELD_ERROR_CODE) + jsonObj.getString(FIELD_ERROR_CODE) + } else if (TOKEN_TYPE_JWT.equals(config.getAccessTokenType(), ignoreCase = true)) { + val errorType = if (jsonObj.has(FIELD_DETAIL)) FIELD_DETAIL else FIELD_DEVELOPER_MESSAGE + jsonObj.getString(errorType) } else { - return if (TOKEN_TYPE_JWT.equals(config.getAccessTokenType(), ignoreCase = true)) { - val errorType = - if (jsonObj.has(FIELD_DETAIL)) FIELD_DETAIL else FIELD_DEVELOPER_MESSAGE - jsonObj.getString(errorType) - } else { - val errorCode = jsonObj - .optJSONObject(FIELD_DEVELOPER_MESSAGE) - ?.optString(FIELD_ERROR_CODE, "") ?: "" - if (errorCode != "") { - errorCode - } else { - null - } - } + jsonObj.optJSONObject(FIELD_DEVELOPER_MESSAGE) + ?.optString(FIELD_ERROR_CODE, "") + ?.takeIf { it.isNotEmpty() } } - } catch (ex: JSONException) { + } catch (_: JSONException) { Log.d("OauthRefreshTokenAuthenticator", "Unable to get error_code from 401 response") - return null + null } } @@ -269,5 +271,7 @@ class OauthRefreshTokenAuthenticator( * unauthorized access token during async requests. */ private const val REFRESH_TOKEN_INTERVAL_MINIMUM = 60 * 1000 + + private const val REFRESH_TOKEN_THREAD_SLEEP = 1500L } } diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt index ab18b7e23..3c8ea881e 100644 --- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt +++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt @@ -17,8 +17,13 @@ import org.openedx.profile.data.model.Account import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.whatsnew.data.storage.WhatsNewPreferences -class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences, - WhatsNewPreferences, InAppReviewPreferences, CoursePreferences, CalendarPreferences { +class PreferencesManager(context: Context) : + CorePreferences, + ProfilePreferences, + WhatsNewPreferences, + InAppReviewPreferences, + CoursePreferences, + CalendarPreferences { private val sharedPreferences = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt index a55d45ff6..6061eb6b1 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt @@ -38,196 +38,98 @@ class DeepLinkRouter( fun makeRoute(fm: FragmentManager, deepLink: DeepLink) { when (deepLink.type) { - // Discovery - DeepLinkType.DISCOVERY -> { - navigateToDiscoveryScreen(fm = fm) - return - } - - DeepLinkType.DISCOVERY_COURSE_DETAIL -> { - navigateToCourseDetail( - fm = fm, - deepLink = deepLink - ) - return - } - - DeepLinkType.DISCOVERY_PROGRAM_DETAIL -> { - navigateToProgramDetail( - fm = fm, - deepLink = deepLink - ) - return - } - - else -> { - //ignore - } + DeepLinkType.DISCOVERY -> navigateToDiscoveryScreen(fm) + DeepLinkType.DISCOVERY_COURSE_DETAIL -> navigateToCourseDetail(fm, deepLink) + DeepLinkType.DISCOVERY_PROGRAM_DETAIL -> navigateToProgramDetail(fm, deepLink) + else -> handleLoggedOutOrUserNavigation(fm, deepLink) } + } + private fun handleLoggedOutOrUserNavigation(fm: FragmentManager, deepLink: DeepLink) { if (!isUserLoggedIn) { - navigateToSignIn(fm = fm) - return + navigateToSignIn(fm) + } else { + handleProgramAndProfileNavigation(fm, deepLink) } + } + private fun handleProgramAndProfileNavigation(fm: FragmentManager, deepLink: DeepLink) { when (deepLink.type) { - // Program - DeepLinkType.PROGRAM -> { - navigateToProgram( - fm = fm, - deepLink = deepLink - ) - return - } - // Profile - DeepLinkType.PROFILE, - DeepLinkType.USER_PROFILE -> { - navigateToProfile(fm = fm) - return - } - else -> { - //ignore - } + DeepLinkType.PROGRAM -> navigateToProgram(fm, deepLink) + DeepLinkType.PROFILE, DeepLinkType.USER_PROFILE -> navigateToProfile(fm) + else -> handleCourseRelatedNavigation(fm, deepLink) } + } + private fun handleCourseRelatedNavigation(fm: FragmentManager, deepLink: DeepLink) { launch(Dispatchers.Main) { - val courseId = deepLink.courseId ?: return@launch navigateToDashboard(fm = fm) - val course = getCourseDetails(courseId) ?: return@launch navigateToDashboard(fm = fm) - if (!course.isEnrolled) { - navigateToDashboard(fm = fm) - return@launch - } + val courseId = deepLink.courseId ?: return@launch navigateToDashboard(fm) + val course = getCourseDetails(courseId) ?: return@launch navigateToDashboard(fm) + if (!course.isEnrolled) return@launch navigateToDashboard(fm) - when (deepLink.type) { - // Course - DeepLinkType.COURSE_DASHBOARD, DeepLinkType.ENROLL, DeepLinkType.ADD_BETA_TESTER -> { - navigateToDashboard(fm = fm) - navigateToCourseDashboard( - fm = fm, - deepLink = deepLink, - courseTitle = course.name - ) - } - - DeepLinkType.UNENROLL, DeepLinkType.REMOVE_BETA_TESTER -> { - navigateToDashboard(fm = fm) - } - - DeepLinkType.COURSE_VIDEOS -> { - navigateToDashboard(fm = fm) - navigateToCourseVideos( - fm = fm, - deepLink = deepLink - ) - } + handleSpecificCourseNavigation(fm, deepLink, course.name) + } + } - DeepLinkType.COURSE_DATES -> { - navigateToDashboard(fm = fm) - navigateToCourseDates( - fm = fm, - deepLink = deepLink - ) - } + private fun handleSpecificCourseNavigation(fm: FragmentManager, deepLink: DeepLink, courseTitle: String) { + navigateToDashboard(fm) + when (deepLink.type) { + DeepLinkType.COURSE_DASHBOARD, DeepLinkType.ENROLL, DeepLinkType.ADD_BETA_TESTER -> { + navigateToCourseDashboard(fm, deepLink, courseTitle) + } - DeepLinkType.COURSE_DISCUSSION -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.UNENROLL, DeepLinkType.REMOVE_BETA_TESTER -> {} // Just navigate to dashboard + DeepLinkType.COURSE_VIDEOS -> navigateToCourseVideos(fm, deepLink) + DeepLinkType.COURSE_DATES -> navigateToCourseDates(fm, deepLink) + DeepLinkType.COURSE_DISCUSSION -> navigateToCourseDiscussion(fm, deepLink) + DeepLinkType.COURSE_HANDOUT -> navigateToCourseHandoutWithMore(fm, deepLink) + DeepLinkType.COURSE_ANNOUNCEMENT -> navigateToCourseAnnouncementWithMore(fm, deepLink) + DeepLinkType.COURSE_COMPONENT -> navigateToCourseComponentWithDashboard(fm, deepLink, courseTitle) + DeepLinkType.DISCUSSION_TOPIC -> navigateToDiscussionTopicWithDiscussion(fm, deepLink) + DeepLinkType.DISCUSSION_POST -> navigateToDiscussionPostWithDiscussion(fm, deepLink) + DeepLinkType.DISCUSSION_COMMENT, DeepLinkType.FORUM_RESPONSE -> { + navigateToDiscussionResponseWithDiscussion(fm, deepLink) + } - DeepLinkType.COURSE_HANDOUT -> { - navigateToDashboard(fm = fm) - navigateToCourseMore( - fm = fm, - deepLink = deepLink - ) - navigateToCourseHandout( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.FORUM_COMMENT -> navigateToDiscussionCommentWithDiscussion(fm, deepLink) + else -> {} // ignore + } + } - DeepLinkType.COURSE_ANNOUNCEMENT -> { - navigateToDashboard(fm = fm) - navigateToCourseMore( - fm = fm, - deepLink = deepLink - ) - navigateToCourseAnnouncement( - fm = fm, - deepLink = deepLink - ) - } + // Additional helper methods to encapsulate grouped navigation + private fun navigateToCourseHandoutWithMore(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseMore(fm, deepLink) + navigateToCourseHandout(fm, deepLink) + } - DeepLinkType.COURSE_COMPONENT -> { - navigateToDashboard(fm = fm) - navigateToCourseDashboard( - fm = fm, - deepLink = deepLink, - courseTitle = course.name - ) - navigateToCourseComponent( - fm = fm, - deepLink = deepLink - ) - } + private fun navigateToCourseAnnouncementWithMore(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseMore(fm, deepLink) + navigateToCourseAnnouncement(fm, deepLink) + } - // Discussions - DeepLinkType.DISCUSSION_TOPIC -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionTopic( - fm = fm, - deepLink = deepLink - ) - } + private fun navigateToCourseComponentWithDashboard(fm: FragmentManager, deepLink: DeepLink, courseTitle: String) { + navigateToCourseDashboard(fm, deepLink, courseTitle) + navigateToCourseComponent(fm, deepLink) + } - DeepLinkType.DISCUSSION_POST -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionPost( - fm = fm, - deepLink = deepLink - ) - } + private fun navigateToDiscussionTopicWithDiscussion(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseDiscussion(fm, deepLink) + navigateToDiscussionTopic(fm, deepLink) + } - DeepLinkType.DISCUSSION_COMMENT, DeepLinkType.FORUM_RESPONSE -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionResponse( - fm = fm, - deepLink = deepLink - ) - } + private fun navigateToDiscussionPostWithDiscussion(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseDiscussion(fm, deepLink) + navigateToDiscussionPost(fm, deepLink) + } - DeepLinkType.FORUM_COMMENT -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionComment( - fm = fm, - deepLink = deepLink - ) - } + private fun navigateToDiscussionResponseWithDiscussion(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseDiscussion(fm, deepLink) + navigateToDiscussionResponse(fm, deepLink) + } - else -> { - //ignore - } - } - } + private fun navigateToDiscussionCommentWithDiscussion(fm: FragmentManager, deepLink: DeepLink) { + navigateToCourseDiscussion(fm, deepLink) + navigateToDiscussionComment(fm, deepLink) } // Returns true if there was a successful redirect to the discovery screen diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index 05d68cc49..a0458b0df 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -30,7 +30,6 @@ import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.CalendarRouter import org.openedx.core.R import org.openedx.core.config.Config -import org.openedx.core.data.model.CourseEnrollmentDetails import org.openedx.core.data.model.CourseEnrollments import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences @@ -44,7 +43,7 @@ import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics import org.openedx.core.presentation.dialog.appreview.AppReviewManager import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.global.WhatsNewGlobalManager -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRouter import org.openedx.core.system.AppCookieManager import org.openedx.core.system.CalendarManager import org.openedx.core.system.connection.NetworkConnection diff --git a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt index aae32b433..6360e7fba 100644 --- a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt +++ b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt @@ -57,7 +57,6 @@ val networkingModule = module { single { provideApi(get()) } } - inline fun provideApi(retrofit: Retrofit): T { return retrofit.create(T::class.java) } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 31ebf741e..c134e6de6 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -184,7 +184,7 @@ val screenModule = module { profileRouter = get(), ) } - viewModel { (account: Account) -> EditProfileViewModel(get(), get(), get(), get(), account) } + viewModel { (account: Account) -> EditProfileViewModel(get(), get(), get(), get(), get(), account) } viewModel { VideoSettingsViewModel(get(), get(), get(), get()) } viewModel { (qualityType: String) -> VideoQualityViewModel(qualityType, get(), get(), get()) } viewModel { DeleteProfileViewModel(get(), get(), get(), get(), get()) } @@ -475,5 +475,4 @@ val screenModule = module { get(), ) } - } diff --git a/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt b/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt index 60917940e..2d5b47410 100644 --- a/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt +++ b/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt @@ -68,7 +68,8 @@ class OpenEdXFirebaseMessagingService : FirebaseMessagingService() { .setContentTitle(notification.title) .setStyle( NotificationCompat.BigTextStyle() - .bigText(notification.body)) + .bigText(notification.body) + ) .setAutoCancel(true) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) diff --git a/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt b/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt index ed4d841eb..6c45a3ab4 100644 --- a/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt +++ b/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt @@ -10,7 +10,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.openedx.app.data.api.NotificationsApi import org.openedx.core.data.storage.CorePreferences -import org.openedx.core.module.DownloadWorker class SyncFirebaseTokenWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params), @@ -21,7 +20,6 @@ class SyncFirebaseTokenWorker(context: Context, params: WorkerParameters) : override suspend fun doWork(): Result { if (preferences.user != null && preferences.pushToken.isNotEmpty()) { - api.syncFirebaseToken(preferences.pushToken) return Result.success() diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index f0e748b62..23b1c4120 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -40,7 +40,7 @@ class AppViewModelTest { @get:Rule val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule() - private val dispatcher = StandardTestDispatcher()//UnconfinedTestDispatcher() + private val dispatcher = StandardTestDispatcher() // UnconfinedTestDispatcher() private val config = mockk() private val notifier = mockk() diff --git a/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt b/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt index 903cbd62e..673168c57 100644 --- a/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt +++ b/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt @@ -5,9 +5,14 @@ import org.openedx.auth.data.model.PasswordResetResponse import org.openedx.auth.data.model.RegistrationFields import org.openedx.auth.data.model.ValidationFields import org.openedx.core.ApiConstants -import org.openedx.core.data.model.* +import org.openedx.core.data.model.User import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Field +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path interface AuthApi { @@ -59,4 +64,4 @@ interface AuthApi { @FormUrlEncoded @POST(ApiConstants.URL_PASSWORD_RESET) suspend fun passwordReset(@Field("email") email: String): PasswordResetResponse -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/data/model/PasswordResetResponse.kt b/auth/src/main/java/org/openedx/auth/data/model/PasswordResetResponse.kt index 1be96a795..f2feeda2b 100644 --- a/auth/src/main/java/org/openedx/auth/data/model/PasswordResetResponse.kt +++ b/auth/src/main/java/org/openedx/auth/data/model/PasswordResetResponse.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class PasswordResetResponse( @SerializedName("success") val success: Boolean -) \ No newline at end of file +) diff --git a/auth/src/main/java/org/openedx/auth/data/model/RegistrationFields.kt b/auth/src/main/java/org/openedx/auth/data/model/RegistrationFields.kt index ef300156f..b59ccab2d 100644 --- a/auth/src/main/java/org/openedx/auth/data/model/RegistrationFields.kt +++ b/auth/src/main/java/org/openedx/auth/data/model/RegistrationFields.kt @@ -74,5 +74,4 @@ data class RegistrationFields( ) } } - -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/data/model/ValidationFields.kt b/auth/src/main/java/org/openedx/auth/data/model/ValidationFields.kt index 29c97ab33..5c335b2cc 100644 --- a/auth/src/main/java/org/openedx/auth/data/model/ValidationFields.kt +++ b/auth/src/main/java/org/openedx/auth/data/model/ValidationFields.kt @@ -7,4 +7,4 @@ data class ValidationFields( val validationResult: Map ) { fun hasValidationError() = validationResult.values.any { it != "" } -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt index 6cf54a7f1..617006afe 100644 --- a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt +++ b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt @@ -32,7 +32,7 @@ class AuthRepository( } suspend fun socialLogin(token: String?, authType: AuthType) { - if (token.isNullOrBlank()) throw IllegalArgumentException("Token is null") + require(!token.isNullOrBlank()) { "Token is null" } api.exchangeAccessToken( accessToken = token, clientId = config.getOAuthClientId(), diff --git a/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt b/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt index 00fe509af..cdce0dbdf 100644 --- a/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt +++ b/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt @@ -33,5 +33,4 @@ class AuthInteractor(private val repository: AuthRepository) { suspend fun passwordReset(email: String): Boolean { return repository.passwordReset(email) } - -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt index 6faca63ce..a05951ca4 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt @@ -101,7 +101,6 @@ private fun LogistrationScreen( onSignInClick: () -> Unit, isRegistrationEnabled: Boolean, ) { - var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt index 6f02f231c..332aa6faa 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt @@ -58,7 +58,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.auth.presentation.ui.LoginTextField import org.openedx.core.AppUpdateState import org.openedx.core.R -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRequiredScreen import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OpenEdXButton @@ -366,7 +366,6 @@ private fun RestorePasswordScreen( } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_NO) @@ -397,4 +396,4 @@ fun RestorePasswordTabletPreview() { onRestoreButtonClick = {} ) } -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordUIState.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordUIState.kt index 779adaf12..cfec43ad3 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordUIState.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordUIState.kt @@ -1,7 +1,7 @@ package org.openedx.auth.presentation.restore sealed class RestorePasswordUIState { - object Initial : RestorePasswordUIState() - object Loading : RestorePasswordUIState() + data object Initial : RestorePasswordUIState() + data object Loading : RestorePasswordUIState() class Success(val email: String) : RestorePasswordUIState() -} \ No newline at end of file +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt index 504f55a7e..6c5e3adf1 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt @@ -60,7 +60,9 @@ class RestorePasswordViewModel( } else { _uiState.value = RestorePasswordUIState.Initial _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email)) + UIMessage.SnackBarMessage( + resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email) + ) logResetPasswordEvent(false) } } catch (e: Exception) { @@ -70,10 +72,14 @@ class RestorePasswordViewModel( _uiMessage.value = UIMessage.SnackBarMessage(e.error) } else if (e.isInternetError()) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) } else { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) } } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt index d5f11ea0a..8f55d334b 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt @@ -16,7 +16,7 @@ import org.koin.core.parameter.parametersOf import org.openedx.auth.data.model.AuthType import org.openedx.auth.presentation.signin.compose.LoginScreen import org.openedx.core.AppUpdateState -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRequiredScreen import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.foundation.presentation.rememberWindowSize diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt index be4e9bf53..d4608e4f8 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt @@ -128,7 +128,7 @@ internal fun LoginScreen( Image( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(0.3f), + .fillMaxHeight(fraction = 0.3f), painter = painterResource(id = coreR.drawable.core_top_header), contentScale = ContentScale.FillBounds, contentDescription = null @@ -284,7 +284,7 @@ private fun AuthForm( onEvent(AuthEvent.ForgotPasswordClick) }, text = stringResource(id = R.string.auth_forgot_password), - color = MaterialTheme.appColors.info_variant, + color = MaterialTheme.appColors.infoVariant, style = MaterialTheme.appTypography.labelLarge ) } @@ -379,8 +379,11 @@ private fun PasswordTextField( keyboardType = KeyboardType.Password, imeAction = ImeAction.Done ), - visualTransformation = if (isPasswordVisible) VisualTransformation.None - else PasswordVisualTransformation(), + visualTransformation = if (isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, keyboardActions = KeyboardActions { focusManager.clearFocus() onPressDone() diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt index dabcc0e31..150eacb1a 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt @@ -17,7 +17,7 @@ import org.openedx.auth.data.model.AuthType import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.signup.compose.SignUpView import org.openedx.core.AppUpdateState -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRequiredScreen import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.foundation.presentation.rememberWindowSize 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 35da6c030..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,71 +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/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt index 05c5c1d4e..8b917ebaa 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/compose/SignUpView.kt @@ -141,7 +141,7 @@ internal fun SignUpView( LaunchedEffect(uiState.validationError) { if (uiState.validationError) { coroutine.launch { - scrollState.animateScrollTo(0, tween(300)) + scrollState.animateScrollTo(0, tween(durationMillis = 300)) haptic.performHapticFeedback(HapticFeedbackType.LongPress) } } @@ -151,7 +151,7 @@ internal fun SignUpView( if (uiState.socialAuth != null) { coroutine.launch { showErrorMap.clear() - scrollState.animateScrollTo(0, tween(300)) + scrollState.animateScrollTo(0, tween(durationMillis = 300)) } } } @@ -173,7 +173,6 @@ internal fun SignUpView( .navigationBarsPadding(), backgroundColor = MaterialTheme.appColors.background ) { - val topBarPadding by remember { mutableStateOf( windowSize.windowSizeValue( @@ -246,7 +245,7 @@ internal fun SignUpView( Image( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(0.3f), + .fillMaxHeight(fraction = 0.3f), painter = painterResource(id = coreR.drawable.core_top_header), contentScale = ContentScale.FillBounds, contentDescription = null @@ -296,8 +295,8 @@ internal fun SignUpView( ) { if (uiState.isLoading) { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt index 8e1a31d05..ccd790512 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/AuthUI.kt @@ -136,9 +136,7 @@ fun RequiredFields( ) } - RegistrationFieldType.UNKNOWN -> { - - } + RegistrationFieldType.UNKNOWN -> {} } } } @@ -155,7 +153,8 @@ fun OptionalFields( Column { fields.forEach { field -> when (field.type) { - RegistrationFieldType.TEXT, RegistrationFieldType.EMAIL, RegistrationFieldType.CONFIRM_EMAIL, RegistrationFieldType.PASSWORD -> { + RegistrationFieldType.TEXT, RegistrationFieldType.EMAIL, + RegistrationFieldType.CONFIRM_EMAIL, RegistrationFieldType.PASSWORD -> { InputRegistrationField( modifier = Modifier.fillMaxWidth(), isErrorShown = showErrorMap[field.name] @@ -202,7 +201,8 @@ fun OptionalFields( ?: "", onClick = { serverName, list -> onSelectClick(serverName, field, list) - }) + } + ) } RegistrationFieldType.TEXTAREA -> { @@ -579,9 +579,7 @@ fun SelectRegistrationFieldPreview() { field, false, initialValue = "", - onClick = { _, _ -> - - } + onClick = { _, _ -> } ) } } @@ -597,9 +595,7 @@ fun InputRegistrationFieldPreview() { modifier = Modifier.fillMaxWidth(), isErrorShown = false, registrationField = field, - onValueChanged = { _, _, _ -> - - } + onValueChanged = { _, _, _ -> } ) } } @@ -613,7 +609,7 @@ private fun OptionalFieldsPreview() { Column(Modifier.background(MaterialTheme.appColors.background)) { val optionalField = field.copy(required = false) OptionalFields( - fields = List(3) { optionalField }, + fields = List(size = 3) { optionalField }, showErrorMap = SnapshotStateMap(), selectableNamesMap = SnapshotStateMap(), onSelectClick = { _, _, _ -> }, diff --git a/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt b/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt index 028439290..12b707033 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/ui/SocialAuthView.kt @@ -140,6 +140,6 @@ internal fun SocialAuthView( @Composable private fun SocialAuthViewPreview() { OpenEdXTheme { - SocialAuthView() {} + SocialAuthView {} } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt index 580688a48..4e780121d 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt @@ -176,7 +176,6 @@ class RestorePasswordViewModelTest { assertEquals(somethingWrong, message?.message) } - @Test fun `success restore password`() = runTest { val viewModel = diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index 90ef8728f..7426f752b 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -385,7 +385,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.getRegistrationFields() } verify(exactly = 1) { appNotifier.notifier } - //val fields = viewModel.uiState.value as? SignUpUIState.Fields + // val fields = viewModel.uiState.value as? SignUpUIState.Fields assertFalse(viewModel.uiState.value.isLoading) } diff --git a/build.gradle b/build.gradle index e7f7d673b..390d02699 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,4 @@ +import io.gitlab.arturbosch.detekt.Detekt import org.edx.builder.ConfigHelper import java.util.regex.Matcher @@ -8,6 +9,7 @@ buildscript { //Depends on versions in OEXFoundation kotlin_version = '2.0.0' room_version = '2.6.1' + detekt_version = '1.23.7' } } @@ -19,6 +21,7 @@ plugins { id "com.google.firebase.crashlytics" version "3.0.2" apply false id "com.google.devtools.ksp" version "2.0.0-1.0.24" apply false id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version" apply false + id 'io.gitlab.arturbosch.detekt' version "$detekt_version" apply false } tasks.register('clean', Delete) { @@ -66,3 +69,37 @@ def getCurrentFlavor() { tasks.register('generateMockedRawFile') { doLast { configHelper.generateMicrosoftConfig() } } + +def projectSource = file(projectDir) +def configFile = files("$rootDir/config/detekt.yml") +def basePathFile = rootProject.projectDir.absolutePath +def kotlinFiles = "**/*.kt" +def resourceFiles = "**/resources/**" +def buildFiles = "**/build/**" + +apply plugin: 'io.gitlab.arturbosch.detekt' + +tasks.register("detektAll", Detekt) { + def autoFix = project.hasProperty('detektAutoFix') + + description = "Custom DETEKT build for all modules" + parallel = true + ignoreFailures = false + autoCorrect = autoFix + buildUponDefaultConfig = true + setSource(projectSource) + config.setFrom(configFile) + include(kotlinFiles) + basePath(basePathFile) + exclude(resourceFiles, buildFiles) + reports { + html.enabled(true) + xml.enabled(true) + txt.enabled(false) + sarif.enabled(true) + } +} + +dependencies { + detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version" +} diff --git a/config/detekt.yml b/config/detekt.yml new file mode 100644 index 000000000..37d257629 --- /dev/null +++ b/config/detekt.yml @@ -0,0 +1,90 @@ +build: + maxIssues: 0 + weights: + complexity: 2 + LongParameterList: 1 + style: 1 + +config: + validation: true + +processors: + active: true + exclude: + - 'FunctionCountProcessor' + - 'PropertyCountProcessor' + +console-reports: + active: true + +naming: + active: true + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + FunctionNaming: + active: true + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + ignoreAnnotated: [ 'Composable' ] + +style: + active: true + MagicNumber: + active: true + ignorePropertyDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreNumbers: [ '-1', '0', '1', '2', '10', '100', '90', '-90', '180', '1000', '400', '402', '401', '403', '404', '426', '500' ] + ignoreNamedArgument: true + ignoreEnums: true + UnusedPrivateMember: + active: true + ignoreAnnotated: + - 'Preview' + +complexity: + active: true + LongMethod: + active: true + ignoreAnnotated: [ 'Composable' ] + ignoreFunction: [ 'onCreateView' ] + LongParameterList: + active: true + functionThreshold: 15 + constructorThreshold: 20 + ignoreDataClasses: true + ignoreAnnotated: [ 'Composable' ] + TooManyFunctions: + active: true + thresholdInClasses: 21 + thresholdInInterfaces: 20 + ignoreAnnotatedFunctions: [ 'Composable' ] + ignoreOverridden: true + ignorePrivate: true + CyclomaticComplexMethod: + active: true + ignoreAnnotated: [ 'Composable' ] + ComplexCondition: + active: true + threshold: 6 + +exceptions: + active: true + TooGenericExceptionCaught: + active: false + PrintStackTrace: + active: false + InstanceOfCheckForException: + active: false + +performance: + active: true + SpreadOperator: + active: false + +formatting: + active: true + Indentation: + active: false diff --git a/core/src/androidTest/java/org/openedx/core/ExampleInstrumentedTest.kt b/core/src/androidTest/java/org/openedx/core/ExampleInstrumentedTest.kt index 0c5df88c3..a3fa4cf52 100644 --- a/core/src/androidTest/java/org/openedx/core/ExampleInstrumentedTest.kt +++ b/core/src/androidTest/java/org/openedx/core/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package org.openedx.core -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("org.openedx.core.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/AppUpdateState.kt b/core/src/main/java/org/openedx/core/AppUpdateState.kt index 6d6a8e357..0f92d145b 100644 --- a/core/src/main/java/org/openedx/core/AppUpdateState.kt +++ b/core/src/main/java/org/openedx/core/AppUpdateState.kt @@ -15,6 +15,7 @@ object AppUpdateState { try { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${context.packageName}"))) } catch (e: ActivityNotFoundException) { + e.printStackTrace() context.startActivity( Intent( Intent.ACTION_VIEW, @@ -31,4 +32,4 @@ object AppUpdateState { val onAppUpgradeRecommendedBoxClick: () -> Unit = {}, val onAppUpgradeRequired: () -> Unit = {}, ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/BlockType.kt b/core/src/main/java/org/openedx/core/BlockType.kt index 07a7bf882..b680c68e2 100644 --- a/core/src/main/java/org/openedx/core/BlockType.kt +++ b/core/src/main/java/org/openedx/core/BlockType.kt @@ -1,42 +1,80 @@ package org.openedx.core enum class BlockType { - CHAPTER{ override fun isContainer() = true }, - COURSE{ override fun isContainer() = true }, - DISCUSSION{ override fun isContainer() = false }, - DRAG_AND_DROP_V2{ override fun isContainer() = false }, - HTML{ override fun isContainer() = false }, - LTI_CONSUMER{ override fun isContainer() = false }, - OPENASSESSMENT{ override fun isContainer() = false }, - OTHERS{ override fun isContainer() = false }, - PROBLEM{ override fun isContainer() = false }, - SECTION{ override fun isContainer() = true }, - SEQUENTIAL{ override fun isContainer() = true }, - VERTICAL{ override fun isContainer() = true }, - VIDEO{ override fun isContainer() = false }, - WORD_CLOUD{ override fun isContainer() = false }, - SURVEY{ override fun isContainer() = false }; + CHAPTER { + override fun isContainer() = true + }, + COURSE { + override fun isContainer() = true + }, + DISCUSSION { + override fun isContainer() = false + }, + DRAG_AND_DROP_V2 { + override fun isContainer() = false + }, + HTML { + override fun isContainer() = false + }, + LTI_CONSUMER { + override fun isContainer() = false + }, + OPENASSESSMENT { + override fun isContainer() = false + }, + OTHERS { + override fun isContainer() = false + }, + PROBLEM { + override fun isContainer() = false + }, + SECTION { + override fun isContainer() = true + }, + SEQUENTIAL { + override fun isContainer() = true + }, + VERTICAL { + override fun isContainer() = true + }, + VIDEO { + override fun isContainer() = false + }, + WORD_CLOUD { + override fun isContainer() = false + }, + SURVEY { + override fun isContainer() = false + }; - abstract fun isContainer() : Boolean + abstract fun isContainer(): Boolean + + companion object { + private const val PROBLEM_PRIORITY = 1 + private const val VIDEO_PRIORITY = 2 + private const val DISCUSSION_PRIORITY = 3 + private const val HTML_PRIORITY = 4 - companion object{ fun getBlockType(type: String): BlockType { - val actualType = if (type.contains("-")){ + val actualType = if (type.contains("-")) { type.replace("-", "_") - } else type + } else { + type + } return try { BlockType.valueOf(actualType.uppercase()) - } catch (e : Exception){ + } catch (e: Exception) { + e.printStackTrace() OTHERS } } fun sortByPriority(blockTypes: List): List { val priorityMap = mapOf( - PROBLEM to 1, - VIDEO to 2, - DISCUSSION to 3, - HTML to 4 + PROBLEM to PROBLEM_PRIORITY, + VIDEO to VIDEO_PRIORITY, + DISCUSSION to DISCUSSION_PRIORITY, + HTML to HTML_PRIORITY ) val comparator = Comparator { blockType1, blockType2 -> val priority1 = priorityMap[blockType1] ?: Int.MAX_VALUE @@ -47,4 +85,3 @@ enum class BlockType { } } } - diff --git a/core/src/main/java/org/openedx/core/FragmentViewType.kt b/core/src/main/java/org/openedx/core/FragmentViewType.kt index 97ebfeed5..e66618bb8 100644 --- a/core/src/main/java/org/openedx/core/FragmentViewType.kt +++ b/core/src/main/java/org/openedx/core/FragmentViewType.kt @@ -1,5 +1,5 @@ package org.openedx.core enum class FragmentViewType { - MAIN_CONTENT, FULL_CONTENT; -} \ No newline at end of file + MAIN_CONTENT, FULL_CONTENT +} diff --git a/core/src/main/java/org/openedx/core/Validator.kt b/core/src/main/java/org/openedx/core/Validator.kt index cb3a66ae6..9ae9f38a6 100644 --- a/core/src/main/java/org/openedx/core/Validator.kt +++ b/core/src/main/java/org/openedx/core/Validator.kt @@ -7,7 +7,8 @@ class Validator { fun isEmailOrUserNameValid(input: String): Boolean { return if (input.contains("@")) { val validEmailAddressRegex = Pattern.compile( - "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE + "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", + Pattern.CASE_INSENSITIVE ) validEmailAddressRegex.matcher(input).find() } else { @@ -18,5 +19,4 @@ class Validator { fun isPasswordValid(password: String): Boolean { return password.length >= 2 } - } diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index e38a923b5..1b58c7e44 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -8,6 +8,7 @@ import com.google.gson.JsonParser import org.openedx.core.domain.model.AgreementUrls import java.io.InputStreamReader +@Suppress("TooManyFunctions") class Config(context: Context) { private var configProperties: JsonObject = try { @@ -15,6 +16,7 @@ class Config(context: Context) { val config = JsonParser.parseReader(InputStreamReader(inputStream)) config.asJsonObject } catch (e: Exception) { + e.printStackTrace() JsonObject() } @@ -133,13 +135,15 @@ class Config(context: Context) { try { cls.getDeclaredConstructor().newInstance() } catch (e: InstantiationException) { - throw RuntimeException(e) + throw ConfigParsingException(e) } catch (e: IllegalAccessException) { - throw RuntimeException(e) + throw ConfigParsingException(e) } } } + class ConfigParsingException(cause: Throwable) : Exception(cause) + private fun getObject(key: String): JsonElement? { return configProperties.get(key) } diff --git a/core/src/main/java/org/openedx/core/config/ProgramConfig.kt b/core/src/main/java/org/openedx/core/config/ProgramConfig.kt index ce34365ec..869dfb93a 100644 --- a/core/src/main/java/org/openedx/core/config/ProgramConfig.kt +++ b/core/src/main/java/org/openedx/core/config/ProgramConfig.kt @@ -7,7 +7,7 @@ data class ProgramConfig( private val viewType: String = Config.ViewType.NATIVE.name, @SerializedName("WEBVIEW") val webViewConfig: ProgramWebViewConfig = ProgramWebViewConfig(), -){ +) { fun isViewTypeWebView(): Boolean { return Config.ViewType.WEBVIEW.name.equals(viewType, ignoreCase = true) } diff --git a/core/src/main/java/org/openedx/core/data/api/CookiesApi.kt b/core/src/main/java/org/openedx/core/data/api/CookiesApi.kt index c25cad2c0..276f2eda3 100644 --- a/core/src/main/java/org/openedx/core/data/api/CookiesApi.kt +++ b/core/src/main/java/org/openedx/core/data/api/CookiesApi.kt @@ -1,13 +1,11 @@ package org.openedx.core.data.api -import org.openedx.core.ApiConstants import okhttp3.RequestBody +import org.openedx.core.ApiConstants import retrofit2.Response import retrofit2.http.POST interface CookiesApi { - @POST(ApiConstants.URL_LOGIN) suspend fun userCookies(): Response - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt index 32d401f7b..8b5f0913a 100644 --- a/core/src/main/java/org/openedx/core/data/api/CourseApi.kt +++ b/core/src/main/java/org/openedx/core/data/api/CourseApi.kt @@ -34,7 +34,8 @@ interface CourseApi { @GET( "/api/mobile/{api_version}/course_info/blocks/?" + "depth=all&" + - "requested_fields=contains_gated_content,show_gated_sections,special_exam_info,graded,format,student_view_multi_device,due,completion&" + + "requested_fields=contains_gated_content,show_gated_sections,special_exam_info,graded,format," + + "student_view_multi_device,due,completion&" + "student_view_data=video,discussion&" + "block_counts=video&" + "nav_depth=3" diff --git a/core/src/main/java/org/openedx/core/data/model/AnnouncementModel.kt b/core/src/main/java/org/openedx/core/data/model/AnnouncementModel.kt index 99dc6b92c..4bc3fd3da 100644 --- a/core/src/main/java/org/openedx/core/data/model/AnnouncementModel.kt +++ b/core/src/main/java/org/openedx/core/data/model/AnnouncementModel.kt @@ -9,6 +9,7 @@ data class AnnouncementModel( val content: String ) { fun mapToDomain() = org.openedx.core.domain.model.AnnouncementModel( - date, content + date, + content ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/Block.kt b/core/src/main/java/org/openedx/core/data/model/Block.kt index c4b50df63..8ac8a8378 100644 --- a/core/src/main/java/org/openedx/core/data/model/Block.kt +++ b/core/src/main/java/org/openedx/core/data/model/Block.kt @@ -46,26 +46,18 @@ data class Block( val offlineDownload: OfflineDownload?, ) { fun mapToDomain(blockData: Map): DomainBlock { - val blockType = BlockType.getBlockType(type ?: "") - val descendantsType = if (blockType == BlockType.VERTICAL) { - val types = descendants?.map { descendant -> - BlockType.getBlockType(blockData[descendant]?.type ?: "") - } ?: emptyList() - val sortedBlockTypes = BlockType.sortByPriority(types) - sortedBlockTypes.firstOrNull() ?: blockType - } else { - blockType - } + val blockType = BlockType.getBlockType(type.orEmpty()) + val descendantsType = determineDescendantsType(blockType, blockData) return DomainBlock( - id = id ?: "", - blockId = blockId ?: "", - lmsWebUrl = lmsWebUrl ?: "", - legacyWebUrl = legacyWebUrl ?: "", - studentViewUrl = studentViewUrl ?: "", + id = id.orEmpty(), + blockId = blockId.orEmpty(), + lmsWebUrl = lmsWebUrl.orEmpty(), + legacyWebUrl = legacyWebUrl.orEmpty(), + studentViewUrl = studentViewUrl.orEmpty(), type = blockType, - displayName = displayName ?: "", - descendants = descendants ?: emptyList(), + displayName = displayName.orEmpty(), + descendants = descendants.orEmpty(), descendantsType = descendantsType, graded = graded ?: false, studentViewData = studentViewData?.mapToDomain(), @@ -74,10 +66,20 @@ data class Block( completion = completion ?: 0.0, containsGatedContent = containsGatedContent ?: false, assignmentProgress = assignmentProgress?.mapToDomain(), - due = TimeUtils.iso8601ToDate(due ?: ""), + due = TimeUtils.iso8601ToDate(due.orEmpty()), offlineDownload = offlineDownload?.mapToDomain() ) } + + private fun determineDescendantsType(blockType: BlockType, blockData: Map): BlockType { + if (blockType != BlockType.VERTICAL) return blockType + + val types = descendants?.map { descendant -> + BlockType.getBlockType(blockData[descendant]?.type.orEmpty()) + }.orEmpty() + + return BlockType.sortByPriority(types).firstOrNull() ?: blockType + } } data class StudentViewData( @@ -94,15 +96,13 @@ data class StudentViewData( @SerializedName("topic_id") val topicId: String? ) { - fun mapToDomain(): DomainStudentViewData { - return DomainStudentViewData( - onlyOnWeb = onlyOnWeb ?: false, - duration = duration ?: "", - transcripts = transcripts, - encodedVideos = encodedVideos?.mapToDomain(), - topicId = topicId ?: "" - ) - } + fun mapToDomain() = DomainStudentViewData( + onlyOnWeb = onlyOnWeb ?: false, + duration = duration ?: "", + transcripts = transcripts, + encodedVideos = encodedVideos?.mapToDomain(), + topicId = topicId.orEmpty() + ) } data class EncodedVideos( @@ -119,17 +119,14 @@ data class EncodedVideos( @SerializedName("mobile_low") var mobileLow: VideoInfo? ) { - - fun mapToDomain(): DomainEncodedVideos { - return DomainEncodedVideos( - youtube = videoInfo?.mapToDomain(), - hls = hls?.mapToDomain(), - fallback = fallback?.mapToDomain(), - desktopMp4 = desktopMp4?.mapToDomain(), - mobileHigh = mobileHigh?.mapToDomain(), - mobileLow = mobileLow?.mapToDomain() - ) - } + fun mapToDomain() = DomainEncodedVideos( + youtube = videoInfo?.mapToDomain(), + hls = hls?.mapToDomain(), + fallback = fallback?.mapToDomain(), + desktopMp4 = desktopMp4?.mapToDomain(), + mobileHigh = mobileHigh?.mapToDomain(), + mobileLow = mobileLow?.mapToDomain() + ) } data class VideoInfo( @@ -138,21 +135,17 @@ data class VideoInfo( @SerializedName("file_size") var fileSize: Long? ) { - fun mapToDomain(): DomainVideoInfo { - return DomainVideoInfo( - url = url ?: "", - fileSize = fileSize ?: 0 - ) - } + fun mapToDomain() = DomainVideoInfo( + url = url.orEmpty(), + fileSize = fileSize ?: 0 + ) } data class BlockCounts( @SerializedName("video") var video: Int? ) { - fun mapToDomain(): DomainBlockCounts { - return DomainBlockCounts( - video = video ?: 0 - ) - } + fun mapToDomain() = DomainBlockCounts( + video = video ?: 0 + ) } diff --git a/core/src/main/java/org/openedx/core/data/model/BlocksCompletionBody.kt b/core/src/main/java/org/openedx/core/data/model/BlocksCompletionBody.kt index fa373a2ea..860a1c1e8 100644 --- a/core/src/main/java/org/openedx/core/data/model/BlocksCompletionBody.kt +++ b/core/src/main/java/org/openedx/core/data/model/BlocksCompletionBody.kt @@ -9,4 +9,4 @@ data class BlocksCompletionBody( val courseId: String, @SerializedName("blocks") val blocks: Map -) \ No newline at end of file +) diff --git a/core/src/main/java/org/openedx/core/data/model/Certificate.kt b/core/src/main/java/org/openedx/core/data/model/Certificate.kt index 8324e4cc7..f82cdf921 100644 --- a/core/src/main/java/org/openedx/core/data/model/Certificate.kt +++ b/core/src/main/java/org/openedx/core/data/model/Certificate.kt @@ -15,4 +15,4 @@ data class Certificate( } fun mapToRoomEntity() = CertificateDb(certificateURL) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/CourseComponentStatus.kt b/core/src/main/java/org/openedx/core/data/model/CourseComponentStatus.kt index c907f932a..a423ab1f1 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseComponentStatus.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseComponentStatus.kt @@ -13,4 +13,4 @@ data class CourseComponentStatus( lastVisitedBlockId = lastVisitedBlockId ?: "" ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt index d29e7a7ea..1ad692a1c 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDateBlock.kt @@ -12,7 +12,7 @@ data class CourseDateBlock( @SerializedName("complete") val complete: Boolean = false, @SerializedName("date") - val date: String = "", // ISO 8601 compliant format + val date: String = "", // ISO 8601 compliant format @SerializedName("assignment_type") val assignmentType: String? = "", @SerializedName("date_type") diff --git a/core/src/main/java/org/openedx/core/data/model/CourseDates.kt b/core/src/main/java/org/openedx/core/data/model/CourseDates.kt index 97fc3180f..c0472c894 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseDates.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseDates.kt @@ -51,22 +51,22 @@ data class CourseDates( courseDatesResponse[DatesSection.TODAY] = datesList.filter { it.date.isToday() }.also { datesList.removeAll(it) } - //Update the date for upcoming comparison without time + // Update the date for upcoming comparison without time currentDate.clearTime() // for current week except today courseDatesResponse[DatesSection.THIS_WEEK] = datesList.filter { - it.date.after(currentDate) && it.date.before(currentDate.addDays(8)) + it.date.after(currentDate) && it.date.before(currentDate.addDays(days = 8)) }.also { datesList.removeAll(it) } // for coming week courseDatesResponse[DatesSection.NEXT_WEEK] = datesList.filter { - it.date.after(currentDate.addDays(7)) && it.date.before(currentDate.addDays(15)) + it.date.after(currentDate.addDays(days = 7)) && it.date.before(currentDate.addDays(days = 15)) }.also { datesList.removeAll(it) } // for upcoming courseDatesResponse[DatesSection.UPCOMING] = datesList.filter { - it.date.after(currentDate.addDays(14)) + it.date.after(currentDate.addDays(days = 14)) }.also { datesList.removeAll(it) } return courseDatesResponse diff --git a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt index ca28740fe..76d4d900f 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseEnrollments.kt @@ -44,7 +44,8 @@ data class CourseEnrollments( (json as JsonObject).get("primary"), EnrolledCourse::class.java ) - } catch (ex: Exception) { + } catch (e: Exception) { + e.printStackTrace() null } } @@ -55,7 +56,8 @@ data class CourseEnrollments( (json as JsonObject).get("enrollments"), DashboardCourseList::class.java ) - } catch (ex: Exception) { + } catch (e: Exception) { + e.printStackTrace() DashboardCourseList( next = null, previous = null, @@ -83,7 +85,7 @@ data class CourseEnrollments( config.asString, AppConfig::class.java ) - } catch (ex: Exception) { + } catch (_: Exception) { AppConfig() } } diff --git a/core/src/main/java/org/openedx/core/data/model/CourseSharingUtmParameters.kt b/core/src/main/java/org/openedx/core/data/model/CourseSharingUtmParameters.kt index 8b2651ddd..42bb0969a 100644 --- a/core/src/main/java/org/openedx/core/data/model/CourseSharingUtmParameters.kt +++ b/core/src/main/java/org/openedx/core/data/model/CourseSharingUtmParameters.kt @@ -21,4 +21,4 @@ data class CourseSharingUtmParameters( facebook = facebook ?: "", twitter = twitter ?: "" ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/CoursewareAccess.kt b/core/src/main/java/org/openedx/core/data/model/CoursewareAccess.kt index d92ffc336..87d2fede7 100644 --- a/core/src/main/java/org/openedx/core/data/model/CoursewareAccess.kt +++ b/core/src/main/java/org/openedx/core/data/model/CoursewareAccess.kt @@ -40,5 +40,4 @@ data class CoursewareAccess( userFragment = userFragment ?: "" ) } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/DashboardCourseList.kt b/core/src/main/java/org/openedx/core/data/model/DashboardCourseList.kt index c75eeef33..996775d3a 100644 --- a/core/src/main/java/org/openedx/core/data/model/DashboardCourseList.kt +++ b/core/src/main/java/org/openedx/core/data/model/DashboardCourseList.kt @@ -29,5 +29,4 @@ data class DashboardCourseList( results.map { it.mapToDomain() } ) } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/EnrolledCourseData.kt b/core/src/main/java/org/openedx/core/data/model/EnrolledCourseData.kt index 4afc9ef71..38acd4401 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrolledCourseData.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrolledCourseData.kt @@ -51,51 +51,53 @@ data class EnrolledCourseData( fun mapToDomain(): EnrolledCourseData { return EnrolledCourseData( - id = id ?: "", - name = name ?: "", - number = number ?: "", - org = org ?: "", - start = TimeUtils.iso8601ToDate(start ?: ""), - startDisplay = startDisplay ?: "", - startType = startType ?: "", - end = TimeUtils.iso8601ToDate(end ?: ""), - dynamicUpgradeDeadline = dynamicUpgradeDeadline ?: "", - subscriptionId = subscriptionId ?: "", + id = id.orEmpty(), + name = name.orEmpty(), + number = number.orEmpty(), + org = org.orEmpty(), + start = parseDate(start), + startDisplay = startDisplay.orEmpty(), + startType = startType.orEmpty(), + end = parseDate(end), + dynamicUpgradeDeadline = dynamicUpgradeDeadline.orEmpty(), + subscriptionId = subscriptionId.orEmpty(), coursewareAccess = coursewareAccess?.mapToDomain(), media = media?.mapToDomain(), - courseImage = courseImage ?: "", - courseAbout = courseAbout ?: "", + courseImage = courseImage.orEmpty(), + courseAbout = courseAbout.orEmpty(), courseSharingUtmParameters = courseSharingUtmParameters?.mapToDomain()!!, - courseUpdates = courseUpdates ?: "", - courseHandouts = courseHandouts ?: "", - discussionUrl = discussionUrl ?: "", - videoOutline = videoOutline ?: "", + courseUpdates = courseUpdates.orEmpty(), + courseHandouts = courseHandouts.orEmpty(), + discussionUrl = discussionUrl.orEmpty(), + videoOutline = videoOutline.orEmpty(), isSelfPaced = isSelfPaced ?: false ) } fun mapToRoomEntity(): EnrolledCourseDataDb { return EnrolledCourseDataDb( - id = id ?: "", - name = name ?: "", - number = number ?: "", - org = org ?: "", - start = start ?: "", - startDisplay = startDisplay ?: "", - startType = startType ?: "", - end = end ?: "", - dynamicUpgradeDeadline = dynamicUpgradeDeadline ?: "", - subscriptionId = subscriptionId ?: "", + id = id.orEmpty(), + name = name.orEmpty(), + number = number.orEmpty(), + org = org.orEmpty(), + start = start.orEmpty(), + startDisplay = startDisplay.orEmpty(), + startType = startType.orEmpty(), + end = end.orEmpty(), + dynamicUpgradeDeadline = dynamicUpgradeDeadline.orEmpty(), + subscriptionId = subscriptionId.orEmpty(), coursewareAccess = coursewareAccess?.mapToRoomEntity(), media = MediaDb.createFrom(media), - courseImage = courseImage ?: "", - courseAbout = courseAbout ?: "", + courseImage = courseImage.orEmpty(), + courseAbout = courseAbout.orEmpty(), courseSharingUtmParameters = courseSharingUtmParameters?.mapToRoomEntity()!!, - courseUpdates = courseUpdates ?: "", - courseHandouts = courseHandouts ?: "", - discussionUrl = discussionUrl ?: "", - videoOutline = videoOutline ?: "", + courseUpdates = courseUpdates.orEmpty(), + courseHandouts = courseHandouts.orEmpty(), + discussionUrl = discussionUrl.orEmpty(), + videoOutline = videoOutline.orEmpty(), isSelfPaced = isSelfPaced ?: false ) } -} \ No newline at end of file + + private fun parseDate(date: String?) = TimeUtils.iso8601ToDate(date.orEmpty()) +} diff --git a/core/src/main/java/org/openedx/core/data/model/EnrollmentDetails.kt b/core/src/main/java/org/openedx/core/data/model/EnrollmentDetails.kt index 668e97f07..d1998ed29 100644 --- a/core/src/main/java/org/openedx/core/data/model/EnrollmentDetails.kt +++ b/core/src/main/java/org/openedx/core/data/model/EnrollmentDetails.kt @@ -1,11 +1,8 @@ package org.openedx.core.data.model -import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize import org.openedx.core.data.model.room.discovery.EnrollmentDetailsDB import org.openedx.core.utils.TimeUtils - import org.openedx.core.domain.model.EnrollmentDetails as DomainEnrollmentDetails data class EnrollmentDetails( diff --git a/core/src/main/java/org/openedx/core/data/model/ErrorResponse.kt b/core/src/main/java/org/openedx/core/data/model/ErrorResponse.kt index f76ea2edb..1365a3099 100644 --- a/core/src/main/java/org/openedx/core/data/model/ErrorResponse.kt +++ b/core/src/main/java/org/openedx/core/data/model/ErrorResponse.kt @@ -7,4 +7,4 @@ data class ErrorResponse( val error: String?, @SerializedName("error_description", alternate = ["value", "developer_message"]) val errorDescription: String? -) \ No newline at end of file +) diff --git a/core/src/main/java/org/openedx/core/data/model/Media.kt b/core/src/main/java/org/openedx/core/data/model/Media.kt index 2a7a05000..7b4998175 100644 --- a/core/src/main/java/org/openedx/core/data/model/Media.kt +++ b/core/src/main/java/org/openedx/core/data/model/Media.kt @@ -1,7 +1,6 @@ package org.openedx.core.data.model import com.google.gson.annotations.SerializedName -import org.openedx.core.data.model.room.discovery.* import org.openedx.core.domain.model.Media data class Media( @@ -23,7 +22,6 @@ data class Media( image = image?.mapToDomain() ) } - } data class Image( @@ -80,4 +78,4 @@ data class BannerImage( uriAbsolute = uriAbsolute ?: "" ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/Pagination.kt b/core/src/main/java/org/openedx/core/data/model/Pagination.kt index e375ac6fe..0d72b9fd1 100644 --- a/core/src/main/java/org/openedx/core/data/model/Pagination.kt +++ b/core/src/main/java/org/openedx/core/data/model/Pagination.kt @@ -19,4 +19,4 @@ data class Pagination( numPages = numPages ?: 0, previous = previous ?: "" ) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/User.kt b/core/src/main/java/org/openedx/core/data/model/User.kt index 99194624b..6ba76efaa 100644 --- a/core/src/main/java/org/openedx/core/data/model/User.kt +++ b/core/src/main/java/org/openedx/core/data/model/User.kt @@ -15,7 +15,10 @@ data class User( ) { fun mapToDomain(): User { return User( - id, username, email, name?:"" + id, + username, + email, + name ?: "" ) } } diff --git a/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt b/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt index 70ddfdf79..a60d9e68c 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/BlockDb.kt @@ -149,7 +149,6 @@ data class StudentViewDataDb( topicId = studentViewData?.topicId ?: "" ) } - } } @@ -190,7 +189,6 @@ data class EncodedVideosDb( ) } } - } data class VideoInfoDb( diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt index 49862d683..9ad7e4cc2 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseStructureEntity.kt @@ -64,5 +64,4 @@ data class CourseStructureEntity( progress.mapToDomain() ) } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/room/MediaDb.kt b/core/src/main/java/org/openedx/core/data/model/room/MediaDb.kt index be5bf45b4..6d61f9820 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/MediaDb.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/MediaDb.kt @@ -1,7 +1,11 @@ package org.openedx.core.data.model.room import androidx.room.ColumnInfo -import org.openedx.core.domain.model.* +import org.openedx.core.domain.model.BannerImage +import org.openedx.core.domain.model.CourseImage +import org.openedx.core.domain.model.CourseVideo +import org.openedx.core.domain.model.Image +import org.openedx.core.domain.model.Media data class MediaDb( @ColumnInfo("bannerImage") @@ -44,7 +48,9 @@ data class ImageDb( val small: String ) { fun mapToDomain() = Image( - large, raw, small + large, + raw, + small ) companion object { @@ -57,7 +63,6 @@ data class ImageDb( } } - data class CourseVideoDb( @ColumnInfo("uri") val uri: String @@ -103,5 +108,4 @@ data class BannerImageDb( uriAbsolute = bannerImage?.uriAbsolute ?: "" ) } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt index 59de42e53..2bcf3c664 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/discovery/EnrolledCourseEntity.kt @@ -153,7 +153,6 @@ data class CoursewareAccessDb( userFragment ) } - } data class CertificateDb( @@ -170,7 +169,8 @@ data class CourseSharingUtmParametersDb( val twitter: String, ) { fun mapToDomain() = CourseSharingUtmParameters( - facebook, twitter + facebook, + twitter ) } @@ -198,7 +198,10 @@ data class CourseStatusDb( val lastVisitedUnitDisplayName: String, ) { fun mapToDomain() = CourseStatus( - lastVisitedModuleId, lastVisitedModulePath, lastVisitedBlockId, lastVisitedUnitDisplayName + lastVisitedModuleId, + lastVisitedModulePath, + lastVisitedBlockId, + lastVisitedUnitDisplayName ) } diff --git a/core/src/main/java/org/openedx/core/data/storage/InAppReviewPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/InAppReviewPreferences.kt index c9bf0638c..590ff021d 100644 --- a/core/src/main/java/org/openedx/core/data/storage/InAppReviewPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/InAppReviewPreferences.kt @@ -25,4 +25,4 @@ interface InAppReviewPreferences { val default = VersionName(Int.MIN_VALUE, Int.MIN_VALUE) } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt b/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt index 97750957f..a64e09655 100644 --- a/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt +++ b/core/src/main/java/org/openedx/core/domain/model/AppConfig.kt @@ -1,14 +1,18 @@ package org.openedx.core.domain.model -import java.io.Serializable +import com.google.gson.annotations.SerializedName data class AppConfig( val courseDatesCalendarSync: CourseDatesCalendarSync = CourseDatesCalendarSync(), -) : Serializable +) data class CourseDatesCalendarSync( + @SerializedName("is_enabled") val isEnabled: Boolean = false, + @SerializedName("is_self_paced_enabled") val isSelfPacedEnabled: Boolean = false, + @SerializedName("is_instructor_paced_enabled") val isInstructorPacedEnabled: Boolean = false, + @SerializedName("is_deep_link_enabled") val isDeepLinkEnabled: Boolean = false, -) : Serializable +) 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 3ebf8c8b6..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 { @@ -120,11 +118,11 @@ data class EncodedVideos( isPreferredVideoInfo(mobileLow) val hasNonYoutubeVideo: Boolean - get() = mobileHigh?.url != null - || mobileLow?.url != null - || desktopMp4?.url != null - || hls?.url != null - || fallback?.url != null + get() = mobileHigh?.url != null || + mobileLow?.url != null || + desktopMp4?.url != null || + hls?.url != null || + fallback?.url != null val videoUrl: String get() = fallback?.url @@ -158,29 +156,16 @@ data class EncodedVideos( } private fun getDefaultVideoInfoForDownloading(): VideoInfo? { - if (isPreferredVideoInfo(mobileLow)) { - return mobileLow - } - if (isPreferredVideoInfo(mobileHigh)) { - return mobileHigh - } - if (isPreferredVideoInfo(desktopMp4)) { - return desktopMp4 - } - fallback?.let { - if (isPreferredVideoInfo(it) && - !VideoUtil.videoHasFormat(it.url, AppDataConstants.VIDEO_FORMAT_M3U8) - ) { - return fallback - } - } - hls?.let { - if (isPreferredVideoInfo(it) - ) { - return hls - } + return when { + isPreferredVideoInfo(mobileLow) -> mobileLow + isPreferredVideoInfo(mobileHigh) -> mobileHigh + isPreferredVideoInfo(desktopMp4) -> desktopMp4 + fallback != null && isPreferredVideoInfo(fallback) && + !VideoUtil.videoHasFormat(fallback!!.url, AppDataConstants.VIDEO_FORMAT_M3U8) -> fallback + + hls != null && isPreferredVideoInfo(hls) -> hls + else -> null } - return null } private fun isPreferredVideoInfo(videoInfo: VideoInfo?): Boolean { @@ -188,7 +173,6 @@ data class EncodedVideos( URLUtil.isNetworkUrl(videoInfo.url) && VideoUtil.isValidVideoUrl(videoInfo.url) } - } @Parcelize diff --git a/core/src/main/java/org/openedx/core/domain/model/Certificate.kt b/core/src/main/java/org/openedx/core/domain/model/Certificate.kt index 83430d697..054b75511 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Certificate.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Certificate.kt @@ -8,5 +8,4 @@ data class Certificate( val certificateURL: String? ) : Parcelable { fun isCertificateEarned() = certificateURL?.isNotEmpty() == true - } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt index feb039fc7..9c60747ca 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseAssignments.kt @@ -7,4 +7,4 @@ import kotlinx.parcelize.Parcelize data class CourseAssignments( val futureAssignments: List?, val pastAssignments: List? -): Parcelable +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt index 9249d6a23..6c2165dca 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseDateBlock.kt @@ -18,13 +18,14 @@ data class CourseDateBlock( val assignmentType: String? = "", ) : Parcelable { fun isCompleted(): Boolean { - return complete || (dateType in setOf( + val dateTypeInSet = dateType in setOf( DateType.COURSE_START_DATE, DateType.COURSE_END_DATE, DateType.CERTIFICATE_AVAILABLE_DATE, DateType.VERIFIED_UPGRADE_DEADLINE, - DateType.VERIFICATION_DEADLINE_DATE, - ) && date.before(Date())) + DateType.VERIFICATION_DEADLINE_DATE + ) + return complete || (dateTypeInSet && date.before(Date())) } override fun equals(other: Any?): Boolean { diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseDatesBannerInfo.kt b/core/src/main/java/org/openedx/core/domain/model/CourseDatesBannerInfo.kt index 3281ca045..f7d840681 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseDatesBannerInfo.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseDatesBannerInfo.kt @@ -61,5 +61,5 @@ enum class CourseBannerType( headerResId = R.string.core_dates_reset_dates_banner_header, bodyResId = R.string.core_dates_reset_dates_banner_body, buttonResId = R.string.core_dates_reset_dates_banner_button - ); + ) } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt b/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt index f09f057db..186ef85fd 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseSharingUtmParameters.kt @@ -7,4 +7,4 @@ import kotlinx.parcelize.Parcelize data class CourseSharingUtmParameters( val facebook: String, val twitter: String -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt b/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt index 187c995b6..5dd48d94e 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CoursewareAccess.kt @@ -11,4 +11,4 @@ data class CoursewareAccess( val userMessage: String, val additionalContextUserMessage: String, val userFragment: String -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/DatesSection.kt b/core/src/main/java/org/openedx/core/domain/model/DatesSection.kt index 111d6e65e..d641c79d8 100644 --- a/core/src/main/java/org/openedx/core/domain/model/DatesSection.kt +++ b/core/src/main/java/org/openedx/core/domain/model/DatesSection.kt @@ -9,5 +9,5 @@ enum class DatesSection(val stringResId: Int) { THIS_WEEK(R.string.core_date_type_this_week), NEXT_WEEK(R.string.core_date_type_next_week), UPCOMING(R.string.core_date_type_upcoming), - NONE(R.string.core_date_type_none); + NONE(R.string.core_date_type_none) } diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourseData.kt b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourseData.kt index 2a66cccde..58fdaebf2 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrolledCourseData.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrolledCourseData.kt @@ -2,7 +2,7 @@ package org.openedx.core.domain.model import android.os.Parcelable import kotlinx.parcelize.Parcelize -import java.util.* +import java.util.Date @Parcelize data class EnrolledCourseData( @@ -26,4 +26,4 @@ data class EnrolledCourseData( val discussionUrl: String, val videoOutline: String, val isSelfPaced: Boolean -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt b/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt index 01882167b..c9d39ec35 100644 --- a/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt +++ b/core/src/main/java/org/openedx/core/domain/model/EnrollmentDetails.kt @@ -1,10 +1,7 @@ package org.openedx.core.domain.model import android.os.Parcelable -import androidx.room.ColumnInfo import kotlinx.parcelize.Parcelize -import org.openedx.core.data.model.EnrollmentDetails -import org.openedx.core.extension.isNotNull import java.util.Date @Parcelize @@ -14,4 +11,3 @@ data class EnrollmentDetails( val isActive: Boolean, val upgradeDeadline: Date?, ) : Parcelable - diff --git a/core/src/main/java/org/openedx/core/domain/model/HandoutsModel.kt b/core/src/main/java/org/openedx/core/domain/model/HandoutsModel.kt index 80a84d4b7..bdac364ba 100644 --- a/core/src/main/java/org/openedx/core/domain/model/HandoutsModel.kt +++ b/core/src/main/java/org/openedx/core/domain/model/HandoutsModel.kt @@ -2,4 +2,4 @@ package org.openedx.core.domain.model data class HandoutsModel( val handoutsHtml: String -) \ No newline at end of file +) diff --git a/core/src/main/java/org/openedx/core/domain/model/Media.kt b/core/src/main/java/org/openedx/core/domain/model/Media.kt index 16d6f66c3..51fa6dda5 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Media.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Media.kt @@ -26,11 +26,11 @@ data class CourseVideo( @Parcelize data class CourseImage( val uri: String, - val name : String + val name: String ) : Parcelable @Parcelize data class BannerImage( val uri: String, val uriAbsolute: String -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/Pagination.kt b/core/src/main/java/org/openedx/core/domain/model/Pagination.kt index 267eb6392..28bc025c8 100644 --- a/core/src/main/java/org/openedx/core/domain/model/Pagination.kt +++ b/core/src/main/java/org/openedx/core/domain/model/Pagination.kt @@ -5,4 +5,4 @@ data class Pagination( val next: String, val numPages: Int, val previous: String -) \ No newline at end of file +) diff --git a/core/src/main/java/org/openedx/core/domain/model/ProfileImage.kt b/core/src/main/java/org/openedx/core/domain/model/ProfileImage.kt index ef07f5b33..48fc89620 100644 --- a/core/src/main/java/org/openedx/core/domain/model/ProfileImage.kt +++ b/core/src/main/java/org/openedx/core/domain/model/ProfileImage.kt @@ -10,4 +10,4 @@ data class ProfileImage( val imageUrlMedium: String, val imageUrlSmall: String, val hasImage: Boolean -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/core/src/main/java/org/openedx/core/domain/model/StartType.kt b/core/src/main/java/org/openedx/core/domain/model/StartType.kt index 38ace6fff..8f167bd6a 100644 --- a/core/src/main/java/org/openedx/core/domain/model/StartType.kt +++ b/core/src/main/java/org/openedx/core/domain/model/StartType.kt @@ -20,4 +20,4 @@ enum class StartType(val type: String) { */ @SerializedName("empty") EMPTY("empty") -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/domain/model/User.kt b/core/src/main/java/org/openedx/core/domain/model/User.kt index a14d19951..8fae9464e 100644 --- a/core/src/main/java/org/openedx/core/domain/model/User.kt +++ b/core/src/main/java/org/openedx/core/domain/model/User.kt @@ -1,9 +1,8 @@ package org.openedx.core.domain.model - data class User( val id: Long, val username: String?, val email: String?, val name: String -) \ No newline at end of file +) diff --git a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt index 4f411551e..28248d403 100644 --- a/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt +++ b/core/src/main/java/org/openedx/core/domain/model/VideoSettings.kt @@ -46,5 +46,5 @@ enum class VideoQuality( width = 1280, height = 720, tagId = "high", - ); + ) } diff --git a/core/src/main/java/org/openedx/core/exception/NoCachedDataException.kt b/core/src/main/java/org/openedx/core/exception/NoCachedDataException.kt index d917c5c49..234e53a3f 100644 --- a/core/src/main/java/org/openedx/core/exception/NoCachedDataException.kt +++ b/core/src/main/java/org/openedx/core/exception/NoCachedDataException.kt @@ -1,3 +1,3 @@ package org.openedx.core.exception -class NoCachedDataException : Exception() \ No newline at end of file +class NoCachedDataException : Exception() diff --git a/core/src/main/java/org/openedx/core/extension/StringExt.kt b/core/src/main/java/org/openedx/core/extension/StringExt.kt index 301e9deb9..1ed4f3fb6 100644 --- a/core/src/main/java/org/openedx/core/extension/StringExt.kt +++ b/core/src/main/java/org/openedx/core/extension/StringExt.kt @@ -5,7 +5,7 @@ import java.net.URL fun String?.equalsHost(host: String?): Boolean { return try { host?.startsWith(URL(this).host, ignoreCase = true) == true - } catch (e: Exception) { + } catch (_: Exception) { false } } diff --git a/core/src/main/java/org/openedx/core/extension/TextConverter.kt b/core/src/main/java/org/openedx/core/extension/TextConverter.kt index e6a60d989..22879220e 100644 --- a/core/src/main/java/org/openedx/core/extension/TextConverter.kt +++ b/core/src/main/java/org/openedx/core/extension/TextConverter.kt @@ -70,6 +70,7 @@ object TextConverter : KoinComponent { fun isLinkValid(link: String) = Patterns.WEB_URL.matcher(link.lowercase()).matches() + @Suppress("MagicNumber") private fun getHeaders(document: Document): List { val headersList = mutableListOf() for (index in 1..6) { diff --git a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt index b3c211916..afb2f6383 100644 --- a/core/src/main/java/org/openedx/core/module/DownloadWorker.kt +++ b/core/src/main/java/org/openedx/core/module/DownloadWorker.kt @@ -64,9 +64,11 @@ class DownloadWorker( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createChannel() } - val serviceType = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0 + val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + } return ForegroundInfo( NOTIFICATION_ID, @@ -87,7 +89,7 @@ class DownloadWorker( val progress = 100 * value / size // Update no more than 5 times per sec if (!fileDownloader.isCanceled && - (System.currentTimeMillis() - lastUpdateTime > 200) + (System.currentTimeMillis() - lastUpdateTime > PROGRESS_UPDATE_INTERVAL) ) { lastUpdateTime = System.currentTimeMillis() @@ -177,6 +179,6 @@ class DownloadWorker( private const val CHANNEL_ID = "download_channel_ID" private const val CHANNEL_NAME = "download_channel_name" private const val NOTIFICATION_ID = 10 + private const val PROGRESS_UPDATE_INTERVAL = 200L } - } 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 6db81533c..b80500ad1 100644 --- a/core/src/main/java/org/openedx/core/module/TranscriptManager.kt +++ b/core/src/main/java/org/openedx/core/module/TranscriptManager.kt @@ -26,14 +26,14 @@ class TranscriptManager( get() = OkHttpClient.Builder().build() } - var transcriptObject: TimedTextObject? = null + private var transcriptObject: TimedTextObject? = null - fun has(url: String): Boolean { + private fun has(url: String): Boolean { val transcriptDir = getTranscriptDir() ?: return false val hash = Sha1Util.SHA1(url) val file = File(transcriptDir, hash) return file.exists() && System.currentTimeMillis() - file.lastModified() < TimeUnit.HOURS.toMillis( - 5 + FILE_VALIDITY_DURATION_HOURS ) } @@ -56,7 +56,9 @@ class TranscriptManager( return if (!file.exists()) { // not in cache null - } else FileInputStream(file) + } else { + FileInputStream(file) + } } private suspend fun startTranscriptDownload(downloadLink: String) { @@ -102,20 +104,15 @@ class TranscriptManager( return timedTextObject } - fun fetchTranscriptResponse(url: String?): InputStream? { - if (url == null) { - return null - } - val response: InputStream? - try { - if (has(url)) { - response = getInputStream(url) - return response - } + private fun fetchTranscriptResponse(url: String?): InputStream? { + 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? { @@ -128,4 +125,8 @@ class TranscriptManager( } return null } + + companion object { + private const val FILE_VALIDITY_DURATION_HOURS = 5L + } } diff --git a/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt b/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt index 4e1a2f2cf..6414b67c7 100644 --- a/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt +++ b/core/src/main/java/org/openedx/core/module/db/DownloadModelEntity.kt @@ -56,7 +56,5 @@ data class DownloadModelEntity( ) } } - } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt b/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt index 146cc1fc3..d2c6d8c74 100644 --- a/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt +++ b/core/src/main/java/org/openedx/core/module/download/AbstractDownloader.kt @@ -38,41 +38,43 @@ abstract class AbstractDownloader : KoinComponent { ): DownloadResult { isCanceled = false return try { - val response = downloadApi.downloadFile(url).body() - if (response != null) { - val file = File(path) - if (file.exists()) { - file.delete() + val responseBody = downloadApi.downloadFile(url).body() ?: return DownloadResult.ERROR + initializeFile(path) + responseBody.byteStream().use { inputStream -> + FileOutputStream(File(path)).use { outputStream -> + writeToFile(inputStream, outputStream) } - file.createNewFile() - input = response.byteStream() - currentDownloadingFilePath = path - fos = FileOutputStream(file) - fos.use { output -> - val buffer = ByteArray(4 * 1024) - var read: Int - while (input!!.read(buffer).also { read = it } != -1) { - output?.write(buffer, 0, read) - } - output?.flush() - } - DownloadResult.SUCCESS - } else { - DownloadResult.ERROR } + DownloadResult.SUCCESS } catch (e: Exception) { e.printStackTrace() - if (isCanceled) { - DownloadResult.CANCELED - } else { - DownloadResult.ERROR - } + if (isCanceled) DownloadResult.CANCELED else DownloadResult.ERROR } finally { - fos?.close() - input?.close() + closeResources() } } + private fun initializeFile(path: String) { + val file = File(path) + if (file.exists()) file.delete() + file.createNewFile() + currentDownloadingFilePath = path + } + + private fun writeToFile(inputStream: InputStream, outputStream: FileOutputStream) { + val buffer = ByteArray(BUFFER_SIZE) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + outputStream.flush() + } + + private fun closeResources() { + fos?.close() + input?.close() + } + suspend fun cancelDownloading() { isCanceled = true withContext(Dispatchers.IO) { @@ -94,4 +96,8 @@ abstract class AbstractDownloader : KoinComponent { enum class DownloadResult { SUCCESS, CANCELED, ERROR } + + companion object { + private const val BUFFER_SIZE = 4 * 1024 + } } diff --git a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt index b6635047f..0fcf962a3 100644 --- a/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt +++ b/core/src/main/java/org/openedx/core/module/download/BaseDownloadViewModel.kt @@ -60,33 +60,54 @@ abstract class BaseDownloadViewModel( private suspend fun updateDownloadModelsStatus(models: List) { val downloadModelMap = models.associateBy { it.id } - for (item in downloadableChildrenMap) { - var downloadingCount = 0 - var downloadedCount = 0 - item.value.forEach { blockId -> - val downloadModel = downloadModelMap[blockId] - if (downloadModel != null) { - if (downloadModel.downloadedState.isWaitingOrDownloading) { - downloadModelsStatus[blockId] = DownloadedState.DOWNLOADING - downloadingCount++ - } else if (downloadModel.downloadedState.isDownloaded) { - downloadModelsStatus[blockId] = DownloadedState.DOWNLOADED - downloadedCount++ - } - } else { - downloadModelsStatus[blockId] = DownloadedState.NOT_DOWNLOADED + + downloadableChildrenMap.forEach { (parentId, children) -> + val (downloadingCount, downloadedCount) = updateChildrenStatus(children, downloadModelMap) + updateParentStatus(parentId, children.size, downloadingCount, downloadedCount) + } + + downloadingModelsList = models.filter { it.downloadedState.isWaitingOrDownloading } + _downloadingModelsFlow.emit(downloadingModelsList) + } + + private fun updateChildrenStatus( + children: List, + downloadModelMap: Map + ): Pair { + var downloadingCount = 0 + var downloadedCount = 0 + + children.forEach { blockId -> + val downloadModel = downloadModelMap[blockId] + downloadModelsStatus[blockId] = when { + downloadModel?.downloadedState?.isWaitingOrDownloading == true -> { + downloadingCount++ + DownloadedState.DOWNLOADING + } + + downloadModel?.downloadedState?.isDownloaded == true -> { + downloadedCount++ + DownloadedState.DOWNLOADED } - } - downloadModelsStatus[item.key] = when { - downloadingCount > 0 -> DownloadedState.DOWNLOADING - downloadedCount == item.value.size -> DownloadedState.DOWNLOADED else -> DownloadedState.NOT_DOWNLOADED } } - downloadingModelsList = models.filter { it.downloadedState.isWaitingOrDownloading } - _downloadingModelsFlow.emit(downloadingModelsList) + return downloadingCount to downloadedCount + } + + private fun updateParentStatus( + parentId: String, + childrenSize: Int, + downloadingCount: Int, + downloadedCount: Int + ) { + downloadModelsStatus[parentId] = when { + downloadingCount > 0 -> DownloadedState.DOWNLOADING + downloadedCount == childrenSize -> DownloadedState.DOWNLOADED + else -> DownloadedState.NOT_DOWNLOADED + } } protected fun setBlocks(list: List) { @@ -200,23 +221,27 @@ abstract class BaseDownloadViewModel( } } + @Suppress("NestedBlockDepth") protected fun addDownloadableChildrenForSequentialBlock(sequentialBlock: Block) { - for (item in sequentialBlock.descendants) { - allBlocks[item]?.let { blockDescendant -> - if (blockDescendant.type == BlockType.VERTICAL) { - for (unitBlockId in blockDescendant.descendants) { - val block = allBlocks[unitBlockId] - if (block?.isDownloadable == true) { - val id = sequentialBlock.id - val children = downloadableChildrenMap[id] ?: listOf() - downloadableChildrenMap[id] = children + block.id - } + sequentialBlock.descendants.forEach { descendantId -> + val blockDescendant = allBlocks[descendantId] ?: return@forEach + + if (blockDescendant.type == BlockType.VERTICAL) { + blockDescendant.descendants.forEach { unitBlockId -> + val block = allBlocks[unitBlockId] + if (block?.isDownloadable == true) { + addDownloadableChild(sequentialBlock.id, block.id) } } } } } + private fun addDownloadableChild(parentId: String, childId: String) { + val children = downloadableChildrenMap[parentId] ?: listOf() + downloadableChildrenMap[parentId] = children + childId + } + fun logBulkDownloadToggleEvent(toggle: Boolean) { logEvent( CoreAnalyticsEvent.VIDEO_BULK_DOWNLOAD_TOGGLE, @@ -232,7 +257,8 @@ abstract class BaseDownloadViewModel( buildMap { put(CoreAnalyticsKey.BLOCK_ID.key, subsectionId) put(CoreAnalyticsKey.NUMBER_OF_VIDEOS.key, numberOfVideos) - }) + } + ) } private fun logSubsectionDeleteEvent(subsectionId: String, numberOfVideos: Int) { @@ -241,7 +267,8 @@ abstract class BaseDownloadViewModel( buildMap { put(CoreAnalyticsKey.BLOCK_ID.key, subsectionId) put(CoreAnalyticsKey.NUMBER_OF_VIDEOS.key, numberOfVideos) - }) + } + ) } private fun logEvent(event: CoreAnalyticsEvent, param: Map = emptyMap()) { diff --git a/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt b/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt index 79e44ab3c..327d4814e 100644 --- a/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt +++ b/core/src/main/java/org/openedx/core/module/download/DownloadHelper.kt @@ -93,22 +93,14 @@ class DownloadHelper( } private fun calculateDirectorySize(directory: File): Long { - var size: Long = 0 + if (!directory.exists()) return 0 - if (directory.exists()) { - val files = directory.listFiles() - - if (files != null) { - for (file in files) { - size += if (file.isDirectory) { - calculateDirectorySize(file) - } else { - file.length() - } - } + return directory.listFiles()?.sumOf { file -> + if (file.isDirectory) { + calculateDirectorySize(file) + } else { + file.length() } - } - - return size + } ?: 0 } } diff --git a/core/src/main/java/org/openedx/core/module/download/DownloadModelsSize.kt b/core/src/main/java/org/openedx/core/module/download/DownloadModelsSize.kt index b40876c99..8db4d05b6 100644 --- a/core/src/main/java/org/openedx/core/module/download/DownloadModelsSize.kt +++ b/core/src/main/java/org/openedx/core/module/download/DownloadModelsSize.kt @@ -7,4 +7,3 @@ data class DownloadModelsSize( val allCount: Int, val allSize: Long ) - diff --git a/core/src/main/java/org/openedx/core/module/download/DownloadType.kt b/core/src/main/java/org/openedx/core/module/download/DownloadType.kt index 1810ee23d..5310e10cd 100644 --- a/core/src/main/java/org/openedx/core/module/download/DownloadType.kt +++ b/core/src/main/java/org/openedx/core/module/download/DownloadType.kt @@ -2,4 +2,4 @@ package org.openedx.core.module.download enum class DownloadType { VIDEO, SCORM, HTML -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/module/download/FileDownloader.kt b/core/src/main/java/org/openedx/core/module/download/FileDownloader.kt index fe68e696f..350cad365 100644 --- a/core/src/main/java/org/openedx/core/module/download/FileDownloader.kt +++ b/core/src/main/java/org/openedx/core/module/download/FileDownloader.kt @@ -14,12 +14,14 @@ class FileDownloader : AbstractDownloader(), ProgressListener { private var firstUpdate = true override val client: OkHttpClient = OkHttpClient.Builder() - .addNetworkInterceptor(Interceptor { chain: Interceptor.Chain -> - val originalResponse: Response = chain.proceed(chain.request()) - originalResponse.newBuilder() - .body(ProgressResponseBody(originalResponse.body!!, this)) - .build() - }) + .addNetworkInterceptor( + Interceptor { chain: Interceptor.Chain -> + val originalResponse: Response = chain.proceed(chain.request()) + originalResponse.newBuilder() + .body(ProgressResponseBody(originalResponse.body!!, this)) + .build() + } + ) .build() var progressListener: CurrentProgress? = null @@ -42,7 +44,6 @@ class FileDownloader : AbstractDownloader(), ProgressListener { } } } - } interface CurrentProgress { @@ -54,5 +55,4 @@ interface DownloadApi { @Streaming @GET suspend fun downloadFile(@Url fileUrl: String): retrofit2.Response - } diff --git a/core/src/main/java/org/openedx/core/module/download/ProgressListener.kt b/core/src/main/java/org/openedx/core/module/download/ProgressListener.kt index 0acf4f320..53f2f2de3 100644 --- a/core/src/main/java/org/openedx/core/module/download/ProgressListener.kt +++ b/core/src/main/java/org/openedx/core/module/download/ProgressListener.kt @@ -2,7 +2,11 @@ package org.openedx.core.module.download import okhttp3.MediaType import okhttp3.ResponseBody -import okio.* +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer class ProgressResponseBody( private val responseBody: ResponseBody, @@ -38,12 +42,10 @@ class ProgressResponseBody( ) return bytesRead } - } } } - interface ProgressListener { fun update(bytesRead: Long, contentLength: Long, done: Boolean) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/course/CourseViewMode.kt b/core/src/main/java/org/openedx/core/presentation/course/CourseViewMode.kt index c2aaf97cb..8a73475ed 100644 --- a/core/src/main/java/org/openedx/core/presentation/course/CourseViewMode.kt +++ b/core/src/main/java/org/openedx/core/presentation/course/CourseViewMode.kt @@ -3,4 +3,4 @@ package org.openedx.core.presentation.course enum class CourseViewMode { FULL, VIDEOS -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/alert/ActionDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/alert/ActionDialogFragment.kt index 451d94915..28f357896 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/alert/ActionDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/alert/ActionDialogFragment.kt @@ -34,8 +34,8 @@ import org.openedx.core.config.Config import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.CoreAnalyticsEvent import org.openedx.core.presentation.CoreAnalyticsKey -import org.openedx.core.presentation.global.app_upgrade.DefaultTextButton -import org.openedx.core.presentation.global.app_upgrade.TransparentTextButton +import org.openedx.core.presentation.global.appupgrade.DefaultTextButton +import org.openedx.core.presentation.global.appupgrade.TransparentTextButton import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/alert/InfoDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/alert/InfoDialogFragment.kt index bc41d936d..77c413924 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/alert/InfoDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/alert/InfoDialogFragment.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import org.openedx.core.R -import org.openedx.core.presentation.global.app_upgrade.DefaultTextButton +import org.openedx.core.presentation.global.appupgrade.DefaultTextButton import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewManager.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewManager.kt index c825a8e9b..06a2a278a 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewManager.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewManager.kt @@ -17,11 +17,11 @@ class AppReviewManager( isDialogShowed = true val currentVersionName = reviewPreferences.formatVersionName(appData.versionName) // Check is app wasn't positive rated AND 2 minor OR 1 major app versions passed since the last review - if ( - !reviewPreferences.wasPositiveRated - && (currentVersionName.minorVersion - 2 >= reviewPreferences.lastReviewVersion.minorVersion - || currentVersionName.majorVersion - 1 >= reviewPreferences.lastReviewVersion.majorVersion) - ) { + val minorVersionPassed = + currentVersionName.minorVersion - 2 >= reviewPreferences.lastReviewVersion.minorVersion + val majorVersionPassed = + currentVersionName.majorVersion - 1 >= reviewPreferences.lastReviewVersion.majorVersion + if (!reviewPreferences.wasPositiveRated && (minorVersionPassed || majorVersionPassed)) { val dialog = RateDialogFragment.newInstance() dialog.show( supportFragmentManager, @@ -30,4 +30,4 @@ class AppReviewManager( } } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewUI.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewUI.kt index a1df55a05..632669c11 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewUI.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewUI.kt @@ -333,7 +333,7 @@ fun RatingBar( } .pointerInput(Unit) { detectTapGestures { offset -> - rating.intValue = round(offset.x / maxXValue * stars + 0.8f).toInt() + rating.intValue = round(x = offset.x / maxXValue * stars + 0.8f).toInt() } }, horizontalArrangement = Arrangement.Center @@ -418,4 +418,4 @@ private fun ThankYouDialogWithoutButtonsPreview() { onRateUsClick = {} ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt index 03d449c5f..1bb9d8156 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt @@ -76,7 +76,6 @@ class FeedbackDialogFragment : BaseAppReviewDialogFragment() { ) } - override fun dismiss() { onDismiss() } @@ -86,4 +85,4 @@ class FeedbackDialogFragment : BaseAppReviewDialogFragment() { return FeedbackDialogFragment() } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt index c8f49153c..945b81819 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt @@ -36,7 +36,7 @@ class RateDialogFragment : BaseAppReviewDialogFragment() { private fun onSubmitClick(rating: Int) { onSubmitRatingClick(rating) - if (rating > 3) { + if (rating > MIN_RATE) { openThankYouDialog() } else { openFeedbackDialog() @@ -66,8 +66,10 @@ class RateDialogFragment : BaseAppReviewDialogFragment() { } companion object { + private const val MIN_RATE = 3 + fun newInstance(): RateDialogFragment { return RateDialogFragment() } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt index 137672f45..9efdd694a 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt @@ -61,7 +61,7 @@ class ThankYouDialogFragment : BaseAppReviewDialogFragment() { private fun closeDialogDelay(isFeedbackPositive: Boolean) { if (!isFeedbackPositive) { lifecycleScope.launch { - delay(3000) + delay(timeMillis = 3000) dismiss() } } diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt index 4c5c4ce56..6e7a4c301 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt @@ -7,9 +7,9 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.DialogFragment -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendDialog -import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.AppUpdateState +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRecommendDialog +import org.openedx.core.ui.theme.OpenEdXTheme class AppUpgradeDialogFragment : DialogFragment() { @@ -48,5 +48,4 @@ class AppUpgradeDialogFragment : DialogFragment() { return AppUpgradeDialogFragment() } } - } diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt index 8eca02a99..6b7f5ffcf 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt @@ -129,5 +129,4 @@ class SelectBottomDialogFragment : BottomSheetDialogFragment() { return dialog } } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt index 84d6d1407..f215974ce 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt @@ -18,5 +18,4 @@ class SelectDialogViewModel( notifier.send(CourseSubtitleLanguageChanged(value)) } } - } diff --git a/core/src/main/java/org/openedx/core/presentation/global/FragmentViewBindingDelegate.kt b/core/src/main/java/org/openedx/core/presentation/global/FragmentViewBindingDelegate.kt index 35163150c..56708b051 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/FragmentViewBindingDelegate.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/FragmentViewBindingDelegate.kt @@ -40,7 +40,7 @@ class FragmentViewBindingDelegate( val lifecycle = fragment.viewLifecycleOwner.lifecycle if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { - throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") + error("Should not attempt to get bindings when Fragment views are destroyed.") } return viewBindingFactory(thisRef.requireView()).also { this.binding = it } @@ -54,4 +54,4 @@ inline fun AppCompatActivity.viewBinding( crossinline bindingInflater: (LayoutInflater) -> T, ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/InsetHolder.kt b/core/src/main/java/org/openedx/core/presentation/global/InsetHolder.kt index 26996f162..9224f09d1 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/InsetHolder.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/InsetHolder.kt @@ -1,8 +1,7 @@ package org.openedx.core.presentation.global - interface InsetHolder { val topInset: Int val bottomInset: Int val cutoutInset: Int -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/WhatsNewGlobalManager.kt b/core/src/main/java/org/openedx/core/presentation/global/WhatsNewGlobalManager.kt index e2cf46a4e..9c91d4ea3 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/WhatsNewGlobalManager.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/WhatsNewGlobalManager.kt @@ -2,4 +2,4 @@ package org.openedx.core.presentation.global interface WhatsNewGlobalManager { fun shouldShowWhatsNew(): Boolean -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpdateUI.kt similarity index 99% rename from core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt rename to core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpdateUI.kt index 3f8dd6fa9..e0cbae480 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpdateUI.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.global.app_upgrade +package org.openedx.core.presentation.global.appupgrade import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.UI_MODE_NIGHT_NO diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpgradeRouter.kt similarity index 68% rename from core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt rename to core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpgradeRouter.kt index 482c91093..fa4a13f80 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/AppUpgradeRouter.kt @@ -1,7 +1,7 @@ -package org.openedx.core.presentation.global.app_upgrade +package org.openedx.core.presentation.global.appupgrade import androidx.fragment.app.FragmentManager interface AppUpgradeRouter { fun navigateToUserProfile(fm: FragmentManager) -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/UpgradeRequiredFragment.kt similarity index 96% rename from core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt rename to core/src/main/java/org/openedx/core/presentation/global/appupgrade/UpgradeRequiredFragment.kt index da8685435..4176146c9 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/appupgrade/UpgradeRequiredFragment.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.global.app_upgrade +package org.openedx.core.presentation.global.appupgrade import android.os.Bundle import android.view.LayoutInflater @@ -8,8 +8,8 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult -import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.AppUpdateState +import org.openedx.core.ui.theme.OpenEdXTheme class UpgradeRequiredFragment : Fragment() { @@ -39,4 +39,4 @@ class UpgradeRequiredFragment : Fragment() { const val REQUEST_KEY = "UpgradeRequiredFragmentRequestKey" const val OPEN_ACCOUNT_SETTINGS_KEY = "openAccountSettings" } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/webview/WebContentFragment.kt b/core/src/main/java/org/openedx/core/presentation/global/webview/WebContentFragment.kt index 567a8ccce..17c9e20b2 100644 --- a/core/src/main/java/org/openedx/core/presentation/global/webview/WebContentFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/global/webview/WebContentFragment.kt @@ -34,7 +34,8 @@ class WebContentFragment : Fragment() { contentUrl = requireArguments().getString(ARG_URL, ""), onBackClick = { requireActivity().supportFragmentManager.popBackStack() - }) + } + ) } } } diff --git a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt index f53e27e90..15f94d338 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import org.openedx.core.R -import org.openedx.core.presentation.global.app_upgrade.TransparentTextButton +import org.openedx.core.presentation.global.appupgrade.TransparentTextButton import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes diff --git a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt index daab61fa5..4df3017e3 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt @@ -40,5 +40,5 @@ enum class CalendarSyncDialogType( LOADING_DIALOG( titleResId = R.string.core_title_syncing_calendar ), - NONE; + NONE } diff --git a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt index 660a52a94..b370cd56d 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt @@ -81,10 +81,11 @@ class VideoQualityFragment : Fragment() { val windowSize = rememberWindowSize() val title = stringResource( - id = if (viewModel.getQualityType() == VideoQualityType.Streaming) + id = if (viewModel.getQualityType() == VideoQualityType.Streaming) { R.string.core_video_streaming_quality - else + } else { R.string.core_video_download_quality + } ) val videoQuality by viewModel.videoQuality.observeAsState(viewModel.getCurrentVideoQuality()) @@ -97,7 +98,8 @@ class VideoQualityFragment : Fragment() { }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() - }) + } + ) } } } @@ -260,7 +262,7 @@ private fun VideoQualityScreenPreview() { title = "", selectedVideoQuality = VideoQuality.OPTION_720P, onQualityChanged = {}, - onBackClick = {}) + onBackClick = {} + ) } } - diff --git a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt index 2f8935e7a..95ecca130 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt @@ -29,9 +29,11 @@ class VideoQualityViewModel( } fun getCurrentVideoQuality(): VideoQuality { - return if (getQualityType() == VideoQualityType.Streaming) - preferencesManager.videoSettings.videoStreamingQuality else + return if (getQualityType() == VideoQualityType.Streaming) { + preferencesManager.videoSettings.videoStreamingQuality + } else { preferencesManager.videoSettings.videoDownloadQuality + } } fun setVideoQuality(quality: VideoQuality) { @@ -51,11 +53,11 @@ class VideoQualityViewModel( fun getQualityType() = VideoQualityType.valueOf(qualityType) private fun logVideoQualityChangedEvent(oldQuality: VideoQuality, newQuality: VideoQuality) { - val event = - if (getQualityType() == VideoQualityType.Streaming) + val event = if (getQualityType() == VideoQualityType.Streaming) { CoreAnalyticsEvent.VIDEO_STREAMING_QUALITY_CHANGED - else + } else { CoreAnalyticsEvent.VIDEO_DOWNLOAD_QUALITY_CHANGED + } analytics.logEvent( event.eventName, diff --git a/core/src/main/java/org/openedx/core/system/EdxError.kt b/core/src/main/java/org/openedx/core/system/EdxError.kt index f9ea93d56..bdc8692bd 100644 --- a/core/src/main/java/org/openedx/core/system/EdxError.kt +++ b/core/src/main/java/org/openedx/core/system/EdxError.kt @@ -7,4 +7,4 @@ sealed class EdxError : IOException() { class UserNotActiveException : EdxError() class ValidationException(val error: String) : EdxError() data class UnknownException(val error: String) : EdxError() -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/system/connection/NetworkConnection.kt b/core/src/main/java/org/openedx/core/system/connection/NetworkConnection.kt index 570ce2c71..12ec7a815 100644 --- a/core/src/main/java/org/openedx/core/system/connection/NetworkConnection.kt +++ b/core/src/main/java/org/openedx/core/system/connection/NetworkConnection.kt @@ -9,20 +9,14 @@ class NetworkConnection( ) { fun isOnline(): Boolean { - val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val capabilities = - connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) - if (capabilities != null) { - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - return true - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - return true - } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { - return true - } - } - return false + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + + return capabilities != null && ( + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + ) } fun isWifiConnected(): Boolean { @@ -37,5 +31,4 @@ class NetworkConnection( } return false } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseCompletionSet.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseCompletionSet.kt index ae2450a9c..038da1bd7 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseCompletionSet.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseCompletionSet.kt @@ -1,3 +1,3 @@ package org.openedx.core.system.notifier -class CourseCompletionSet : CourseEvent \ No newline at end of file +class CourseCompletionSet : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseEvent.kt index a79fe7e70..a45dd971c 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseEvent.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseEvent.kt @@ -1,3 +1,3 @@ package org.openedx.core.system.notifier -interface CourseEvent \ No newline at end of file +interface CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseStructureUpdated.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseStructureUpdated.kt index 0587f5eb4..c63cbdf94 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseStructureUpdated.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseStructureUpdated.kt @@ -2,4 +2,4 @@ package org.openedx.core.system.notifier class CourseStructureUpdated( val courseId: String -) : CourseEvent \ No newline at end of file +) : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/CourseVideoPositionChanged.kt b/core/src/main/java/org/openedx/core/system/notifier/CourseVideoPositionChanged.kt index af7a0583e..bdeba1114 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/CourseVideoPositionChanged.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/CourseVideoPositionChanged.kt @@ -4,4 +4,4 @@ data class CourseVideoPositionChanged( val videoUrl: String, val videoTime: Long, val isPlaying: Boolean -) : CourseEvent \ No newline at end of file +) : CourseEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/DownloadNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/DownloadNotifier.kt index 9c0c698cf..4ee889b0c 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/DownloadNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/DownloadNotifier.kt @@ -12,5 +12,4 @@ class DownloadNotifier { suspend fun send(event: DownloadProgressChanged) = channel.emit(event) suspend fun send(event: DownloadFailed) = channel.emit(event) - } diff --git a/core/src/main/java/org/openedx/core/system/notifier/DownloadProgressChanged.kt b/core/src/main/java/org/openedx/core/system/notifier/DownloadProgressChanged.kt index 474b25f2f..1e4d4a331 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/DownloadProgressChanged.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/DownloadProgressChanged.kt @@ -1,5 +1,7 @@ package org.openedx.core.system.notifier data class DownloadProgressChanged( - val id: String, val value: Long, val size: Long + val id: String, + val value: Long, + val size: Long ) : DownloadEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt index 804d84a65..d453abfb3 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt @@ -15,5 +15,4 @@ class AppNotifier { suspend fun send(event: LogoutEvent) = channel.emit(event) suspend fun send(event: AppUpgradeEvent) = channel.emit(event) - } diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt index 81dba6177..89451c744 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt @@ -1,6 +1,6 @@ package org.openedx.core.system.notifier.app -sealed class AppUpgradeEvent: AppEvent { - object UpgradeRequiredEvent : AppUpgradeEvent() +sealed class AppUpgradeEvent : AppEvent { + data object UpgradeRequiredEvent : AppUpgradeEvent() class UpgradeRecommendedEvent(val newVersionName: String) : AppUpgradeEvent() } diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 23f0d3315..8e5a75cc1 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1,5 +1,6 @@ package org.openedx.core.ui +import android.os.Build import android.os.Build.VERSION.SDK_INT import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi @@ -131,19 +132,21 @@ fun StaticSearchBar( Row( modifier = modifier .testTag("tf_search") - .then(Modifier - .background( - MaterialTheme.appColors.textFieldBackground, - MaterialTheme.appShapes.textFieldShape - ) - .clip(MaterialTheme.appShapes.textFieldShape) - .border( - 1.dp, - MaterialTheme.appColors.textFieldBorder, - MaterialTheme.appShapes.textFieldShape - ) - .clickable { onClick() } - .padding(horizontal = 20.dp)), + .then( + Modifier + .background( + MaterialTheme.appColors.textFieldBackground, + MaterialTheme.appShapes.textFieldShape + ) + .clip(MaterialTheme.appShapes.textFieldShape) + .border( + 1.dp, + MaterialTheme.appColors.textFieldBorder, + MaterialTheme.appShapes.textFieldShape + ) + .clickable { onClick() } + .padding(horizontal = 20.dp) + ), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -264,7 +267,11 @@ fun SearchBar( }, colors = TextFieldDefaults.outlinedTextFieldColors( textColor = MaterialTheme.appColors.textPrimary, - backgroundColor = if (isFocused) MaterialTheme.appColors.background else MaterialTheme.appColors.textFieldBackground, + backgroundColor = if (isFocused) { + MaterialTheme.appColors.background + } else { + MaterialTheme.appColors.textFieldBackground + }, focusedBorderColor = MaterialTheme.appColors.primary, unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, cursorColor = MaterialTheme.appColors.primary, @@ -355,7 +362,11 @@ fun SearchBarStateless( }, colors = TextFieldDefaults.outlinedTextFieldColors( textColor = MaterialTheme.appColors.textPrimary, - backgroundColor = if (isFocused) MaterialTheme.appColors.background else MaterialTheme.appColors.textFieldBackground, + backgroundColor = if (isFocused) { + MaterialTheme.appColors.background + } else { + MaterialTheme.appColors.textFieldBackground + }, focusedBorderColor = MaterialTheme.appColors.primary, unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, cursorColor = MaterialTheme.appColors.primary, @@ -450,7 +461,6 @@ fun HyperlinkText( ) for ((key, value) in hyperLinks) { - val startIndex = fullText.indexOf(key) val endIndex = startIndex + key.length addStyle( @@ -584,7 +594,7 @@ fun HyperlinkImageText( val context = LocalContext.current val imageLoader = ImageLoader.Builder(context) .components { - if (SDK_INT >= 28) { + if (SDK_INT >= Build.VERSION_CODES.P) { add(ImageDecoderDecoder.Factory()) } else { add(GifDecoder.Factory()) @@ -660,15 +670,21 @@ fun SheetContent( }, onValueChanged = { textField -> searchValueChanged(textField) - }, onClearValue = { + }, + onClearValue = { searchValueChanged("") } ) Spacer(Modifier.height(10.dp)) - LazyColumn(Modifier.fillMaxSize(), listState) { - items(expandedList.filter { - it.name.startsWith(searchValue.text, true) - }) { item -> + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState + ) { + items( + expandedList.filter { + it.name.startsWith(searchValue.text, true) + } + ) { item -> Text( modifier = Modifier .testTag("txt_${item.value.tagId()}_title") @@ -723,15 +739,20 @@ fun SheetContent( }, onValueChanged = { textField -> searchValueChanged(textField) - }, onClearValue = { + }, + onClearValue = { searchValueChanged("") } ) Spacer(Modifier.height(10.dp)) - LazyColumn(Modifier.fillMaxWidth()) { - items(expandedList.filter { - it.first.startsWith(searchValue.text, true) - }) { item -> + LazyColumn( + Modifier.fillMaxWidth() + ) { + items( + expandedList.filter { + it.first.startsWith(searchValue.text, true) + } + ) { item -> Text( modifier = Modifier .fillMaxWidth() @@ -891,7 +912,7 @@ fun IconText( Icon( modifier = Modifier .testTag("ic_${text.tagId()}") - .size((textStyle.fontSize.value + 4).dp), + .size(size = (textStyle.fontSize.value + 4).dp), imageVector = icon, contentDescription = null, tint = color @@ -929,7 +950,7 @@ fun IconText( Icon( modifier = Modifier .testTag("ic_${text.tagId()}") - .size((textStyle.fontSize.value + 4).dp), + .size(size = (textStyle.fontSize.value + 4).dp), painter = painter, contentDescription = null, tint = color @@ -965,7 +986,7 @@ fun TextIcon( ) { Text(text = text, color = color, style = textStyle) Icon( - modifier = iconModifier ?: Modifier.size((textStyle.fontSize.value + 4).dp), + modifier = iconModifier ?: Modifier.size(size = (textStyle.fontSize.value + 4).dp), imageVector = icon, contentDescription = null, tint = color @@ -995,7 +1016,7 @@ fun TextIcon( Text(text = text, color = color, style = textStyle) Icon( modifier = iconModifier - .size((textStyle.fontSize.value + 4).dp), + .size(size = (textStyle.fontSize.value + 4).dp), painter = painter, contentDescription = null, tint = color @@ -1032,7 +1053,8 @@ fun OfflineModeDialog( modifier = Modifier.size(20.dp), onClick = { onReloadClick() - }) { + } + ) { Icon( modifier = Modifier.size(20.dp), painter = painterResource(R.drawable.core_ic_reload), @@ -1044,7 +1066,8 @@ fun OfflineModeDialog( modifier = Modifier.size(20.dp), onClick = { onDismissCLick() - }) { + } + ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Filled.Close, @@ -1133,8 +1156,12 @@ fun BackBtn( tint: Color = MaterialTheme.appColors.primary, onBackClick: () -> Unit, ) { - IconButton(modifier = modifier.testTag("ib_back"), - onClick = { onBackClick() }) { + IconButton( + modifier = modifier.testTag("ib_back"), + onClick = { + onBackClick() + } + ) { Icon( painter = painterResource(id = R.drawable.core_ic_back), contentDescription = stringResource(id = R.string.core_accessibility_btn_back), @@ -1169,7 +1196,7 @@ fun FullScreenErrorView( ) Spacer(Modifier.height(28.dp)) Text( - modifier = Modifier.fillMaxWidth(0.8f), + modifier = Modifier.fillMaxWidth(fraction = 0.8f), text = stringResource(id = errorType.titleResId), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleLarge, @@ -1177,7 +1204,7 @@ fun FullScreenErrorView( ) Spacer(Modifier.height(16.dp)) Text( - modifier = Modifier.fillMaxWidth(0.8f), + modifier = Modifier.fillMaxWidth(fraction = 0.8f), text = stringResource(id = errorType.descriptionResId), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.bodyLarge, @@ -1220,7 +1247,7 @@ fun NoContentScreen(message: String, icon: Painter) { ) Spacer(Modifier.height(24.dp)) Text( - modifier = Modifier.fillMaxWidth(0.8f), + modifier = Modifier.fillMaxWidth(fraction = 0.8f), text = message, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.bodyMedium, @@ -1255,7 +1282,9 @@ fun AuthButtonsPanel( .testTag("btn_sign_in") .then( if (showRegisterButton) { - Modifier.width(100.dp).padding(start = 16.dp) + Modifier + .width(100.dp) + .padding(start = 16.dp) } else { Modifier.weight(1f) } @@ -1294,15 +1323,25 @@ fun RoundTabsBar( ) { itemsIndexed(items) { index, item -> val isSelected = pagerState.currentPage == index - val backgroundColor = - if (isSelected) MaterialTheme.appColors.primary else MaterialTheme.appColors.tabUnselectedBtnBackground - val contentColor = - if (isSelected) MaterialTheme.appColors.tabSelectedBtnContent else MaterialTheme.appColors.tabUnselectedBtnContent - val border = if (!isSystemInDarkTheme()) Modifier.border( - 1.dp, - MaterialTheme.appColors.primary, - CircleShape - ) else Modifier + val backgroundColor = if (isSelected) { + MaterialTheme.appColors.primary + } else { + MaterialTheme.appColors.tabUnselectedBtnBackground + } + val contentColor = if (isSelected) { + MaterialTheme.appColors.tabSelectedBtnContent + } else { + MaterialTheme.appColors.tabUnselectedBtnContent + } + val border = if (!isSystemInDarkTheme()) { + Modifier.border( + 1.dp, + MaterialTheme.appColors.primary, + CircleShape + ) + } else { + Modifier + } RoundTab( modifier = Modifier 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 5165619b6..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,12 +38,11 @@ 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 +const val KEYBOARD_VISIBILITY_THRESHOLD = 0.15f + inline val isPreview: Boolean @ReadOnlyComposable @Composable @@ -107,7 +104,8 @@ fun Modifier.displayCutoutForLandscape(): Modifier = composed { inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed { this then Modifier.clickable( indication = null, - interactionSource = remember { MutableInteractionSource() }) { + interactionSource = remember { MutableInteractionSource() } + ) { onClick() } } @@ -167,7 +165,7 @@ fun isImeVisibleState(): State { view.getWindowVisibleDisplayFrame(rect) val screenHeight = view.rootView.height val keypadHeight = screenHeight - rect.bottom - keyboardState.value = keypadHeight > screenHeight * 0.15 + keyboardState.value = keypadHeight > screenHeight * KEYBOARD_VISIBILITY_THRESHOLD } view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) @@ -179,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/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt index 807acd918..70f320368 100644 --- a/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt +++ b/core/src/main/java/org/openedx/core/ui/WebContentScreen.kt @@ -65,7 +65,6 @@ fun WebContentScreen( scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -114,7 +113,8 @@ fun WebContentScreen( contentUrl = contentUrl, onWebPageLoaded = { webViewAlpha = 1f - }) + } + ) } } } @@ -147,10 +147,7 @@ private fun WebViewContent( request: WebResourceRequest? ): Boolean { val clickUrl = request?.url?.toString() ?: "" - return if (clickUrl.isNotEmpty() && - (clickUrl.startsWith("http://") || - clickUrl.startsWith("https://")) - ) { + return if (clickUrl.isNotEmpty() && clickUrl.startsWith("http")) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true } else if (clickUrl.startsWith("mailto:")) { diff --git a/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt b/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt index 37783b820..12da2cfce 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/AppColors.kt @@ -42,7 +42,7 @@ data class AppColors( val bottomSheetToggle: Color, val warning: Color, val info: Color, - val info_variant: Color, + val infoVariant: Color, val onWarning: Color, val onInfo: Color, diff --git a/core/src/main/java/org/openedx/core/ui/theme/Shape.kt b/core/src/main/java/org/openedx/core/ui/theme/AppShapes.kt similarity index 100% rename from core/src/main/java/org/openedx/core/ui/theme/Shape.kt rename to core/src/main/java/org/openedx/core/ui/theme/AppShapes.kt diff --git a/core/src/main/java/org/openedx/core/ui/theme/Type.kt b/core/src/main/java/org/openedx/core/ui/theme/AppTypography.kt similarity index 100% rename from core/src/main/java/org/openedx/core/ui/theme/Type.kt rename to core/src/main/java/org/openedx/core/ui/theme/AppTypography.kt diff --git a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt index 8fe1eb8ff..2ad2a4eae 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt @@ -60,7 +60,7 @@ private val DarkColorPalette = AppColors( warning = dark_warning, info = dark_info, - info_variant = dark_info_variant, + infoVariant = dark_info_variant, onWarning = dark_onWarning, onInfo = dark_onInfo, @@ -149,7 +149,7 @@ private val LightColorPalette = AppColors( warning = light_warning, info = light_info, - info_variant = light_info_variant, + infoVariant = light_info_variant, onWarning = light_onWarning, onInfo = light_onInfo, @@ -204,7 +204,7 @@ fun OpenEdXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composabl MaterialTheme( colors = colors.material, - //typography = LocalTypography.current.material, + // typography = LocalTypography.current.material, shapes = LocalShapes.current.material, ) { CompositionLocalProvider( diff --git a/core/src/main/java/org/openedx/core/utils/EmailUtil.kt b/core/src/main/java/org/openedx/core/utils/EmailUtil.kt index c56b606ae..c163240ff 100644 --- a/core/src/main/java/org/openedx/core/utils/EmailUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/EmailUtil.kt @@ -55,15 +55,16 @@ object EmailUtil { targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) it.startActivity(targetIntent) } - } catch (ex: ActivityNotFoundException) { - //There is no activity which can perform the intended share Intent + } catch (e: ActivityNotFoundException) { + // There is no activity which can perform the intended share Intent + e.printStackTrace() context?.let { Toast.makeText( - it, it.getString(R.string.core_email_client_not_present), + it, + it.getString(R.string.core_email_client_not_present), Toast.LENGTH_SHORT ).show() } } } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/utils/IOUtils.kt b/core/src/main/java/org/openedx/core/utils/IOUtils.kt index 0405168d4..2c3ee5870 100644 --- a/core/src/main/java/org/openedx/core/utils/IOUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/IOUtils.kt @@ -17,5 +17,4 @@ object IOUtils { fun copy(input: InputStream, out: OutputStream) { out.sink().buffer().writeAll(input.source()) } - } diff --git a/core/src/main/java/org/openedx/core/utils/LocaleUtils.kt b/core/src/main/java/org/openedx/core/utils/LocaleUtils.kt index dd2e4531c..b6ae624f5 100644 --- a/core/src/main/java/org/openedx/core/utils/LocaleUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/LocaleUtils.kt @@ -3,10 +3,13 @@ package org.openedx.core.utils import org.openedx.core.AppDataConstants.USER_MAX_YEAR import org.openedx.core.AppDataConstants.defaultLocale import org.openedx.core.domain.model.RegistrationField -import java.util.* +import java.util.Calendar +import java.util.Locale object LocaleUtils { + private const val MIN_USER_AGE = 13 + fun getBirthYearsRange(): List { val currentYear = Calendar.getInstance().get(Calendar.YEAR) return (currentYear - USER_MAX_YEAR..currentYear - 0).reversed().map { @@ -17,7 +20,7 @@ object LocaleUtils { fun isProfileLimited(inputYear: String?): Boolean { val currentYear = Calendar.getInstance().get(Calendar.YEAR) return if (!inputYear.isNullOrEmpty()) { - currentYear - inputYear.toInt() < 13 + currentYear - inputYear.toInt() < MIN_USER_AGE } else { true } @@ -53,7 +56,6 @@ object LocaleUtils { .sortedBy { it.name } .toList() - private fun getAvailableLanguages() = Locale.getISOLanguages() .asSequence() .filter { it.length == 2 } @@ -66,5 +68,4 @@ object LocaleUtils { fun getDisplayLanguage(languageCode: String): String { return Locale(languageCode, "").getDisplayLanguage(defaultLocale) } - } diff --git a/core/src/main/java/org/openedx/core/utils/Sha1Util.kt b/core/src/main/java/org/openedx/core/utils/Sha1Util.kt index 13a877e68..9839550e7 100644 --- a/core/src/main/java/org/openedx/core/utils/Sha1Util.kt +++ b/core/src/main/java/org/openedx/core/utils/Sha1Util.kt @@ -13,19 +13,28 @@ object Sha1Util { val sha1hash = md.digest() convertToHex(sha1hash) } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() text } catch (e: UnsupportedEncodingException) { + e.printStackTrace() text } } + @Suppress("MagicNumber") fun convertToHex(data: ByteArray): String { val buf = StringBuilder() for (b in data) { var halfbyte = b.toInt() ushr 4 and 0x0F var twoHalfs = 0 do { - buf.append(if (halfbyte in 0..9) ('0'.code + halfbyte).toChar() else ('a'.code + (halfbyte - 10)).toChar()) + buf.append( + if (halfbyte in 0..9) { + ('0'.code + halfbyte).toChar() + } else { + ('a'.code + (halfbyte - 10)).toChar() + } + ) halfbyte = b.toInt() and 0x0F } while (twoHalfs++ < 1) } diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index a2fb3cfc7..d9fe2f853 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -13,8 +13,9 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale -import java.util.concurrent.TimeUnit +import kotlin.math.absoluteValue +@Suppress("MagicNumber") object TimeUtils { private const val FORMAT_ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'" @@ -101,9 +102,13 @@ object TimeUtils { fun iso8601ToDateWithTime(context: Context, text: String): String { return try { - val courseDateFormat = SimpleDateFormat(FORMAT_ISO_8601, Locale.getDefault()) + val courseDateFormat = SimpleDateFormat( + FORMAT_ISO_8601, + Locale.getDefault() + ) val applicationDateFormat = SimpleDateFormat( - context.getString(R.string.core_full_date_with_time), Locale.getDefault() + context.getString(R.string.core_full_date_with_time), + Locale.getDefault() ) applicationDateFormat.format(courseDateFormat.parse(text)!!) } catch (e: Exception) { @@ -114,7 +119,8 @@ object TimeUtils { private fun dateToCourseDate(resourceManager: ResourceManager, date: Date?): String { return formatDate( - format = resourceManager.getString(R.string.core_date_format_MMM_dd_yyyy), date = date + format = resourceManager.getString(R.string.core_date_format_MMM_dd_yyyy), + date = date ) } @@ -147,83 +153,146 @@ object TimeUtils { startType: String, startDisplay: String ): String { - val formattedDate: String val resourceManager = ResourceManager(context) - if (isDatePassed(today, start)) { - if (expiry != null) { - val dayDifferenceInMillis = if (today.after(expiry)) { - today.time - expiry.time - } else { - expiry.time - today.time - } + return when { + isDatePassed(today, start) -> handleDatePassedToday( + resourceManager, + today, + expiry, + start, + end, + startType, + startDisplay + ) - if (isDatePassed(today, expiry)) { - formattedDate = if (dayDifferenceInMillis > SEVEN_DAYS_IN_MILLIS) { - resourceManager.getString( - R.string.core_label_expired_on, - dateToCourseDate(resourceManager, expiry) - ) - } else { - val timeSpan = DateUtils.getRelativeTimeSpanString( - expiry.time, - today.time, - DateUtils.SECOND_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE - ).toString() - resourceManager.getString(R.string.core_label_access_expired, timeSpan) - } - } else { - formattedDate = if (dayDifferenceInMillis > SEVEN_DAYS_IN_MILLIS) { - resourceManager.getString( - R.string.core_label_expires, - dateToCourseDate(resourceManager, expiry) - ) - } else { - val timeSpan = DateUtils.getRelativeTimeSpanString( - expiry.time, - today.time, - DateUtils.SECOND_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE - ).toString() - resourceManager.getString(R.string.core_label_expires, timeSpan) - } - } - } else { - formattedDate = if (end == null) { - if (startType == StartType.TIMESTAMP.type && start != null) { - resourceManager.getString( - R.string.core_label_starting, dateToCourseDate(resourceManager, start) - ) - } else if (startType == StartType.STRING.type && start != null) { - resourceManager.getString(R.string.core_label_starting, startDisplay) - } else { - val soon = resourceManager.getString(R.string.core_assessment_soon) - resourceManager.getString(R.string.core_label_starting, soon) - } - } else if (isDatePassed(today, end)) { + else -> handleDateNotPassedToday(resourceManager, start, startType, startDisplay) + } + } + + private fun handleDatePassedToday( + resourceManager: ResourceManager, + today: Date, + expiry: Date?, + start: Date?, + end: Date?, + startType: String, + startDisplay: String + ): String { + return when { + expiry != null -> handleExpiry(resourceManager, today, expiry) + else -> handleNoExpiry(resourceManager, today, start, end, startType, startDisplay) + } + } + + private fun handleExpiry(resourceManager: ResourceManager, today: Date, expiry: Date): String { + val dayDifferenceInMillis = (today.time - expiry.time).absoluteValue + + return when { + isDatePassed(today, expiry) -> { + if (dayDifferenceInMillis > SEVEN_DAYS_IN_MILLIS) { resourceManager.getString( - R.string.core_label_ended, dateToCourseDate(resourceManager, end) + R.string.core_label_expired_on, + dateToCourseDate(resourceManager, expiry) ) } else { + val timeSpan = DateUtils.getRelativeTimeSpanString( + expiry.time, + today.time, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ).toString() + resourceManager.getString(R.string.core_label_access_expired, timeSpan) + } + } + + else -> { + if (dayDifferenceInMillis > SEVEN_DAYS_IN_MILLIS) { resourceManager.getString( - R.string.core_label_ends, dateToCourseDate(resourceManager, end) + R.string.core_label_expires, + dateToCourseDate(resourceManager, expiry) ) + } else { + val timeSpan = DateUtils.getRelativeTimeSpanString( + expiry.time, + today.time, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ).toString() + resourceManager.getString(R.string.core_label_expires, timeSpan) } } - } else { - formattedDate = if (startType == StartType.TIMESTAMP.type && start != null) { - resourceManager.getString( - R.string.core_label_starting, dateToCourseDate(resourceManager, start) - ) - } else if (startType == StartType.STRING.type && start != null) { - resourceManager.getString(R.string.core_label_starting, startDisplay) - } else { + } + } + + private fun handleNoExpiry( + resourceManager: ResourceManager, + today: Date, + start: Date?, + end: Date?, + startType: String, + startDisplay: String + ): String { + return when { + end == null -> handleNoEndDate(resourceManager, start, startType, startDisplay) + isDatePassed(today, end) -> resourceManager.getString( + R.string.core_label_ended, + dateToCourseDate(resourceManager, end) + ) + + else -> resourceManager.getString( + R.string.core_label_ends, + dateToCourseDate(resourceManager, end) + ) + } + } + + private fun handleDateNotPassedToday( + resourceManager: ResourceManager, + start: Date?, + startType: String, + startDisplay: String + ): String { + return when { + startType == StartType.TIMESTAMP.type && start != null -> resourceManager.getString( + R.string.core_label_starting, + dateToCourseDate(resourceManager, start) + ) + + startType == StartType.STRING.type && start != null -> resourceManager.getString( + R.string.core_label_starting, + startDisplay + ) + + else -> { + val soon = resourceManager.getString(R.string.core_assessment_soon) + resourceManager.getString(R.string.core_label_starting, soon) + } + } + } + + private fun handleNoEndDate( + resourceManager: ResourceManager, + start: Date?, + startType: String, + startDisplay: String + ): String { + return when { + startType == StartType.TIMESTAMP.type && start != null -> resourceManager.getString( + R.string.core_label_starting, + dateToCourseDate(resourceManager, start) + ) + + startType == StartType.STRING.type && start != null -> resourceManager.getString( + R.string.core_label_starting, + startDisplay + ) + + else -> { val soon = resourceManager.getString(R.string.core_assessment_soon) resourceManager.getString(R.string.core_label_starting, soon) } } - return formattedDate } /** diff --git a/core/src/main/java/org/openedx/core/utils/VideoUtil.kt b/core/src/main/java/org/openedx/core/utils/VideoUtil.kt index cb24868af..b86674068 100644 --- a/core/src/main/java/org/openedx/core/utils/VideoUtil.kt +++ b/core/src/main/java/org/openedx/core/utils/VideoUtil.kt @@ -40,5 +40,4 @@ object VideoUtil { } return false } - -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt index 39a6c5507..d7c7d12a7 100644 --- a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -51,6 +51,7 @@ class CalendarSyncWorker( tryToSyncCalendar(courseId) Result.success() } catch (e: Exception) { + e.printStackTrace() calendarNotifier.send(CalendarSyncFailed) Result.failure() } @@ -61,8 +62,11 @@ class CalendarSyncWorker( createChannel() } val serviceType = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + } return ForegroundInfo( NOTIFICATION_ID, diff --git a/core/src/openedx/org/openedx/core/ui/theme/Colors.kt b/core/src/openedx/org/openedx/core/ui/theme/Colors.kt index 69f550018..d2618e6b0 100644 --- a/core/src/openedx/org/openedx/core/ui/theme/Colors.kt +++ b/core/src/openedx/org/openedx/core/ui/theme/Colors.kt @@ -75,7 +75,6 @@ val light_settings_title_content = Color.White val light_progress_bar_color = light_primary val light_progress_bar_background_color = Color(0xFFCCD4E0) - val dark_primary = Color(0xFF3F68F8) val dark_primary_variant = Color(0xFF3700B3) val dark_secondary = Color(0xFF03DAC6) diff --git a/core/src/openedx/org/openedx/core/ui/theme/compose/SignInLogoView.kt b/core/src/openedx/org/openedx/core/ui/theme/compose/SignInLogoView.kt index f1f0a9d04..78d523b79 100644 --- a/core/src/openedx/org/openedx/core/ui/theme/compose/SignInLogoView.kt +++ b/core/src/openedx/org/openedx/core/ui/theme/compose/SignInLogoView.kt @@ -19,7 +19,7 @@ fun SignInLogoView() { Box( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(0.2f), + .fillMaxHeight(fraction = 0.2f), contentAlignment = Alignment.Center ) { Image( diff --git a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt index f79e46066..d9034e4ef 100644 --- a/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt +++ b/course/src/main/java/org/openedx/course/data/repository/CourseRepository.kt @@ -58,7 +58,6 @@ class CourseRepository( ) courseDao.insertCourseStructureEntity(response.mapToRoomEntity()) courseStructure[courseId] = response.mapToDomain() - } else { val cachedCourseStructure = courseDao.getCourseStructureById(courseId) if (cachedCourseStructure != null) { diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt index f71c8593f..8daa7fb13 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseConverter.kt @@ -69,5 +69,4 @@ class CourseConverter { val type = genericType>() return Gson().fromJson(value, type) } - } diff --git a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt index e91b309c3..fdbcdd204 100644 --- a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt @@ -32,37 +32,40 @@ class CourseInteractor( val courseStructure = repository.getCourseStructure(courseId, isNeedRefresh) val blocks = courseStructure.blockData val videoBlocks = blocks.filter { it.type == BlockType.VIDEO } - val resultBlocks = ArrayList() + val resultBlocks = mutableListOf() + videoBlocks.forEach { videoBlock -> - val verticalBlock = blocks.firstOrNull { it.descendants.contains(videoBlock.id) } - if (verticalBlock != null) { - val sequentialBlock = - blocks.firstOrNull { it.descendants.contains(verticalBlock.id) } - if (sequentialBlock != null) { - val chapterBlock = - blocks.firstOrNull { it.descendants.contains(sequentialBlock.id) } - if (chapterBlock != null) { - resultBlocks.add(videoBlock) - val verticalIndex = resultBlocks.indexOfFirst { it.id == verticalBlock.id } - if (verticalIndex == -1) { - resultBlocks.add(verticalBlock.copy(descendants = listOf(videoBlock.id))) - } else { - val block = resultBlocks[verticalIndex] - resultBlocks[verticalIndex] = - block.copy(descendants = block.descendants + videoBlock.id) - } - if (!resultBlocks.contains(sequentialBlock)) { - resultBlocks.add(sequentialBlock) - } - if (!resultBlocks.contains(chapterBlock)) { - resultBlocks.add(chapterBlock) - } - } - } - - } + val verticalBlock = findParentBlock(videoBlock.id, blocks) ?: return@forEach + val sequentialBlock = findParentBlock(verticalBlock.id, blocks) ?: return@forEach + val chapterBlock = findParentBlock(sequentialBlock.id, blocks) ?: return@forEach + + addToResultBlocks(videoBlock, verticalBlock, resultBlocks) + addIfAbsent(resultBlocks, sequentialBlock) + addIfAbsent(resultBlocks, chapterBlock) + } + + return courseStructure.copy(blockData = resultBlocks) + } + + private fun findParentBlock(childId: String, blocks: List): Block? { + return blocks.firstOrNull { it.descendants.contains(childId) } + } + + private fun addToResultBlocks(videoBlock: Block, verticalBlock: Block, resultBlocks: MutableList) { + resultBlocks.add(videoBlock) + val verticalIndex = resultBlocks.indexOfFirst { it.id == verticalBlock.id } + if (verticalIndex == -1) { + resultBlocks.add(verticalBlock.copy(descendants = listOf(videoBlock.id))) + } else { + val block = resultBlocks[verticalIndex] + resultBlocks[verticalIndex] = block.copy(descendants = block.descendants + videoBlock.id) + } + } + + private fun addIfAbsent(resultBlocks: MutableList, block: Block) { + if (!resultBlocks.contains(block)) { + resultBlocks.add(block) } - return courseStructure.copy(blockData = resultBlocks.toList()) } suspend fun getCourseStatus(courseId: String) = repository.getCourseStatus(courseId) diff --git a/course/src/main/java/org/openedx/course/presentation/ChapterEndFragmentDialog.kt b/course/src/main/java/org/openedx/course/presentation/ChapterEndFragmentDialog.kt index 13380ddde..376f06c90 100644 --- a/course/src/main/java/org/openedx/course/presentation/ChapterEndFragmentDialog.kt +++ b/course/src/main/java/org/openedx/course/presentation/ChapterEndFragmentDialog.kt @@ -58,7 +58,7 @@ class ChapterEndFragmentDialog : DialogFragment() { override fun onResume() { super.onResume() if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - setWidthPercent(66) + setWidthPercent(percentage = 66) } } @@ -155,7 +155,7 @@ private fun ChapterEndDialogScreen( ) { Card( modifier = Modifier - .fillMaxWidth(0.95f) + .fillMaxWidth(fraction = 0.95f) .clip(MaterialTheme.appShapes.courseImageShape), backgroundColor = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.courseImageShape @@ -245,7 +245,6 @@ private fun ChapterEndDialogScreen( } } - @Composable private fun ChapterEndDialogScreenLandscape( sectionName: String, diff --git a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt index 65ce5f012..1f874e055 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -56,7 +56,9 @@ interface CourseRouter { ) fun navigateToHandoutsWebView( - fm: FragmentManager, courseId: String, type: HandoutsType + fm: FragmentManager, + courseId: String, + type: HandoutsType ) fun navigateToDownloadQueue(fm: FragmentManager, descendants: List = arrayListOf()) diff --git a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt index c4d1bd844..1e60b1680 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CollapsingLayout.kt @@ -5,7 +5,6 @@ import android.graphics.Bitmap import android.os.Build import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -74,6 +73,12 @@ import org.openedx.core.ui.theme.appColors import org.openedx.foundation.presentation.rememberWindowSize import kotlin.math.roundToInt +private const val FLING_DELAY = 50L +private const val SCROLL_UP_THRESHOLD = 0.15f +private const val SCROLL_DOWN_THRESHOLD = 0.85f +private const val SHADE_HEIGHT_MULTIPLIER = 0.1f +private const val BLUR_PADDING_FACTOR = 3 + @Composable internal fun CollapsingLayout( modifier: Modifier = Modifier, @@ -109,7 +114,7 @@ internal fun CollapsingLayout( val blurImagePaddingPx = with(localDensity) { blurImagePadding.toPx() } val toolbarOffset = (offset.value + backgroundImageHeight.floatValue - blurImagePaddingPx).roundToInt() - val imageStartY = (backgroundImageHeight.floatValue - blurImagePaddingPx) * 0.5f + val imageStartY = (backgroundImageHeight.floatValue - blurImagePaddingPx) / 2f val imageOffsetY = -(offset.value + imageStartY) val toolbarBackgroundOffset = if (toolbarOffset >= 0) { toolbarOffset @@ -187,24 +192,24 @@ internal fun CollapsingLayout( val yEnd = change.position.y val yDelta = yEnd - yStart val scrollDown = yDelta > 0 - val collapsedOffset = - -expandedTopHeight.floatValue - backgroundImageHeight.floatValue + collapsedTopHeight.floatValue + val collapsedOffset = -expandedTopHeight.floatValue - backgroundImageHeight.floatValue + + collapsedTopHeight.floatValue val expandedOffset = 0f launch { // Handle Fling, offset.animateTo does not work if the value changes faster than 10ms - if (change.uptimeMillis - change.previousUptimeMillis <= 50) { - delay(50) + if (change.uptimeMillis - change.previousUptimeMillis <= FLING_DELAY) { + delay(FLING_DELAY) } if (scrollDown) { - if (offset.value > -backgroundImageHeight.floatValue * 0.85) { + if (offset.value > -backgroundImageHeight.floatValue * SCROLL_DOWN_THRESHOLD) { offset.animateTo(expandedOffset) } else { offset.animateTo(collapsedOffset) } } else { - if (offset.value < -backgroundImageHeight.floatValue * 0.15) { + if (offset.value < -backgroundImageHeight.floatValue * SCROLL_UP_THRESHOLD) { offset.animateTo(collapsedOffset) } else { offset.animateTo(expandedOffset) @@ -306,7 +311,11 @@ private fun CollapsingLayoutTablet( modifier = Modifier .background(MaterialTheme.appColors.surface) .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value).toDp() } + blurImagePadding) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value).toDp() + blurImagePadding + } + ) .align(Alignment.Center) ) Image( @@ -322,7 +331,11 @@ private fun CollapsingLayoutTablet( modifier = Modifier .background(MaterialTheme.appColors.courseHomeHeaderShade) .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value).toDp() } * 0.1f) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value).toDp() * SHADE_HEIGHT_MULTIPLIER + } + ) .align(Alignment.BottomCenter) ) } @@ -362,7 +375,11 @@ private fun CollapsingLayoutTablet( Box( modifier = Modifier .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value).toDp() } + blurImagePadding) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value).toDp() + blurImagePadding + } + ) .offset { IntOffset( x = 0, @@ -402,7 +419,6 @@ private fun CollapsingLayoutTablet( contentDescription = null ) - Box( modifier = Modifier .offset { @@ -473,7 +489,11 @@ private fun CollapsingLayoutMobile( modifier = Modifier .background(MaterialTheme.appColors.surface) .fillMaxWidth() - .height(with(localDensity) { (collapsedTopHeight.value + navigationHeight.value).toDp() } + blurImagePadding) + .height( + with(localDensity) { + (collapsedTopHeight.value + navigationHeight.value).toDp() + blurImagePadding + } + ) .align(Alignment.Center) ) Image( @@ -490,7 +510,11 @@ private fun CollapsingLayoutMobile( modifier = Modifier .background(MaterialTheme.appColors.courseHomeHeaderShade) .fillMaxWidth() - .height(with(localDensity) { (collapsedTopHeight.value + navigationHeight.value).toDp() } * 0.1f) + .height( + with(localDensity) { + (collapsedTopHeight.value + navigationHeight.value).toDp() * SHADE_HEIGHT_MULTIPLIER + } + ) .align(Alignment.BottomCenter) ) } @@ -549,7 +573,6 @@ private fun CollapsingLayoutMobile( ) } - Box( modifier = Modifier .displayCutoutForLandscape() @@ -590,12 +613,16 @@ private fun CollapsingLayoutMobile( .background(Color.White) .blur(100.dp) ) { - val adaptiveBlurImagePadding = blurImagePadding.value * (3 - rawFactor) + val adaptiveBlurImagePadding = blurImagePadding.value * (BLUR_PADDING_FACTOR - rawFactor) Box( modifier = Modifier .background(MaterialTheme.appColors.surface) .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value + adaptiveBlurImagePadding).toDp() }) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value + adaptiveBlurImagePadding).toDp() + } + ) .align(Alignment.Center) ) Image( @@ -612,7 +639,11 @@ private fun CollapsingLayoutMobile( modifier = Modifier .background(MaterialTheme.appColors.courseHomeHeaderShade) .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value).toDp() } * 0.1f) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value).toDp() * SHADE_HEIGHT_MULTIPLIER + } + ) .align(Alignment.BottomCenter) ) } @@ -652,7 +683,11 @@ private fun CollapsingLayoutMobile( Box( modifier = Modifier .fillMaxWidth() - .height(with(localDensity) { (expandedTopHeight.value + navigationHeight.value).toDp() } + blurImagePadding) + .height( + with(localDensity) { + (expandedTopHeight.value + navigationHeight.value).toDp() + blurImagePadding + } + ) .offset { IntOffset( x = 0, @@ -720,7 +755,10 @@ private fun CollapsingLayoutMobile( .offset { IntOffset( x = 0, - y = (offset.value + backgroundImageHeight.value + expandedTopHeight.value - adaptiveImagePadding).roundToInt() + y = ( + offset.value + backgroundImageHeight.value + + expandedTopHeight.value - adaptiveImagePadding + ).roundToInt() ) } .onSizeChanged { size -> @@ -729,8 +767,8 @@ private fun CollapsingLayoutMobile( content = navigation, ) - val bodyPadding = - expandedTopHeight.value + offset.value + backgroundImageHeight.value + navigationHeight.value - blurImagePaddingPx * factor + val bodyPadding = expandedTopHeight.value + offset.value + backgroundImageHeight.value + + navigationHeight.value - blurImagePaddingPx * factor val bodyModifier = if (isEnabled) { Modifier .offset { @@ -751,7 +789,6 @@ private fun CollapsingLayoutMobile( } } -@OptIn(ExperimentalFoundationApi::class) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview( diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index c6f452c10..cae577969 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -169,7 +169,6 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { viewModel.fetchCourseDetails() } snackBar?.show() - } viewLifecycleOwner.lifecycleScope.launch { viewModel.showProgress.collect { @@ -512,7 +511,8 @@ private fun DashboardPager( viewModel.courseId, HandoutsType.Announcements ) - }) + } + ) } } } @@ -553,7 +553,6 @@ private fun CourseAccessErrorView( else -> {} } - Box( modifier = Modifier .fillMaxSize() diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index a743730ec..91abbb575 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.launch 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.exception.NoCachedDataException import org.openedx.core.extension.isFalse @@ -207,9 +208,8 @@ class CourseContainerViewModel( } } catch (e: Exception) { e.printStackTrace() - if (e.isInternetError() || e is NoCachedDataException) { - _errorMessage.value = - resourceManager.getString(CoreR.string.core_error_no_connection) + if (isNetworkRelatedError(e)) { + _errorMessage.value = resourceManager.getString(CoreR.string.core_error_no_connection) } else { _courseAccessStatus.value = CourseAccessError.UNKNOWN } @@ -218,6 +218,10 @@ class CourseContainerViewModel( } } + private fun isNetworkRelatedError(e: Exception): Boolean { + return e.isInternetError() || e is NoCachedDataException + } + private fun loadCourseImage(imageUrl: String?) { imageProcessor.loadImage( imageUrl = imageUrl?.toImageLink(config.getApiHostURL()) ?: "", @@ -306,8 +310,18 @@ class CourseContainerViewModel( private fun isCalendarSyncEnabled(): Boolean { val calendarSync = corePreferences.appConfig.courseDatesCalendarSync - return calendarSync.isEnabled && ((calendarSync.isSelfPacedEnabled && _courseDetails?.courseInfoOverview?.isSelfPaced.isTrue()) || - (calendarSync.isInstructorPacedEnabled && _courseDetails?.courseInfoOverview?.isSelfPaced.isFalse())) + return calendarSync.isEnabled && ( + isSelfPacedCalendarSyncEnabled(calendarSync) || + isInstructorPacedCalendarSyncEnabled(calendarSync) + ) + } + + private fun isSelfPacedCalendarSyncEnabled(calendarSync: CourseDatesCalendarSync): Boolean { + return calendarSync.isSelfPacedEnabled && _courseDetails?.courseInfoOverview?.isSelfPaced.isTrue() + } + + private fun isInstructorPacedCalendarSyncEnabled(calendarSync: CourseDatesCalendarSync): Boolean { + return calendarSync.isInstructorPacedEnabled && _courseDetails?.courseInfoOverview?.isSelfPaced.isFalse() } private fun courseDashboardViewed() { @@ -367,8 +381,11 @@ class CourseContainerViewModel( ) put( CourseAnalyticsKey.PACING.key, - if (_courseDetails?.courseInfoOverview?.isSelfPaced.isTrue()) CourseAnalyticsKey.SELF_PACED.key - else CourseAnalyticsKey.INSTRUCTOR_PACED.key + if (_courseDetails?.courseInfoOverview?.isSelfPaced.isTrue()) { + CourseAnalyticsKey.SELF_PACED.key + } else { + CourseAnalyticsKey.INSTRUCTOR_PACED.key + } ) putAll(param) } diff --git a/course/src/main/java/org/openedx/course/presentation/container/NoAccessCourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/NoAccessCourseContainerFragment.kt index e9b3b2e89..d0bcb819c 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/NoAccessCourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/NoAccessCourseContainerFragment.kt @@ -85,10 +85,8 @@ class NoAccessCourseContainerFragment : Fragment() { return fragment } } - } - @Composable private fun NoAccessCourseContainerScreen( windowSize: WindowSize, @@ -164,13 +162,11 @@ private fun NoAccessCourseContainerScreen( ) } } - } } } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -182,4 +178,4 @@ fun NoAccessCourseContainerScreenPreview() { onBackClick = {} ) } -} \ No newline at end of file +} diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt index adb633b98..45417ab8f 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesScreen.kt @@ -147,7 +147,6 @@ fun CourseDatesScreen( fragmentManager, ActionDialogFragment::class.simpleName ) - } } }, @@ -225,7 +224,8 @@ private fun CourseDatesUI( modifier = Modifier .fillMaxSize() .padding(it) - .displayCutoutForLandscape(), contentAlignment = Alignment.TopCenter + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter ) { Surface( modifier = modifierScreenWidth, @@ -362,14 +362,14 @@ fun ExpandableView( val enterTransition = remember { expandVertically( expandFrom = Alignment.Top, - animationSpec = tween(300) - ) + fadeIn(initialAlpha = 0.3f, animationSpec = tween(300)) + animationSpec = tween(durationMillis = 300) + ) + fadeIn(initialAlpha = 0.3f, animationSpec = tween(durationMillis = 300)) } val exitTransition = remember { shrinkVertically( shrinkTowards = Alignment.Top, - animationSpec = tween(300) - ) + fadeOut(animationSpec = tween(300)) + animationSpec = tween(durationMillis = 300) + ) + fadeOut(animationSpec = tween(durationMillis = 300)) } Box( modifier = Modifier @@ -378,10 +378,12 @@ fun ExpandableView( .background(MaterialTheme.appColors.cardViewBackground, MaterialTheme.shapes.medium) .border(0.75.dp, MaterialTheme.appColors.cardViewBorder, MaterialTheme.shapes.medium) ) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, start = 16.dp, end = 8.dp, bottom = 8.dp) - .clickable { expanded = !expanded }) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, start = 16.dp, end = 8.dp, bottom = 8.dp) + .clickable { expanded = !expanded } + ) { Column( modifier = Modifier .weight(1f) @@ -407,7 +409,6 @@ fun ExpandableView( .fillMaxWidth() ) } - } Spacer(modifier = Modifier.width(16.dp)) Icon( @@ -491,7 +492,8 @@ private fun DateBullet( .fillMaxHeight() .padding(top = 2.dp, bottom = 2.dp) .background( - color = barColor, shape = MaterialTheme.shapes.medium + color = barColor, + shape = MaterialTheme.shapes.medium ) ) } @@ -550,15 +552,23 @@ private fun CourseDateItem( modifier = Modifier .fillMaxWidth() .padding(end = 4.dp) - .clickable(enabled = dateBlock.blockId.isNotEmpty() && dateBlock.learnerHasAccess, - onClick = { onItemClick(dateBlock) }) + .clickable( + enabled = dateBlock.blockId.isNotEmpty() && dateBlock.learnerHasAccess, + onClick = { onItemClick(dateBlock) } + ) ) { dateBlock.dateType.drawableResId?.let { icon -> Icon( modifier = Modifier .padding(end = 4.dp) .align(Alignment.CenterVertically), - painter = painterResource(id = if (dateBlock.learnerHasAccess.not()) CoreR.drawable.core_ic_lock else icon), + painter = painterResource( + id = if (dateBlock.learnerHasAccess.not()) { + CoreR.drawable.core_ic_lock + } else { + icon + } + ), contentDescription = null, tint = MaterialTheme.appColors.textDark ) @@ -603,7 +613,6 @@ private fun CourseDateItem( } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -678,23 +687,30 @@ val mockedCourseBannerInfo = CourseDatesBannerInfo( private val mockedResponse: LinkedHashMap> = linkedMapOf( Pair( - DatesSection.COMPLETED, listOf( + DatesSection.COMPLETED, + listOf( CourseDateBlock( title = "Homework 1: ABCD", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-20T15:08:07Z")!!, ) ) - ), Pair( - DatesSection.COMPLETED, listOf( + ), + + Pair( + DatesSection.COMPLETED, + listOf( CourseDateBlock( title = "Homework 1: ABCD", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-20T15:08:07Z")!!, ) ) - ), Pair( - DatesSection.PAST_DUE, listOf( + ), + + Pair( + DatesSection.PAST_DUE, + listOf( CourseDateBlock( title = "Homework 1: ABCD", description = "After this date, course content will be archived", @@ -702,42 +718,58 @@ private val mockedResponse: LinkedHashMap> = dateType = DateType.ASSIGNMENT_DUE_DATE, ) ) - ), Pair( - DatesSection.TODAY, listOf( + ), + + Pair( + DatesSection.TODAY, + listOf( CourseDateBlock( title = "Homework 2: ABCD", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-21T15:08:07Z")!!, ) ) - ), Pair( - DatesSection.THIS_WEEK, listOf( + ), + + Pair( + DatesSection.THIS_WEEK, + listOf( CourseDateBlock( title = "Assignment Due: ABCD", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-22T15:08:07Z")!!, dateType = DateType.ASSIGNMENT_DUE_DATE, - ), CourseDateBlock( + ), + + CourseDateBlock( title = "Assignment Due", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-23T15:08:07Z")!!, dateType = DateType.ASSIGNMENT_DUE_DATE, - ), CourseDateBlock( + ), + + CourseDateBlock( title = "Surprise Assignment", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-24T15:08:07Z")!!, ) ) - ), Pair( - DatesSection.NEXT_WEEK, listOf( + ), + + Pair( + DatesSection.NEXT_WEEK, + listOf( CourseDateBlock( title = "Homework 5: ABCD", description = "After this date, course content will be archived", date = TimeUtils.iso8601ToDate("2023-10-25T15:08:07Z")!!, ) ) - ), Pair( - DatesSection.UPCOMING, listOf( + ), + + Pair( + DatesSection.UPCOMING, + listOf( CourseDateBlock( title = "Last Assignment", description = "After this date, course content will be archived", diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt index 3f716607f..91b5c6ee5 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesViewModel.kt @@ -112,7 +112,9 @@ class CourseDatesViewModel( } catch (e: Exception) { _uiState.value = CourseDatesUIState.Error if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection))) + _uiMessage.emit( + UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection)) + ) } } finally { courseNotifier.send(CourseLoading(false)) @@ -129,9 +131,15 @@ class CourseDatesViewModel( onResetDates(true) } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection))) + _uiMessage.emit( + UIMessage.SnackBarMessage(resourceManager.getString(CoreR.string.core_error_no_connection)) + ) } else { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg) + ) + ) } onResetDates(false) } @@ -143,6 +151,7 @@ class CourseDatesViewModel( courseStructure?.blockData?.getVerticalBlocks() ?.find { it.descendants.contains(blockId) } } catch (e: Exception) { + e.printStackTrace() null } } @@ -152,6 +161,7 @@ class CourseDatesViewModel( courseStructure?.blockData?.getSequentialBlocks() ?.find { it.descendants.contains(blockId) } } catch (e: Exception) { + e.printStackTrace() null } } @@ -229,8 +239,11 @@ class CourseDatesViewModel( put(CourseAnalyticsKey.ENROLLMENT_MODE.key, enrollmentMode) put( CourseAnalyticsKey.PACING.key, - if (isSelfPaced) CourseAnalyticsKey.SELF_PACED.key - else CourseAnalyticsKey.INSTRUCTOR_PACED.key + if (isSelfPaced) { + CourseAnalyticsKey.SELF_PACED.key + } else { + CourseAnalyticsKey.INSTRUCTOR_PACED.key + } ) putAll(param) } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/DatesUIState.kt similarity index 100% rename from course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt rename to course/src/main/java/org/openedx/course/presentation/dates/DatesUIState.kt diff --git a/course/src/main/java/org/openedx/course/presentation/download/DownloadDialogManager.kt b/course/src/main/java/org/openedx/course/presentation/download/DownloadDialogManager.kt index 5a85ba191..434f74c67 100644 --- a/course/src/main/java/org/openedx/course/presentation/download/DownloadDialogManager.kt +++ b/course/src/main/java/org/openedx/course/presentation/download/DownloadDialogManager.kt @@ -34,31 +34,43 @@ class DownloadDialogManager( uiState.collect { state -> val dialog = when { state.isDownloadFailed -> DownloadErrorDialogFragment.newInstance( - dialogType = DownloadErrorDialogType.DOWNLOAD_FAILED, uiState = state + dialogType = DownloadErrorDialogType.DOWNLOAD_FAILED, + uiState = state ) state.isAllBlocksDownloaded -> DownloadConfirmDialogFragment.newInstance( - dialogType = DownloadConfirmDialogType.REMOVE, uiState = state + dialogType = DownloadConfirmDialogType.REMOVE, + uiState = state ) !networkConnection.isOnline() -> DownloadErrorDialogFragment.newInstance( - dialogType = DownloadErrorDialogType.NO_CONNECTION, uiState = state - ) - - StorageManager.getFreeStorage() < state.sizeSum * DOWNLOAD_SIZE_FACTOR -> DownloadStorageErrorDialogFragment.newInstance( + dialogType = DownloadErrorDialogType.NO_CONNECTION, uiState = state ) - corePreferences.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected() -> DownloadErrorDialogFragment.newInstance( - dialogType = DownloadErrorDialogType.WIFI_REQUIRED, uiState = state - ) + StorageManager.getFreeStorage() < state.sizeSum * DOWNLOAD_SIZE_FACTOR -> { + DownloadStorageErrorDialogFragment.newInstance( + uiState = state + ) + } - !corePreferences.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected() -> DownloadConfirmDialogFragment.newInstance( - dialogType = DownloadConfirmDialogType.DOWNLOAD_ON_CELLULAR, uiState = state - ) + corePreferences.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected() -> { + DownloadErrorDialogFragment.newInstance( + dialogType = DownloadErrorDialogType.WIFI_REQUIRED, + uiState = state + ) + } + + !corePreferences.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected() -> { + DownloadConfirmDialogFragment.newInstance( + dialogType = DownloadConfirmDialogType.DOWNLOAD_ON_CELLULAR, + uiState = state + ) + } state.sizeSum >= MAX_CELLULAR_SIZE -> DownloadConfirmDialogFragment.newInstance( - dialogType = DownloadConfirmDialogType.CONFIRM, uiState = state + dialogType = DownloadConfirmDialogType.CONFIRM, + uiState = state ) else -> null diff --git a/course/src/main/java/org/openedx/course/presentation/download/DownloadStorageErrorDialogFragment.kt b/course/src/main/java/org/openedx/course/presentation/download/DownloadStorageErrorDialogFragment.kt index 4c192209f..5b99e6123 100644 --- a/course/src/main/java/org/openedx/course/presentation/download/DownloadStorageErrorDialogFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/download/DownloadStorageErrorDialogFragment.kt @@ -51,6 +51,7 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.course.R import org.openedx.course.domain.model.DownloadDialogResource import org.openedx.course.presentation.download.DownloadDialogManager.Companion.DOWNLOAD_SIZE_FACTOR +import org.openedx.course.presentation.download.DownloadStorageErrorDialogFragment.Companion.STORAGE_BAR_MIN_SIZE import org.openedx.foundation.extension.parcelable import org.openedx.foundation.extension.toFileSize import org.openedx.foundation.system.PreviewFragmentManager @@ -88,6 +89,7 @@ class DownloadStorageErrorDialogFragment : DialogFragment() { companion object { const val DIALOG_TAG = "DownloadStorageErrorDialogFragment" const val ARG_UI_STATE = "uiState" + const val STORAGE_BAR_MIN_SIZE = 0.1f fun newInstance( uiState: DownloadDialogUIState @@ -177,9 +179,8 @@ private fun StorageBar( val cornerRadius = 2.dp val boxPadding = 1.dp val usedSpace = totalSpace - freeSpace - val minSize = 0.1f - val freePercentage = freeSpace / requiredSpace.toFloat() + minSize - val reqPercentage = (requiredSpace - freeSpace) / requiredSpace.toFloat() + minSize + val freePercentage = freeSpace / requiredSpace.toFloat() + STORAGE_BAR_MIN_SIZE + val reqPercentage = (requiredSpace - freeSpace) / requiredSpace.toFloat() + STORAGE_BAR_MIN_SIZE val animReqPercentage = remember { Animatable(Float.MIN_VALUE) } LaunchedEffect(Unit) { diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsScreen.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsScreen.kt index 9720740a2..435b84444 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsScreen.kt @@ -57,7 +57,6 @@ fun HandoutsScreen( scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -153,7 +152,9 @@ private fun HandoutsScreenPreview() { OpenEdXTheme { HandoutsScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - onHandoutsClick = {}, onAnnouncementsClick = {}) + onHandoutsClick = {}, + onAnnouncementsClick = {} + ) } } @@ -164,6 +165,8 @@ private fun HandoutsScreenTabletPreview() { OpenEdXTheme { HandoutsScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), - onHandoutsClick = {}, onAnnouncementsClick = {}) + onHandoutsClick = {}, + onAnnouncementsClick = {} + ) } -} \ No newline at end of file +} diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsType.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsType.kt index da36feda0..6c0e1993e 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsType.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsType.kt @@ -2,4 +2,4 @@ package org.openedx.course.presentation.handouts enum class HandoutsType { Handouts, Announcements -} \ No newline at end of file +} diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index c8d9a87f8..85fad2512 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -52,8 +52,8 @@ class HandoutsViewModel( emptyState = true } } - } catch (e: Exception) { - //ignore e.printStackTrace() + } catch (_: Exception) { + // ignore e.printStackTrace() emptyState = true } if (emptyState) { @@ -95,11 +95,11 @@ class HandoutsViewModel( fun injectDarkMode(content: String, bgColor: ULong, textColor: ULong): String { val darkThemeStyle = "" + " body {\n" + + " background-color: #${getColorFromULong(bgColor)};\n" + + " color: #${getColorFromULong(textColor)};\n" + + " }\n" + + "" val buff = StringBuffer().apply { if (bgColor != ULong.MIN_VALUE) append(darkThemeStyle) append(content) @@ -107,6 +107,7 @@ class HandoutsViewModel( return buff.toString() } + @Suppress("MagicNumber") private fun getColorFromULong(color: ULong): String { if (color == ULong.MIN_VALUE) return "black" return java.lang.Long.toHexString(color.toLong()).substring(2, 8) diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt index 24240954a..744af0d03 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsWebViewFragment.kt @@ -162,8 +162,11 @@ fun HandoutsEmptyScreen( onBackClick: () -> Unit ) { val handoutScreenType = - if (handoutType == HandoutsType.Handouts) NoContentScreenType.COURSE_HANDOUTS - else NoContentScreenType.COURSE_ANNOUNCEMENTS + if (handoutType == HandoutsType.Handouts) { + NoContentScreenType.COURSE_HANDOUTS + } else { + NoContentScreenType.COURSE_ANNOUNCEMENTS + } val scaffoldState = rememberScaffoldState() Scaffold( @@ -176,7 +179,6 @@ fun HandoutsEmptyScreen( scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( diff --git a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineScreen.kt b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineScreen.kt index 9a4374aec..e7c69397a 100644 --- a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineScreen.kt @@ -135,7 +135,8 @@ private fun CourseOfflineUI( modifier = Modifier .fillMaxSize() .padding(it) - .displayCutoutForLandscape(), contentAlignment = Alignment.TopCenter + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter ) { Surface( modifier = modifierScreenWidth, diff --git a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt index 88c8a60c4..19d67f79b 100644 --- a/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/offline/CourseOfflineViewModel.kt @@ -191,7 +191,7 @@ class CourseOfflineViewModel( val realDownloadedSize = completedDownloads.sumOf { it.size } val largestDownloads = completedDownloads .sortedByDescending { it.size } - .take(5) + .take(n = 5) _uiState.update { it.copy( diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt index f1b9119ff..27c4594da 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineScreen.kt @@ -181,7 +181,6 @@ private fun CourseOutlineUI( scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -276,7 +275,6 @@ private fun CourseOutlineUI( } } - val progress = uiState.courseStructure.progress if (progress != null && progress.totalAssignmentsCount > 0) { item { @@ -408,7 +406,6 @@ private fun ResumeCourse( } } - @Composable private fun ResumeCourseTablet( modifier: Modifier = Modifier, @@ -436,7 +433,7 @@ private fun ResumeCourseTablet( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( - modifier = Modifier.size((MaterialTheme.appTypography.titleMedium.fontSize.value + 4).dp), + modifier = Modifier.size(size = (MaterialTheme.appTypography.titleMedium.fontSize.value + 4).dp), painter = painterResource(id = getUnitBlockIcon(block)), contentDescription = null, tint = MaterialTheme.appColors.textPrimary 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 9e997ed5f..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 @@ -31,7 +31,6 @@ import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogTyp import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseDatesShifted -import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseOpenBlock import org.openedx.core.system.notifier.CourseStructureUpdated @@ -40,12 +39,12 @@ import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey import org.openedx.course.presentation.CourseRouter -import org.openedx.course.R as courseR import org.openedx.course.presentation.download.DownloadDialogManager import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import org.openedx.foundation.utils.FileUtil +import org.openedx.course.R as courseR class CourseOutlineViewModel( val courseId: String, @@ -142,7 +141,11 @@ class CourseOutlineViewModel( super.saveDownloadModels(folder, id) } else { viewModelScope.launch { - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(courseR.string.course_can_download_only_with_wifi))) + _uiMessage.emit( + UIMessage.ToastMessage( + resourceManager.getString(courseR.string.course_can_download_only_with_wifi) + ) + ) } } } else { @@ -173,7 +176,6 @@ class CourseOutlineViewModel( ) courseSectionsState[blockId] ?: false - } else { false } @@ -182,85 +184,109 @@ 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 { - val resultBlocks = mutableListOf() if (blocks.isEmpty()) return emptyList() + + val resultBlocks = mutableListOf() blocks.forEach { block -> if (block.type == BlockType.CHAPTER) { resultBlocks.add(block) - block.descendants.forEach { descendant -> - blocks.find { it.id == descendant }?.let { sequentialBlock -> - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(sequentialBlock) - courseSubSectionUnit[sequentialBlock.id] = - sequentialBlock.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[sequentialBlock.id] = - sequentialBlock.getDownloadsCount(blocks) - addDownloadableChildrenForSequentialBlock(sequentialBlock) - } - } + processDescendants(block, blocks) } } - return resultBlocks.toList() + return resultBlocks + } + + private fun processDescendants(block: Block, blocks: List) { + block.descendants.forEach { descendantId -> + val sequentialBlock = blocks.find { it.id == descendantId } ?: return@forEach + addSequentialBlockToSubSections(block, sequentialBlock) + courseSubSectionUnit[sequentialBlock.id] = sequentialBlock.getFirstDescendantBlock(blocks) + subSectionsDownloadsCount[sequentialBlock.id] = sequentialBlock.getDownloadsCount(blocks) + addDownloadableChildrenForSequentialBlock(sequentialBlock) + } + } + + private fun addSequentialBlockToSubSections(block: Block, sequentialBlock: Block) { + courseSubSections.getOrPut(block.id) { mutableListOf() }.add(sequentialBlock) } private fun getResumeBlock( @@ -284,9 +310,17 @@ class CourseOutlineViewModel( onResetDates(true) } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) + ) } else { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_dates_shift_dates_unsuccessful_msg) + ) + ) } onResetDates(false) } diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 7a08bd9b0..75a100ab8 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -286,9 +286,13 @@ private fun CourseSubsectionItem( onClick: (Block) -> Unit, ) { val completedIconPainter = - if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - CoreR.drawable.ic_core_chapter_icon - ) + if (block.isCompleted()) { + painterResource(R.drawable.course_ic_task_alt) + } else { + painterResource( + CoreR.drawable.ic_core_chapter_icon + ) + } val completedIconColor = if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface val completedIconDescription = if (block.isCompleted()) { diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionUIState.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionUIState.kt index 1606de1e7..166da30c2 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionUIState.kt @@ -8,5 +8,5 @@ sealed class CourseSectionUIState { val sectionName: String, val courseName: String ) : CourseSectionUIState() - object Loading : CourseSectionUIState() -} \ No newline at end of file + data object Loading : CourseSectionUIState() +} diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt index d760620af..2ebe2c9b3 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt @@ -91,7 +91,9 @@ class CourseSectionViewModel( if (blockDescendant.type == BlockType.VERTICAL) { resultList.add(blockDescendant) } - } else continue + } else { + continue + } } return resultList } diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 6927c0106..a58af2a79 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -108,6 +108,8 @@ import subtitleFile.TimedTextObject import java.util.Date import org.openedx.core.R as coreR +const val AUTO_SCROLL_DELAY = 3000L + @Composable fun CourseSectionCard( block: Block, @@ -117,7 +119,9 @@ fun CourseSectionCard( ) { val iconModifier = Modifier.size(24.dp) - Column(Modifier.clickable { onItemClick(block) }) { + Column( + modifier = Modifier.clickable { onItemClick(block) } + ) { Row( Modifier .fillMaxWidth() @@ -129,12 +133,16 @@ fun CourseSectionCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - val completedIconPainter = - if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource( - coreR.drawable.ic_core_chapter_icon - ) - val completedIconColor = - if (block.isCompleted()) MaterialTheme.appColors.primary else MaterialTheme.appColors.onSurface + val completedIconPainter = if (block.isCompleted()) { + painterResource(R.drawable.course_ic_task_alt) + } else { + painterResource(coreR.drawable.ic_core_chapter_icon) + } + val completedIconColor = if (block.isCompleted()) { + MaterialTheme.appColors.primary + } else { + MaterialTheme.appColors.onSurface + } val completedIconDescription = if (block.isCompleted()) { stringResource(id = R.string.course_accessibility_section_completed) } else { @@ -160,20 +168,23 @@ fun CourseSectionCard( horizontalArrangement = Arrangement.spacedBy(24.dp), verticalAlignment = Alignment.CenterVertically ) { - if (downloadedState == DownloadedState.DOWNLOADED || downloadedState == DownloadedState.NOT_DOWNLOADED) { + if (downloadedState == DownloadedState.DOWNLOADED || + downloadedState == DownloadedState.NOT_DOWNLOADED + ) { val downloadIcon = if (downloadedState == DownloadedState.DOWNLOADED) { Icons.Default.CloudDone } else { Icons.Outlined.CloudDownload } - val downloadIconDescription = - if (downloadedState == DownloadedState.DOWNLOADED) { - stringResource(id = R.string.course_accessibility_remove_course_section) - } else { - stringResource(id = R.string.course_accessibility_download_course_section) - } - IconButton(modifier = iconModifier, - onClick = { onDownloadClick(block) }) { + val downloadIconDescription = if (downloadedState == DownloadedState.DOWNLOADED) { + stringResource(id = R.string.course_accessibility_remove_course_section) + } else { + stringResource(id = R.string.course_accessibility_download_course_section) + } + IconButton( + modifier = iconModifier, + onClick = { onDownloadClick(block) } + ) { Icon( imageVector = downloadIcon, contentDescription = downloadIconDescription, @@ -182,7 +193,9 @@ fun CourseSectionCard( } } else if (downloadedState != null) { Box(contentAlignment = Alignment.Center) { - if (downloadedState == DownloadedState.DOWNLOADING || downloadedState == DownloadedState.WAITING) { + if (downloadedState == DownloadedState.DOWNLOADING || + downloadedState == DownloadedState.WAITING + ) { CircularProgressIndicator( modifier = Modifier.size(34.dp), backgroundColor = Color.LightGray, @@ -192,10 +205,12 @@ fun CourseSectionCard( } IconButton( modifier = iconModifier.padding(top = 2.dp), - onClick = { onDownloadClick(block) }) { + onClick = { onDownloadClick(block) } + ) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + contentDescription = + stringResource(id = R.string.course_accessibility_stop_downloading_course_section), tint = MaterialTheme.appColors.error ) } @@ -245,7 +260,11 @@ fun OfflineQueueCard( maxLines = 1 ) - val progress = if (progressSize == 0L) 0f else progressValue.toFloat() / progressSize + val progress = if (progressSize == 0L) { + 0f + } else { + progressValue.toFloat() / progressSize + } LinearProgressIndicator( modifier = Modifier .fillMaxWidth() @@ -266,12 +285,14 @@ fun OfflineQueueCard( color = MaterialTheme.appColors.primary ) IconButton( - modifier = iconModifier - .padding(2.dp), - onClick = { onDownloadClick(downloadModel) }) { + modifier = iconModifier.padding(2.dp), + onClick = { onDownloadClick(downloadModel) } + ) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + contentDescription = stringResource( + id = R.string.course_accessibility_stop_downloading_course_section + ), tint = MaterialTheme.appColors.error ) } @@ -481,12 +502,12 @@ fun Indicator( ) { val size by animateDpAsState( targetValue = if (isSelected) selectedSize else defaultRadius, - animationSpec = tween(300), + animationSpec = tween(durationMillis = 300), label = "" ) val color by animateColorAsState( targetValue = if (isSelected) selectedColor else defaultColor, - animationSpec = tween(300), + animationSpec = tween(durationMillis = 300), label = "" ) @@ -509,7 +530,6 @@ fun VideoSubtitles( onSettingsClick: () -> Unit, ) { timedTextObject?.let { - val autoScrollDelay = 3000L var lastScrollTime by remember { mutableLongStateOf(0L) } @@ -518,7 +538,7 @@ fun VideoSubtitles( } LaunchedEffect(key1 = currentIndex) { - if (currentIndex > 1 && lastScrollTime + autoScrollDelay < Date().time) { + if (currentIndex > 1 && lastScrollTime + AUTO_SCROLL_DELAY < Date().time) { listState.animateScrollToItem(currentIndex - 1) } } @@ -600,7 +620,12 @@ fun CourseSection( onDownloadClick: (blocksIds: List) -> Unit, ) { val arrowRotation by animateFloatAsState( - targetValue = if (courseSectionsState == true) -90f else 90f, label = "" + targetValue = if (courseSectionsState == true) { + -90f + } else { + 90f + }, + label = "" ) val subSectionIds = courseSubSections?.map { it.id }.orEmpty() val filteredStatuses = downloadedStateMap.filterKeys { it in subSectionIds }.values @@ -611,15 +636,16 @@ fun CourseSection( else -> DownloadedState.NOT_DOWNLOADED } - Column(modifier = modifier - .clip(MaterialTheme.appShapes.cardShape) - .noRippleClickable { onItemClick(block) } - .background(MaterialTheme.appColors.cardViewBackground) - .border( - 1.dp, - MaterialTheme.appColors.cardViewBorder, - MaterialTheme.appShapes.cardShape - ) + Column( + modifier = modifier + .clip(MaterialTheme.appShapes.cardShape) + .noRippleClickable { onItemClick(block) } + .background(MaterialTheme.appColors.cardViewBackground) + .border( + 1.dp, + MaterialTheme.appColors.cardViewBorder, + MaterialTheme.appShapes.cardShape + ) ) { CourseExpandableChapterCard( block = block, @@ -686,26 +712,25 @@ fun CourseExpandableChapterCard( verticalAlignment = Alignment.CenterVertically ) { if (downloadedState == DownloadedState.DOWNLOADED || downloadedState == DownloadedState.NOT_DOWNLOADED) { - val downloadIcon = - if (downloadedState == DownloadedState.DOWNLOADED) { - Icons.Default.CloudDone - } else { - Icons.Outlined.CloudDownload - } - val downloadIconDescription = - if (downloadedState == DownloadedState.DOWNLOADED) { - stringResource(id = R.string.course_accessibility_remove_course_section) - } else { - stringResource(id = R.string.course_accessibility_download_course_section) - } - val downloadIconTint = - if (downloadedState == DownloadedState.DOWNLOADED) { - MaterialTheme.appColors.successGreen - } else { - MaterialTheme.appColors.textAccent - } - IconButton(modifier = iconModifier, - onClick = { onDownloadClick() }) { + val downloadIcon = if (downloadedState == DownloadedState.DOWNLOADED) { + Icons.Default.CloudDone + } else { + Icons.Outlined.CloudDownload + } + val downloadIconDescription = if (downloadedState == DownloadedState.DOWNLOADED) { + stringResource(id = R.string.course_accessibility_remove_course_section) + } else { + stringResource(id = R.string.course_accessibility_download_course_section) + } + val downloadIconTint = if (downloadedState == DownloadedState.DOWNLOADED) { + MaterialTheme.appColors.successGreen + } else { + MaterialTheme.appColors.textAccent + } + IconButton( + modifier = iconModifier, + onClick = { onDownloadClick() } + ) { Icon( imageVector = downloadIcon, contentDescription = downloadIconDescription, @@ -724,16 +749,21 @@ fun CourseExpandableChapterCard( } else if (downloadedState == DownloadedState.WAITING) { Icon( painter = painterResource(id = R.drawable.course_download_waiting), - contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + contentDescription = stringResource( + id = R.string.course_accessibility_stop_downloading_course_section + ), tint = MaterialTheme.appColors.error ) } IconButton( modifier = iconModifier.padding(2.dp), - onClick = { onDownloadClick() }) { + onClick = { onDownloadClick() } + ) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(id = R.string.course_accessibility_stop_downloading_course_section), + contentDescription = stringResource( + id = R.string.course_accessibility_stop_downloading_course_section + ), tint = MaterialTheme.appColors.error ) } @@ -751,10 +781,16 @@ fun CourseSubSectionItem( onClick: (Block) -> Unit, ) { val context = LocalContext.current - val icon = - if (block.isCompleted()) painterResource(R.drawable.course_ic_task_alt) else painterResource(coreR.drawable.ic_core_chapter_icon) - val iconColor = - if (block.isCompleted()) MaterialTheme.appColors.successGreen else MaterialTheme.appColors.onSurface + val icon = if (block.isCompleted()) { + painterResource(R.drawable.course_ic_task_alt) + } else { + painterResource(coreR.drawable.ic_core_chapter_icon) + } + val iconColor = if (block.isCompleted()) { + MaterialTheme.appColors.successGreen + } else { + MaterialTheme.appColors.onSurface + } val due by rememberSaveable { mutableStateOf(block.due?.let { TimeUtils.formatToString(context, it, useRelativeDates) } ?: "") } @@ -905,8 +941,11 @@ fun SubSectionUnitsList( Column( modifier = Modifier .background( - if (index == selectedUnitIndex) MaterialTheme.appColors.surface else + if (index == selectedUnitIndex) { + MaterialTheme.appColors.surface + } else { MaterialTheme.appColors.background + } ) .clickable { onUnitClick(index, unit) } ) { @@ -960,7 +999,9 @@ fun SubSectionUnitsList( modifier = Modifier .padding(start = 8.dp, end = 8.dp) .weight(1f), - text = stringResource(id = R.string.course_gated_content_label), + text = stringResource( + id = R.string.course_gated_content_label + ), color = MaterialTheme.appColors.textPrimaryVariant, style = MaterialTheme.appTypography.labelSmall, maxLines = 2, @@ -1124,7 +1165,8 @@ fun DatesShiftedSnackBar( borderColor = MaterialTheme.appColors.primary, onClick = { onViewDates() - }) + } + ) } } } @@ -1170,7 +1212,6 @@ fun CourseMessage( color = MaterialTheme.appColors.divider ) } - } @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @@ -1183,7 +1224,8 @@ private fun NavigationUnitsButtonsOnlyNextButtonPreview() { hasNextBlock = true, isVerticalNavigation = true, nextButtonText = "Next", - onPrevClick = {}) {} + onPrevClick = {} + ) {} } } @@ -1197,7 +1239,8 @@ private fun NavigationUnitsButtonsOnlyFinishButtonPreview() { hasNextBlock = false, isVerticalNavigation = true, nextButtonText = "Finish", - onPrevClick = {}) {} + onPrevClick = {} + ) {} } } @@ -1211,7 +1254,8 @@ private fun NavigationUnitsButtonsWithFinishPreview() { hasNextBlock = false, isVerticalNavigation = true, nextButtonText = "Finish", - onPrevClick = {}) {} + onPrevClick = {} + ) {} } } @@ -1225,7 +1269,8 @@ private fun NavigationUnitsButtonsWithNextPreview() { hasNextBlock = true, isVerticalNavigation = true, nextButtonText = "Next", - onPrevClick = {}) {} + onPrevClick = {} + ) {} } } diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt index 72e37ee5b..f0686926f 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseVideosUI.kt @@ -44,7 +44,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -92,7 +91,6 @@ fun CourseVideosScreen( val uiState by viewModel.uiState.collectAsState(CourseVideosUIState.Loading) val uiMessage by viewModel.uiMessage.collectAsState(null) val videoSettings by viewModel.videoSettings.collectAsState() - val context = LocalContext.current val fileUtil: FileUtil = koinInject() CourseVideosUI( @@ -183,7 +181,6 @@ private fun CourseVideosUI( scaffoldState = scaffoldState, backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -347,14 +344,15 @@ private fun CourseVideosUI( } if (isDeleteDownloadsConfirmationShowed) { - val downloadModelsSize = - (uiState as? CourseVideosUIState.CourseData)?.downloadModelsSize + val downloadModelsSize = (uiState as? CourseVideosUIState.CourseData)?.downloadModelsSize val isDownloadedAllVideos = downloadModelsSize?.isAllBlocksDownloadedOrDownloading == true && downloadModelsSize.remainingCount == 0 - val dialogTextId = if (isDownloadedAllVideos) - R.string.course_delete_downloads_confirmation_text else - R.string.course_delete_while_downloading_confirmation_text + val dialogTextId = if (isDownloadedAllVideos) { + R.string.course_delete_confirmation + } else { + R.string.course_delete_in_process_confirmation + } AlertDialog( title = { @@ -533,7 +531,6 @@ private fun AllVideosDownloadItem( } else { onDownloadAllClick(false) } - } else { onDownloadAllClick(true) } @@ -680,7 +677,8 @@ private fun CourseVideosScreenTabletPreview() { remainingSize = 0, allCount = 0, allSize = 0 - ), useRelativeDates = true + ), + useRelativeDates = true ), courseTitle = "", onExpandClick = { }, diff --git a/course/src/main/java/org/openedx/course/presentation/unit/NotAvailableUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/NotAvailableUnitFragment.kt index b983822b2..5fe50a0e6 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/NotAvailableUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/NotAvailableUnitFragment.kt @@ -122,7 +122,6 @@ class NotAvailableUnitFragment : Fragment() { return fragment } } - } @Composable @@ -138,7 +137,6 @@ private fun NotAvailableUnitScreen( modifier = Modifier.fillMaxSize(), scaffoldState = scaffoldState ) { - val contentWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -207,4 +205,4 @@ private fun NotAvailableUnitScreen( } } } -} \ No newline at end of file +} diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt index 0610983e8..2934fba13 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerAdapter.kt @@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter import org.openedx.core.FragmentViewType import org.openedx.core.domain.model.Block +import org.openedx.core.module.db.DownloadModel import org.openedx.course.presentation.unit.NotAvailableUnitFragment import org.openedx.course.presentation.unit.NotAvailableUnitType import org.openedx.course.presentation.unit.html.HtmlUnitFragment @@ -29,36 +30,24 @@ class CourseUnitContainerAdapter( val noNetwork = !viewModel.hasNetworkConnection return when { - noNetwork && block.isDownloadable && offlineUrl.isEmpty() -> { + isBlockNotDownloaded(block, noNetwork, offlineUrl) -> { createNotAvailableUnitFragment(block, NotAvailableUnitType.NOT_DOWNLOADED) } - noNetwork && !block.isDownloadable -> { + isBlockOfflineUnsupported(block, noNetwork) -> { createNotAvailableUnitFragment(block, NotAvailableUnitType.OFFLINE_UNSUPPORTED) } - block.isVideoBlock && block.studentViewData?.encodedVideos?.run { hasVideoUrl || hasYoutubeUrl } == true -> { + isVideoBlockAvailable(block) -> { createVideoFragment(block) } - block.isDiscussionBlock && !block.studentViewData?.topicId.isNullOrEmpty() -> { + isDiscussionBlockAvailable(block) -> { createDiscussionFragment(block) } - block.isHTMLBlock || block.isProblemBlock || block.isOpenAssessmentBlock || block.isDragAndDropBlock || - block.isWordCloudBlock || block.isLTIConsumerBlock || block.isSurveyBlock -> { - val lastModified = if (downloadedModel != null && noNetwork) { - downloadedModel.lastModified ?: "" - } else { - "" - } - HtmlUnitFragment.newInstance( - block.id, - block.studentViewUrl, - viewModel.courseId, - offlineUrl, - lastModified - ) + isSupportedHtmlBlock(block) -> { + createHtmlUnitFragment(block, downloadedModel, noNetwork, offlineUrl) } else -> { @@ -67,6 +56,55 @@ class CourseUnitContainerAdapter( } } + private fun isBlockNotDownloaded(block: Block, noNetwork: Boolean, offlineUrl: String): Boolean { + return noNetwork && block.isDownloadable && offlineUrl.isEmpty() + } + + private fun isBlockOfflineUnsupported(block: Block, noNetwork: Boolean): Boolean { + return noNetwork && !block.isDownloadable + } + + private fun isVideoBlockAvailable(block: Block): Boolean { + val encodedVideos = block.studentViewData?.encodedVideos + val hasVideo = encodedVideos?.hasVideoUrl == true || encodedVideos?.hasYoutubeUrl == true + return block.isVideoBlock && hasVideo + } + + private fun isDiscussionBlockAvailable(block: Block): Boolean { + val topicId = block.studentViewData?.topicId + return block.isDiscussionBlock && !topicId.isNullOrEmpty() + } + + private fun isSupportedHtmlBlock(block: Block): Boolean { + return block.isHTMLBlock || + block.isProblemBlock || + block.isOpenAssessmentBlock || + block.isDragAndDropBlock || + block.isWordCloudBlock || + block.isLTIConsumerBlock || + block.isSurveyBlock + } + + private fun createHtmlUnitFragment( + block: Block, + downloadedModel: DownloadModel?, + noNetwork: Boolean, + offlineUrl: String + ): Fragment { + val lastModified = if (downloadedModel != null && noNetwork) { + downloadedModel.lastModified ?: "" + } else { + "" + } + return HtmlUnitFragment.newInstance( + block.id, + block.studentViewUrl, + viewModel.courseId, + offlineUrl, + lastModified + ) + } + private fun createNotAvailableUnitFragment(block: Block, type: NotAvailableUnitType): Fragment { return NotAvailableUnitFragment.newInstance(block.id, block.lmsWebUrl, type) } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt index d8870914a..be490df74 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -48,7 +48,6 @@ import org.openedx.course.presentation.ui.SubSectionUnitsTitle import org.openedx.course.presentation.ui.VerticalPageIndicator import org.openedx.foundation.extension.serializable - class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_container) { private val binding: FragmentCourseUnitContainerBinding @@ -76,9 +75,9 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta val blocks = viewModel.getUnitBlocks() blocks.getOrNull(position)?.let { currentBlock -> val encodedVideo = currentBlock.studentViewData?.encodedVideos - binding.mediaRouteButton.isVisible = currentBlock.type == BlockType.VIDEO - && encodedVideo?.hasNonYoutubeVideo == true - && !encodedVideo.videoUrl.endsWith(".m3u8") + binding.mediaRouteButton.isVisible = currentBlock.type == BlockType.VIDEO && + encodedVideo?.hasNonYoutubeVideo == true && + !encodedVideo.videoUrl.endsWith(".m3u8") } } } @@ -150,15 +149,31 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupViewPagerInsets() + setupMediaRouteButton() + initViewPager() + handleSavedInstanceState(savedInstanceState) + setupNavigationBar() + setupProgressIndicators() + setupBackButton() + setupSubSectionUnits() + checkUnitsListShown() + setupChapterEndDialogListener() + } + + private fun setupViewPagerInsets() { val insetHolder = requireActivity() as InsetHolder val containerParams = binding.viewPager.layoutParams as ConstraintLayout.LayoutParams containerParams.bottomMargin = insetHolder.bottomInset binding.viewPager.layoutParams = containerParams + } + private fun setupMediaRouteButton() { binding.mediaRouteButton.setAlwaysVisible(true) CastButtonFactory.setUpMediaRouteButton(requireContext(), binding.mediaRouteButton) + } - initViewPager() + private fun handleSavedInstanceState(savedInstanceState: Bundle?) { if (savedInstanceState == null && componentId.isEmpty()) { val currentBlockIndex = viewModel.getUnitBlocks().indexOfFirst { viewModel.getCurrentBlock().id == it.id @@ -167,7 +182,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta binding.viewPager.currentItem = currentBlockIndex } } - if (componentId.isEmpty().not()) { + if (componentId.isNotEmpty()) { lifecycleScope.launch(Dispatchers.Main) { viewModel.indexInContainer.value?.let { index -> binding.viewPager.setCurrentItem(index, true) @@ -176,11 +191,15 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta requireArguments().putString(ARG_COMPONENT_ID, "") componentId = "" } + } + private fun setupNavigationBar() { binding.cvNavigationBar.setContent { NavigationBar() } + } + private fun setupProgressIndicators() { if (viewModel.isCourseUnitProgressEnabled) { binding.horizontalProgress.setContent { OpenEdXTheme { @@ -190,7 +209,8 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta HorizontalPageIndicator( blocks = descendantsBlocks, selectedPage = index, - completedAndSelectedColor = MaterialTheme.appColors.componentHorizontalProgressCompletedAndSelected, + completedAndSelectedColor = + MaterialTheme.appColors.componentHorizontalProgressCompletedAndSelected, completedColor = MaterialTheme.appColors.componentHorizontalProgressCompleted, selectedColor = MaterialTheme.appColors.componentHorizontalProgressSelected, defaultColor = MaterialTheme.appColors.componentHorizontalProgressDefault @@ -198,7 +218,6 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta } } binding.horizontalProgress.isVisible = true - } else { binding.cvCount.setContent { OpenEdXTheme { @@ -212,35 +231,35 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta selectedPage = index, defaultRadius = 3.dp, selectedLength = 5.dp, - modifier = Modifier - .width(24.dp) + modifier = Modifier.width(24.dp) ) } } binding.cvCount.isVisible = true } + } + private fun setupBackButton() { binding.btnBack.setContent { val title = if (viewModel.isCourseExpandableSectionsEnabled) { val unitBlocks by viewModel.subSectionUnitBlocks.collectAsState() unitBlocks.firstOrNull()?.let { viewModel.getSubSectionBlock(it.id).displayName } ?: "" - } else { val index by viewModel.indexInContainer.observeAsState(0) val descendantsBlocks by viewModel.descendantsBlocks.collectAsState() - descendantsBlocks[index].displayName + descendantsBlocks.getOrNull(index)?.displayName ?: "" } CourseUnitToolbar( title = title, - onBackClick = { - navigateToParentFragment() - } + onBackClick = { navigateToParentFragment() } ) } + } + private fun setupSubSectionUnits() { if (viewModel.isCourseExpandableSectionsEnabled) { binding.subSectionUnitsTitle.setContent { val unitBlocks by viewModel.subSectionUnitBlocks.collectAsState() @@ -275,25 +294,29 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta unitId = unit.id, mode = requireArguments().serializable(ARG_MODE)!! ) - } else { handleUnitsClick() } } } } - } else { binding.subSectionUnitsTitle.isGone = true } + } - if (viewModel.unitsListShowed.value == true) handleUnitsClick() + private fun checkUnitsListShown() { + if (viewModel.unitsListShowed.value == true) { + handleUnitsClick() + } + } + private fun setupChapterEndDialogListener() { val chapterEndDialogTag = ChapterEndFragmentDialog::class.simpleName - (requireActivity().supportFragmentManager - .findFragmentByTag(chapterEndDialogTag) as? ChapterEndFragmentDialog)?.let { fragment -> - fragment.listener = dialogListener - } + (requireActivity().supportFragmentManager.findFragmentByTag(chapterEndDialogTag) as? ChapterEndFragmentDialog) + ?.let { fragment -> + fragment.listener = dialogListener + } } override fun onResume() { @@ -400,7 +423,6 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta binding.subSectionUnitsList.visibility = View.GONE binding.subSectionUnitsBg.visibility = View.GONE viewModel.setUnitsListVisibility(false) - } else { binding.subSectionUnitsList.visibility = View.VISIBLE binding.subSectionUnitsBg.visibility = View.VISIBLE diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 5a4cb0393..76ea09dac 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -49,14 +49,12 @@ class CourseUnitContainerViewModel( val isFirstIndexInContainer: Boolean get() { - return _descendantsBlocks.value.firstOrNull() == - _descendantsBlocks.value.getOrNull(currentIndex) + return _descendantsBlocks.value.firstOrNull() == _descendantsBlocks.value.getOrNull(currentIndex) } val isLastIndexInContainer: Boolean get() { - return _descendantsBlocks.value.lastOrNull() == - _descendantsBlocks.value.getOrNull(currentIndex) + return _descendantsBlocks.value.lastOrNull() == _descendantsBlocks.value.getOrNull(currentIndex) } private val _verticalBlockCounts = MutableLiveData() @@ -140,7 +138,6 @@ class CourseUnitContainerViewModel( } _subSectionUnitBlocks.value = getSubSectionUnitBlocks(blocks, getSubSectionId(unitId)) - } else { setNextVerticalIndex() } @@ -173,7 +170,9 @@ class CourseUnitContainerViewModel( if (blockDescendant.type == BlockType.VERTICAL) { resultList.add(blockDescendant.copy(type = getUnitType(blockDescendant.descendants))) } - } else continue + } else { + continue + } } return resultList } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt index 7bf313ac8..607606421 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt @@ -94,114 +94,16 @@ class HtmlUnitFragment : Fragment() { ) = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - OpenEdXTheme { - val windowSize = rememberWindowSize() - - var hasInternetConnection by remember { - mutableStateOf(viewModel.isOnline) - } - - val url by rememberSaveable { - mutableStateOf( - if (!hasInternetConnection && offlineUrl.isNotEmpty()) { - offlineUrl - } else { - blockUrl - } - ) - } - - val injectJSList by viewModel.injectJSList.collectAsState() - val uiState by viewModel.uiState.collectAsState() - - val configuration = LocalConfiguration.current - - val bottomPadding = - if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { - 72.dp - } else { - 0.dp - } - - val border = if (!isSystemInDarkTheme() && !viewModel.isCourseUnitProgressEnabled) { - Modifier.roundBorderWithoutBottom( - borderWidth = 2.dp, - cornerRadius = 30.dp - ) - } else { - Modifier - } - - Surface( - modifier = Modifier - .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)), - color = Color.White - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = bottomPadding) - .background(Color.White) - .then(border), - contentAlignment = Alignment.TopCenter - ) { - if (uiState is HtmlUnitUIState.Initialization) return@Box - if ((uiState is HtmlUnitUIState.Error).not()) { - if (hasInternetConnection || fromDownloadedContent) { - HTMLContentView( - uiState = uiState, - windowSize = windowSize, - url = url, - cookieManager = viewModel.cookieManager, - apiHostURL = viewModel.apiHostURL, - isLoading = uiState is HtmlUnitUIState.Loading, - injectJSList = injectJSList, - onCompletionSet = { - viewModel.notifyCompletionSet() - }, - onWebPageLoading = { - viewModel.onWebPageLoading() - }, - onWebPageLoaded = { - if ((uiState is HtmlUnitUIState.Error).not()) { - viewModel.onWebPageLoaded() - } - if (isAdded) viewModel.setWebPageLoaded(requireContext().assets) - }, - onWebPageLoadError = { - if (!fromDownloadedContent) viewModel.onWebPageLoadError() - }, - saveXBlockProgress = { jsonProgress -> - viewModel.saveXBlockProgress(jsonProgress) - }, - ) - } else { - viewModel.onWebPageLoadError() - } - } else { - val errorType = (uiState as HtmlUnitUIState.Error).errorType - FullScreenErrorView(errorType = errorType) { - hasInternetConnection = viewModel.isOnline - viewModel.onWebPageLoading() - } - } - if (uiState is HtmlUnitUIState.Loading && hasInternetConnection) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } - } - } - } - } + HtmlUnitView( + viewModel = viewModel, + blockUrl = blockUrl, + offlineUrl = offlineUrl, + fromDownloadedContent = fromDownloadedContent, + isFragmentAdded = isAdded + ) } } - companion object { private const val ARG_BLOCK_ID = "blockId" private const val ARG_COURSE_ID = "courseId" @@ -228,6 +130,121 @@ class HtmlUnitFragment : Fragment() { } } +@Composable +fun HtmlUnitView( + viewModel: HtmlUnitViewModel, + blockUrl: String, + offlineUrl: String, + fromDownloadedContent: Boolean, + isFragmentAdded: Boolean, +) { + OpenEdXTheme { + val context = LocalContext.current + val windowSize = rememberWindowSize() + + var hasInternetConnection by remember { + mutableStateOf(viewModel.isOnline) + } + + val url by rememberSaveable { + mutableStateOf( + if (!hasInternetConnection && offlineUrl.isNotEmpty()) { + offlineUrl + } else { + blockUrl + } + ) + } + + val injectJSList by viewModel.injectJSList.collectAsState() + val uiState by viewModel.uiState.collectAsState() + + val configuration = LocalConfiguration.current + + val bottomPadding = + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + 72.dp + } else { + 0.dp + } + + val border = if (!isSystemInDarkTheme() && !viewModel.isCourseUnitProgressEnabled) { + Modifier.roundBorderWithoutBottom( + borderWidth = 2.dp, + cornerRadius = 30.dp + ) + } else { + Modifier + } + + Surface( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)), + color = Color.White + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = bottomPadding) + .background(Color.White) + .then(border), + contentAlignment = Alignment.TopCenter + ) { + if (uiState is HtmlUnitUIState.Initialization) return@Box + if ((uiState is HtmlUnitUIState.Error).not()) { + if (hasInternetConnection || fromDownloadedContent) { + HTMLContentView( + uiState = uiState, + windowSize = windowSize, + url = url, + cookieManager = viewModel.cookieManager, + apiHostURL = viewModel.apiHostURL, + isLoading = uiState is HtmlUnitUIState.Loading, + injectJSList = injectJSList, + onCompletionSet = { + viewModel.notifyCompletionSet() + }, + onWebPageLoading = { + viewModel.onWebPageLoading() + }, + onWebPageLoaded = { + if ((uiState is HtmlUnitUIState.Error).not()) { + viewModel.onWebPageLoaded() + } + if (isFragmentAdded) viewModel.setWebPageLoaded(context.assets) + }, + onWebPageLoadError = { + if (!fromDownloadedContent) viewModel.onWebPageLoadError() + }, + saveXBlockProgress = { jsonProgress -> + viewModel.saveXBlockProgress(jsonProgress) + }, + ) + } else { + viewModel.onWebPageLoadError() + } + } else { + val errorType = (uiState as HtmlUnitUIState.Error).errorType + FullScreenErrorView(errorType = errorType) { + hasInternetConnection = viewModel.isOnline + viewModel.onWebPageLoading() + } + } + if (uiState is HtmlUnitUIState.Loading && hasInternetConnection) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + } + } + } +} + @Composable @SuppressLint("SetJavaScriptEnabled") private fun HTMLContentView( @@ -264,13 +281,16 @@ private fun HTMLContentView( .background(MaterialTheme.appColors.background), factory = { WebView(context).apply { - addJavascriptInterface(object { - @Suppress("unused") - @JavascriptInterface - fun completionSet() { - onCompletionSet() - } - }, "callback") + addJavascriptInterface( + object { + @Suppress("unused") + @JavascriptInterface + fun completionSet() { + onCompletionSet() + } + }, + "callback" + ) addJavascriptInterface( JSBridge( postMessageCallback = { @@ -300,10 +320,7 @@ private fun HTMLContentView( request: WebResourceRequest? ): Boolean { val clickUrl = request?.url?.toString() ?: "" - return if (clickUrl.isNotEmpty() && - (clickUrl.startsWith("http://") || - clickUrl.startsWith("https://")) - ) { + return if (clickUrl.isNotEmpty() && clickUrl.startsWith("http")) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) true } else if (clickUrl.startsWith("mailto:")) { diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt index ca79ce90b..702082746 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt @@ -52,8 +52,13 @@ class HtmlUnitViewModel( } fun onWebPageLoadError() { - _uiState.value = - HtmlUnitUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + _uiState.value = HtmlUnitUIState.Error( + if (networkConnection.isOnline()) { + ErrorType.UNKNOWN_ERROR + } else { + ErrorType.CONNECTION_ERROR + } + ) } fun setWebPageLoaded(assets: AssetManager) { @@ -61,9 +66,9 @@ class HtmlUnitViewModel( val jsList = mutableListOf() - //Injection to intercept completion state for xBlocks + // Injection to intercept completion state for xBlocks assets.readAsText("js_injection/completions.js")?.let { jsList.add(it) } - //Injection to fix CSS issues for Survey xBlock + // Injection to fix CSS issues for Survey xBlock assets.readAsText("js_injection/survey_css.js")?.let { jsList.add(it) } _injectJSList.value = jsList diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt index dec6f70e9..17adfcddf 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt @@ -76,7 +76,6 @@ class EncodedVideoUnitViewModel( _isVideoEnded.value = true markBlockCompleted(blockId, CourseAnalyticsKey.NATIVE.key) } - } override fun onIsPlayingChanged(isPlaying: Boolean) { diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt index 7bbf0bd25..9c8340897 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.core.os.bundleOf import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment @@ -12,6 +13,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.Clock +import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory @@ -93,73 +95,84 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) private fun initPlayer() { - with(binding) { - if (exoPlayer == null) { - val videoQuality = viewModel.getVideoQuality() - val params = DefaultTrackSelector.Parameters.Builder(requireContext()) - .apply { - if (videoQuality != VideoQuality.AUTO) { - setMaxVideoSize(videoQuality.width, videoQuality.height) - setViewportSize(videoQuality.width, videoQuality.height, false) - } - } - .build() - - val factory = AdaptiveTrackSelection.Factory() - val selector = DefaultTrackSelector(requireContext(), factory) - selector.parameters = params - - exoPlayer = ExoPlayer.Builder( - requireContext(), - DefaultRenderersFactory(requireContext()), - DefaultMediaSourceFactory(requireContext(), DefaultExtractorsFactory()), - selector, - DefaultLoadControl(), - DefaultBandwidthMeter.getSingletonInstance(requireContext()), - DefaultAnalyticsCollector(Clock.DEFAULT) - ).build() + if (exoPlayer == null) { + exoPlayer = buildExoPlayer() + } + setupPlayerView() + setupMediaItem() + setupPlayerListeners() + } + + @OptIn(UnstableApi::class) + private fun buildExoPlayer(): ExoPlayer { + val videoQuality = viewModel.getVideoQuality() + val trackSelector = DefaultTrackSelector(requireContext(), AdaptiveTrackSelection.Factory()) + trackSelector.parameters = DefaultTrackSelector.Parameters.Builder(requireContext()).apply { + if (videoQuality != VideoQuality.AUTO) { + setMaxVideoSize(videoQuality.width, videoQuality.height) + setViewportSize(videoQuality.width, videoQuality.height, false) } - playerView.player = exoPlayer - playerView.setShowNextButton(false) - playerView.setShowPreviousButton(false) - val mediaItem = MediaItem.fromUri(viewModel.videoUrl) - setPlayerMedia(mediaItem) - exoPlayer?.prepare() - exoPlayer?.playWhenReady = viewModel.isPlaying ?: false - - playerView.setFullscreenButtonClickListener { _ -> + }.build() + + return ExoPlayer.Builder( + requireContext(), + DefaultRenderersFactory(requireContext()), + DefaultMediaSourceFactory(requireContext(), DefaultExtractorsFactory()), + trackSelector, + DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(requireContext()), + DefaultAnalyticsCollector(Clock.DEFAULT) + ).build() + } + + @OptIn(UnstableApi::class) + private fun setupPlayerView() { + with(binding.playerView) { + player = exoPlayer + setShowNextButton(false) + setShowPreviousButton(false) + setFullscreenButtonClickListener { requireActivity().supportFragmentManager.popBackStackImmediate() } + } + } - exoPlayer?.addListener(object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - viewModel.logPlayPauseEvent( - viewModel.videoUrl, - isPlaying, - viewModel.currentVideoTime, - CourseAnalyticsKey.NATIVE.key - ) - } + private fun setupMediaItem() { + val mediaItem = MediaItem.fromUri(viewModel.videoUrl) + setPlayerMedia(mediaItem) + exoPlayer?.prepare() + exoPlayer?.playWhenReady = viewModel.isPlaying ?: false + } - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) - if (playbackState == Player.STATE_ENDED) { - viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.NATIVE.key) - } - } + private fun setupPlayerListeners() { + exoPlayer?.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + viewModel.logPlayPauseEvent( + viewModel.videoUrl, + isPlaying, + viewModel.currentVideoTime, + CourseAnalyticsKey.NATIVE.key + ) + } - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - super.onPlaybackParametersChanged(playbackParameters) - viewModel.logVideoSpeedEvent( - viewModel.videoUrl, - playbackParameters.speed, - viewModel.currentVideoTime, - CourseAnalyticsKey.NATIVE.key - ) + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == Player.STATE_ENDED) { + viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.NATIVE.key) } - }) - } + } + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + super.onPlaybackParametersChanged(playbackParameters) + viewModel.logVideoSpeedEvent( + viewModel.videoUrl, + playbackParameters.speed, + viewModel.currentVideoTime, + CourseAnalyticsKey.NATIVE.key + ) + } + }) } @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @@ -195,7 +208,6 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { super.onDestroyView() } - @SuppressLint("SourceLockedOrientationActivity") override fun onDestroy() { releasePlayer() diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt index 0cc44dac3..03668033c 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt @@ -140,25 +140,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { binding.connectionError.isVisible = !viewModel.hasInternetConnection && !viewModel.isDownloaded - val orientation = resources.configuration.orientation - val windowMetrics = - WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()) - val currentBounds = windowMetrics.bounds - val layoutParams = binding.playerView.layoutParams as FrameLayout.LayoutParams - if (orientation == Configuration.ORIENTATION_PORTRAIT || windowSize?.isTablet == true) { - val width = currentBounds.width() - requireContext().dpToPixel(32) - val minHeight = requireContext().dpToPixel(194).roundToInt() - val height = (width / 16f * 9f).roundToInt() - layoutParams.height = if (windowSize?.isTablet == true) { - requireContext().dpToPixel(320).roundToInt() - } else if (height < minHeight) { - minHeight - } else { - height - } - } - - binding.playerView.layoutParams = layoutParams + setupPlayerHeight() viewModel.isUpdated.observe(viewLifecycleOwner) { isUpdated -> if (isUpdated) { @@ -173,6 +155,38 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } } + private fun setupPlayerHeight() { + val orientation = resources.configuration.orientation + val windowMetrics = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(requireActivity()) + val currentBounds = windowMetrics.bounds + val layoutParams = binding.playerView.layoutParams as FrameLayout.LayoutParams + + if (orientation == Configuration.ORIENTATION_PORTRAIT || windowSize?.isTablet == true) { + val padding = requireContext().dpToPixel(PLAYER_VIEW_PADDING_DP) + val width = currentBounds.width() - padding + val minHeight = requireContext().dpToPixel(MIN_PLAYER_HEIGHT_DP).roundToInt() + val aspectRatio = VIDEO_ASPECT_RATIO_WIDTH / VIDEO_ASPECT_RATIO_HEIGHT + val calculatedHeight = (width / aspectRatio).roundToInt() + + layoutParams.height = when { + windowSize?.isTablet == true -> { + requireContext().dpToPixel(TABLET_PLAYER_HEIGHT_DP).roundToInt() + } + + calculatedHeight < minHeight -> { + minHeight + } + + else -> { + calculatedHeight + } + } + } + + binding.playerView.layoutParams = layoutParams + } + @androidx.annotation.OptIn(UnstableApi::class) private fun initPlayer() { with(binding) { @@ -225,8 +239,9 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { ) playerView.setFullscreenButtonClickListener { - if (viewModel.isCastActive) + if (viewModel.isCastActive) { return@setFullscreenButtonClickListener + } router.navigateToFullScreenVideo( requireActivity().supportFragmentManager, @@ -258,7 +273,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { binding.playerView.showController() } else { binding.playerView.controllerAutoShow = true - binding.playerView.controllerShowTimeoutMs = 2000 + binding.playerView.controllerShowTimeoutMs = CONTROLLER_SHOW_TIMEOUT } } @@ -285,6 +300,13 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { private const val ARG_TITLE = "title" private const val ARG_DOWNLOADED = "isDownloaded" + private const val PLAYER_VIEW_PADDING_DP = 32 + private const val MIN_PLAYER_HEIGHT_DP = 194 + private const val TABLET_PLAYER_HEIGHT_DP = 320 + private const val VIDEO_ASPECT_RATIO_WIDTH = 16f + private const val VIDEO_ASPECT_RATIO_HEIGHT = 9f + private const val CONTROLLER_SHOW_TIMEOUT = 2000 + fun newInstance( blockId: String, courseId: String, 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 5779b96da..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,17 +88,17 @@ 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) { if (!isBlockAlreadyCompleted) { logLoadedCompletedEvent(videoUrl, false, getCurrentVideoTime(), medium) @@ -111,6 +111,7 @@ open class VideoUnitViewModel( ) notifier.send(CourseCompletionSet()) } catch (e: Exception) { + e.printStackTrace() isBlockAlreadyCompleted = false } } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt index 4ae600eb8..423c825ce 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt @@ -24,7 +24,6 @@ class VideoViewModel( private var isBlockAlreadyCompleted = false - fun sendTime() { if (currentVideoTime != C.TIME_UNSET) { viewModelScope.launch { @@ -51,6 +50,7 @@ class VideoViewModel( ) notifier.send(CourseCompletionSet()) } catch (e: Exception) { + e.printStackTrace() isBlockAlreadyCompleted = false } } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt index 397c36baf..03f8b906a 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt @@ -21,6 +21,8 @@ import org.openedx.core.presentation.global.viewBinding import org.openedx.course.R import org.openedx.course.databinding.FragmentYoutubeVideoFullScreenBinding import org.openedx.course.presentation.CourseAnalyticsKey +import org.openedx.course.presentation.unit.video.YoutubeVideoUnitFragment.Companion.RATE_DIALOG_THRESHOLD +import org.openedx.course.presentation.unit.video.YoutubeVideoUnitFragment.Companion.VIDEO_COMPLETION_THRESHOLD import org.openedx.foundation.extension.requestApplyInsetsWhenAttached class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_full_screen) { @@ -69,62 +71,62 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ .rel(0) .build() - - binding.youtubePlayerView.initialize(object : AbstractYouTubePlayerListener() { - var isMarkBlockCompletedCalled = false - - override fun onStateChange( - youTubePlayer: YouTubePlayer, - state: PlayerConstants.PlayerState, - ) { - super.onStateChange(youTubePlayer, state) - if (state == PlayerConstants.PlayerState.ENDED) { - viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.YOUTUBE.key) - } - viewModel.isPlaying = when (state) { - PlayerConstants.PlayerState.PLAYING -> true - PlayerConstants.PlayerState.PAUSED -> false - else -> return - } - } - - override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) { - super.onCurrentSecond(youTubePlayer, second) - viewModel.currentVideoTime = (second * 1000f).toLong() - val completePercentage = second / youtubeTrackerListener.videoDuration - if (completePercentage >= 0.8f && !isMarkBlockCompletedCalled) { - viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.YOUTUBE.key) - isMarkBlockCompletedCalled = true - } - if (completePercentage >= 0.99f && !appReviewManager.isDialogShowed) { - if (!appReviewManager.isDialogShowed) { - appReviewManager.tryToOpenRateDialog() + binding.youtubePlayerView.initialize( + object : AbstractYouTubePlayerListener() { + var isMarkBlockCompletedCalled = false + + override fun onStateChange( + youTubePlayer: YouTubePlayer, + state: PlayerConstants.PlayerState, + ) { + super.onStateChange(youTubePlayer, state) + if (state == PlayerConstants.PlayerState.ENDED) { + viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.YOUTUBE.key) + } + viewModel.isPlaying = when (state) { + PlayerConstants.PlayerState.PLAYING -> true + PlayerConstants.PlayerState.PAUSED -> false + else -> return } } - } - - override fun onReady(youTubePlayer: YouTubePlayer) { - super.onReady(youTubePlayer) - binding.youtubePlayerView.isVisible = true - val defPlayerUiController = - DefaultPlayerUiController(binding.youtubePlayerView, youTubePlayer) - defPlayerUiController.setFullScreenButtonClickListener { - parentFragmentManager.popBackStack() - } - - binding.youtubePlayerView.setCustomPlayerUi(defPlayerUiController.rootView) - val videoId = viewModel.videoUrl.split("watch?v=")[1] - if (viewModel.isPlaying == true) { - youTubePlayer.loadVideo(videoId, viewModel.currentVideoTime.toFloat() / 1000) - } else { - youTubePlayer.cueVideo(videoId, viewModel.currentVideoTime.toFloat() / 1000) + override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) { + super.onCurrentSecond(youTubePlayer, second) + viewModel.currentVideoTime = (second * 1000f).toLong() + val completePercentage = second / youtubeTrackerListener.videoDuration + if (completePercentage >= VIDEO_COMPLETION_THRESHOLD && !isMarkBlockCompletedCalled) { + viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.YOUTUBE.key) + isMarkBlockCompletedCalled = true + } + if (completePercentage >= RATE_DIALOG_THRESHOLD && !appReviewManager.isDialogShowed) { + if (!appReviewManager.isDialogShowed) { + appReviewManager.tryToOpenRateDialog() + } + } } - youTubePlayer.addListener(youtubeTrackerListener) - } + override fun onReady(youTubePlayer: YouTubePlayer) { + super.onReady(youTubePlayer) + binding.youtubePlayerView.isVisible = true + val defPlayerUiController = + DefaultPlayerUiController(binding.youtubePlayerView, youTubePlayer) + defPlayerUiController.setFullScreenButtonClickListener { + parentFragmentManager.popBackStack() + } + + binding.youtubePlayerView.setCustomPlayerUi(defPlayerUiController.rootView) - }, options) + val videoId = viewModel.videoUrl.split("watch?v=")[1] + if (viewModel.isPlaying == true) { + youTubePlayer.loadVideo(videoId, viewModel.currentVideoTime.toFloat() / 1000) + } else { + youTubePlayer.cueVideo(videoId, viewModel.currentVideoTime.toFloat() / 1000) + } + youTubePlayer.addListener(youtubeTrackerListener) + } + }, + options + ) } override fun onDestroyView() { @@ -157,5 +159,4 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ return fragment } } - } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 58aaaf377..c1cd33aa3 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -151,11 +151,11 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) super.onCurrentSecond(youTubePlayer, second) viewModel.setCurrentVideoTime((second * 1000f).toLong()) val completePercentage = second / youtubeTrackerListener.videoDuration - if (completePercentage >= 0.8f && !isMarkBlockCompletedCalled) { + if (completePercentage >= VIDEO_COMPLETION_THRESHOLD && !isMarkBlockCompletedCalled) { viewModel.markBlockCompleted(blockId, CourseAnalyticsKey.YOUTUBE.key) isMarkBlockCompletedCalled = true } - if (completePercentage >= 0.99f && !appReviewManager.isDialogShowed) { + if (completePercentage >= RATE_DIALOG_THRESHOLD && !appReviewManager.isDialogShowed) { appReviewManager.tryToOpenRateDialog() } } @@ -202,11 +202,13 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) viewModel.videoUrl.split("watch?v=").getOrNull(1)?.let { videoId -> if (viewModel.isPlaying && isResumed) { youTubePlayer.loadVideo( - videoId, viewModel.getCurrentVideoTime().toFloat() / 1000 + videoId, + viewModel.getCurrentVideoTime().toFloat() / 1000 ) } else { youTubePlayer.cueVideo( - videoId, viewModel.getCurrentVideoTime().toFloat() / 1000 + videoId, + viewModel.getCurrentVideoTime().toFloat() / 1000 ) } } @@ -246,6 +248,9 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "blockTitle" + const val VIDEO_COMPLETION_THRESHOLD = 0.8f + const val RATE_DIALOG_THRESHOLD = 0.99f + fun newInstance( blockId: String, courseId: String, diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt index 3d197859f..809a399eb 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideoViewModel.kt @@ -129,7 +129,11 @@ class CourseVideoViewModel( super.saveDownloadModels(folder, id) } else { viewModelScope.launch { - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.course_can_download_only_with_wifi))) + _uiMessage.emit( + UIMessage.ToastMessage( + resourceManager.getString(R.string.course_can_download_only_with_wifi) + ) + ) } } } else { @@ -140,7 +144,9 @@ class CourseVideoViewModel( override fun saveAllDownloadModels(folder: String) { if (preferencesManager.videoSettings.wifiDownloadOnly && !networkConnection.isWifiConnected()) { viewModelScope.launch { - _uiMessage.emit(UIMessage.ToastMessage(resourceManager.getString(R.string.course_can_download_only_with_wifi))) + _uiMessage.emit( + UIMessage.ToastMessage(resourceManager.getString(R.string.course_can_download_only_with_wifi)) + ) } return } @@ -178,6 +184,7 @@ class CourseVideoViewModel( } courseNotifier.send(CourseLoading(false)) } catch (e: Exception) { + e.printStackTrace() _uiState.value = CourseVideosUIState.Empty } } @@ -196,34 +203,58 @@ class CourseVideoViewModel( fun sequentialClickedEvent(blockId: String, blockName: String) { val currentState = uiState.value if (currentState is CourseVideosUIState.CourseData) { - analytics.sequentialClickedEvent(courseId, courseTitle, blockId, blockName) + analytics.sequentialClickedEvent( + courseId, + courseTitle, + blockId, + blockName + ) } } fun onChangingVideoQualityWhileDownloading() { viewModelScope.launch { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.course_change_quality_when_downloading))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.course_change_quality_when_downloading) + ) + ) } } private fun sortBlocks(blocks: List): List { - val resultBlocks = mutableListOf() if (blocks.isEmpty()) return emptyList() + + val resultBlocks = mutableListOf() blocks.forEach { block -> if (block.type == BlockType.CHAPTER) { resultBlocks.add(block) - block.descendants.forEach { descendant -> - blocks.find { it.id == descendant }?.let { - courseSubSections.getOrPut(block.id) { mutableListOf() } - .add(it) - courseSubSectionUnit[it.id] = it.getFirstDescendantBlock(blocks) - subSectionsDownloadsCount[it.id] = it.getDownloadsCount(blocks) - addDownloadableChildrenForSequentialBlock(it) - } - } + processDescendants(block, blocks) } } - return resultBlocks.toList() + return resultBlocks + } + + private fun processDescendants(chapterBlock: Block, blocks: List) { + chapterBlock.descendants.forEach { descendantId -> + val sequentialBlock = blocks.find { it.id == descendantId } ?: return@forEach + addToSubSections(chapterBlock, sequentialBlock) + updateSubSectionUnit(sequentialBlock, blocks) + updateDownloadsCount(sequentialBlock, blocks) + addDownloadableChildrenForSequentialBlock(sequentialBlock) + } + } + + private fun addToSubSections(chapterBlock: Block, sequentialBlock: Block) { + courseSubSections.getOrPut(chapterBlock.id) { mutableListOf() }.add(sequentialBlock) + } + + private fun updateSubSectionUnit(sequentialBlock: Block, blocks: List) { + courseSubSectionUnit[sequentialBlock.id] = sequentialBlock.getFirstDescendantBlock(blocks) + } + + private fun updateDownloadsCount(sequentialBlock: Block, blocks: List) { + subSectionsDownloadsCount[sequentialBlock.id] = sequentialBlock.getDownloadsCount(blocks) } fun downloadBlocks(blocksIds: List, fragmentManager: FragmentManager) { diff --git a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt index ceea27806..4f63f6883 100644 --- a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt +++ b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueFragment.kt @@ -184,11 +184,17 @@ private fun DownloadQueueScreen( LazyColumn { items(uiState.downloadingModels) { model -> val progressValue = - if (model.id == uiState.currentProgressId) - uiState.currentProgressValue else 0 + if (model.id == uiState.currentProgressId) { + uiState.currentProgressValue + } else { + 0 + } val progressSize = - if (model.id == uiState.currentProgressId) - uiState.currentProgressSize else 0 + if (model.id == uiState.currentProgressId) { + uiState.currentProgressSize + } else { + 0 + } OfflineQueueCard( downloadModel = model, @@ -212,7 +218,6 @@ private fun DownloadQueueScreen( } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, device = Devices.TABLET) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt index 1c74e3b80..03c3c01c2 100644 --- a/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt +++ b/course/src/main/java/org/openedx/course/settings/download/DownloadQueueViewModel.kt @@ -40,7 +40,6 @@ class DownloadQueueViewModel( if (descendants.isEmpty()) models else models.filter { descendants.contains(it.id) } if (filteredModels.isEmpty()) { _uiState.value = DownloadQueueUIState.Empty - } else { if (_uiState.value is DownloadQueueUIState.Models) { val state = _uiState.value as DownloadQueueUIState.Models diff --git a/course/src/main/java/org/openedx/course/utils/ImageProcessor.kt b/course/src/main/java/org/openedx/course/utils/ImageProcessor.kt index b83f4a5e5..c909f97cf 100644 --- a/course/src/main/java/org/openedx/course/utils/ImageProcessor.kt +++ b/course/src/main/java/org/openedx/course/utils/ImageProcessor.kt @@ -41,7 +41,7 @@ class ImageProcessor(private val context: Context) { ScriptIntrinsicBlur.create(renderScript, bitmapAlloc.element).apply { setRadius(blurRadio) setInput(bitmapAlloc) - repeat(3) { + repeat(times = 3) { forEach(bitmapAlloc) } } diff --git a/course/src/main/java/org/openedx/course/worker/OfflineProgressSyncWorker.kt b/course/src/main/java/org/openedx/course/worker/OfflineProgressSyncWorker.kt index d41e9909e..83b56a997 100644 --- a/course/src/main/java/org/openedx/course/worker/OfflineProgressSyncWorker.kt +++ b/course/src/main/java/org/openedx/course/worker/OfflineProgressSyncWorker.kt @@ -45,8 +45,11 @@ class OfflineProgressSyncWorker( createChannel() } val serviceType = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + } return ForegroundInfo( NOTIFICATION_ID, diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index eefe590d8..59c536295 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -59,8 +59,8 @@ (Untitled) Download The videos you\'ve selected are larger than 1 GB. Do you want to download these videos? - Turning off the switch will stop downloading and delete all downloaded videos for \"%s\"? - Are you sure you want to delete all video(s) for \"%s\"? + Turning off the switch will stop downloading and delete all downloaded videos for \"%s\"? + Are you sure you want to delete all video(s) for \"%s\"? Are you sure you want to delete video(s) for \"%s\"? %1$s - %2$s - %3$d / %4$d Downloading this content requires an active internet connection. Please connect to the internet and try again. diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 98cf58a8b..003bbb4b3 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -100,22 +100,34 @@ class CourseContainerViewModelTest { false, null, coursewareAccess = CoursewareAccess( - false, "", "", "", - "", "" - + false, + "", + "", + "", + "", + "" ) ), certificate = null, enrollmentDetails = EnrollmentDetails( - null, "audit", false, Date() + null, + "audit", + false, + Date() ), courseInfoOverview = CourseInfoOverview( - "Open edX Demo Course", "", "OpenedX", Date(), - "", "", null, false, null, + "Open edX Demo Course", + "", + "OpenedX", + Date(), + "", + "", + null, + false, + null, CourseSharingUtmParameters("", ""), "", ) - ) private val courseStructure = CourseStructure( @@ -154,17 +166,31 @@ class CourseContainerViewModelTest { false, null, CoursewareAccess( - false, "", "", "", - "", "" + false, + "", + "", + "", + "", + "" ) ), certificate = null, enrollmentDetails = EnrollmentDetails( - null, "", false, null + null, + "", + false, + null ), courseInfoOverview = CourseInfoOverview( - "Open edX Demo Course", "", "OpenedX", null, - "", "", null, false, null, + "Open edX Demo Course", + "", + "OpenedX", + null, + "", + "", + null, + false, + null, CourseSharingUtmParameters("", ""), "", ) diff --git a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt index 41074294a..981c88783 100644 --- a/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/handouts/HandoutsViewModelTest.kt @@ -36,12 +36,6 @@ class HandoutsViewModelTest { private val interactor = mockk() private val analytics = mockk() - //region mockHandoutsModel - - private val handoutsModel = HandoutsModel("") - - //endregion - @Before fun setUp() { Dispatchers.setMain(dispatcher) diff --git a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt index 58574b5bd..663409188 100644 --- a/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/outline/CourseOutlineViewModelTest.kt @@ -231,7 +231,9 @@ class CourseOutlineViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload + every { + resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) + } returns cantDownload every { config.getApiHostURL() } returns "http://localhost:8000" every { downloadDialogManager.showDownloadFailedPopup(any(), any()) } returns Unit every { preferencesManager.isRelativeDatesEnabled } returns true diff --git a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt index e1c6a98ca..02eda9622 100644 --- a/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/section/CourseSectionViewModelTest.kt @@ -37,7 +37,6 @@ import org.openedx.core.module.db.DownloadModel import org.openedx.core.module.db.DownloadModelEntity import org.openedx.core.module.db.DownloadedState import org.openedx.core.module.db.FileType -import org.openedx.core.module.download.DownloadHelper import org.openedx.core.presentation.CoreAnalytics import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.system.connection.NetworkConnection @@ -66,7 +65,6 @@ class CourseSectionViewModelTest { private val notifier = mockk() private val analytics = mockk() private val coreAnalytics = mockk() - private val downloadHelper = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -180,7 +178,9 @@ class CourseSectionViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) } returns cantDownload + every { + resourceManager.getString(org.openedx.course.R.string.course_can_download_only_with_wifi) + } returns cantDownload } @After @@ -341,5 +341,4 @@ class CourseSectionViewModelTest { assert(viewModel.uiState.value is CourseSectionUIState.Blocks) } - } diff --git a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt index ffb1d124d..9d0f0c7c1 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModelTest.kt @@ -330,5 +330,4 @@ class CourseUnitContainerViewModelTest { coVerify(exactly = 0) { interactor.getCourseStructure(any()) } coVerify(exactly = 1) { interactor.getCourseStructureForVideos(any()) } } - } diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt index 4270dba82..effd426a0 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoUnitViewModelTest.kt @@ -44,7 +44,6 @@ class VideoUnitViewModelTest { private val transcriptManager = mockk() private val courseAnalytics = mockk() - @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -162,5 +161,4 @@ class VideoUnitViewModelTest { assert(viewModel.currentVideoTime.value == 10L) assert(viewModel.isUpdated.value == true) } - -} \ No newline at end of file +} diff --git a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt index 3f476fe29..ad04283d7 100644 --- a/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/unit/video/VideoViewModelTest.kt @@ -90,7 +90,6 @@ class VideoViewModelTest { any() ) } - } @Test diff --git a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt index e5df7e948..b84bb61eb 100644 --- a/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/videos/CourseVideoViewModelTest.kt @@ -209,8 +209,9 @@ class CourseVideoViewModelTest { @Test fun `getVideos empty list`() = runTest { every { config.getCourseUIConfig().isCourseDropdownNavigationEnabled } returns false - coEvery { interactor.getCourseStructureForVideos(any()) } returns - courseStructure.copy(blockData = emptyList()) + coEvery { + interactor.getCourseStructureForVideos(any()) + } returns courseStructure.copy(blockData = emptyList()) every { downloadDao.getAllDataFlow() } returns flow { emit(emptyList()) } every { preferencesManager.videoSettings } returns VideoSettings.default val viewModel = CourseVideoViewModel( @@ -268,7 +269,6 @@ class CourseVideoViewModelTest { downloadHelper, ) - viewModel.getVideos() advanceUntilIdle() @@ -457,6 +457,4 @@ class CourseVideoViewModelTest { assert(message.await()?.message.isNullOrEmpty()) } - - } diff --git a/dashboard/src/main/java/org/openedx/DashboardUI.kt b/dashboard/src/main/java/org/openedx/DashboardUI.kt index 13a3f42d1..9e8d35305 100644 --- a/dashboard/src/main/java/org/openedx/DashboardUI.kt +++ b/dashboard/src/main/java/org/openedx/DashboardUI.kt @@ -28,7 +28,7 @@ fun Lock(modifier: Modifier = Modifier) { .size(32.dp) .padding(top = 8.dp, end = 8.dp) .background( - color = MaterialTheme.appColors.onPrimary.copy(0.5f), + color = MaterialTheme.appColors.onPrimary.copy(alpha = 0.5f), shape = CircleShape ) .padding(4.dp) diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt index e59a73fde..d70f01832 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesFragment.kt @@ -24,4 +24,10 @@ class AllEnrolledCoursesFragment : Fragment() { } } } + + companion object { + const val LOAD_MORE_THRESHOLD = 4 + const val TABLET_GRID_COLUMNS = 3 + const val MOBILE_GRID_COLUMNS = 2 + } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt index 10fefe8f1..9d26e39df 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -1,7 +1,6 @@ package org.openedx.courses.presentation import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -93,6 +92,9 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.TimeUtils +import org.openedx.courses.presentation.AllEnrolledCoursesFragment.Companion.LOAD_MORE_THRESHOLD +import org.openedx.courses.presentation.AllEnrolledCoursesFragment.Companion.MOBILE_GRID_COLUMNS +import org.openedx.courses.presentation.AllEnrolledCoursesFragment.Companion.TABLET_GRID_COLUMNS import org.openedx.dashboard.domain.CourseStatusFilter import org.openedx.foundation.extension.toImageLink import org.openedx.foundation.presentation.UIMessage @@ -153,7 +155,8 @@ fun AllEnrolledCoursesView( ) } -@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) +@Suppress("MaximumLineLength") +@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable private fun AllEnrolledCoursesView( apiHostUrl: String, @@ -166,7 +169,7 @@ private fun AllEnrolledCoursesView( val layoutDirection = LocalLayoutDirection.current val scaffoldState = rememberScaffoldState() val scrollState = rememberLazyGridState() - val columns = if (windowSize.isTablet) 3 else 2 + val columns = if (windowSize.isTablet) TABLET_GRID_COLUMNS else MOBILE_GRID_COLUMNS val pullRefreshState = rememberPullRefreshState( refreshing = state.refreshing, onRefresh = { onAction(AllEnrolledCoursesAction.SwipeRefresh) } @@ -212,7 +215,6 @@ private fun AllEnrolledCoursesView( ) } - val emptyStatePaddings by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -346,7 +348,7 @@ private fun AllEnrolledCoursesView( } ) } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { onAction(AllEnrolledCoursesAction.EndOfPage) } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt index ccba20242..c8363e24d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesViewModel.kt @@ -86,9 +86,17 @@ class AllEnrolledCoursesViewModel( _uiState.update { it.copy(courses = coursesList.toList()) } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } _uiState.update { it.copy(refreshing = false, showProgress = false) } @@ -128,9 +136,17 @@ class AllEnrolledCoursesViewModel( _uiState.update { it.copy(courses = coursesList.toList()) } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } _uiState.update { it.copy(refreshing = false, showProgress = false) } @@ -160,7 +176,8 @@ class AllEnrolledCoursesViewModel( fun navigateToCourseSearch(fragmentManager: FragmentManager) { dashboardRouter.navigateToCourseSearch( - fragmentManager, "" + fragmentManager, + "" ) } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt index b0309785c..5a00301e6 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryFragment.kt @@ -21,4 +21,9 @@ class DashboardGalleryFragment : Fragment() { } } } + + companion object { + const val TABLET_COURSE_LIST_ITEM_COUNT = 7 + const val MOBILE_COURSE_LIST_ITEM_COUNT = 7 + } } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt index 40e2b0318..2c44c2c61 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -66,6 +65,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import coil.compose.AsyncImage import coil.request.ImageRequest import org.koin.androidx.compose.koinViewModel @@ -94,6 +94,8 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.TimeUtils +import org.openedx.courses.presentation.DashboardGalleryFragment.Companion.MOBILE_COURSE_LIST_ITEM_COUNT +import org.openedx.courses.presentation.DashboardGalleryFragment.Companion.TABLET_COURSE_LIST_ITEM_COUNT import org.openedx.dashboard.R import org.openedx.foundation.extension.toImageLink import org.openedx.foundation.presentation.UIMessage @@ -327,7 +329,11 @@ private fun SecondaryCourses( onViewAllClick: () -> Unit ) { val windowSize = rememberWindowSize() - val itemsCount = if (windowSize.isTablet) 7 else 5 + val itemsCount = if (windowSize.isTablet) { + TABLET_COURSE_LIST_ITEM_COUNT + } else { + MOBILE_COURSE_LIST_ITEM_COUNT + } val rows = if (windowSize.isTablet) 2 else 1 val height = if (windowSize.isTablet) 322.dp else 152.dp val items = courses.take(itemsCount) @@ -552,7 +558,8 @@ private fun PrimaryCourseCard( .height(140.dp) ) val progress: Float = try { - primaryCourse.progress.assignmentsCompleted.toFloat() / primaryCourse.progress.totalAssignmentsCount.toFloat() + primaryCourse.progress.assignmentsCompleted.toFloat() / + primaryCourse.progress.totalAssignmentsCount.toFloat() } catch (_: ArithmeticException) { 0f } diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index b40e662f3..aacb85719 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -97,9 +97,17 @@ class DashboardGalleryViewModel( } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } finally { _updating.value = false diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/DashboardDao.kt b/dashboard/src/main/java/org/openedx/dashboard/data/DashboardDao.kt index d24afd05d..774ef7121 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/DashboardDao.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/DashboardDao.kt @@ -16,6 +16,5 @@ interface DashboardDao { suspend fun clearCachedData() @Query("SELECT * FROM course_enrolled_table") - suspend fun readAllData() : List - -} \ No newline at end of file + suspend fun readAllData(): List +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt index 17c41e07d..6f52021b3 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/data/repository/DashboardRepository.kt @@ -25,8 +25,10 @@ class DashboardRepository( preferencesManager.appConfig = result.configs.mapToDomain() if (page == 1) dao.clearCachedData() - dao.insertEnrolledCourseEntity(*result.enrollments.results.map { it.mapToRoomEntity() } - .toTypedArray()) + dao.insertEnrolledCourseEntity( + *result.enrollments.results.map { it.mapToRoomEntity() } + .toTypedArray() + ) return result.enrollments.mapToDomain() } @@ -57,8 +59,11 @@ class DashboardRepository( preferencesManager.appConfig = result.configs.mapToDomain() dao.clearCachedData() - dao.insertEnrolledCourseEntity(*result.enrollments.results.map { it.mapToRoomEntity() } - .toTypedArray()) + dao.insertEnrolledCourseEntity( + *result.enrollments.results + .map { it.mapToRoomEntity() } + .toTypedArray() + ) return result.enrollments.mapToDomain() } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index 2e7669bb1..642f6257a 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -37,7 +37,6 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.AccessTime import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -83,7 +82,7 @@ import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRecommendedBox import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog @@ -95,6 +94,7 @@ import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R +import org.openedx.dashboard.presentation.DashboardListFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.foundation.extension.toImageLink import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize @@ -164,6 +164,10 @@ class DashboardListFragment : Fragment() { } } } + + companion object { + const val LOAD_MORE_THRESHOLD = 4 + } } @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @@ -245,7 +249,6 @@ internal fun DashboardListView( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally ) { - Surface( color = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.screenBackgroundShape @@ -258,8 +261,9 @@ internal fun DashboardListView( when (state) { is DashboardUIState.Loading -> { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -282,7 +286,10 @@ internal fun DashboardListView( apiHostUrl, course, windowSize, - onClick = { onItemClick(it) }) + onClick = { + onItemClick(it) + } + ) Divider() } item { @@ -297,8 +304,9 @@ internal fun DashboardListView( } } } - }) - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + } + ) + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallback() } } @@ -525,7 +533,8 @@ private fun CourseItemPreview() { "http://localhost:8000", mockCourseEnrolled, WindowSize(WindowType.Compact, WindowType.Compact), - onClick = {}) + onClick = {} + ) } } @@ -591,7 +600,6 @@ private fun DashboardListViewTabletPreview() { } } - @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index e04ddb258..e9945f18e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -181,5 +181,4 @@ class DashboardListViewModel( fun dashboardCourseClickedEvent(courseId: String, courseName: String) { analytics.dashboardCourseClickedEvent(courseId, courseName) } - } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index a0e304170..94e649535 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -215,7 +215,8 @@ private fun LearnDropdownMenu( when (currentValue) { LearnType.COURSES -> 0 LearnType.PROGRAMS -> 1 - }, false + }, + false ) } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt index 1eb943cca..4a54b8f36 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardListViewModelTest.kt @@ -341,5 +341,4 @@ class DashboardListViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } verify(exactly = 1) { appNotifier.notifier } } - } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt index 090dc7987..bf230a9e9 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt @@ -37,7 +37,6 @@ class LearnViewModelTest { } } - @Test fun `getProgramFragment returns correct program fragment`() = runTest { viewModel.getProgramFragment diff --git a/discovery/src/main/java/org/openedx/discovery/data/converter/DiscoveryConverter.kt b/discovery/src/main/java/org/openedx/discovery/data/converter/DiscoveryConverter.kt index 22ab5cac7..9497d5e76 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/converter/DiscoveryConverter.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/converter/DiscoveryConverter.kt @@ -60,4 +60,4 @@ class DiscoveryConverter { if (value.isEmpty()) return null return Gson().fromJson(value, CourseVideoDb::class.java) } -} \ No newline at end of file +} diff --git a/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt b/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt index 5cafc1516..d7e10a4cf 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/model/CourseDetails.kt @@ -52,27 +52,33 @@ data class CourseDetails( fun mapToDomain(): Course { return Course( - id = id ?: "", - blocksUrl = blocksUrl ?: "", - courseId = courseId ?: "", - effort = effort ?: "", - enrollmentStart = TimeUtils.iso8601ToDate(enrollmentStart ?: ""), - enrollmentEnd = TimeUtils.iso8601ToDate(enrollmentEnd ?: ""), + id = id.orEmpty(), + blocksUrl = blocksUrl.orEmpty(), + courseId = courseId.orEmpty(), + effort = effort.orEmpty(), + enrollmentStart = parseEnrollmentStartDate(), + enrollmentEnd = parseEnrollmentEndDate(), hidden = hidden ?: false, invitationOnly = invitationOnly ?: false, mobileAvailable = mobileAvailable ?: false, - name = name ?: "", - number = number ?: "", - org = organization ?: "", - shortDescription = shortDescription ?: "", - start = start ?: "", - end = end ?: "", - startDisplay = startDisplay ?: "", - startType = startType ?: "", - pacing = pacing ?: "", - overview = overview ?: "", + name = name.orEmpty(), + number = number.orEmpty(), + org = organization.orEmpty(), + shortDescription = shortDescription.orEmpty(), + start = start.orEmpty(), + end = end.orEmpty(), + startDisplay = startDisplay.orEmpty(), + startType = startType.orEmpty(), + pacing = pacing.orEmpty(), + overview = overview.orEmpty(), isEnrolled = isEnrolled ?: false, - media = media?.mapToDomain() ?: org.openedx.core.domain.model.Media() + media = mapMediaToDomain() ) } + + private fun parseEnrollmentStartDate() = TimeUtils.iso8601ToDate(enrollmentStart.orEmpty()) + + private fun parseEnrollmentEndDate() = TimeUtils.iso8601ToDate(enrollmentEnd.orEmpty()) + + private fun mapMediaToDomain() = media?.mapToDomain() ?: org.openedx.core.domain.model.Media() } diff --git a/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt b/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt index ecf76c24d..a9eff0f98 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/model/room/CourseEntity.kt @@ -84,31 +84,29 @@ data class CourseEntity( companion object { fun createFrom(model: CourseDetails): CourseEntity { - with(model) { - return CourseEntity( - id = id ?: "", - blocksUrl = blocksUrl ?: "", - courseId = courseId ?: "", - effort = effort ?: "", - enrollmentStart = enrollmentStart ?: "", - enrollmentEnd = enrollmentEnd ?: "", - hidden = hidden ?: false, - invitationOnly = invitationOnly ?: false, - mobileAvailable = mobileAvailable ?: false, - name = name ?: "", - number = number ?: "", - org = organization ?: "", - shortDescription = shortDescription ?: "", - start = start ?: "", - end = end ?: "", - startDisplay = startDisplay ?: "", - startType = startType ?: "", - pacing = pacing ?: "", - overview = overview ?: "", - media = MediaDb.createFrom(media), - isEnrolled = isEnrolled ?: false - ) - } + return CourseEntity( + id = model.id.orEmpty(), + blocksUrl = model.blocksUrl.orEmpty(), + courseId = model.courseId.orEmpty(), + effort = model.effort.orEmpty(), + enrollmentStart = model.enrollmentStart.orEmpty(), + enrollmentEnd = model.enrollmentEnd.orEmpty(), + hidden = model.hidden ?: false, + invitationOnly = model.invitationOnly ?: false, + mobileAvailable = model.mobileAvailable ?: false, + name = model.name.orEmpty(), + number = model.number.orEmpty(), + org = model.organization.orEmpty(), + shortDescription = model.shortDescription.orEmpty(), + start = model.start.orEmpty(), + end = model.end.orEmpty(), + startDisplay = model.startDisplay.orEmpty(), + startType = model.startType.orEmpty(), + pacing = model.pacing.orEmpty(), + overview = model.overview.orEmpty(), + media = MediaDb.createFrom(model.media), + isEnrolled = model.isEnrolled ?: false + ) } } } diff --git a/discovery/src/main/java/org/openedx/discovery/data/repository/DiscoveryRepository.kt b/discovery/src/main/java/org/openedx/discovery/data/repository/DiscoveryRepository.kt index bdadccecc..05da75d29 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/repository/DiscoveryRepository.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/repository/DiscoveryRepository.kt @@ -9,7 +9,6 @@ import org.openedx.discovery.data.storage.DiscoveryDao import org.openedx.discovery.domain.model.Course import org.openedx.discovery.domain.model.CourseList - class DiscoveryRepository( private val api: DiscoveryApi, private val dao: DiscoveryDao, diff --git a/discovery/src/main/java/org/openedx/discovery/data/storage/DiscoveryDao.kt b/discovery/src/main/java/org/openedx/discovery/data/storage/DiscoveryDao.kt index 434d425fa..2c72514fa 100644 --- a/discovery/src/main/java/org/openedx/discovery/data/storage/DiscoveryDao.kt +++ b/discovery/src/main/java/org/openedx/discovery/data/storage/DiscoveryDao.kt @@ -24,5 +24,4 @@ interface DiscoveryDao { @Query("SELECT * FROM course_discovery_table") suspend fun readAllData(): List - } diff --git a/discovery/src/main/java/org/openedx/discovery/domain/interactor/DiscoveryInteractor.kt b/discovery/src/main/java/org/openedx/discovery/domain/interactor/DiscoveryInteractor.kt index a1991b655..32f65e3e4 100644 --- a/discovery/src/main/java/org/openedx/discovery/domain/interactor/DiscoveryInteractor.kt +++ b/discovery/src/main/java/org/openedx/discovery/domain/interactor/DiscoveryInteractor.kt @@ -30,5 +30,4 @@ class DiscoveryInteractor(private val repository: DiscoveryRepository) { suspend fun getCoursesListFromCache(): List { return repository.getCachedCoursesList() } - } diff --git a/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt b/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt index ae615821c..6e7f428e9 100644 --- a/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt +++ b/discovery/src/main/java/org/openedx/discovery/domain/model/Course.kt @@ -3,7 +3,6 @@ package org.openedx.discovery.domain.model import org.openedx.core.domain.model.Media import java.util.Date - data class Course( val id: String, val blocksUrl: String, diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt index 5efb7a5b0..28976b4a7 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt @@ -61,7 +61,7 @@ import org.openedx.core.AppUpdateState import org.openedx.core.AppUpdateState.wasUpdateDialogClosed import org.openedx.core.domain.model.Media import org.openedx.core.presentation.dialog.appupgrade.AppUpgradeDialogFragment -import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox +import org.openedx.core.presentation.global.appupgrade.AppUpgradeRecommendedBox import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.AuthButtonsPanel import org.openedx.core.ui.BackBtn @@ -77,6 +77,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.discovery.R import org.openedx.discovery.domain.model.Course +import org.openedx.discovery.presentation.NativeDiscoveryFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discovery.presentation.ui.DiscoveryCourseItem import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize @@ -140,7 +141,8 @@ class NativeDiscoveryFragment : Fragment() { onSearchClick = { viewModel.discoverySearchBarClickedEvent() router.navigateToCourseSearch( - requireActivity().supportFragmentManager, "" + requireActivity().supportFragmentManager, + "" ) }, paginationCallback = { @@ -176,7 +178,8 @@ class NativeDiscoveryFragment : Fragment() { LaunchedEffect(uiState) { if (querySearch.isNotEmpty()) { router.navigateToCourseSearch( - requireActivity().supportFragmentManager, querySearch + requireActivity().supportFragmentManager, + querySearch ) arguments?.putString(ARG_SEARCH_QUERY, "") } @@ -187,6 +190,7 @@ class NativeDiscoveryFragment : Fragment() { companion object { private const val ARG_SEARCH_QUERY = "query_search" + const val LOAD_MORE_THRESHOLD = 4 fun newInstance(querySearch: String = ""): NativeDiscoveryFragment { val fragment = NativeDiscoveryFragment() fragment.arguments = bundleOf( @@ -197,7 +201,6 @@ class NativeDiscoveryFragment : Fragment() { } } - @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable internal fun DiscoveryScreen( @@ -261,7 +264,6 @@ internal fun DiscoveryScreen( } } ) { - val searchTabWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -354,8 +356,9 @@ internal fun DiscoveryScreen( when (state) { is DiscoveryUIState.Loading -> { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -399,7 +402,8 @@ internal fun DiscoveryScreen( windowSize = windowSize, onClick = { onItemClick(course) - }) + } + ) Divider() } item { @@ -415,7 +419,7 @@ internal fun DiscoveryScreen( } } } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallback() } } @@ -485,7 +489,8 @@ private fun CourseItemPreview() { apiHostUrl = "", course = mockCourse, windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - onClick = {}) + onClick = {} + ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt index 923846e8a..0d4673e23 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt @@ -76,7 +76,9 @@ class NativeDiscoveryViewModel( isLoading = true val response = if (networkConnection.isOnline() || page > 1) { interactor.getCoursesList(username, organization, page) - } else null + } else { + null + } if (response != null) { if (response.pagination.next.isNotEmpty() && page != response.pagination.numPages) { _canLoadMore.value = true @@ -149,7 +151,6 @@ class NativeDiscoveryViewModel( _isUpdating.value = false } } - } fun fetchMore() { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt index 72ce6126c..5e01c88a2 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -50,6 +49,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.presentation.dialog.alert.ActionDialogFragment diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt index 2cb7afd69..14778833c 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt @@ -54,8 +54,13 @@ class WebViewDiscoveryViewModel( } fun onWebPageLoadError() { - _uiState.value = - WebViewUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + _uiState.value = WebViewUIState.Error( + if (networkConnection.isOnline()) { + ErrorType.UNKNOWN_ERROR + } else { + ErrorType.CONNECTION_ERROR + } + ) } fun updateDiscoveryUrl(url: String) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt index aaac503a3..770a4dc42 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import org.openedx.foundation.extension.applyDarkModeIfEnabled import org.openedx.core.extension.equalsHost +import org.openedx.foundation.extension.applyDarkModeIfEnabled import org.openedx.discovery.presentation.catalog.WebViewLink.Authority as linkAuthority @SuppressLint("SetJavaScriptEnabled", "ComposableNaming") 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 a482fc581..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 the URI scheme - if (uriScheme != uri.scheme) { - return null - } - - // Validate the Uri authority - val uriAuthority = Authority.entries.find { it.key == uri.authority } ?: return null - - // Parse the Uri params - val params = uri.getQueryParams() + // Validate URI scheme and authority + val isSchemeValid = uriScheme == uri.scheme + val uriAuthority = Authority.entries.find { it.key == uri.authority } - return WebViewLink(uriAuthority, params) + return if (isSchemeValid && uriAuthority != null) { + // Parse the URI params + val params = uri.getQueryParams() + WebViewLink(uriAuthority, params) + } else { + null + } } } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt index 056ce8bae..556f61459 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsFragment.kt @@ -200,7 +200,6 @@ class CourseDetailsFragment : Fragment() { } } - @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun CourseDetailsScreen( @@ -246,7 +245,6 @@ internal fun CourseDetailsScreen( } } ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -366,7 +364,8 @@ internal fun CourseDetailsScreen( body = htmlBody, onWebPageLoaded = { webViewAlpha = 1f - }) + } + ) } } } @@ -392,7 +391,6 @@ internal fun CourseDetailsScreen( } } - @Composable private fun CourseDetailNativeContent( windowSize: WindowSize, @@ -431,7 +429,7 @@ private fun CourseDetailNativeContent( Box(contentAlignment = Alignment.Center) { ImageHeader( modifier = Modifier - .aspectRatio(1.86f) + .aspectRatio(ratio = 1.86f) .padding(6.dp), apiHostUrl = apiHostUrl, courseImage = course.media.image?.large, @@ -500,7 +498,6 @@ private fun CourseDetailNativeContent( } } - @Composable private fun CourseDetailNativeContentLandscape( windowSize: WindowSize, @@ -533,7 +530,7 @@ private fun CourseDetailNativeContentLandscape( Column( Modifier .fillMaxHeight() - .weight(3f), + .weight(weight = 3f), verticalArrangement = Arrangement.SpaceBetween ) { Column { @@ -643,11 +640,10 @@ private fun CourseDescription( request: WebResourceRequest? ): Boolean { val clickUrl = request?.url?.toString() ?: "" - return if (clickUrl.isNotEmpty() && - (clickUrl.startsWith("http://") || - clickUrl.startsWith("https://")) - ) { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))) + return if (clickUrl.isNotEmpty() && clickUrl.startsWith("http")) { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl)) + ) true } else if (clickUrl.startsWith("mailto:")) { val email = clickUrl.replace("mailto:", "") diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt index b512ea99e..b212c588f 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModel.kt @@ -130,7 +130,10 @@ class CourseDetailsViewModel( private fun getColorFromULong(color: ULong): String { if (color == ULong.MIN_VALUE) return "black" - return java.lang.Long.toHexString(color.toLong()).substring(2, 8) + return java.lang.Long.toHexString(color.toLong()).substring( + startIndex = 2, + endIndex = 8 + ) } private fun courseEnrollClickedEvent(courseId: String, courseTitle: String) { @@ -143,7 +146,8 @@ class CourseDetailsViewModel( private fun logEvent( event: DiscoveryAnalyticsEvent, - courseId: String, courseTitle: String, + courseId: String, + courseTitle: String, ) { analytics.logEvent( event.eventName, diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt index 3c6cb6c31..e7b03f8a9 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt @@ -358,7 +358,6 @@ private fun CourseInfoWebView( onUriClick: (String, linkAuthority) -> Unit, onWebPageLoadError: () -> Unit ) { - val webView = CatalogWebViewScreen( url = contentUrl, uriScheme = uriScheme, diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt index fd88591ca..877f8630f 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt @@ -200,8 +200,13 @@ class CourseInfoViewModel( } fun onWebPageError() { - _webViewUIState.value = - WebViewUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + _webViewUIState.value = WebViewUIState.Error( + if (networkConnection.isOnline()) { + ErrorType.UNKNOWN_ERROR + } else { + ErrorType.CONNECTION_ERROR + } + ) } fun onWebPageLoading() { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt index 07e59dc11..87989400b 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt @@ -100,7 +100,6 @@ class ProgramFragment : Fragment() { if (isNestedFragment.not()) { DisposableEffect(uiState is ProgramUIState.CourseEnrolled) { if (uiState is ProgramUIState.CourseEnrolled) { - val courseId = (uiState as ProgramUIState.CourseEnrolled).courseId val isEnrolled = (uiState as ProgramUIState.CourseEnrolled).isEnrolled diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt index b4ad7341d..1f468a843 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt @@ -1,7 +1,7 @@ package org.openedx.discovery.presentation.program -import org.openedx.foundation.presentation.UIMessage import org.openedx.core.presentation.global.ErrorType +import org.openedx.foundation.presentation.UIMessage sealed class ProgramUIState { data object Loading : ProgramUIState() diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt index bacc9b3a1..2a77a8044 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt @@ -107,7 +107,15 @@ class ProgramViewModel( fun onPageLoadError() { viewModelScope.launch { - _uiState.emit(ProgramUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR)) + _uiState.emit( + ProgramUIState.Error( + if (networkConnection.isOnline()) { + ErrorType.UNKNOWN_ERROR + } else { + ErrorType.CONNECTION_ERROR + } + ) + ) } } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt index fc2af30a6..a38420a5e 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchFragment.kt @@ -75,6 +75,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.discovery.domain.model.Course import org.openedx.discovery.presentation.DiscoveryRouter +import org.openedx.discovery.presentation.search.CourseSearchFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discovery.presentation.ui.DiscoveryCourseItem import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize @@ -101,7 +102,8 @@ class CourseSearchFragment : Fragment() { val uiState by viewModel.uiState.observeAsState( CourseSearchUIState.Courses( - emptyList(), 0 + emptyList(), + 0 ) ) val uiMessage by viewModel.uiMessage.observeAsState() @@ -150,6 +152,7 @@ class CourseSearchFragment : Fragment() { companion object { private const val ARG_SEARCH_QUERY = "query_search" + const val LOAD_MORE_THRESHOLD = 4 fun newInstance(querySearch: String): CourseSearchFragment { val fragment = CourseSearchFragment() fragment.arguments = bundleOf( @@ -160,7 +163,6 @@ class CourseSearchFragment : Fragment() { } } - @OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class) @Composable private fun CourseSearchScreen( @@ -205,7 +207,6 @@ private fun CourseSearchScreen( focusManager.clearFocus() } - Scaffold( scaffoldState = scaffoldState, modifier = Modifier @@ -231,7 +232,6 @@ private fun CourseSearchScreen( } } ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -386,7 +386,8 @@ private fun CourseSearchScreen( windowSize = windowSize, onClick = { courseId -> onItemClick(courseId) - }) + } + ) Divider() } item { @@ -401,7 +402,7 @@ private fun CourseSearchScreen( } } } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallback() } } @@ -474,7 +475,6 @@ fun CourseSearchScreenTabletPreview() { } } - private val mockCourse = Course( id = "id", blocksUrl = "blocksUrl", diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt index d1ae276d8..f001b46eb 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/search/CourseSearchViewModel.kt @@ -65,7 +65,7 @@ class CourseSearchViewModel( viewModelScope.launch { queryChannel .asSharedFlow() - .debounce(400) + .debounce(SEARCH_DEBOUNCE) .collect { nextPage = 1 currentQuery = it @@ -143,4 +143,7 @@ class CourseSearchViewModel( } } + companion object { + private const val SEARCH_DEBOUNCE = 400L + } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt index 5413543c9..e4c7687a6 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/ui/DiscoveryUI.kt @@ -92,7 +92,6 @@ fun DiscoveryCourseItem( windowSize: WindowSize, onClick: (String) -> Unit, ) { - val imageWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -139,7 +138,8 @@ fun DiscoveryCourseItem( modifier = Modifier .testTag("txt_course_org") .padding(top = 12.dp), - text = course.org, color = MaterialTheme.appColors.textFieldHint, + text = course.org, + color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.labelMedium ) Text( diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt index 83550dc42..9a88b445a 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt @@ -38,7 +38,6 @@ class NativeDiscoveryViewModelTest { @get:Rule val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule() - private val dispatcher = StandardTestDispatcher() private val config = mockk() @@ -158,7 +157,8 @@ class NativeDiscoveryViewModelTest { "2", 7, "1" - ), emptyList() + ), + emptyList() ) advanceUntilIdle() @@ -188,7 +188,8 @@ class NativeDiscoveryViewModelTest { "", 7, "1" - ), emptyList() + ), + emptyList() ) advanceUntilIdle() @@ -200,7 +201,6 @@ class NativeDiscoveryViewModelTest { assert(viewModel.canLoadMore.value == false) } - @Test fun `updateData no internet connection`() = runTest { val viewModel = NativeDiscoveryViewModel( @@ -269,7 +269,8 @@ class NativeDiscoveryViewModelTest { "2", 7, "1" - ), emptyList() + ), + emptyList() ) viewModel.updateData() advanceUntilIdle() @@ -300,7 +301,8 @@ class NativeDiscoveryViewModelTest { "", 7, "1" - ), emptyList() + ), + emptyList() ) viewModel.updateData() advanceUntilIdle() @@ -312,5 +314,4 @@ class NativeDiscoveryViewModelTest { assert(viewModel.canLoadMore.value == false) assert(viewModel.uiState.value is DiscoveryUIState.Courses) } - } diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt index 3e0f4906f..13c1f3895 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/detail/CourseDetailsViewModelTest.kt @@ -256,7 +256,6 @@ class CourseDetailsViewModelTest { ) } returns Unit - viewModel.enrollInACourse("", "") advanceUntilIdle() @@ -305,7 +304,6 @@ class CourseDetailsViewModelTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getCourseDetails(any()) } returns mockCourse - delay(200) viewModel.enrollInACourse("", "") advanceUntilIdle() diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt index 8b5a1bab4..150d02e3e 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/search/CourseSearchViewModelTest.kt @@ -38,7 +38,6 @@ class CourseSearchViewModelTest { @get:Rule val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule() - private val dispatcher = UnconfinedTestDispatcher() private val config = mockk() @@ -148,7 +147,8 @@ class CourseSearchViewModelTest { "", 5, "" - ), emptyList() + ), + emptyList() ) every { analytics.discoveryCourseSearchEvent(any(), any()) } returns Unit @@ -174,7 +174,8 @@ class CourseSearchViewModelTest { "2", 5, "" - ), listOf(mockCourse, mockCourse) + ), + listOf(mockCourse, mockCourse) ) coEvery { interactor.getCoursesListByQuery( @@ -209,7 +210,8 @@ class CourseSearchViewModelTest { "2", 5, "" - ), listOf(mockCourse, mockCourse) + ), + listOf(mockCourse, mockCourse) ) coEvery { interactor.getCoursesListByQuery( @@ -245,7 +247,8 @@ class CourseSearchViewModelTest { "2", 5, "" - ), listOf(mockCourse, mockCourse) + ), + listOf(mockCourse, mockCourse) ) viewModel.updateSearchQuery() diff --git a/discussion/src/androidTest/java/org/openedx/discussion/ExampleInstrumentedTest.kt b/discussion/src/androidTest/java/org/openedx/discussion/ExampleInstrumentedTest.kt index ef1311ede..733b313ce 100644 --- a/discussion/src/androidTest/java/org/openedx/discussion/ExampleInstrumentedTest.kt +++ b/discussion/src/androidTest/java/org/openedx/discussion/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package org.openedx.discussion -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("org.openedx.discussion.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt index 4d0343d69..37f84aa3a 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt @@ -1,14 +1,24 @@ package org.openedx.discussion.data.api -import org.json.JSONObject import org.openedx.core.data.model.BlocksCompletionBody -import org.openedx.discussion.data.model.request.* +import org.openedx.discussion.data.model.request.CommentBody +import org.openedx.discussion.data.model.request.FollowBody +import org.openedx.discussion.data.model.request.ReadBody +import org.openedx.discussion.data.model.request.ReportBody +import org.openedx.discussion.data.model.request.ThreadBody +import org.openedx.discussion.data.model.request.VoteBody import org.openedx.discussion.data.model.response.CommentResult import org.openedx.discussion.data.model.response.CommentsResponse import org.openedx.discussion.data.model.response.ThreadsResponse import org.openedx.discussion.data.model.response.ThreadsResponse.Thread import org.openedx.discussion.data.model.response.TopicsResponse -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query interface DiscussionApi { @@ -117,15 +127,14 @@ interface DiscussionApi { @POST("/api/discussion/v1/comments/") suspend fun createComment( @Body commentBody: CommentBody - ) : CommentResult + ): CommentResult @POST("/api/discussion/v1/threads/") - suspend fun createThread(@Body threadBody: ThreadBody) : ThreadsResponse.Thread + suspend fun createThread(@Body threadBody: ThreadBody): Thread @POST("/api/completion/v1/completion-batch") suspend fun markBlocksCompletion( @Body blocksCompletionBody: BlocksCompletionBody ) - -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/request/FollowBody.kt b/discussion/src/main/java/org/openedx/discussion/data/model/request/FollowBody.kt index eec64c7b3..f83d7289b 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/request/FollowBody.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/request/FollowBody.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class FollowBody( @SerializedName("following") val following: Boolean -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/request/ReadBody.kt b/discussion/src/main/java/org/openedx/discussion/data/model/request/ReadBody.kt index 054d11f9f..012165559 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/request/ReadBody.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/request/ReadBody.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class ReadBody( @SerializedName("read") val read: Boolean -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/request/ReportBody.kt b/discussion/src/main/java/org/openedx/discussion/data/model/request/ReportBody.kt index 05b556ce8..bae817a41 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/request/ReportBody.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/request/ReportBody.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class ReportBody( @SerializedName("abuse_flagged") val abuseFlagged: Boolean -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/request/ThreadBody.kt b/discussion/src/main/java/org/openedx/discussion/data/model/request/ThreadBody.kt index d9b8d9cff..2712c0d55 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/request/ThreadBody.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/request/ThreadBody.kt @@ -15,4 +15,4 @@ data class ThreadBody( val rawBody: String, @SerializedName("following") val following: Boolean = true -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/request/VoteBody.kt b/discussion/src/main/java/org/openedx/discussion/data/model/request/VoteBody.kt index cb93889de..cfa71cbbd 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/request/VoteBody.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/request/VoteBody.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName data class VoteBody( @SerializedName("voted") val voted: Boolean -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt b/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt index a2248b036..77b50e504 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt @@ -96,4 +96,4 @@ data class CommentResult( users?.entries?.associate { it.key to it.value.mapToDomain() } ) } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/response/ThreadsResponse.kt b/discussion/src/main/java/org/openedx/discussion/data/model/response/ThreadsResponse.kt index 2aea8cd43..b34005c04 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/response/ThreadsResponse.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/response/ThreadsResponse.kt @@ -132,17 +132,19 @@ data class ThreadsResponse( ) } - fun serverTypeToLocalType(): DiscussionType { + private fun serverTypeToLocalType(): DiscussionType { val actualType = if (type.contains("-")) { type.replace("-", "_") - } else type + } else { + type + } return try { DiscussionType.valueOf(actualType.uppercase()) } catch (e: Exception) { - throw IllegalStateException("Unknown thread type") + e.printStackTrace() + error("Unknown thread type") } } - } fun mapToDomain(): ThreadsData { @@ -152,6 +154,4 @@ data class ThreadsResponse( pagination.mapToDomain() ) } - } - diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/response/TopicsResponse.kt b/discussion/src/main/java/org/openedx/discussion/data/model/response/TopicsResponse.kt index b1e751755..a8cd6cd3b 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/response/TopicsResponse.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/response/TopicsResponse.kt @@ -36,4 +36,4 @@ data class TopicsResponse( nonCoursewareTopics = nonCoursewareTopics?.map { it.mapToDomain() } ?: emptyList() ) } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt index ac07087cd..6e8143a36 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt @@ -137,7 +137,6 @@ class DiscussionRepository( ) = api.createComment(CommentBody(threadId, rawBody, parentId)).mapToDomain() - suspend fun createThread( topicId: String, courseId: String, @@ -156,5 +155,4 @@ class DiscussionRepository( ) return api.markBlocksCompletion(blocksCompletionBody) } - } diff --git a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt index 90960011c..3a267c1cb 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt @@ -1,7 +1,6 @@ package org.openedx.discussion.domain.interactor import org.openedx.discussion.data.repository.DiscussionRepository -import org.openedx.discussion.domain.model.CommentsData import org.openedx.discussion.domain.model.DiscussionComment class DiscussionInteractor( diff --git a/discussion/src/main/java/org/openedx/discussion/domain/model/CommentsData.kt b/discussion/src/main/java/org/openedx/discussion/domain/model/CommentsData.kt index 01937f16e..f033770e0 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/model/CommentsData.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/model/CommentsData.kt @@ -5,4 +5,4 @@ import org.openedx.core.domain.model.Pagination data class CommentsData( val results: List, val pagination: Pagination -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/domain/model/DiscussionComment.kt b/discussion/src/main/java/org/openedx/discussion/domain/model/DiscussionComment.kt index 9f71a0617..13a2fba9c 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/model/DiscussionComment.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/model/DiscussionComment.kt @@ -1,9 +1,9 @@ package org.openedx.discussion.domain.model import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.openedx.core.domain.model.ProfileImage import org.openedx.core.extension.LinkedImageText -import kotlinx.parcelize.Parcelize @Parcelize data class DiscussionComment( @@ -30,4 +30,4 @@ data class DiscussionComment( val children: List, val profileImage: ProfileImage?, val users: Map? -) : Parcelable \ No newline at end of file +) : Parcelable diff --git a/discussion/src/main/java/org/openedx/discussion/domain/model/Thread.kt b/discussion/src/main/java/org/openedx/discussion/domain/model/Thread.kt index 8d2572788..9b7f2498c 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/model/Thread.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/model/Thread.kt @@ -1,10 +1,10 @@ package org.openedx.discussion.domain.model import android.os.Parcelable +import kotlinx.parcelize.Parcelize import org.openedx.core.domain.model.ProfileImage import org.openedx.core.extension.LinkedImageText import org.openedx.discussion.R -import kotlinx.parcelize.Parcelize @Parcelize data class Thread( diff --git a/discussion/src/main/java/org/openedx/discussion/domain/model/ThreadsData.kt b/discussion/src/main/java/org/openedx/discussion/domain/model/ThreadsData.kt index 2575d1cf6..92f8d8cc6 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/model/ThreadsData.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/model/ThreadsData.kt @@ -7,6 +7,3 @@ data class ThreadsData( val textSearchRewrite: String, val pagination: Pagination ) - - - diff --git a/discussion/src/main/java/org/openedx/discussion/domain/model/TopicsData.kt b/discussion/src/main/java/org/openedx/discussion/domain/model/TopicsData.kt index 12d9ad926..72a8e5489 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/model/TopicsData.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/model/TopicsData.kt @@ -1,6 +1,5 @@ package org.openedx.discussion.domain.model - data class TopicsData( val coursewareTopics: List, val nonCoursewareTopics: List @@ -11,4 +10,4 @@ data class Topic( val name: String, val threadListUrl: String, val children: List -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt index 54f519004..481049907 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionRouter.kt @@ -41,4 +41,4 @@ interface DiscussionRouter { fm: FragmentManager, username: String ) -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt index 2c7f03bd0..b33646b9a 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsFragment.kt @@ -86,6 +86,7 @@ import org.openedx.discussion.R import org.openedx.discussion.domain.model.DiscussionComment import org.openedx.discussion.domain.model.DiscussionType import org.openedx.discussion.presentation.DiscussionRouter +import org.openedx.discussion.presentation.comments.DiscussionCommentsFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discussion.presentation.ui.CommentItem import org.openedx.discussion.presentation.ui.ThreadMainItem import org.openedx.foundation.extension.parcelable @@ -95,7 +96,6 @@ import org.openedx.foundation.presentation.WindowType import org.openedx.foundation.presentation.rememberWindowSize import org.openedx.foundation.presentation.windowSizeValue - class DiscussionCommentsFragment : Fragment() { private val viewModel by viewModel { @@ -121,7 +121,6 @@ class DiscussionCommentsFragment : Fragment() { val uiState by viewModel.uiState.observeAsState(DiscussionCommentsUIState.Loading) val uiMessage by viewModel.uiMessage.observeAsState() val canLoadMore by viewModel.canLoadMore.observeAsState(false) - val scrollToBottom by viewModel.scrollToBottom.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) DiscussionCommentsScreen( @@ -130,7 +129,6 @@ class DiscussionCommentsFragment : Fragment() { uiMessage = uiMessage, title = viewModel.title, canLoadMore = canLoadMore, - scrollToBottom = scrollToBottom, refreshing = refreshing, onSwipeRefresh = { viewModel.updateThreadComments() @@ -156,12 +154,15 @@ class DiscussionCommentsFragment : Fragment() { }, onCommentClick = { router.navigateToDiscussionResponses( - requireActivity().supportFragmentManager, it, viewModel.thread.closed + requireActivity().supportFragmentManager, + it, + viewModel.thread.closed ) }, onUserPhotoClick = { username -> router.navigateToAnothersProfile( - requireActivity().supportFragmentManager, username + requireActivity().supportFragmentManager, + username ) }, onAddResponseClick = { @@ -181,6 +182,7 @@ class DiscussionCommentsFragment : Fragment() { const val ACTION_UPVOTE_THREAD = "action_upvote_thread" const val ACTION_REPORT_THREAD = "action_report_thread" const val ACTION_FOLLOW_THREAD = "action_follow_thread" + const val LOAD_MORE_THRESHOLD = 4 private const val ARG_THREAD = "argThread" @@ -192,7 +194,6 @@ class DiscussionCommentsFragment : Fragment() { return fragment } } - } @OptIn(ExperimentalMaterialApi::class) @@ -203,7 +204,6 @@ private fun DiscussionCommentsScreen( uiMessage: UIMessage?, title: String, canLoadMore: Boolean, - scrollToBottom: Boolean, refreshing: Boolean, onSwipeRefresh: () -> Unit, paginationCallBack: () -> Unit, @@ -349,7 +349,7 @@ private fun DiscussionCommentsScreen( .padding(horizontal = paddingContent) .padding(top = 24.dp, bottom = 4.dp), text = pluralStringResource( - id = org.openedx.discussion.R.plurals.discussion_responses_capitalized, + id = R.plurals.discussion_responses_capitalized, uiState.count, uiState.count ), @@ -375,7 +375,8 @@ private fun DiscussionCommentsScreen( }, onUserPhotoClick = { onUserPhotoClick(comment.author) - }) + } + ) } item { if (canLoadMore) { @@ -388,7 +389,7 @@ private fun DiscussionCommentsScreen( } } } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallBack() } if (!isSystemInDarkTheme()) { @@ -453,7 +454,9 @@ private fun DiscussionCommentsScreen( Icon( modifier = Modifier.padding(7.dp), painter = painterResource(id = R.drawable.discussion_ic_send), - contentDescription = stringResource(id = R.string.discussion_add_response), + contentDescription = stringResource( + id = R.string.discussion_add_response + ), tint = iconButtonColor ) } @@ -464,8 +467,9 @@ private fun DiscussionCommentsScreen( is DiscussionCommentsUIState.Loading -> { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -482,14 +486,13 @@ private fun DiscussionCommentsScreen( } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_5_Dark", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun DiscussionCommentsScreenPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionCommentsScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiState = DiscussionCommentsUIState.Success( @@ -501,13 +504,10 @@ private fun DiscussionCommentsScreenPreview() { title = "Test Screen", canLoadMore = false, paginationCallBack = {}, - onItemClick = { _, _, _ -> - - }, + onItemClick = { _, _, _ -> }, onCommentClick = {}, onAddResponseClick = {}, onBackClick = {}, - scrollToBottom = false, refreshing = false, onSwipeRefresh = {}, onUserPhotoClick = {} @@ -515,12 +515,11 @@ private fun DiscussionCommentsScreenPreview() { } } - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun DiscussionCommentsScreenTabletPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionCommentsScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiState = DiscussionCommentsUIState.Success( @@ -532,13 +531,10 @@ private fun DiscussionCommentsScreenTabletPreview() { title = "Test Screen", canLoadMore = false, paginationCallBack = {}, - onItemClick = { _, _, _ -> - - }, + onItemClick = { _, _, _ -> }, onCommentClick = {}, onAddResponseClick = {}, onBackClick = {}, - scrollToBottom = false, refreshing = false, onSwipeRefresh = {}, onUserPhotoClick = {} @@ -605,4 +601,4 @@ private val mockComment = DiscussionComment( emptyList(), profileImage = ProfileImage("", "", "", "", false), mapOf() -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsUIState.kt index 389e8b44c..f3fe81a12 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsUIState.kt @@ -2,7 +2,6 @@ package org.openedx.discussion.presentation.comments import org.openedx.discussion.domain.model.DiscussionComment - sealed class DiscussionCommentsUIState { data class Success( val thread: org.openedx.discussion.domain.model.Thread, @@ -10,5 +9,5 @@ sealed class DiscussionCommentsUIState { val count: Int ) : DiscussionCommentsUIState() - object Loading : DiscussionCommentsUIState() -} \ No newline at end of file + data object Loading : DiscussionCommentsUIState() +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt index 33e858b6b..fbd5b464e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModel.kt @@ -48,10 +48,6 @@ class DiscussionCommentsViewModel( val isUpdating: LiveData get() = _isUpdating - private val _scrollToBottom = MutableLiveData() - val scrollToBottom: LiveData - get() = _scrollToBottom - private val comments = mutableListOf() private var page = 1 private var isLoading = false @@ -68,10 +64,11 @@ class DiscussionCommentsViewModel( comments.toList(), commentCount ) - _scrollToBottom.value = true } else { _uiMessage.value = - UIMessage.ToastMessage(resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added)) + UIMessage.ToastMessage( + resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) + ) } thread = thread.copy(commentCount = thread.commentCount + 1) sendThreadUpdated() @@ -290,7 +287,9 @@ class DiscussionCommentsViewModel( comments.add(response) } else { _uiMessage.value = - UIMessage.ToastMessage(resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added)) + UIMessage.ToastMessage( + resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) + ) } _uiState.value = DiscussionCommentsUIState.Success(thread, comments.toList(), commentCount) @@ -305,5 +304,4 @@ class DiscussionCommentsViewModel( } } } - -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt index ebf0fb1b9..736455a7e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesFragment.kt @@ -88,6 +88,7 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.discussion.domain.model.DiscussionComment import org.openedx.discussion.presentation.DiscussionRouter import org.openedx.discussion.presentation.comments.DiscussionCommentsFragment +import org.openedx.discussion.presentation.responses.DiscussionResponsesFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discussion.presentation.ui.CommentMainItem import org.openedx.foundation.extension.parcelable import org.openedx.foundation.presentation.UIMessage @@ -149,6 +150,7 @@ class DiscussionResponsesFragment : Fragment() { ) } } + DiscussionCommentsFragment.ACTION_REPORT_COMMENT -> { viewModel.setCommentReported( id, @@ -165,7 +167,8 @@ class DiscussionResponsesFragment : Fragment() { }, onUserPhotoClick = { username -> router.navigateToAnothersProfile( - requireActivity().supportFragmentManager, username + requireActivity().supportFragmentManager, + username ) } ) @@ -176,6 +179,7 @@ class DiscussionResponsesFragment : Fragment() { companion object { private const val ARG_COMMENT = "comment" private const val ARG_IS_CLOSED = "isClosed" + const val LOAD_MORE_THRESHOLD = 4 fun newInstance( comment: DiscussionComment, @@ -240,7 +244,6 @@ private fun DiscussionResponsesScreen( .navigationBarsPadding(), backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -346,7 +349,7 @@ private fun DiscussionResponsesScreen( bool ) }, - onUserPhotoClick = {username -> + onUserPhotoClick = { username -> onUserPhotoClick(username) } ) @@ -392,7 +395,7 @@ private fun DiscussionResponsesScreen( onClick = { action, commentId, bool -> onItemClick(action, commentId, bool) }, - onUserPhotoClick = {username -> + onUserPhotoClick = { username -> onUserPhotoClick(username) } ) @@ -409,7 +412,7 @@ private fun DiscussionResponsesScreen( } } } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallBack() } } @@ -445,7 +448,9 @@ private fun DiscussionResponsesScreen( shape = MaterialTheme.appShapes.buttonShape, placeholder = { Text( - text = stringResource(id = org.openedx.discussion.R.string.discussion_add_comment), + text = stringResource( + id = org.openedx.discussion.R.string.discussion_add_comment + ), color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.labelLarge, ) @@ -474,7 +479,9 @@ private fun DiscussionResponsesScreen( ) { Icon( modifier = Modifier.padding(7.dp), - painter = painterResource(id = org.openedx.discussion.R.drawable.discussion_ic_send), + painter = painterResource( + id = org.openedx.discussion.R.drawable.discussion_ic_send + ), contentDescription = null, tint = iconButtonColor ) @@ -483,10 +490,12 @@ private fun DiscussionResponsesScreen( } } } + is DiscussionResponsesUIState.Loading -> { Box( - Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -509,11 +518,12 @@ private fun DiscussionResponsesScreen( @Preview(name = "NEXUS_5_Dark", device = Devices.NEXUS_5, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun DiscussionResponsesScreenPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionResponsesScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiState = DiscussionResponsesUIState.Success( - mockComment, listOf( + mockComment, + listOf( mockComment, mockComment ) @@ -523,12 +533,8 @@ private fun DiscussionResponsesScreenPreview() { refreshing = false, onSwipeRefresh = {}, paginationCallBack = { }, - onItemClick = { _, _, _ -> - - }, - addCommentClick = { - - }, + onItemClick = { _, _, _ -> }, + addCommentClick = {}, onBackClick = {}, isClosed = false, onUserPhotoClick = {} @@ -540,11 +546,12 @@ private fun DiscussionResponsesScreenPreview() { @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun DiscussionResponsesScreenTabletPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionResponsesScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), uiState = DiscussionResponsesUIState.Success( - mockComment, listOf( + mockComment, + listOf( mockComment, mockComment ) @@ -554,12 +561,8 @@ private fun DiscussionResponsesScreenTabletPreview() { refreshing = false, onSwipeRefresh = {}, paginationCallBack = { }, - onItemClick = { _, _, _ -> - - }, - addCommentClick = { - - }, + onItemClick = { _, _, _ -> }, + addCommentClick = {}, onBackClick = {}, isClosed = false, onUserPhotoClick = {} @@ -592,6 +595,3 @@ private val mockComment = DiscussionComment( ProfileImage("", "", "", "", false), mapOf() ) - - - diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesUIState.kt index 95f216988..dce4cf147 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesUIState.kt @@ -9,4 +9,4 @@ sealed class DiscussionResponsesUIState { ) : DiscussionResponsesUIState() object Loading : DiscussionResponsesUIState() -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt index ed8390c44..e4c675609 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModel.kt @@ -87,10 +87,14 @@ class DiscussionResponsesViewModel( } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) } else { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) } } finally { isLoading = false @@ -117,10 +121,14 @@ class DiscussionResponsesViewModel( } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) } else { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) } } } @@ -143,10 +151,14 @@ class DiscussionResponsesViewModel( } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) } else { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) } } } @@ -162,20 +174,25 @@ class DiscussionResponsesViewModel( comments.add(response) } else { _uiMessage.value = - UIMessage.ToastMessage(resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added)) + UIMessage.ToastMessage( + resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) + ) } _uiState.value = DiscussionResponsesUIState.Success(comment, comments.toList()) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) } else { _uiMessage.value = - UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) } } } } - -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt index 76c645e37..a8a835603 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadFragment.kt @@ -71,6 +71,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.discussion.domain.model.DiscussionType import org.openedx.discussion.presentation.DiscussionRouter +import org.openedx.discussion.presentation.search.DiscussionSearchThreadFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discussion.presentation.ui.ThreadItem import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize @@ -104,7 +105,8 @@ class DiscussionSearchThreadFragment : Fragment() { val uiState by viewModel.uiState.observeAsState( DiscussionSearchThreadUIState.Threads( - emptyList(), 0 + emptyList(), + 0 ) ) val uiMessage by viewModel.uiMessage.observeAsState() @@ -134,9 +136,9 @@ class DiscussionSearchThreadFragment : Fragment() { } } - companion object { private const val ARG_COURSE_ID = "courseId" + const val LOAD_MORE_THRESHOLD = 4 fun newInstance( courseId: String ): DiscussionSearchThreadFragment { @@ -147,7 +149,6 @@ class DiscussionSearchThreadFragment : Fragment() { return fragment } } - } @OptIn(ExperimentalMaterialApi::class) @@ -189,7 +190,6 @@ private fun DiscussionSearchThreadScreen( .navigationBarsPadding(), backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -348,7 +348,7 @@ private fun DiscussionSearchThreadScreen( } } } - if (scrollState.shouldLoadMore(firstVisibleIndex, 4)) { + if (scrollState.shouldLoadMore(firstVisibleIndex, LOAD_MORE_THRESHOLD)) { paginationCallback() } } @@ -366,7 +366,6 @@ private fun DiscussionSearchThreadScreen( } } - @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -382,9 +381,8 @@ fun DiscussionSearchThreadScreenPreview() { onSearchTextChanged = {}, onSwipeRefresh = {}, paginationCallback = {}, - onBackClick = { - - }) + onBackClick = {} + ) } } @@ -403,9 +401,8 @@ fun DiscussionSearchThreadScreenTabletPreview() { onSearchTextChanged = {}, onSwipeRefresh = {}, paginationCallback = {}, - onBackClick = { - - }) + onBackClick = {} + ) } } @@ -442,4 +439,4 @@ private val mockThread = org.openedx.discussion.domain.model.Thread( 10, false, false -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadUIState.kt index b3f75f9e3..f134bce82 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadUIState.kt @@ -4,5 +4,5 @@ sealed class DiscussionSearchThreadUIState { class Threads(val data: List, val count: Int) : DiscussionSearchThreadUIState() - object Loading : DiscussionSearchThreadUIState() -} \ No newline at end of file + data object Loading : DiscussionSearchThreadUIState() +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt index c101ae7b9..d95dcba9e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModel.kt @@ -34,7 +34,8 @@ class DiscussionSearchThreadViewModel( private val _uiState = MutableLiveData( DiscussionSearchThreadUIState.Threads( - emptyList(), 0 + emptyList(), + 0 ) ) val uiState: LiveData @@ -52,7 +53,6 @@ class DiscussionSearchThreadViewModel( val isUpdating: LiveData get() = _isUpdating - private var nextPage: Int? = 1 private var currentQuery: String? = null private val threadsList = mutableListOf() @@ -90,7 +90,7 @@ class DiscussionSearchThreadViewModel( viewModelScope.launch { queryChannel .asSharedFlow() - .debounce(400) + .debounce(SEARCH_DEBOUNCE) .collect { query -> nextPage = 1 threadsList.clear() @@ -168,4 +168,7 @@ class DiscussionSearchThreadViewModel( .launchIn(viewModelScope) } -} \ No newline at end of file + companion object { + private const val SEARCH_DEBOUNCE = 400L + } +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt index 82ad75a17..c66838fb0 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadFragment.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout @@ -136,7 +135,6 @@ class DiscussionAddThreadFragment : Fragment() { if (success != null) { viewModel.sendThreadAdded() requireActivity().supportFragmentManager.popBackStack() - } } } @@ -160,8 +158,6 @@ class DiscussionAddThreadFragment : Fragment() { } } - -@OptIn(ExperimentalMaterialApi::class) @Composable private fun DiscussionAddThreadScreen( windowSize: WindowSize, @@ -219,7 +215,6 @@ private fun DiscussionAddThreadScreen( .navigationBarsPadding(), backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -278,7 +273,6 @@ private fun DiscussionAddThreadScreen( ) } ) { - HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) Box( @@ -345,10 +339,12 @@ private fun DiscussionAddThreadScreen( color = MaterialTheme.appColors.textPrimary ) Spacer(Modifier.height(16.dp)) - Tabs(tabs = listOf( - stringResource(id = discussionR.string.discussion_discussion), - stringResource(id = discussionR.string.discussion_question) - ), currentPage = currentPage, + Tabs( + tabs = listOf( + stringResource(id = discussionR.string.discussion_discussion), + stringResource(id = discussionR.string.discussion_question) + ), + currentPage = currentPage, onItemClick = { bool -> if (bool) { discussionType = DiscussionType.QUESTION.value @@ -357,7 +353,8 @@ private fun DiscussionAddThreadScreen( discussionType = DiscussionType.DISCUSSION.value currentPage = 0 } - }) + } + ) Spacer(Modifier.height(24.dp)) SelectableField( text = postToTopic.first, @@ -369,7 +366,8 @@ private fun DiscussionAddThreadScreen( bottomSheetScaffoldState.show() } } - }) + } + ) Spacer(Modifier.height(24.dp)) OpenEdXOutlinedTextField( modifier = Modifier @@ -390,9 +388,13 @@ private fun DiscussionAddThreadScreen( modifier = Modifier .fillMaxWidth() .height(150.dp), - title = if (currentPage == 0) stringResource(id = org.openedx.discussion.R.string.discussion_discussion) else stringResource( - id = discussionR.string.discussion_question - ), + title = if (currentPage == 0) { + stringResource(id = org.openedx.discussion.R.string.discussion_discussion) + } else { + stringResource( + id = discussionR.string.discussion_question + ) + }, isSingleLine = false, withRequiredMark = true, imeAction = ImeAction.Default, @@ -418,7 +420,8 @@ private fun DiscussionAddThreadScreen( checked = followPost, onCheckedChange = { followPost = it - }) + } + ) Spacer(Modifier.width(6.dp)) Text( text = if (currentPage == 0) { @@ -473,13 +476,18 @@ private fun Tabs( isLimited: Boolean = false, ) { val isFirstPage = currentPage == 0 - TabRow(selectedTabIndex = currentPage, + TabRow( + selectedTabIndex = currentPage, backgroundColor = MaterialTheme.appColors.surface, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) - .clip(RoundedCornerShape(20)) - .border(1.dp, MaterialTheme.appColors.cardViewBorder, RoundedCornerShape(20)), + .clip(RoundedCornerShape(percent = 20)) + .border( + 1.dp, + MaterialTheme.appColors.cardViewBorder, + RoundedCornerShape(percent = 20) + ), indicator = { _ -> Box {} } @@ -492,16 +500,19 @@ private fun Tabs( MaterialTheme.appColors.textPrimaryVariant } Tab( - modifier = if (selected) Modifier - .clip(RoundedCornerShape(20)) - .background( - MaterialTheme.appColors.primary - ) - else Modifier - .clip(RoundedCornerShape(20)) - .background( - MaterialTheme.appColors.surface - ), + modifier = if (selected) { + Modifier + .clip(RoundedCornerShape(percent = 20)) + .background( + MaterialTheme.appColors.primary + ) + } else { + Modifier + .clip(RoundedCornerShape(percent = 20)) + .background( + MaterialTheme.appColors.surface + ) + }, selected = selected, onClick = { if (!isLimited && !selected) { @@ -519,7 +530,7 @@ private fun SelectableField( text: String, onClick: () -> Unit, ) { - Column() { + Column { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = discussionR.string.discussion_topic), @@ -557,23 +568,19 @@ private fun SelectableField( } } - @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun DiscussionAddThreadScreenPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionAddThreadScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), topicData = Pair("", "General"), topics = emptyList(), uiMessage = null, isLoading = false, - onBackClick = { - }, - onPostDiscussionClick = { _, _, _, _, _ -> - - } + onBackClick = {}, + onPostDiscussionClick = { _, _, _, _, _ -> } ) } } @@ -582,18 +589,15 @@ private fun DiscussionAddThreadScreenPreview() { @Preview(uiMode = UI_MODE_NIGHT_YES, device = Devices.NEXUS_9) @Composable private fun DiscussionAddThreadScreenTabletPreview() { - OpenEdXTheme() { + OpenEdXTheme { DiscussionAddThreadScreen( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), topicData = Pair("", "General"), topics = emptyList(), uiMessage = null, isLoading = false, - onBackClick = { - }, - onPostDiscussionClick = { _, _, _, _, _ -> - - } + onBackClick = {}, + onPostDiscussionClick = { _, _, _, _, _ -> } ) } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt index d17837747..b16b9f300 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModel.kt @@ -74,5 +74,4 @@ class DiscussionAddThreadViewModel( notifier.send(DiscussionThreadAdded()) } } - } 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 35884fec0..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 @@ -91,6 +91,7 @@ import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.discussion.domain.model.DiscussionType import org.openedx.discussion.presentation.DiscussionRouter +import org.openedx.discussion.presentation.threads.DiscussionThreadsFragment.Companion.LOAD_MORE_THRESHOLD import org.openedx.discussion.presentation.ui.ThreadItem import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.presentation.WindowSize @@ -185,6 +186,7 @@ class DiscussionThreadsFragment : Fragment() { private const val ARG_TOPIC_ID = "topicId" private const val ARG_TITLE = "title" private const val ARG_FRAGMENT_VIEW_TYPE = "fragmentViewType" + const val LOAD_MORE_THRESHOLD = 4 fun newInstance( threadType: String, @@ -208,6 +210,7 @@ class DiscussionThreadsFragment : Fragment() { } } +@Suppress("MaximumLineLength", "MaxLineLength") @OptIn(ExperimentalMaterialApi::class) @Composable private fun DiscussionThreadsScreen( @@ -226,7 +229,6 @@ private fun DiscussionThreadsScreen( paginationCallback: () -> Unit, onBackClick: () -> Unit ) { - val scaffoldState = rememberScaffoldState() val bottomSheetScaffoldState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden, @@ -299,7 +301,6 @@ private fun DiscussionThreadsScreen( modifier = scaffoldModifier, backgroundColor = MaterialTheme.appColors.background ) { - val contentWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -413,7 +414,13 @@ private fun DiscussionThreadsScreen( } } Surface( - modifier = Modifier.padding(top = if (viewType == FragmentViewType.FULL_CONTENT) 6.dp else 0.dp), + modifier = Modifier.padding( + top = if (viewType == FragmentViewType.FULL_CONTENT) { + 6.dp + } else { + 0.dp + } + ), color = MaterialTheme.appColors.background ) { Box(Modifier.pullRefresh(pullRefreshState)) { @@ -437,7 +444,9 @@ private fun DiscussionThreadsScreen( ) { IconText( text = filterType.first, - painter = painterResource(id = discussionR.drawable.discussion_ic_filter), + painter = painterResource( + id = discussionR.drawable.discussion_ic_filter + ), textStyle = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textPrimary, onClick = { @@ -467,7 +476,9 @@ private fun DiscussionThreadsScreen( ) IconText( text = sortType.first, - painter = painterResource(id = discussionR.drawable.discussion_ic_sort), + painter = painterResource( + id = discussionR.drawable.discussion_ic_sort + ), textStyle = MaterialTheme.appTypography.labelMedium, color = MaterialTheme.appColors.textPrimary, onClick = { @@ -521,7 +532,9 @@ private fun DiscussionThreadsScreen( Modifier .size(40.dp) .clip(CircleShape) - .background(MaterialTheme.appColors.secondaryButtonBackground) + .background( + MaterialTheme.appColors.secondaryButtonBackground + ) .clickable { onCreatePostClick() }, @@ -529,8 +542,12 @@ private fun DiscussionThreadsScreen( ) { Icon( modifier = Modifier.size(16.dp), - painter = painterResource(id = discussionR.drawable.discussion_ic_add_comment), - contentDescription = stringResource(id = discussionR.string.discussion_add_comment), + painter = painterResource( + discussionR.drawable.discussion_ic_add_comment + ), + contentDescription = stringResource( + discussionR.string.discussion_add_comment + ), tint = MaterialTheme.appColors.primaryButtonText ) } @@ -550,13 +567,15 @@ private fun DiscussionThreadsScreen( .padding(vertical = 16.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) + CircularProgressIndicator( + color = MaterialTheme.appColors.primary + ) } } } if (scrollState.shouldLoadMore( firstVisibleIndex, - 4 + LOAD_MORE_THRESHOLD ) ) { paginationCallback() @@ -582,7 +601,9 @@ private fun DiscussionThreadsScreen( Spacer(modifier = Modifier.height(20.dp)) Icon( modifier = Modifier.size(100.dp), - painter = painterResource(id = discussionR.drawable.discussion_ic_empty), + painter = painterResource( + id = discussionR.drawable.discussion_ic_empty + ), contentDescription = null, tint = MaterialTheme.appColors.textPrimary ) @@ -597,7 +618,9 @@ private fun DiscussionThreadsScreen( Spacer(Modifier.height(12.dp)) Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(discussionR.string.discussion_click_button_create_discussion), + text = stringResource( + discussionR.string.discussion_click_button_create_discussion + ), style = MaterialTheme.appTypography.bodyLarge, color = MaterialTheme.appColors.textPrimary, textAlign = TextAlign.Center @@ -606,19 +629,25 @@ private fun DiscussionThreadsScreen( OpenEdXOutlinedButton( modifier = Modifier .widthIn(184.dp, Dp.Unspecified), - text = stringResource(id = discussionR.string.discussion_create_post), + text = stringResource( + id = discussionR.string.discussion_create_post + ), onClick = { onCreatePostClick() }, content = { Icon( - painter = painterResource(id = discussionR.drawable.discussion_ic_add_comment), + painter = painterResource( + id = discussionR.drawable.discussion_ic_add_comment + ), contentDescription = null, tint = MaterialTheme.appColors.primary ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = stringResource(id = discussionR.string.discussion_create_post), + text = stringResource( + id = discussionR.string.discussion_create_post + ), color = MaterialTheme.appColors.primary, style = MaterialTheme.appTypography.labelLarge ) @@ -635,7 +664,8 @@ private fun DiscussionThreadsScreen( is DiscussionThreadsUIState.Loading -> { Box( Modifier - .fillMaxSize(), contentAlignment = Alignment.Center + .fillMaxSize(), + contentAlignment = Alignment.Center ) { CircularProgressIndicator(color = MaterialTheme.appColors.primary) } @@ -737,4 +767,4 @@ private val mockThread = org.openedx.discussion.domain.model.Thread( 10, false, false -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsUIState.kt index 94106edc0..8587f2697 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsUIState.kt @@ -5,4 +5,4 @@ sealed class DiscussionThreadsUIState { DiscussionThreadsUIState() object Loading : DiscussionThreadsUIState() -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt index 944152606..e79c7672b 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModel.kt @@ -245,4 +245,4 @@ class DiscussionThreadsViewModel( } } } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/FilterType.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/FilterType.kt index 377717f45..afab6a45a 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/FilterType.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/FilterType.kt @@ -13,4 +13,4 @@ enum class FilterType( companion object { const val type = "filter_type" } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/threads/SortType.kt b/discussion/src/main/java/org/openedx/discussion/presentation/threads/SortType.kt index 1aeac0c33..6c0211b6a 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/threads/SortType.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/threads/SortType.kt @@ -13,4 +13,4 @@ enum class SortType( companion object { const val type = "sort_type" } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt index 75bbe2eaa..1f4876eb4 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsScreen.kt @@ -110,7 +110,6 @@ private fun DiscussionTopicsUI( modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background ) { - val screenWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( @@ -203,7 +202,9 @@ private fun DiscussionTopicsUI( ) { ThreadItemCategory( name = stringResource(id = R.string.discussion_all_posts), - painterResource = painterResource(id = R.drawable.discussion_all_posts), + painterResource = painterResource( + id = R.drawable.discussion_all_posts + ), modifier = Modifier .weight(1f) .height(categoriesHeight), @@ -213,7 +214,8 @@ private fun DiscussionTopicsUI( "", context.getString(R.string.discussion_all_posts) ) - }) + } + ) ThreadItemCategory( name = stringResource(id = R.string.discussion_posts_following), painterResource = painterResource(id = R.drawable.discussion_star), @@ -226,7 +228,8 @@ private fun DiscussionTopicsUI( "", context.getString(R.string.discussion_posts_following) ) - }) + } + ) } } itemsIndexed(uiState.data) { index, topic -> @@ -322,4 +325,4 @@ private val mockTopic = Topic( name = "All Topics", threadListUrl = "", children = emptyList() -) \ No newline at end of file +) diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt index f1becc420..c85a8bcad 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsUIState.kt @@ -2,7 +2,6 @@ package org.openedx.discussion.presentation.topics import org.openedx.discussion.domain.model.Topic - sealed class DiscussionTopicsUIState { data class Topics(val data: List) : DiscussionTopicsUIState() data object Loading : DiscussionTopicsUIState() diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt index 16572da7c..84a5d3e15 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModel.kt @@ -55,7 +55,11 @@ class DiscussionTopicsViewModel( } catch (e: Exception) { _uiState.value = DiscussionTopicsUIState.Error if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_no_connection) + ) + ) } } finally { courseNotifier.send(CourseLoading(false)) @@ -94,4 +98,4 @@ class DiscussionTopicsViewModel( const val ALL_POSTS = "All posts" const val FOLLOWING_POSTS = "Following" } -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt b/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt index cd87e0498..376e3118e 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/ui/DiscussionUI.kt @@ -147,12 +147,19 @@ fun ThreadMainItem( } IconText( text = stringResource(id = R.string.discussion_follow), - painter = painterResource(if (thread.following) R.drawable.discussion_star_filled else R.drawable.discussion_star), + painter = painterResource( + if (thread.following) { + R.drawable.discussion_star_filled + } else { + R.drawable.discussion_star + } + ), textStyle = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textPrimaryVariant, onClick = { onClick(DiscussionCommentsFragment.ACTION_FOLLOW_THREAD, !thread.following) - }) + } + ) } Spacer(modifier = Modifier.height(24.dp)) HyperlinkImageText( @@ -193,7 +200,6 @@ fun ThreadMainItem( Spacer(modifier = Modifier.height(16.dp)) Divider(color = MaterialTheme.appColors.cardViewBorder) } - } @Composable @@ -306,7 +312,8 @@ fun CommentItem( comment.id, !comment.abuseFlagged ) - }) + } + ) } Spacer(modifier = Modifier.height(14.dp)) HyperlinkImageText( @@ -352,12 +359,10 @@ fun CommentItem( } ) } - } } } - @Composable fun CommentMainItem( modifier: Modifier, @@ -489,9 +494,9 @@ fun CommentMainItem( comment.id, !comment.abuseFlagged ) - }) + } + ) } - } } } @@ -539,13 +544,13 @@ fun ThreadItem( ) { Box { Icon( - modifier = Modifier.size((MaterialTheme.appTypography.labelSmall.fontSize.value + 4).dp), + modifier = Modifier.size((MaterialTheme.appTypography.labelLarge.fontSize.value).dp), painter = painterResource(id = R.drawable.discussion_ic_unread_replies), tint = MaterialTheme.appColors.textPrimaryVariant, contentDescription = null ) Image( - modifier = Modifier.size((MaterialTheme.appTypography.labelSmall.fontSize.value + 4).dp), + modifier = Modifier.size((MaterialTheme.appTypography.labelLarge.fontSize.value).dp), painter = painterResource(id = R.drawable.discussion_ic_unread_replies_dot), contentDescription = null ) @@ -593,7 +598,6 @@ fun ThreadItem( } } - @Composable fun ThreadItemCategory( name: String, @@ -610,7 +614,8 @@ fun ThreadItemCategory( MaterialTheme.appShapes.cardShape ) .clip(MaterialTheme.appShapes.cardShape) - .clickable { onClick() }), + .clickable { onClick() } + ), shape = MaterialTheme.appShapes.cardShape, backgroundColor = MaterialTheme.appColors.surface ) { @@ -651,7 +656,8 @@ fun TopicItem( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = topic.name, style = MaterialTheme.appTypography.titleMedium, + text = topic.name, + style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textPrimary ) Icon( @@ -660,17 +666,16 @@ fun TopicItem( contentDescription = "Expandable Arrow" ) } - } @Preview @Composable private fun TopicItemPreview() { - OpenEdXTheme() { - TopicItem(topic = mockTopic, - onClick = { _, _ -> - - }) + OpenEdXTheme { + TopicItem( + topic = mockTopic, + onClick = { _, _ -> } + ) } } @@ -678,17 +683,18 @@ private fun TopicItemPreview() { @Preview(uiMode = UI_MODE_NIGHT_YES) @Composable private fun ThreadItemPreview() { - OpenEdXTheme() { + OpenEdXTheme { ThreadItem( thread = mockThread, - onClick = {}) + onClick = {} + ) } } @Preview @Composable private fun CommentItemPreview() { - OpenEdXTheme() { + OpenEdXTheme { CommentItem( modifier = Modifier.fillMaxWidth(), comment = mockComment, diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentAdded.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentAdded.kt index d90f329a8..8cdfb6649 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentAdded.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentAdded.kt @@ -2,7 +2,6 @@ package org.openedx.discussion.system.notifier import org.openedx.discussion.domain.model.DiscussionComment - data class DiscussionCommentAdded( val comment: DiscussionComment -) : DiscussionEvent \ No newline at end of file +) : DiscussionEvent diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentDataChanged.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentDataChanged.kt index 04d46aa1e..a727e3afc 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentDataChanged.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionCommentDataChanged.kt @@ -2,4 +2,4 @@ package org.openedx.discussion.system.notifier import org.openedx.discussion.domain.model.DiscussionComment -class DiscussionCommentDataChanged(val discussionComment: DiscussionComment) : DiscussionEvent \ No newline at end of file +class DiscussionCommentDataChanged(val discussionComment: DiscussionComment) : DiscussionEvent diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionEvent.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionEvent.kt index abd2c1891..dcf442ffa 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionEvent.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionEvent.kt @@ -1,4 +1,3 @@ package org.openedx.discussion.system.notifier -interface DiscussionEvent { -} \ No newline at end of file +interface DiscussionEvent diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionNotifier.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionNotifier.kt index 2d1d01206..23569fc27 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionNotifier.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionNotifier.kt @@ -15,5 +15,4 @@ class DiscussionNotifier { suspend fun send(event: DiscussionCommentDataChanged) = channel.emit(event) suspend fun send(event: DiscussionThreadDataChanged) = channel.emit(event) suspend fun send(event: DiscussionThreadAdded) = channel.emit(event) - -} \ No newline at end of file +} diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadAdded.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadAdded.kt index 2b2f23525..e6db4b8ec 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadAdded.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadAdded.kt @@ -1,3 +1,3 @@ package org.openedx.discussion.system.notifier -class DiscussionThreadAdded : DiscussionEvent \ No newline at end of file +class DiscussionThreadAdded : DiscussionEvent diff --git a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadDataChanged.kt b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadDataChanged.kt index 752814095..feb71317e 100644 --- a/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadDataChanged.kt +++ b/discussion/src/main/java/org/openedx/discussion/system/notifier/DiscussionThreadDataChanged.kt @@ -2,4 +2,4 @@ package org.openedx.discussion.system.notifier class DiscussionThreadDataChanged( val thread: org.openedx.discussion.domain.model.Thread -) : DiscussionEvent \ No newline at end of file +) : DiscussionEvent diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt index 933a3bd5b..e9323270e 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/comments/DiscussionCommentsViewModelTest.kt @@ -40,6 +40,7 @@ import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager import java.net.UnknownHostException +@Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) class DiscussionCommentsViewModelTest { @@ -124,11 +125,11 @@ class DiscussionCommentsViewModelTest { mapOf() ) - //endregion - + // endregion private val comments = listOf( - mockComment.copy(id = "0"), mockComment.copy(id = "1") + mockComment.copy(id = "0"), + mockComment.copy(id = "1") ) @Before @@ -136,7 +137,9 @@ class DiscussionCommentsViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) } returns commentAddedSuccessfully + every { + resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) + } returns commentAddedSuccessfully } @After @@ -283,7 +286,6 @@ class DiscussionCommentsViewModelTest { coVerify(exactly = 0) { interactor.getThreadQuestionComments(any(), any(), any()) } coVerify(exactly = 1) { interactor.setThreadRead(any()) } - assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) assert(viewModel.isUpdating.value == false) @@ -306,7 +308,6 @@ class DiscussionCommentsViewModelTest { mockThread ) - coEvery { interactor.getThreadQuestionComments(any(), any(), any()) } returns CommentsData( comments, Pagination(10, "", 4, "1") @@ -374,7 +375,6 @@ class DiscussionCommentsViewModelTest { mockThread ) - coEvery { interactor.setThreadVoted(any(), any()) } throws UnknownHostException() viewModel.setThreadUpvoted(true) @@ -461,7 +461,6 @@ class DiscussionCommentsViewModelTest { mockThread ) - coEvery { interactor.setCommentFlagged(any(), any()) } throws UnknownHostException() viewModel.setCommentReported("", true) @@ -491,7 +490,6 @@ class DiscussionCommentsViewModelTest { mockThread ) - coEvery { interactor.setCommentFlagged(any(), any()) } throws Exception() viewModel.setCommentReported("", true) @@ -531,7 +529,6 @@ class DiscussionCommentsViewModelTest { assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } - @Test fun `setCommentUpvoted no internet connection exception`() = runTest { coEvery { interactor.getThreadComments(any(), any()) } returns CommentsData( @@ -698,7 +695,6 @@ class DiscussionCommentsViewModelTest { assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } - @Test fun `setThreadFollowed no internet connection exception`() = runTest { coEvery { interactor.getThreadComments(any(), any()) } returns CommentsData( @@ -743,7 +739,6 @@ class DiscussionCommentsViewModelTest { mockThread ) - coEvery { interactor.setThreadFollowed(any(), any()) } throws Exception() viewModel.setThreadFollowed(true) @@ -814,10 +809,8 @@ class DiscussionCommentsViewModelTest { assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) - assert(viewModel.scrollToBottom.value == true) } - @Test fun `DiscussionCommentAdded notifier test all comments not loaded`() = runTest { coEvery { interactor.getThreadComments(any(), any()) } returns CommentsData( @@ -850,7 +843,6 @@ class DiscussionCommentsViewModelTest { val message = viewModel.uiMessage.value as? UIMessage.ToastMessage assert(commentAddedSuccessfully == message?.message) assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) - assert(viewModel.scrollToBottom.value == null) } @Test @@ -910,7 +902,6 @@ class DiscussionCommentsViewModelTest { val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage Assert.assertEquals(noInternet, message?.message) - } @Test @@ -937,7 +928,6 @@ class DiscussionCommentsViewModelTest { val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage Assert.assertEquals(somethingWrong, message?.message) - } @Test @@ -987,7 +977,6 @@ class DiscussionCommentsViewModelTest { viewModel.createComment("") advanceUntilIdle() - } @Test @@ -1037,5 +1026,4 @@ class DiscussionCommentsViewModelTest { assert(viewModel.uiState.value is DiscussionCommentsUIState.Success) } - -} \ No newline at end of file +} diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt index e3e0aa8ca..ac57556bc 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/responses/DiscussionResponsesViewModelTest.kt @@ -118,9 +118,9 @@ class DiscussionResponsesViewModelTest { //endregion - private val comments = listOf( - mockComment.copy(id = "0"), mockComment.copy(id = "1") + mockComment.copy(id = "0"), + mockComment.copy(id = "1") ) @Before @@ -128,7 +128,9 @@ class DiscussionResponsesViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) } returns commentAddedSuccessfully + every { + resourceManager.getString(org.openedx.discussion.R.string.discussion_comment_added) + } returns commentAddedSuccessfully } @After @@ -474,7 +476,6 @@ class DiscussionResponsesViewModelTest { val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage Assert.assertEquals(noInternet, message?.message) - } @Test @@ -498,7 +499,6 @@ class DiscussionResponsesViewModelTest { val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage Assert.assertEquals(somethingWrong, message?.message) - } @Test @@ -520,7 +520,6 @@ class DiscussionResponsesViewModelTest { coVerify(exactly = 1) { interactor.createComment(any(), any(), any()) } - assert(viewModel.uiMessage.value != null) assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } @@ -566,5 +565,4 @@ class DiscussionResponsesViewModelTest { assert(viewModel.uiState.value is DiscussionResponsesUIState.Success) } - -} \ No newline at end of file +} diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt index 8cf079a35..39e01c194 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/search/DiscussionSearchThreadViewModelTest.kt @@ -287,7 +287,6 @@ class DiscussionSearchThreadViewModelTest { assert((viewModel.uiState.value as DiscussionSearchThreadUIState.Threads).data.isEmpty()) } - @Test fun `notifier DiscussionThreadDataChanged with list`() = runTest { val viewModel = DiscussionSearchThreadViewModel(interactor, resourceManager, notifier, "") @@ -321,5 +320,4 @@ class DiscussionSearchThreadViewModelTest { assert(viewModel.uiState.value is DiscussionSearchThreadUIState.Threads) assert((viewModel.uiState.value as DiscussionSearchThreadUIState.Threads).data.size == 1) } - -} \ No newline at end of file +} diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt index 27c74ae00..9dc8ba339 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionAddThreadViewModelTest.kt @@ -19,12 +19,8 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.R -import org.openedx.core.data.storage.CorePreferences -import org.openedx.core.domain.model.ProfileImage import org.openedx.core.extension.TextConverter import org.openedx.discussion.domain.interactor.DiscussionInteractor -import org.openedx.discussion.domain.model.DiscussionComment -import org.openedx.discussion.domain.model.DiscussionProfile import org.openedx.discussion.domain.model.DiscussionType import org.openedx.discussion.domain.model.Topic import org.openedx.discussion.system.notifier.DiscussionNotifier @@ -43,15 +39,12 @@ class DiscussionAddThreadViewModelTest { private val resourceManager = mockk() private val interactor = mockk() - private val preferencesManager = mockk() private val notifier = mockk(relaxed = true) private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" - private val commentAddedSuccessfully = "Comment Successfully added" //region mockThread - val mockThread = org.openedx.discussion.domain.model.Thread( "", "", @@ -86,67 +79,9 @@ class DiscussionAddThreadViewModelTest { false, false ) - - //endregion - - //region mockComment - - private val mockComment = DiscussionComment( - "", - "", - "", - "", - "", - "", - "", - TextConverter.textToLinkedImageText(""), - false, - true, - 20, - emptyList(), - false, - "", - "", - false, - "", - "", - "", - 21, - emptyList(), - null, - emptyMap() - ) - - private val mockCommentAdded = DiscussionComment( - "", - "", - "", - "", - "", - "", - "", - TextConverter.textToLinkedImageText(""), - false, - true, - 20, - emptyList(), - false, - "", - "", - false, - "", - "", - "", - 21, - emptyList(), - null, - mapOf("" to DiscussionProfile(ProfileImage("", "", "", "", false))) - ) - //endregion //region mockTopic - private val mockTopic = Topic( id = "", name = "All Topics", @@ -162,10 +97,6 @@ class DiscussionAddThreadViewModelTest { //endregion - private val comments = listOf( - mockComment.copy(id = "0"), mockComment.copy(id = "1") - ) - @Before fun setUp() { Dispatchers.setMain(dispatcher) @@ -280,6 +211,4 @@ class DiscussionAddThreadViewModelTest { assert(viewModel.getHandledTopicById("10").second == "0") } - - -} \ No newline at end of file +} diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt index 435981520..ae4f966ba 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/threads/DiscussionThreadsViewModelTest.kt @@ -488,7 +488,6 @@ class DiscussionThreadsViewModelTest { DiscussionTopicsViewModel.TOPIC ) - val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) lifecycleRegistry.addObserver(viewModel) @@ -537,6 +536,4 @@ class DiscussionThreadsViewModelTest { coVerify(exactly = 2) { interactor.getThreads(any(), any(), any(), any(), any()) } } - - -} \ No newline at end of file +} diff --git a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt index 676595929..4241976c6 100644 --- a/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt +++ b/discussion/src/test/java/org/openedx/discussion/presentation/topics/DiscussionTopicsViewModelTest.kt @@ -72,7 +72,15 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -89,7 +97,15 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -107,7 +123,15 @@ class DiscussionTopicsViewModelTest { @Test fun `getCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } returns listOf(mockTopic, mockTopic) advanceUntilIdle() @@ -124,7 +148,15 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics no internet exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws UnknownHostException() val message = async { @@ -141,7 +173,15 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics unknown exception`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } throws Exception() val message = async { @@ -159,7 +199,15 @@ class DiscussionTopicsViewModelTest { @Test fun `updateCourseTopics success`() = runTest(UnconfinedTestDispatcher()) { - val viewModel = DiscussionTopicsViewModel("id", "", interactor, resourceManager, analytics, courseNotifier, router) + val viewModel = DiscussionTopicsViewModel( + "id", + "", + interactor, + resourceManager, + analytics, + courseNotifier, + router + ) coEvery { interactor.getCourseTopics(any()) } returns listOf(mockTopic, mockTopic) val message = async { @@ -174,5 +222,4 @@ class DiscussionTopicsViewModelTest { assert(message.await()?.message.isNullOrEmpty()) assert(viewModel.uiState.value is DiscussionTopicsUIState.Topics) } - } diff --git a/profile/src/androidTest/java/org/openedx/profile/ExampleInstrumentedTest.kt b/profile/src/androidTest/java/org/openedx/profile/ExampleInstrumentedTest.kt index 12ab3a4c1..814b1bed3 100644 --- a/profile/src/androidTest/java/org/openedx/profile/ExampleInstrumentedTest.kt +++ b/profile/src/androidTest/java/org/openedx/profile/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package org.openedx.profile -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("org.openedx.profile.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/data/api/ProfileApi.kt b/profile/src/main/java/org/openedx/profile/data/api/ProfileApi.kt index 1b9bb6750..1f206ed77 100644 --- a/profile/src/main/java/org/openedx/profile/data/api/ProfileApi.kt +++ b/profile/src/main/java/org/openedx/profile/data/api/ProfileApi.kt @@ -1,11 +1,21 @@ package org.openedx.profile.data.api -import org.openedx.core.ApiConstants -import org.openedx.profile.data.model.Account import okhttp3.RequestBody import okhttp3.ResponseBody +import org.openedx.core.ApiConstants +import org.openedx.profile.data.model.Account import retrofit2.Response -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query interface ProfileApi { @@ -26,7 +36,7 @@ interface ProfileApi { suspend fun updateAccount( @Path("username") username: String, @Body fields: Map - ) : Account + ): Account @Headers("Cache-Control: no-cache") @POST("/api/user/v1/accounts/{username}/image") @@ -35,7 +45,7 @@ interface ProfileApi { @Header("Content-Disposition") contentDisposition: String?, @Query("mobile") mobile: Boolean = true, @Body file: RequestBody? - ) : Response + ): Response @Headers("Cache-Control: no-cache") @DELETE("/api/user/v1/accounts/{username}/image") @@ -46,5 +56,4 @@ interface ProfileApi { suspend fun deactivateAccount( @Field("password") password: String ): Response - -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/data/model/Account.kt b/profile/src/main/java/org/openedx/profile/data/model/Account.kt index ff069376a..bea2468bf 100644 --- a/profile/src/main/java/org/openedx/profile/data/model/Account.kt +++ b/profile/src/main/java/org/openedx/profile/data/model/Account.kt @@ -3,7 +3,7 @@ package org.openedx.profile.data.model import com.google.gson.annotations.SerializedName import org.openedx.core.data.model.ProfileImage import org.openedx.profile.domain.model.Account -import java.util.* +import java.util.Date import org.openedx.profile.domain.model.Account as DomainAccount data class Account( @@ -44,6 +44,7 @@ data class Account( enum class Privacy { @SerializedName("private") PRIVATE, + @SerializedName("all_users") ALL_USERS } @@ -51,7 +52,7 @@ data class Account( fun mapToDomain(): Account { return Account( username = username ?: "", - bio = bio?:"", + bio = bio ?: "", requiresParentalConsent = requiresParentalConsent ?: false, name = name ?: "", country = country ?: "", @@ -67,9 +68,11 @@ data class Account( mailingAddress = mailingAddress ?: "", email = email, dateJoined = dateJoined, - accountPrivacy = if (accountPrivacy == Privacy.PRIVATE) DomainAccount.Privacy.PRIVATE else DomainAccount.Privacy.ALL_USERS + accountPrivacy = if (accountPrivacy == Privacy.PRIVATE) { + DomainAccount.Privacy.PRIVATE + } else { + DomainAccount.Privacy.ALL_USERS + } ) } } - - diff --git a/profile/src/main/java/org/openedx/profile/data/model/LanguageProficiency.kt b/profile/src/main/java/org/openedx/profile/data/model/LanguageProficiency.kt index 5a88b03e3..181c03348 100644 --- a/profile/src/main/java/org/openedx/profile/data/model/LanguageProficiency.kt +++ b/profile/src/main/java/org/openedx/profile/data/model/LanguageProficiency.kt @@ -12,4 +12,4 @@ data class LanguageProficiency( code = code ?: "" ) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt b/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt index aba477e0a..34da01e43 100644 --- a/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt +++ b/profile/src/main/java/org/openedx/profile/data/storage/ProfilePreferences.kt @@ -4,4 +4,4 @@ import org.openedx.profile.data.model.Account interface ProfilePreferences { var profile: Account? -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/domain/interactor/ProfileInteractor.kt b/profile/src/main/java/org/openedx/profile/domain/interactor/ProfileInteractor.kt index cbad3b4fe..c07a8bee6 100644 --- a/profile/src/main/java/org/openedx/profile/domain/interactor/ProfileInteractor.kt +++ b/profile/src/main/java/org/openedx/profile/domain/interactor/ProfileInteractor.kt @@ -22,4 +22,4 @@ class ProfileInteractor(private val repository: ProfileRepository) { suspend fun logout() { repository.logout() } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/domain/model/Account.kt b/profile/src/main/java/org/openedx/profile/domain/model/Account.kt index f338fc452..0031807b6 100644 --- a/profile/src/main/java/org/openedx/profile/domain/model/Account.kt +++ b/profile/src/main/java/org/openedx/profile/domain/model/Account.kt @@ -35,9 +35,8 @@ data class Account( fun isLimited() = accountPrivacy == Privacy.PRIVATE - fun isOlderThanMinAge() : Boolean { + fun isOlderThanMinAge(): Boolean { val currentYear = Calendar.getInstance().get(Calendar.YEAR) return yearOfBirth != null && currentYear - yearOfBirth > USER_MIN_YEAR } - } diff --git a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt index 8404bbae1..6a5061723 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileFragment.kt @@ -226,7 +226,6 @@ private fun ProfileScreenPreview() { } } - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_YES) @Composable @@ -243,7 +242,8 @@ private fun ProfileScreenTabletPreview() { private val mockAccount = Account( username = "thom84", - bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", + bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper " + + "questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", requiresParentalConsent = true, name = "Thomas", country = "Ukraine", diff --git a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileUIState.kt index 29cac7c1a..dc13fa814 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileUIState.kt @@ -4,5 +4,5 @@ import org.openedx.profile.domain.model.Account sealed class AnothersProfileUIState { data class Data(val account: Account) : AnothersProfileUIState() - object Loading : AnothersProfileUIState() -} \ No newline at end of file + data object Loading : AnothersProfileUIState() +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt index 82b906207..90559aa9b 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/anothersaccount/AnothersProfileViewModel.kt @@ -46,4 +46,4 @@ class AnothersProfileViewModel( } } } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt index 361fa5776..b4d3bb1c9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt @@ -3,17 +3,26 @@ package org.openedx.profile.presentation.calendar import androidx.annotation.StringRes import org.openedx.profile.R +private const val ACCENT_COLOR = 0xFFD13329L +private const val RED_COLOR = 0xFFFF2967L +private const val ORANGE_COLOR = 0xFFFF9501L +private const val YELLOW_COLOR = 0xFFFFCC01L +private const val GREEN_COLOR = 0xFF64DA38L +private const val BLUE_COLOR = 0xFF1AAEF8L +private const val PURPLE_COLOR = 0xFFCC73E1L +private const val BROWN_COLOR = 0xFFA2845EL + enum class CalendarColor( @StringRes val title: Int, val color: Long ) { - ACCENT(R.string.calendar_color_accent, 0xFFD13329), - RED(R.string.calendar_color_red, 0xFFFF2967), - ORANGE(R.string.calendar_color_orange, 0xFFFF9501), - YELLOW(R.string.calendar_color_yellow, 0xFFFFCC01), - GREEN(R.string.calendar_color_green, 0xFF64DA38), - BLUE(R.string.calendar_color_blue, 0xFF1AAEF8), - PURPLE(R.string.calendar_color_purple, 0xFFCC73E1), - BROWN(R.string.calendar_color_brown, 0xFFA2845E); + ACCENT(R.string.calendar_color_accent, ACCENT_COLOR), + RED(R.string.calendar_color_red, RED_COLOR), + ORANGE(R.string.calendar_color_orange, ORANGE_COLOR), + YELLOW(R.string.calendar_color_yellow, YELLOW_COLOR), + GREEN(R.string.calendar_color_green, GREEN_COLOR), + BLUE(R.string.calendar_color_blue, BLUE_COLOR), + PURPLE(R.string.calendar_color_purple, PURPLE_COLOR), + BROWN(R.string.calendar_color_brown, BROWN_COLOR) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt index 363dc70eb..2011d065c 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt @@ -185,7 +185,7 @@ fun CalendarSetUpView( ) Spacer(modifier = Modifier.height(16.dp)) OpenEdXButton( - modifier = Modifier.fillMaxWidth(0.75f), + modifier = Modifier.fillMaxWidth(fraction = 0.75f), text = stringResource(id = R.string.profile_set_up_calendar_sync), onClick = { setUpCalendarSync() diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt index 85f217446..45ca74658 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt @@ -40,7 +40,11 @@ class CalendarViewModel( get() = CalendarUIState( isCalendarExist = isCalendarExist(), calendarData = null, - calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, + calendarSyncState = if (networkConnection.isOnline()) { + CalendarSyncState.SYNCED + } else { + CalendarSyncState.OFFLINE + }, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, coursesSynced = null, isRelativeDateEnabled = corePreferences.isRelativeDatesEnabled, @@ -141,6 +145,7 @@ class CalendarViewModel( calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && calendarManager.isCalendarExist(calendarPreferences.calendarId) } catch (e: SecurityException) { + e.printStackTrace() false } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt index fed719696..6eb27762e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt @@ -255,7 +255,6 @@ private fun SyncCourseTabRow( } } - @Composable private fun CourseCheckboxList( selectedTab: SyncCourseTab, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt index cf7f8b24d..015df8e2b 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt @@ -68,7 +68,12 @@ class CoursesToSyncViewModel( val coursesCalendarState = calendarInteractor.getAllCourseCalendarStateFromCache() _uiState.update { it.copy(coursesCalendarState = coursesCalendarState) } } catch (e: Exception) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error))) + e.printStackTrace() + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } } @@ -80,9 +85,19 @@ class CoursesToSyncViewModel( _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus) } } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } finally { _uiState.update { it.copy(isLoading = false) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt index 361bd965e..e7bbecae5 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt @@ -75,6 +75,7 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.foundation.extension.parcelable import org.openedx.foundation.extension.toastMessage import org.openedx.profile.R +import org.openedx.profile.presentation.calendar.NewCalendarDialogFragment.Companion.MAX_CALENDAR_TITLE_LENGTH import androidx.compose.ui.graphics.Color as ComposeColor import org.openedx.core.R as CoreR @@ -124,6 +125,7 @@ class NewCalendarDialogFragment : DialogFragment() { companion object { const val DIALOG_TAG = "NewCalendarDialogFragment" const val ARG_DIALOG_TYPE = "ARG_DIALOG_TYPE" + const val MAX_CALENDAR_TITLE_LENGTH = 40 fun newInstance( newCalendarDialogType: NewCalendarDialogType @@ -239,7 +241,6 @@ private fun CalendarTitleTextField( onValueChanged: (String) -> Unit ) { val focusManager = LocalFocusManager.current - val maxChar = 40 var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue("") @@ -260,7 +261,7 @@ private fun CalendarTitleTextField( .height(48.dp), value = textFieldValue, onValueChange = { - if (it.text.length <= maxChar) textFieldValue = it + if (it.text.length <= MAX_CALENDAR_TITLE_LENGTH) textFieldValue = it onValueChanged(it.text.trim()) }, colors = TextFieldDefaults.outlinedTextFieldColors( diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt index f9d481466..770e67b40 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragment.kt @@ -124,7 +124,6 @@ class DeleteProfileFragment : Fragment() { } } } - } @OptIn(ExperimentalComposeUiApi::class) @@ -291,7 +290,6 @@ fun DeleteProfileScreen( } } - @Preview( name = "PIXEL_3A_Light", device = Devices.PIXEL_3A, diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragmentUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragmentUIState.kt index 128642077..b2bb74e9e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragmentUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileFragmentUIState.kt @@ -1,8 +1,8 @@ package org.openedx.profile.presentation.delete sealed class DeleteProfileFragmentUIState { - object Initial: DeleteProfileFragmentUIState() - object Loading: DeleteProfileFragmentUIState() - data class Error(val message: String): DeleteProfileFragmentUIState() - object Success: DeleteProfileFragmentUIState() -} \ No newline at end of file + data object Initial : DeleteProfileFragmentUIState() + data object Loading : DeleteProfileFragmentUIState() + data class Error(val message: String) : DeleteProfileFragmentUIState() + data object Success : DeleteProfileFragmentUIState() +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt index d29e70ab3..8ab22c87e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/delete/DeleteProfileViewModel.kt @@ -38,7 +38,9 @@ class DeleteProfileViewModel( logDeleteProfileClickedEvent() if (!validator.isPasswordValid(password)) { _uiState.value = - DeleteProfileFragmentUIState.Error(resourceManager.getString(org.openedx.profile.R.string.profile_invalid_password)) + DeleteProfileFragmentUIState.Error( + resourceManager.getString(org.openedx.profile.R.string.profile_invalid_password) + ) return } viewModelScope.launch { @@ -59,7 +61,9 @@ class DeleteProfileViewModel( _uiState.value = DeleteProfileFragmentUIState.Initial } else { _uiState.value = - DeleteProfileFragmentUIState.Error(resourceManager.getString(org.openedx.profile.R.string.profile_password_is_incorrect)) + DeleteProfileFragmentUIState.Error( + resourceManager.getString(org.openedx.profile.R.string.profile_password_is_incorrect) + ) } logDeleteProfileEvent(false) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFields.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFields.kt index e13b7ad4d..1aae60aa4 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFields.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFields.kt @@ -4,4 +4,4 @@ const val YEAR_OF_BIRTH = "year_of_birth" const val LANGUAGE = "language_proficiencies" const val COUNTRY = "country" const val BIO = "bio" -const val ACCOUNT_PRIVACY = "account_privacy" \ No newline at end of file +const val ACCOUNT_PRIVACY = "account_privacy" diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt index 4005d1191..62727f822 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileFragment.kt @@ -137,6 +137,7 @@ import org.openedx.foundation.presentation.rememberWindowSize import org.openedx.foundation.presentation.windowSizeValue import org.openedx.profile.R import org.openedx.profile.domain.model.Account +import org.openedx.profile.presentation.edit.EditProfileFragment.Companion.LEAVE_PROFILE_WIDTH_FACTOR import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -248,31 +249,38 @@ class EditProfileFragment : Fragment() { MediaStore.Images.Media.getBitmap(requireContext().contentResolver, uri) } val rotatedBitmap = Bitmap.createBitmap( - originalBitmap, 0, 0, originalBitmap.width, originalBitmap.height, matrix, true + originalBitmap, + 0, + 0, + originalBitmap.width, + originalBitmap.height, + matrix, + true ) val newFile = File.createTempFile( - "Image_${System.currentTimeMillis()}", ".jpg", + "Image_${System.currentTimeMillis()}", + ".jpg", requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES) ) - val ratio: Float = rotatedBitmap.width.toFloat() / 500 + val ratio: Float = rotatedBitmap.width.toFloat() / TARGET_IMAGE_WIDTH val newBitmap = Bitmap.createScaledBitmap( rotatedBitmap, - 500, + TARGET_IMAGE_WIDTH, (rotatedBitmap.height.toFloat() / ratio).toInt(), false ) val bos = ByteArrayOutputStream() - newBitmap.compress(Bitmap.CompressFormat.JPEG, 90, bos) + newBitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY, bos) val bitmapData = bos.toByteArray() val fos = FileOutputStream(newFile) fos.write(bitmapData) fos.flush() fos.close() - //TODO: get applicationId instead of packageName return FileProvider.getUriForFile( - requireContext(), requireContext().packageName + ".fileprovider", + requireContext(), + viewModel.config.getAppId() + ".fileprovider", newFile )!! } @@ -280,27 +288,34 @@ class EditProfileFragment : Fragment() { private fun getImageOrientation(uri: Uri): Int { var rotation = 0 val exif = ExifInterface(requireActivity().contentResolver.openInputStream(uri)!!) - when (exif.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_NORMAL - )) { - ExifInterface.ORIENTATION_ROTATE_270 -> rotation = 270 - ExifInterface.ORIENTATION_ROTATE_180 -> rotation = 180 - ExifInterface.ORIENTATION_ROTATE_90 -> rotation = 90 + when ( + exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ) + ) { + ExifInterface.ORIENTATION_ROTATE_270 -> rotation = ORIENTATION_ROTATE_270 + ExifInterface.ORIENTATION_ROTATE_180 -> rotation = ORIENTATION_ROTATE_180 + ExifInterface.ORIENTATION_ROTATE_90 -> rotation = ORIENTATION_ROTATE_90 } return rotation } - companion object { private const val ARG_ACCOUNT = "argAccount" + const val LEAVE_PROFILE_WIDTH_FACTOR = 0.7f + private const val ORIENTATION_ROTATE_270 = 270 + private const val ORIENTATION_ROTATE_180 = 180 + private const val ORIENTATION_ROTATE_90 = 90 + private const val IMAGE_QUALITY = 90 + private const val TARGET_IMAGE_WIDTH = 500 + fun newInstance(account: Account): EditProfileFragment { val fragment = EditProfileFragment() fragment.arguments = bundleOf(ARG_ACCOUNT to account) return fragment } } - } @OptIn(ExperimentalComposeUiApi::class) @@ -358,13 +373,15 @@ private fun EditProfileScreen( ) } - val saveButtonEnabled = !(uiState.account.yearOfBirth.toString() == mapFields[YEAR_OF_BIRTH] - && uiState.account.languageProficiencies == mapFields[LANGUAGE] - && uiState.account.country == mapFields[COUNTRY] - && uiState.account.bio == mapFields[BIO] - && selectedImageUri == null - && !isImageDeleted - && uiState.isLimited == uiState.account.isLimited()) + val saveButtonEnabled = !( + uiState.account.yearOfBirth.toString() == mapFields[YEAR_OF_BIRTH] && + uiState.account.languageProficiencies == mapFields[LANGUAGE] && + uiState.account.country == mapFields[COUNTRY] && + uiState.account.bio == mapFields[BIO] && + selectedImageUri == null && + !isImageDeleted && + uiState.isLimited == uiState.account.isLimited() + ) onDataChanged(saveButtonEnabled) val serverFieldName = rememberSaveable { @@ -485,8 +502,8 @@ private fun EditProfileScreen( searchValue = TextFieldValue(it) } ) - }) { - + } + ) { HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) if (isOpenChangeImageDialogState && uiState.account.isOlderThanMinAge()) { @@ -572,7 +589,6 @@ private fun EditProfileScreen( onSaveClick(mapFields.toMap()) } ) - } } } @@ -595,7 +611,13 @@ private fun EditProfileScreen( ) { Text( modifier = Modifier.testTag("txt_edit_profile_type_label"), - text = stringResource(if (uiState.isLimited) R.string.profile_limited_profile else R.string.profile_full_profile), + text = stringResource( + if (uiState.isLimited) { + R.string.profile_limited_profile + } else { + R.string.profile_full_profile + } + ), color = MaterialTheme.appColors.textSecondary, style = MaterialTheme.appTypography.titleSmall ) @@ -622,7 +644,6 @@ private fun EditProfileScreen( .padding(2.dp) .size(100.dp) .clip(CircleShape) - .noRippleClickable { isOpenChangeImageDialogState = true if (!uiState.account.isOlderThanMinAge()) { @@ -706,8 +727,9 @@ private fun EditProfileScreen( coroutine.launch { val index = expandedList.indexOfFirst { option -> if (serverFieldName.value == LANGUAGE) { - option.value == (mapFields[serverFieldName.value] as List) - .getOrNull(0)?.code + option.value == + (mapFields[serverFieldName.value] as List) + .getOrNull(0)?.code } else { option.value == mapFields[serverFieldName.value] } @@ -737,7 +759,6 @@ private fun EditProfileScreen( } } } - } } } @@ -876,7 +897,6 @@ private fun ChangeImageDialog( Spacer(Modifier.height(20.dp)) } } - } } @@ -892,8 +912,12 @@ private fun ProfileFields( val languageProficiency = (mapFields[LANGUAGE] as List) val lang = if (languageProficiency.isNotEmpty()) { LocaleUtils.getLanguageByLanguageCode(languageProficiency[0].code) - } else "" - Column(verticalArrangement = Arrangement.spacedBy(20.dp)) { + } else { + "" + } + Column( + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { SelectableField( name = stringResource(id = R.string.profile_year), initialValue = mapFields[YEAR_OF_BIRTH].toString(), @@ -1140,7 +1164,8 @@ private fun LeaveProfile( onClick = onDismissRequest ) } - }) + } + ) } @Composable @@ -1160,7 +1185,7 @@ private fun LeaveProfileLandscape( content = { Card( modifier = Modifier - .width(screenWidth * 0.7f) + .width(screenWidth * LEAVE_PROFILE_WIDTH_FACTOR) .clip(MaterialTheme.appShapes.courseImageShape) .semantics { testTagsAsResourceId = true }, backgroundColor = MaterialTheme.appColors.background, @@ -1241,7 +1266,8 @@ private fun LeaveProfileLandscape( } } } - }) + } + ) } @Preview @@ -1329,7 +1355,6 @@ private fun EditProfileScreenTabletPreview() { } } - private val mockAccount = Account( username = "thom84", bio = "designer", diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt index 5654800ca..841dae6a1 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileUIState.kt @@ -7,5 +7,3 @@ data class EditProfileUIState( val isUpdating: Boolean = false, val isLimited: Boolean ) - - diff --git a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt index 33e804d28..dd8781cf9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/edit/EditProfileViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.core.R +import org.openedx.core.config.Config import org.openedx.foundation.extension.isInternetError import org.openedx.foundation.presentation.BaseViewModel import org.openedx.foundation.presentation.UIMessage @@ -24,6 +25,7 @@ class EditProfileViewModel( private val resourceManager: ResourceManager, private val notifier: ProfileNotifier, private val analytics: ProfileAnalytics, + val config: Config, account: Account, ) : BaseViewModel() { @@ -56,8 +58,11 @@ class EditProfileViewModel( buildMap { put( ProfileAnalyticsKey.ACTION.key, - if (isLimitedProfile) ProfileAnalyticsKey.LIMITED_PROFILE.key - else ProfileAnalyticsKey.FULL_PROFILE.key + if (isLimitedProfile) { + ProfileAnalyticsKey.LIMITED_PROFILE.key + } else { + ProfileAnalyticsKey.FULL_PROFILE.key + } ) } ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt index 3e25214a1..d8297d1bd 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/ManageAccountViewModel.kt @@ -75,9 +75,17 @@ class ManageAccountViewModel( ) } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } finally { _isUpdating.value = false diff --git a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/compose/ManageAccountView.kt b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/compose/ManageAccountView.kt index 016f1e90c..3873f8c5c 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/manageaccount/compose/ManageAccountView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/manageaccount/compose/ManageAccountView.kt @@ -71,7 +71,8 @@ internal fun ManageAccountView( val pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = { onAction(ManageAccountViewAction.SwipeRefresh) }) + onRefresh = { onAction(ManageAccountViewAction.SwipeRefresh) } + ) Scaffold( modifier = Modifier @@ -185,7 +186,8 @@ internal fun ManageAccountView( color = MaterialTheme.appColors.error, onClick = { onAction(ManageAccountViewAction.DeleteAccount) - }) + } + ) Spacer(modifier = Modifier.height(12.dp)) } } @@ -219,7 +221,6 @@ private fun ManageAccountViewPreview() { } } - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt index 7a0d90b16..e897b37c6 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/compose/ProfileView.kt @@ -67,7 +67,8 @@ internal fun ProfileView( val pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = { onAction(ProfileViewAction.SwipeRefresh) }) + onRefresh = { onAction(ProfileViewAction.SwipeRefresh) } + ) Scaffold( modifier = Modifier @@ -186,7 +187,6 @@ private fun ProfileScreenPreview() { } } - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Dark", device = Devices.NEXUS_9, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt index 1746fa167..217a35258 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsFragment.kt @@ -121,4 +121,3 @@ internal interface SettingsScreenAction { object ManageAccountClick : SettingsScreenAction object CalendarSettingsClick : SettingsScreenAction } - diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt index 21d0fe1fb..d692db7e6 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt @@ -514,7 +514,7 @@ private fun AppVersionItemAppToDate(versionName: String) { ) { Icon( modifier = Modifier.size( - (MaterialTheme.appTypography.labelLarge.fontSize.value + 4).dp + size = (MaterialTheme.appTypography.labelLarge.fontSize.value + 4).dp ), painter = painterResource(id = R.drawable.core_ic_check), contentDescription = null, @@ -597,7 +597,7 @@ fun AppVersionItemUpgradeRequired( ) { Image( modifier = Modifier - .size((MaterialTheme.appTypography.labelLarge.fontSize.value + 8).dp), + .size(size = (MaterialTheme.appTypography.labelLarge.fontSize.value + 8).dp), painter = painterResource(id = R.drawable.core_ic_warning), contentDescription = null ) @@ -639,7 +639,6 @@ private val mockUiState = SettingsUIState.Data( configuration = mockConfiguration ) - @Preview @Composable private fun AppVersionItemAppToDatePreview() { diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt index a483d0b91..59548d1c9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt @@ -97,9 +97,17 @@ class SettingsViewModel( ) } catch (e: Exception) { if (e.isInternetError()) { - _uiMessage.emit(UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection))) + _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))) + _uiMessage.emit( + UIMessage.SnackBarMessage( + resourceManager.getString(R.string.core_error_unknown_error) + ) + ) } } finally { cookieManager.clearWebViewCookie() diff --git a/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt b/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt index d0004256f..c87afd492 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ui/ProfileUI.kt @@ -118,7 +118,8 @@ fun ProfileInfoSection(account: Account) { val mockAccount = Account( username = "thom84", - bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", + bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper " + + "questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", requiresParentalConsent = true, name = "Thomas", country = "Ukraine", diff --git a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsFragment.kt index 2213b083d..d9b434130 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsFragment.kt @@ -106,7 +106,6 @@ class VideoSettingsFragment : Fragment() { } } } - } @OptIn(ExperimentalComposeUiApi::class) diff --git a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt index f2fd90c61..670447ddb 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/video/VideoSettingsViewModel.kt @@ -61,13 +61,15 @@ class VideoSettingsViewModel( fun navigateToVideoStreamingQuality(fragmentManager: FragmentManager) { router.navigateToVideoQuality( - fragmentManager, VideoQualityType.Streaming + fragmentManager, + VideoQualityType.Streaming ) } fun navigateToVideoDownloadQuality(fragmentManager: FragmentManager) { router.navigateToVideoQuality( - fragmentManager, VideoQualityType.Download + fragmentManager, + VideoQualityType.Download ) } diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt index 71e2dbf1d..93d98f316 100644 --- a/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt +++ b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt @@ -14,5 +14,4 @@ class ProfileNotifier { suspend fun send(event: AccountUpdated) = channel.emit(event) suspend fun send(event: AccountDeactivated) = channel.emit(event) - } diff --git a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt index 9cc8c79ff..9ea2f1d5f 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/edit/EditProfileViewModelTest.kt @@ -20,6 +20,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.R +import org.openedx.core.config.Config import org.openedx.core.domain.model.ProfileImage import org.openedx.foundation.presentation.UIMessage import org.openedx.foundation.system.ResourceManager @@ -43,10 +44,12 @@ class EditProfileViewModelTest { private val interactor = mockk() private val notifier = mockk() private val analytics = mockk() + private val config = mockk() private val account = Account( username = "thom84", - bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", + bio = "He as compliment unreserved projecting. Between had observe pretend delight for believe. Do newspaper " + + "questions consulted sweetness do. Our sportsman his unwilling fulfilled departure law.", requiresParentalConsent = true, name = "Thomas", country = "Ukraine", @@ -83,7 +86,7 @@ class EditProfileViewModelTest { @Test fun `updateAccount no internet connection`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.updateAccount(any()) } throws UnknownHostException() viewModel.updateAccount(emptyMap()) advanceUntilIdle() @@ -98,7 +101,7 @@ class EditProfileViewModelTest { @Test fun `updateAccount unknown exception`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.updateAccount(any()) } throws Exception() viewModel.updateAccount(emptyMap()) @@ -114,7 +117,7 @@ class EditProfileViewModelTest { @Test fun `updateAccount success`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.updateAccount(any()) } returns account coEvery { notifier.send(any()) } returns Unit every { analytics.logEvent(any(), any()) } returns Unit @@ -131,7 +134,7 @@ class EditProfileViewModelTest { @Test fun `updateAccountAndImage no internet connection`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.setProfileImage(any(), any()) } throws UnknownHostException() coEvery { interactor.updateAccount(any()) } returns account coEvery { notifier.send(AccountUpdated()) } returns Unit @@ -151,7 +154,7 @@ class EditProfileViewModelTest { @Test fun `updateAccountAndImage unknown exception`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.setProfileImage(any(), any()) } throws Exception() coEvery { interactor.updateAccount(any()) } returns account coEvery { notifier.send(AccountUpdated()) } returns Unit @@ -171,7 +174,7 @@ class EditProfileViewModelTest { @Test fun `updateAccountAndImage success`() = runTest { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) coEvery { interactor.setProfileImage(any(), any()) } returns Unit coEvery { interactor.updateAccount(any()) } returns account coEvery { notifier.send(any()) } returns Unit @@ -193,10 +196,9 @@ class EditProfileViewModelTest { @Test fun `setImageUri set new value`() { val viewModel = - EditProfileViewModel(interactor, resourceManager, notifier, analytics, account) + EditProfileViewModel(interactor, resourceManager, notifier, analytics, config, account) viewModel.setImageUri(mockk()) assert(viewModel.selectedImageUri.value != null) } - } diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt index 6bdd07b82..8f7fdf53a 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/AnothersProfileViewModelTest.kt @@ -122,4 +122,4 @@ class AnothersProfileViewModelTest { assert(viewModel.uiState.value is AnothersProfileUIState.Data) assert(viewModel.uiMessage.value == null) } -} \ No newline at end of file +} diff --git a/whatsnew/src/androidTest/java/org/openedx/whatsnew/ExampleInstrumentedTest.kt b/whatsnew/src/androidTest/java/org/openedx/whatsnew/ExampleInstrumentedTest.kt index 5b65b0c9d..597e60f30 100644 --- a/whatsnew/src/androidTest/java/org/openedx/whatsnew/ExampleInstrumentedTest.kt +++ b/whatsnew/src/androidTest/java/org/openedx/whatsnew/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package org.openedx.whatsnew -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("org.openedx.whatsnew.test", appContext.packageName) } -} \ No newline at end of file +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewManager.kt b/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewManager.kt index 71a51d3b6..011df85a1 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewManager.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/WhatsNewManager.kt @@ -24,8 +24,8 @@ class WhatsNewManager( override fun shouldShowWhatsNew(): Boolean { val dataVersion = getNewestData().version - return appData.versionName == dataVersion - && whatsNewPreferences.lastWhatsNewVersion != dataVersion - && config.isWhatsNewEnabled() + return appData.versionName == dataVersion && + whatsNewPreferences.lastWhatsNewVersion != dataVersion && + config.isWhatsNewEnabled() } } diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/data/model/WhatsNewItem.kt b/whatsnew/src/main/java/org/openedx/whatsnew/data/model/WhatsNewItem.kt index e0c029d53..ef897bbde 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/data/model/WhatsNewItem.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/data/model/WhatsNewItem.kt @@ -13,4 +13,4 @@ data class WhatsNewItem( version = version, messages = messages.map { it.mapToDomain(context) } ) -} \ No newline at end of file +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/data/storage/WhatsNewPreferences.kt b/whatsnew/src/main/java/org/openedx/whatsnew/data/storage/WhatsNewPreferences.kt index 6270f809e..237ef95bb 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/data/storage/WhatsNewPreferences.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/data/storage/WhatsNewPreferences.kt @@ -2,4 +2,4 @@ package org.openedx.whatsnew.data.storage interface WhatsNewPreferences { var lastWhatsNewVersion: String -} \ No newline at end of file +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/ui/WhatsNewUI.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/ui/WhatsNewUI.kt index 7d97eee40..9c34603f1 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/ui/WhatsNewUI.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/ui/WhatsNewUI.kt @@ -88,7 +88,6 @@ fun PageIndicatorView( animationDurationInMillis: Int, modifier: Modifier = Modifier, ) { - val color: Color by animateColorAsState( targetValue = if (isSelected) { selectedColor @@ -164,7 +163,7 @@ fun PrevButton( ) { val prevButtonAnimationFactor by animateFloatAsState( targetValue = if (hasPrevPage) 1f else 0f, - animationSpec = tween(300), + animationSpec = tween(durationMillis = 300), label = "" ) @@ -308,7 +307,8 @@ private fun NavigationUnitsButtonsPrevInTheEnd() { private fun PageIndicatorViewPreview() { OpenEdXTheme { PageIndicator( - numberOfPages = 4, selectedPage = 2 + numberOfPages = 4, + selectedPage = 2 ) } -} \ No newline at end of file +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt index 8b9523eaa..0cab35466 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.animation.Crossfade -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -69,6 +68,7 @@ import org.openedx.whatsnew.domain.model.WhatsNewItem import org.openedx.whatsnew.domain.model.WhatsNewMessage import org.openedx.whatsnew.presentation.ui.NavigationUnitsButtons import org.openedx.whatsnew.presentation.ui.PageIndicator +import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment.Companion.BASE_ALPHA_VALUE class WhatsNewFragment : Fragment() { @@ -109,6 +109,7 @@ class WhatsNewFragment : Fragment() { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_INFO_TYPE = "info_type" + const val BASE_ALPHA_VALUE = 0.2f fun newInstance(courseId: String? = null, infoType: String? = null): WhatsNewFragment { val fragment = WhatsNewFragment() @@ -230,7 +231,6 @@ private fun WhatsNewTopBar( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun WhatsNewScreenPortrait( modifier: Modifier = Modifier, @@ -339,7 +339,6 @@ private fun WhatsNewScreenPortrait( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun WhatsNewScreenLandscape( modifier: Modifier = Modifier, @@ -366,7 +365,7 @@ private fun WhatsNewScreenLandscape( state = pagerState ) { page -> val image = whatsNewItem.messages[page].image - val alpha = (0.2f + pagerState.calculateCurrentOffsetForPage(page)) * 10 + val alpha = (BASE_ALPHA_VALUE + pagerState.calculateCurrentOffsetForPage(page)) * 10 Image( modifier = Modifier .alpha(alpha) @@ -443,7 +442,7 @@ private fun WhatsNewScreenLandscape( } PageIndicator( - modifier = Modifier.weight(0.25f), + modifier = Modifier.weight(weight = 0.25f), numberOfPages = pagerState.pageCount, selectedPage = pagerState.currentPage, defaultRadius = 12.dp, @@ -465,7 +464,6 @@ val whatsNewItemPreview = WhatsNewItem( messages = listOf(whatsNewMessagePreview, whatsNewMessagePreview, whatsNewMessagePreview) ) -@OptIn(ExperimentalFoundationApi::class) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -474,12 +472,13 @@ private fun WhatsNewPortraitPreview() { WhatsNewScreenPortrait( whatsNewItem = whatsNewItemPreview, onDoneClick = {}, - pagerState = rememberPagerState { 4 } + pagerState = rememberPagerState( + pageCount = { 4 } + ) ) } } -@OptIn(ExperimentalFoundationApi::class) @Preview( uiMode = Configuration.UI_MODE_NIGHT_NO, device = Devices.AUTOMOTIVE_1024p, @@ -498,7 +497,9 @@ private fun WhatsNewLandscapePreview() { WhatsNewScreenLandscape( whatsNewItem = whatsNewItemPreview, onDoneClick = {}, - pagerState = rememberPagerState { 4 } + pagerState = rememberPagerState( + pageCount = { 4 } + ) ) } } diff --git a/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt b/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt index c6cbe3573..d99555c49 100644 --- a/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt +++ b/whatsnew/src/test/java/org/openedx/whatsnew/WhatsNewViewModelTest.kt @@ -41,4 +41,4 @@ class WhatsNewViewModelTest { verify(exactly = 1) { whatsNewManager.getNewestData() } assert(viewModel.whatsNewItem.value == whatsNewItem) } -} \ No newline at end of file +}