From 4ff1d215561191abd6f89433153ed5a197908327 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 30 Apr 2024 18:19:37 +0300 Subject: [PATCH 01/26] feat: Created calendar setting screen --- app/src/main/AndroidManifest.xml | 2 + .../main/java/org/openedx/app/AppRouter.kt | 9 +- .../main/java/org/openedx/app/di/AppModule.kt | 2 +- .../java/org/openedx/app/di/ScreenModule.kt | 4 +- .../calendarsync/CalendarSyncDialog.kt | 6 +- .../calendarsync/CalendarSyncDialogType.kt | 44 +++ .../calendarsync/CalendarSyncUIState.kt | 2 +- .../calendarsync/DialogProperties.kt | 2 +- .../{ => video}/VideoQualityFragment.kt | 2 +- .../settings/{ => video}/VideoQualityType.kt | 2 +- .../{ => video}/VideoQualityViewModel.kt | 2 +- .../openedx/core/system}/CalendarManager.kt | 7 +- core/src/main/res/values/strings.xml | 35 +++ course/src/main/AndroidManifest.xml | 5 - .../course/presentation/CourseRouter.kt | 2 +- .../calendarsync/CalendarSyncDialogType.kt | 45 --- .../container/CourseContainerFragment.kt | 4 +- .../container/CourseContainerViewModel.kt | 15 +- .../presentation/dates/CourseDatesFragment.kt | 18 +- .../dates/CourseDatesViewModel.kt | 6 +- .../outline/CourseOutlineViewModel.kt | 2 +- .../videos/CourseVideosFragment.kt | 2 +- course/src/main/res/values/strings.xml | 35 --- .../container/CourseContainerViewModelTest.kt | 2 +- .../dates/CourseDatesViewModelTest.kt | 2 +- .../profile/presentation/ProfileRouter.kt | 4 +- .../presentation/calendar/CalendarFragment.kt | 256 ++++++++++++++++++ .../calendar/CalendarViewModel.kt | 18 ++ .../presentation/settings/SettingsFragment.kt | 11 +- .../presentation/settings/SettingsScreenUI.kt | 66 +++-- .../settings/SettingsViewModel.kt | 5 + .../profile/presentation/ui/SettingsUI.kt | 17 +- .../video/VideoSettingsViewModel.kt | 2 +- profile/src/main/res/values/strings.xml | 4 + 34 files changed, 472 insertions(+), 168 deletions(-) rename {course/src/main/java/org/openedx/course/presentation => core/src/main/java/org/openedx/core/presentation/settings}/calendarsync/CalendarSyncDialog.kt (98%) create mode 100644 core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt rename {course/src/main/java/org/openedx/course/presentation => core/src/main/java/org/openedx/core/presentation/settings}/calendarsync/CalendarSyncUIState.kt (89%) rename {course/src/main/java/org/openedx/course/presentation => core/src/main/java/org/openedx/core/presentation/settings}/calendarsync/DialogProperties.kt (78%) rename core/src/main/java/org/openedx/core/presentation/settings/{ => video}/VideoQualityFragment.kt (99%) rename core/src/main/java/org/openedx/core/presentation/settings/{ => video}/VideoQualityType.kt (51%) rename core/src/main/java/org/openedx/core/presentation/settings/{ => video}/VideoQualityViewModel.kt (98%) rename {course/src/main/java/org/openedx/course/presentation/calendarsync => core/src/main/java/org/openedx/core/system}/CalendarManager.kt (98%) delete mode 100644 course/src/main/AndroidManifest.xml delete mode 100644 course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialogType.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8020f6b74..3e8282acb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 21f3b5aee..a68b550a2 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -13,8 +13,8 @@ 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.webview.WebContentFragment -import org.openedx.core.presentation.settings.VideoQualityFragment -import org.openedx.core.presentation.settings.VideoQualityType +import org.openedx.core.presentation.settings.video.VideoQualityFragment +import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.container.CourseContainerFragment import org.openedx.course.presentation.container.NoAccessCourseContainerFragment @@ -44,6 +44,7 @@ import org.openedx.discussion.presentation.threads.DiscussionThreadsFragment import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.presentation.anothersaccount.AnothersProfileFragment +import org.openedx.profile.presentation.calendar.CalendarFragment import org.openedx.profile.presentation.delete.DeleteProfileFragment import org.openedx.profile.presentation.edit.EditProfileFragment import org.openedx.profile.presentation.manageaccount.ManageAccountFragment @@ -370,6 +371,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToManageAccount(fm: FragmentManager) { replaceFragmentWithBackStack(fm, ManageAccountFragment()) } + + override fun navigateToCalendarSettings(fm: FragmentManager) { + replaceFragmentWithBackStack(fm, CalendarFragment()) + } //endregion private fun replaceFragmentWithBackStack(fm: FragmentManager, fragment: Fragment) { 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 dc0a70335..42315224e 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -39,6 +39,7 @@ 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.system.AppCookieManager +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.AppUpgradeNotifier @@ -49,7 +50,6 @@ import org.openedx.core.system.notifier.VideoNotifier import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseRouter -import org.openedx.course.presentation.calendarsync.CalendarManager import org.openedx.dashboard.presentation.DashboardAnalytics import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.discovery.presentation.DiscoveryAnalytics 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 02787e391..ca1cc23cc 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -13,7 +13,7 @@ import org.openedx.auth.presentation.signin.SignInViewModel import org.openedx.auth.presentation.signup.SignUpViewModel import org.openedx.core.Validator import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel -import org.openedx.core.presentation.settings.VideoQualityViewModel +import org.openedx.core.presentation.settings.video.VideoQualityViewModel import org.openedx.course.data.repository.CourseRepository import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.container.CourseContainerViewModel @@ -53,6 +53,7 @@ import org.openedx.profile.data.repository.ProfileRepository import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.anothersaccount.AnothersProfileViewModel +import org.openedx.profile.presentation.calendar.CalendarViewModel import org.openedx.profile.presentation.delete.DeleteProfileViewModel import org.openedx.profile.presentation.edit.EditProfileViewModel import org.openedx.profile.presentation.manageaccount.ManageAccountViewModel @@ -150,6 +151,7 @@ val screenModule = module { viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) } viewModel { SettingsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } + viewModel { CalendarViewModel(get()) } single { CourseRepository(get(), get(), get(), get()) } factory { CourseInteractor(get()) } diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt similarity index 98% rename from course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt rename to core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt index a3775c99b..ac358228e 100644 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialog.kt @@ -1,4 +1,4 @@ -package org.openedx.course.presentation.calendarsync +package org.openedx.core.presentation.settings.calendarsync import android.content.res.Configuration import androidx.compose.foundation.background @@ -23,13 +23,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.extension.takeIfNotEmpty import org.openedx.core.presentation.global.app_upgrade.TransparentTextButton import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography -import org.openedx.course.R import androidx.compose.ui.window.DialogProperties as AlertDialogProperties import org.openedx.core.R as CoreR @@ -192,7 +192,7 @@ private fun SyncDialog() { verticalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = stringResource(id = R.string.course_title_syncing_calendar), + text = stringResource(id = R.string.core_title_syncing_calendar), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium, maxLines = 2, 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 new file mode 100644 index 000000000..daab61fa5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncDialogType.kt @@ -0,0 +1,44 @@ +package org.openedx.core.presentation.settings.calendarsync + +import org.openedx.core.R + +enum class CalendarSyncDialogType( + val titleResId: Int = 0, + val messageResId: Int = 0, + val positiveButtonResId: Int = 0, + val negativeButtonResId: Int = 0, +) { + SYNC_DIALOG( + titleResId = R.string.core_title_add_course_calendar, + messageResId = R.string.core_message_add_course_calendar, + positiveButtonResId = R.string.core_ok, + negativeButtonResId = R.string.core_cancel + ), + UN_SYNC_DIALOG( + titleResId = R.string.core_title_remove_course_calendar, + messageResId = R.string.core_message_remove_course_calendar, + positiveButtonResId = R.string.core_label_remove, + negativeButtonResId = R.string.core_cancel + ), + PERMISSION_DIALOG( + titleResId = R.string.core_title_request_calendar_permission, + messageResId = R.string.core_message_request_calendar_permission, + positiveButtonResId = R.string.core_ok, + negativeButtonResId = R.string.core_label_do_not_allow + ), + EVENTS_DIALOG( + messageResId = R.string.core_message_course_calendar_added, + positiveButtonResId = R.string.core_label_view_events, + negativeButtonResId = R.string.core_label_done + ), + OUT_OF_SYNC_DIALOG( + titleResId = R.string.core_title_calendar_out_of_date, + messageResId = R.string.core_message_calendar_out_of_date, + positiveButtonResId = R.string.core_label_update_now, + negativeButtonResId = R.string.core_label_remove_course_calendar, + ), + LOADING_DIALOG( + titleResId = R.string.core_title_syncing_calendar + ), + NONE; +} diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncUIState.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncUIState.kt similarity index 89% rename from course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncUIState.kt rename to core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncUIState.kt index 24d2212e2..e3062d970 100644 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncUIState.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncUIState.kt @@ -1,4 +1,4 @@ -package org.openedx.course.presentation.calendarsync +package org.openedx.core.presentation.settings.calendarsync import org.openedx.core.domain.model.CourseDateBlock import java.util.concurrent.atomic.AtomicReference diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/DialogProperties.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/DialogProperties.kt similarity index 78% rename from course/src/main/java/org/openedx/course/presentation/calendarsync/DialogProperties.kt rename to core/src/main/java/org/openedx/core/presentation/settings/calendarsync/DialogProperties.kt index cefded76c..cfca43193 100644 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/DialogProperties.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/DialogProperties.kt @@ -1,4 +1,4 @@ -package org.openedx.course.presentation.calendarsync +package org.openedx.core.presentation.settings.calendarsync data class DialogProperties( val title: String, diff --git a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityFragment.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt similarity index 99% rename from core/src/main/java/org/openedx/core/presentation/settings/VideoQualityFragment.kt rename to core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt index e26d882eb..edd00ce53 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityFragment.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.settings +package org.openedx.core.presentation.settings.video import android.content.res.Configuration import android.os.Bundle diff --git a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityType.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityType.kt similarity index 51% rename from core/src/main/java/org/openedx/core/presentation/settings/VideoQualityType.kt rename to core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityType.kt index 4c7973d6a..c39b6d220 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityType.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityType.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.settings +package org.openedx.core.presentation.settings.video enum class VideoQualityType { Streaming, Download diff --git a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityViewModel.kt b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt similarity index 98% rename from core/src/main/java/org/openedx/core/presentation/settings/VideoQualityViewModel.kt rename to core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt index c6d5176ea..bf30bbe30 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/VideoQualityViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/video/VideoQualityViewModel.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.settings +package org.openedx.core.presentation.settings.video import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt similarity index 98% rename from course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarManager.kt rename to core/src/main/java/org/openedx/core/system/CalendarManager.kt index 54639e922..53d7a1e1f 100644 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -1,4 +1,4 @@ -package org.openedx.course.presentation.calendarsync +package org.openedx.core.system import android.annotation.SuppressLint import android.content.ContentUris @@ -10,12 +10,11 @@ import android.database.Cursor import android.net.Uri import android.provider.CalendarContract import androidx.core.content.ContextCompat +import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseDateBlock -import org.openedx.core.system.ResourceManager import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar -import org.openedx.course.R import java.util.Calendar import java.util.TimeZone import java.util.concurrent.TimeUnit @@ -165,7 +164,7 @@ class CalendarManager( put(CalendarContract.Events.DTEND, endMillis) put( CalendarContract.Events.TITLE, - "${resourceManager.getString(R.string.course_assignment_due_tag)} : $courseName" + "${resourceManager.getString(R.string.core_assignment_due_tag)} : $courseName" ) put( CalendarContract.Events.DESCRIPTION, diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 820782a9e..347f8c393 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -131,4 +131,39 @@ Video streaming quality Video download quality Manage Account + + Assignment Due + Syncing calendar… + + + Sync to calendar + Automatically sync all deadlines and due dates for this course to your calendar. + + \“%s\” Would Like to Access Your Calendar + %s would like to use your calendar list to subscribe to your personalized %s calendar for this course. + Don’t allow + + Add Course Dates to Calendar + Would you like to add \“%s\” dates to your calendar? \n\nYou can edit or remove your course dates at any time from your calendar or settings. + + \“%s\” has been added to your phone\'s calendar. + View Events + Done + + Remove Course Dates from Calendar + Would you like to remove the \“%s\” dates from your calendar? + Remove + + Your course calendar is out of date + Your course dates have been shifted and your course calendar is no longer up to date with your new schedule. + Update Now + Remove Course Calendar + + Your course calendar has been added. + Your course calendar has been removed. + Your course calendar has been updated. + Error Adding Calendar, Please try later + + + diff --git a/course/src/main/AndroidManifest.xml b/course/src/main/AndroidManifest.xml deleted file mode 100644 index 5c18ebdbf..000000000 --- a/course/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - 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 b2f520679..9b34e7617 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -2,7 +2,7 @@ package org.openedx.course.presentation import androidx.fragment.app.FragmentManager import org.openedx.core.presentation.course.CourseViewMode -import org.openedx.core.presentation.settings.VideoQualityType +import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.course.presentation.handouts.HandoutsType interface CourseRouter { diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialogType.kt b/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialogType.kt deleted file mode 100644 index 57d6c0dac..000000000 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialogType.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.openedx.course.presentation.calendarsync - -import org.openedx.course.R -import org.openedx.core.R as CoreR - -enum class CalendarSyncDialogType( - val titleResId: Int = 0, - val messageResId: Int = 0, - val positiveButtonResId: Int = 0, - val negativeButtonResId: Int = 0, -) { - SYNC_DIALOG( - titleResId = R.string.course_title_add_course_calendar, - messageResId = R.string.course_message_add_course_calendar, - positiveButtonResId = CoreR.string.core_ok, - negativeButtonResId = CoreR.string.core_cancel - ), - UN_SYNC_DIALOG( - titleResId = R.string.course_title_remove_course_calendar, - messageResId = R.string.course_message_remove_course_calendar, - positiveButtonResId = R.string.course_label_remove, - negativeButtonResId = CoreR.string.core_cancel - ), - PERMISSION_DIALOG( - titleResId = R.string.course_title_request_calendar_permission, - messageResId = R.string.course_message_request_calendar_permission, - positiveButtonResId = CoreR.string.core_ok, - negativeButtonResId = R.string.course_label_do_not_allow - ), - EVENTS_DIALOG( - messageResId = R.string.course_message_course_calendar_added, - positiveButtonResId = R.string.course_label_view_events, - negativeButtonResId = R.string.course_label_done - ), - OUT_OF_SYNC_DIALOG( - titleResId = R.string.course_title_calendar_out_of_date, - messageResId = R.string.course_message_calendar_out_of_date, - positiveButtonResId = R.string.course_label_update_now, - negativeButtonResId = R.string.course_label_remove_course_calendar, - ), - LOADING_DIALOG( - titleResId = R.string.course_title_syncing_calendar - ), - NONE; -} 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 ee4cea674..38db0ab9c 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 @@ -19,12 +19,12 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.global.viewBinding +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialog +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.course.R import org.openedx.course.databinding.FragmentCourseContainerBinding import org.openedx.course.presentation.CourseRouter -import org.openedx.course.presentation.calendarsync.CalendarSyncDialog -import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.presentation.container.CourseContainerTab import org.openedx.course.presentation.dates.CourseDatesFragment import org.openedx.course.presentation.handouts.HandoutsFragment 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 43a3be5c1..e93473e70 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 @@ -17,6 +17,9 @@ import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.exception.NoCachedDataException import org.openedx.core.extension.isInternetError +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent @@ -25,7 +28,6 @@ import org.openedx.core.system.notifier.CourseCompletionSet import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.utils.TimeUtils -import org.openedx.course.R import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CalendarSyncDialog @@ -33,9 +35,6 @@ import org.openedx.course.presentation.CalendarSyncSnackbar import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey -import org.openedx.course.presentation.calendarsync.CalendarManager -import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType -import org.openedx.course.presentation.calendarsync.CalendarSyncUIState import java.util.Date import java.util.concurrent.atomic.AtomicReference import org.openedx.core.R as CoreR @@ -188,7 +187,7 @@ class CourseContainerViewModel( val calendarId = getCalendarId() if (calendarId == CalendarManager.CALENDAR_DOES_NOT_EXIST) { - setUiMessage(R.string.course_snackbar_course_calendar_error) + setUiMessage(CoreR.string.core_snackbar_course_calendar_error) setCalendarSyncDialogType(CalendarSyncDialogType.NONE) return @@ -219,10 +218,10 @@ class CourseContainerViewModel( if (updatedEvent) { logCalendarSyncSnackbar(CalendarSyncSnackbar.UPDATED) - setUiMessage(R.string.course_snackbar_course_calendar_updated) + setUiMessage(CoreR.string.core_snackbar_course_calendar_updated) } else if (coursePreferences.isCalendarSyncEventsDialogShown(courseName)) { logCalendarSyncSnackbar(CalendarSyncSnackbar.ADDED) - setUiMessage(R.string.course_snackbar_course_calendar_added) + setUiMessage(CoreR.string.core_snackbar_course_calendar_added) } else { coursePreferences.setCalendarSyncEventsDialogShown(courseName) setCalendarSyncDialogType(CalendarSyncDialogType.EVENTS_DIALOG) @@ -266,7 +265,7 @@ class CourseContainerViewModel( } logCalendarSyncSnackbar(CalendarSyncSnackbar.REMOVED) - setUiMessage(R.string.course_snackbar_course_calendar_removed) + setUiMessage(CoreR.string.core_snackbar_course_calendar_removed) } } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesFragment.kt b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesFragment.kt index b65532f0b..70f9cf77c 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/CourseDatesFragment.kt @@ -93,6 +93,7 @@ import org.openedx.core.extension.isNotEmptyThenLet import org.openedx.core.presentation.CoreAnalyticsScreen import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.dialog.alert.ActionDialogFragment +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.WindowSize @@ -109,13 +110,12 @@ import org.openedx.core.utils.clearTime import org.openedx.course.DatesShiftedSnackBar import org.openedx.course.R import org.openedx.course.presentation.CourseRouter -import org.openedx.course.presentation.calendarsync.CalendarSyncUIState import org.openedx.course.presentation.container.CourseContainerFragment import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet import org.openedx.course.presentation.ui.DatesShiftedSnackBar import java.util.concurrent.atomic.AtomicReference -import org.openedx.core.R as coreR +import org.openedx.core.R as CoreR class CourseDatesFragment : Fragment() { @@ -190,10 +190,10 @@ class CourseDatesFragment : Fragment() { } ?: { viewModel.logCourseComponentTapped(false, block) ActionDialogFragment.newInstance( - title = getString(coreR.string.core_leaving_the_app), + title = getString(CoreR.string.core_leaving_the_app), message = getString( - coreR.string.core_leaving_the_app_message, - getString(coreR.string.platform_name) + CoreR.string.core_leaving_the_app_message, + getString(CoreR.string.platform_name) ), url = block.link, source = CoreAnalyticsScreen.COURSE_DATES.screenName @@ -491,7 +491,7 @@ fun CalendarSyncCard( modifier = Modifier .padding(start = 8.dp, end = 8.dp) .weight(1f), - text = stringResource(id = R.string.course_header_sync_to_calendar), + text = stringResource(id = CoreR.string.core_header_sync_to_calendar), style = MaterialTheme.appTypography.titleMedium, color = MaterialTheme.appColors.textDark ) @@ -511,7 +511,7 @@ fun CalendarSyncCard( .fillMaxWidth() .padding(top = 8.dp) .height(40.dp), - text = stringResource(id = R.string.course_body_sync_to_calendar), + text = stringResource(id = CoreR.string.core_body_sync_to_calendar), style = MaterialTheme.appTypography.bodyMedium, color = MaterialTheme.appColors.textDark, ) @@ -566,7 +566,7 @@ fun ExpandableView( AnimatedVisibility(visible = expanded.not()) { Text( text = pluralStringResource( - id = coreR.plurals.core_date_items_hidden, + id = CoreR.plurals.core_date_items_hidden, count = sectionDates.size, formatArgs = arrayOf(sectionDates.size) ), @@ -726,7 +726,7 @@ private fun CourseDateItem( 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 ) 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 52fe7c5e3..afb5aae8d 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 @@ -20,6 +20,9 @@ import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent @@ -30,9 +33,6 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey -import org.openedx.course.presentation.calendarsync.CalendarManager -import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType -import org.openedx.course.presentation.calendarsync.CalendarSyncUIState import org.openedx.core.R as CoreR class CourseDatesViewModel( 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 eeabab539..d293fdbea 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 @@ -23,6 +23,7 @@ import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.download.BaseDownloadViewModel import org.openedx.core.presentation.CoreAnalytics +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent @@ -33,7 +34,6 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey -import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.R as courseR class CourseOutlineViewModel( diff --git a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosFragment.kt b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosFragment.kt index aa17ad783..03d7a7749 100644 --- a/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/videos/CourseVideosFragment.kt @@ -15,7 +15,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.R import org.openedx.core.presentation.course.CourseViewMode -import org.openedx.core.presentation.settings.VideoQualityType +import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.course.presentation.CourseRouter diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index 002fc06a1..bab7ede8b 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -50,41 +50,6 @@ Course dates are not currently available. - - Sync to calendar - Automatically sync all deadlines and due dates for this course to your calendar. - - \“%s\” Would Like to Access Your Calendar - %s would like to use your calendar list to subscribe to your personalized %s calendar for this course. - Don’t allow - - Add Course Dates to Calendar - Would you like to add \“%s\” dates to your calendar? \n\nYou can edit or remove your course dates at any time from your calendar or settings. - - Syncing calendar… - - \“%s\” has been added to your phone\'s calendar. - View Events - Done - - Remove Course Dates from Calendar - Would you like to remove the \“%s\” dates from your calendar? - Remove - - Your course calendar is out of date - Your course dates have been shifted and your course calendar is no longer up to date with your new schedule. - Update Now - Remove Course Calendar - - Your course calendar has been added. - Your course calendar has been removed. - Your course calendar has been updated. - Error Adding Calendar, Please try later - - Assignment Due - - - Video player Remove course section 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 0dc214f81..90adb0864 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 @@ -29,6 +29,7 @@ import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.CourseDatesCalendarSync import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier @@ -37,7 +38,6 @@ import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent -import org.openedx.course.presentation.calendarsync.CalendarManager import java.net.UnknownHostException import java.util.Date diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 82c3728e4..f843633c2 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -33,13 +33,13 @@ import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.DatesSection +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics -import org.openedx.course.presentation.calendarsync.CalendarManager import java.net.UnknownHostException import java.util.Date diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt index a4b194de4..fd7514bd5 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt @@ -1,7 +1,7 @@ package org.openedx.profile.presentation import androidx.fragment.app.FragmentManager -import org.openedx.core.presentation.settings.VideoQualityType +import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.profile.domain.model.Account interface ProfileRouter { @@ -21,4 +21,6 @@ interface ProfileRouter { fun navigateToWebContent(fm: FragmentManager, title: String, url: String) fun navigateToManageAccount(fm: FragmentManager) + + fun navigateToCalendarSettings(fm: FragmentManager) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt new file mode 100644 index 000000000..0966fe8b9 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -0,0 +1,256 @@ +package org.openedx.profile.presentation.calendar + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Autorenew +import androidx.compose.material.icons.rounded.CalendarToday +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.settingsHeaderBackground +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +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.ui.windowSizeValue +import org.openedx.profile.R +import org.openedx.core.R as CoreR + +class CalendarFragment : Fragment() { + + private val viewModel by viewModel() + + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { isGranted -> + if (!isGranted.containsValue(false)) { + + } else { + + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + + CalendarScreen( + windowSize = windowSize, + setUpCalendarSync = { + viewModel.setUpCalendarSync(permissionLauncher) + }, + onBackClick = { + requireActivity().supportFragmentManager.popBackStack() + } + ) + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun CalendarScreen( + windowSize: WindowSize, + setUpCalendarSync: () -> Unit, + onBackClick: () -> Unit +) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, + scaffoldState = scaffoldState + ) { paddingValues -> + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 420.dp), + compact = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + ) + } + + val topBarWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier + .fillMaxWidth() + ) + ) + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .settingsHeaderBackground() + .statusBarsInset(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Toolbar( + modifier = topBarWidth + .displayCutoutForLandscape(), + label = stringResource(id = R.string.profile_dates_and_calendar), + canShowBackBtn = true, + labelTint = MaterialTheme.appColors.settingsTitleContent, + iconTint = MaterialTheme.appColors.settingsTitleContent, + onBackClick = onBackClick + ) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.appShapes.screenBackgroundShape) + .background(MaterialTheme.appColors.background) + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = contentWidth.padding(vertical = 28.dp), + ) { + Text( + modifier = Modifier.testTag("txt_settings"), + text = stringResource(id = CoreR.string.core_settings), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textSecondary + ) + Spacer(modifier = Modifier.height(14.dp)) + Card( + shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, + backgroundColor = MaterialTheme.appColors.cardViewBackground + ) { + Column( + modifier = Modifier + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .padding(vertical = 28.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier + .fillMaxWidth() + .height(148.dp), + tint = MaterialTheme.appColors.textDark, + imageVector = Icons.Rounded.CalendarToday, + contentDescription = null + ) + Icon( + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp) + .height(60.dp), + tint = MaterialTheme.appColors.textDark, + imageVector = Icons.Default.Autorenew, + contentDescription = null + ) + } + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.profile_calendar_sync), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.profile_calendar_sync_description), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + Spacer(modifier = Modifier.height(16.dp)) + OpenEdXButton( + modifier = Modifier.fillMaxWidth(0.75f), + text = stringResource(id = R.string.profile_set_up_calendar_sync), + onClick = { + setUpCalendarSync() + } + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun VideoQualityScreenPreview() { + OpenEdXTheme { + CalendarScreen( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + setUpCalendarSync = {}, + onBackClick = {} + ) + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..d516a0d0b --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarViewModel.kt @@ -0,0 +1,18 @@ +package org.openedx.profile.presentation.calendar + +import androidx.activity.result.ActivityResultLauncher +import org.openedx.core.BaseViewModel +import org.openedx.core.system.CalendarManager + +class CalendarViewModel( + private val calendarManager: CalendarManager +) : BaseViewModel() { + + fun setUpCalendarSync(permissionLauncher: ActivityResultLauncher>) { + if (!calendarManager.hasPermissions()) { + permissionLauncher.launch(calendarManager.permissions) + } else { + + } + } +} 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 fbdd0b4af..7ac402330 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 @@ -83,11 +83,17 @@ class SettingsFragment : Fragment() { ) } - SettingsScreenAction.ManageAccount -> { + SettingsScreenAction.ManageAccountClick -> { viewModel.manageAccountClicked( requireActivity().supportFragmentManager ) } + + SettingsScreenAction.CalendarSettingsClick -> { + viewModel.calendarSettingsClicked( + requireActivity().supportFragmentManager + ) + } } } ) @@ -112,6 +118,7 @@ internal interface SettingsScreenAction { object TermsClick : SettingsScreenAction object SupportClick : SettingsScreenAction object VideoSettingsClick : SettingsScreenAction - object ManageAccount : 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 f5c0a7bc5..419172232 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 @@ -21,7 +21,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -68,6 +67,7 @@ import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.windowSizeValue import org.openedx.profile.domain.model.Configuration +import org.openedx.profile.presentation.ui.SettingsDivider import org.openedx.profile.presentation.ui.SettingsItem import org.openedx.profile.R as profileR @@ -170,14 +170,19 @@ internal fun SettingsScreen( Spacer(Modifier.height(30.dp)) ManageAccountSection(onManageAccountClick = { - onAction(SettingsScreenAction.ManageAccount) + onAction(SettingsScreenAction.ManageAccountClick) }) Spacer(modifier = Modifier.height(24.dp)) - SettingsSection(onVideoSettingsClick = { - onAction(SettingsScreenAction.VideoSettingsClick) - }) + SettingsSection( + onVideoSettingsClick = { + onAction(SettingsScreenAction.VideoSettingsClick) + }, + onCalendarSettingsClick = { + onAction(SettingsScreenAction.CalendarSettingsClick) + } + ) Spacer(modifier = Modifier.height(24.dp)) @@ -205,7 +210,10 @@ internal fun SettingsScreen( } @Composable -private fun SettingsSection(onVideoSettingsClick: () -> Unit) { +private fun SettingsSection( + onVideoSettingsClick: () -> Unit, + onCalendarSettingsClick: () -> Unit +) { Column { Text( modifier = Modifier.testTag("txt_settings"), @@ -225,6 +233,11 @@ private fun SettingsSection(onVideoSettingsClick: () -> Unit) { text = stringResource(id = profileR.string.profile_video), onClick = onVideoSettingsClick ) + SettingsDivider() + SettingsItem( + text = stringResource(id = profileR.string.profile_dates_and_calendar), + onClick = onCalendarSettingsClick + ) } } } @@ -273,46 +286,31 @@ private fun SupportInfoSection( SettingsItem(text = stringResource(id = profileR.string.profile_contact_support)) { onAction(SettingsScreenAction.SupportClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } if (uiState.configuration.agreementUrls.tosUrl.isNotBlank()) { SettingsItem(text = stringResource(id = R.string.core_terms_of_use)) { onAction(SettingsScreenAction.TermsClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } if (uiState.configuration.agreementUrls.privacyPolicyUrl.isNotBlank()) { SettingsItem(text = stringResource(id = R.string.core_privacy_policy)) { onAction(SettingsScreenAction.PrivacyPolicyClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } if (uiState.configuration.agreementUrls.cookiePolicyUrl.isNotBlank()) { SettingsItem(text = stringResource(id = R.string.core_cookie_policy)) { onAction(SettingsScreenAction.CookiePolicyClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } if (uiState.configuration.agreementUrls.dataSellConsentUrl.isNotBlank()) { SettingsItem(text = stringResource(id = R.string.core_data_sell)) { onAction(SettingsScreenAction.DataSellClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } if (uiState.configuration.faqUrl.isNotBlank()) { val uriHandler = LocalUriHandler.current @@ -323,10 +321,7 @@ private fun SupportInfoSection( uriHandler.openUri(uiState.configuration.faqUrl) onAction(SettingsScreenAction.FaqClick) } - Divider( - modifier = Modifier.padding(vertical = 4.dp), - color = MaterialTheme.appColors.divider - ) + SettingsDivider() } AppVersionItem( versionName = uiState.configuration.versionName, @@ -344,16 +339,17 @@ private fun LogoutButton(onClick: () -> Unit) { Card( modifier = Modifier .testTag("btn_logout") - .fillMaxWidth() - .clickable { - onClick() - }, + .fillMaxWidth(), shape = MaterialTheme.appShapes.cardShape, elevation = 0.dp, backgroundColor = MaterialTheme.appColors.cardViewBackground ) { Row( - modifier = Modifier.padding(20.dp), + modifier = Modifier + .clickable { + onClick() + } + .padding(20.dp), horizontalArrangement = Arrangement.SpaceBetween ) { Text( 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 2c7471ebd..545afabc9 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 @@ -185,6 +185,11 @@ class SettingsViewModel( router.navigateToManageAccount(fragmentManager) } + fun calendarSettingsClicked(fragmentManager: FragmentManager) { + router.navigateToCalendarSettings(fragmentManager) + } + + fun restartApp(fragmentManager: FragmentManager) { router.restartApp( fragmentManager, diff --git a/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt b/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt index 6960a0864..df6c719ca 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ui/SettingsUI.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -38,7 +39,10 @@ fun SettingsItem( .testTag("btn_${text.tagId()}") .fillMaxWidth() .clickable { onClick() } - .padding(20.dp), + .padding( + vertical = 24.dp, + horizontal = 20.dp + ), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -59,3 +63,14 @@ fun SettingsItem( ) } } + +@Composable +fun SettingsDivider() { + Divider( + modifier = Modifier + .padding( + horizontal = 20.dp + ), + color = MaterialTheme.appColors.divider + ) +} 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 b98ec8709..f0ed7622a 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 @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.VideoSettings -import org.openedx.core.presentation.settings.VideoQualityType +import org.openedx.core.presentation.settings.video.VideoQualityType import org.openedx.core.system.notifier.VideoNotifier import org.openedx.core.system.notifier.VideoQualityChanged import org.openedx.profile.presentation.ProfileAnalytics diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index efdb04c30..d31bc3ec3 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -37,7 +37,11 @@ Contact Support Support Video + Dates & Calendar Wi-fi only download Only download content when wi-fi is turned on + Calendar Sync + Set up calendar sync to show your upcoming assignments and course milestones on your calendar. New assignments and shifted course dates will sync automatically + Set Up Calendar Sync From 6e09d3567bf7a1fad49b7a16af4dfd43a662c661 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 16 May 2024 15:10:40 +0300 Subject: [PATCH 02/26] feat: CalendarAccessDialog --- .../java/org/openedx/core/config/Config.kt | 5 + .../core/presentation/dialog/DialogUI.kt | 56 +++++++ .../dialog/appreview/AppReviewUI.kt | 51 +----- .../res/drawable/core_ic_external_link.xml | 9 + .../calendar/CalendarAccessDialogFragment.kt | 155 ++++++++++++++++++ .../presentation/calendar/CalendarFragment.kt | 8 +- .../calendar/CalendarViewModel.kt | 6 +- profile/src/main/res/values/strings.xml | 3 + 8 files changed, 239 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt create mode 100644 core/src/main/res/drawable/core_ic_external_link.xml create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt 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 4b40fbc29..e73ed22f3 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -23,6 +23,10 @@ class Config(context: Context) { } } + fun getAppId(): String { + return getString(API_APP_ID, "") + } + fun getApiHostURL(): String { return getString(API_HOST_URL, "") } @@ -146,6 +150,7 @@ class Config(context: Context) { } companion object { + private const val API_APP_ID = "APPLICATION_ID" private const val API_HOST_URL = "API_HOST_URL" private const val URI_SCHEME = "URI_SCHEME" private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID" diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt b/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt new file mode 100644 index 000000000..d0cea9171 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt @@ -0,0 +1,56 @@ +package org.openedx.core.presentation.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes + +@Composable +fun DefaultDialogBox( + modifier: Modifier = Modifier, + onDismissClick: () -> Unit, + content: @Composable (BoxScope.() -> Unit) +) { + Surface( + modifier = modifier, + color = Color.Transparent + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 4.dp) + .noRippleClickable { + onDismissClick() + }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .widthIn(max = 640.dp) + .fillMaxWidth() + .clip(MaterialTheme.appShapes.cardShape) + .noRippleClickable {} + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + ) { + content.invoke(this) + } + } + } +} \ 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 e2d6a471f..1297b983a 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 @@ -4,26 +4,20 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons @@ -40,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale @@ -54,7 +47,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.openedx.core.R -import org.openedx.core.ui.noRippleClickable +import org.openedx.core.presentation.dialog.DefaultDialogBox import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes @@ -78,7 +71,7 @@ fun ThankYouDialog( DefaultDialogBox( modifier = modifier, - onDismissClock = onNotNowClick + onDismissClick = onNotNowClick ) { Column( modifier = Modifier @@ -139,7 +132,7 @@ fun FeedbackDialog( DefaultDialogBox( modifier = modifier, - onDismissClock = onNotNowClick + onDismissClick = onNotNowClick ) { Column( modifier = Modifier @@ -210,7 +203,7 @@ fun RateDialog( ) { DefaultDialogBox( modifier = modifier, - onDismissClock = onNotNowClick + onDismissClick = onNotNowClick ) { Column( modifier = Modifier @@ -252,42 +245,6 @@ fun RateDialog( } } -@Composable -fun DefaultDialogBox( - modifier: Modifier = Modifier, - onDismissClock: () -> Unit, - content: @Composable (BoxScope.() -> Unit) -) { - Surface( - modifier = modifier, - color = Color.Transparent - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 4.dp) - .noRippleClickable { - onDismissClock() - }, - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .widthIn(max = 640.dp) - .fillMaxWidth() - .clip(MaterialTheme.appShapes.cardShape) - .noRippleClickable {} - .background( - color = MaterialTheme.appColors.background, - shape = MaterialTheme.appShapes.cardShape - ) - ) { - content.invoke(this) - } - } - } -} - @Composable fun TransparentTextButton( text: String, diff --git a/core/src/main/res/drawable/core_ic_external_link.xml b/core/src/main/res/drawable/core_ic_external_link.xml new file mode 100644 index 000000000..05d38676d --- /dev/null +++ b/core/src/main/res/drawable/core_ic_external_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt new file mode 100644 index 000000000..a449f7dad --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt @@ -0,0 +1,155 @@ +package org.openedx.profile.presentation.calendar + +import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.fragment.app.DialogFragment +import org.koin.android.ext.android.inject +import org.openedx.core.config.Config +import org.openedx.core.presentation.dialog.DefaultDialogBox +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.OpenEdXOutlinedButton +import org.openedx.core.ui.TextIcon +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appTypography +import org.openedx.profile.R +import org.openedx.core.R as CoreR + + +class CalendarAccessDialogFragment : DialogFragment() { + + private val config by inject() + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + if (dialog != null && dialog!!.window != null) { + dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + CalendarAccessDialog( + onCancelClick = { + dismiss() + }, + onGrantCalendarAccessClick = { + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:" + config.getAppId()) + ) + startActivity(intent) + } + ) + } + } + } + + companion object { + fun newInstance(): CalendarAccessDialogFragment { + return CalendarAccessDialogFragment() + } + } +} + +@Composable +private fun CalendarAccessDialog( + modifier: Modifier = Modifier, + onCancelClick: () -> Unit, + onGrantCalendarAccessClick: () -> Unit +) { + DefaultDialogBox( + modifier = modifier, + onDismissClick = onCancelClick + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(id = CoreR.drawable.core_ic_warning), + contentDescription = null + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_calendar_access_dialog_title), + style = MaterialTheme.appTypography.titleLarge + ) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_calendar_access_dialog_description), + style = MaterialTheme.appTypography.bodyMedium + ) + OpenEdXButton( + modifier = Modifier.fillMaxWidth(), + onClick = { + onGrantCalendarAccessClick() + }, + content = { + TextIcon( + text = stringResource(id = R.string.profile_grant_access_calendar), + painter = painterResource(id = CoreR.drawable.core_ic_external_link), + color = MaterialTheme.appColors.buttonText, + textStyle = MaterialTheme.appTypography.labelLarge, + iconModifier = Modifier.padding(start = 4.dp) + ) + } + ) + OpenEdXOutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = CoreR.string.core_cancel), + backgroundColor = MaterialTheme.appColors.background, + borderColor = MaterialTheme.appColors.buttonBackground, + textColor = MaterialTheme.appColors.buttonBackground, + onClick = { + onCancelClick() + } + ) + } + } +} + +@Preview +@Composable +private fun CalendarAccessDialogPreview() { + OpenEdXTheme { + CalendarAccessDialog( + onCancelClick = { }, + onGrantCalendarAccessClick = { } + ) + } +} \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index 0966fe8b9..f41eb5548 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -67,9 +67,13 @@ class CalendarFragment : Fragment() { ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (!isGranted.containsValue(false)) { - + //TODO } else { - + val dialog = CalendarAccessDialogFragment.newInstance() + dialog.show( + requireActivity().supportFragmentManager, + CalendarAccessDialogFragment::class.simpleName + ) } } 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 d516a0d0b..316b689b4 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 @@ -9,10 +9,6 @@ class CalendarViewModel( ) : BaseViewModel() { fun setUpCalendarSync(permissionLauncher: ActivityResultLauncher>) { - if (!calendarManager.hasPermissions()) { - permissionLauncher.launch(calendarManager.permissions) - } else { - - } + permissionLauncher.launch(calendarManager.permissions) } } diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index d31bc3ec3..56531fffd 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -43,5 +43,8 @@ Calendar Sync Set up calendar sync to show your upcoming assignments and course milestones on your calendar. New assignments and shifted course dates will sync automatically Set Up Calendar Sync + Calendar Access + To show upcoming assignments and course milestones on your calendar, we need permission to access your calendar. + Grant Calendar Access From ddd9c6426b7d0674b00d11f0a1a298f479f50c3a Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 17 May 2024 16:30:35 +0300 Subject: [PATCH 03/26] feat: NewCalendarDialog --- .../core/presentation/dialog/DialogUI.kt | 2 +- .../org/openedx/core/ui/ComposeExtensions.kt | 19 +- .../container/CourseContainerViewModelTest.kt | 1 - .../calendar/CalendarAccessDialogFragment.kt | 13 +- .../presentation/calendar/CalendarColor.kt | 19 + .../presentation/calendar/CalendarFragment.kt | 8 +- .../calendar/NewCalendarDialogFragment.kt | 390 ++++++++++++++++++ profile/src/main/res/values/strings.xml | 14 + 8 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt b/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt index d0cea9171..17b1d2874 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/DialogUI.kt @@ -53,4 +53,4 @@ fun DefaultDialogBox( } } } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt index 6cf198f53..1659a0417 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeExtensions.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalInspectionMode 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 @@ -192,4 +194,19 @@ fun Modifier.settingsHeaderBackground(): Modifier = composed { contentScale = ContentScale.FillWidth, alignment = Alignment.TopCenter ) -} \ No newline at end of file +} + +fun Modifier.crop( + horizontal: Dp = 0.dp, + vertical: Dp = 0.dp, +): Modifier = this.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + fun Dp.toPxInt(): Int = this.toPx().toInt() + + layout( + placeable.width - (horizontal * 2).toPxInt(), + placeable.height - (vertical * 2).toPxInt() + ) { + placeable.placeRelative(-horizontal.toPx().toInt(), -vertical.toPx().toInt()) + } +} 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 bb404e22f..aff60b21e 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 @@ -43,7 +43,6 @@ import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseRouter -import org.openedx.course.presentation.calendarsync.CalendarManager import java.net.UnknownHostException import java.util.Date diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt index a449f7dad..9dc19e51a 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt @@ -1,6 +1,7 @@ package org.openedx.profile.presentation.calendar import android.content.Intent +import android.content.res.Configuration import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri @@ -39,7 +40,6 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.profile.R import org.openedx.core.R as CoreR - class CalendarAccessDialogFragment : DialogFragment() { private val config by inject() @@ -106,13 +106,15 @@ private fun CalendarAccessDialog( Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.profile_calendar_access_dialog_title), - style = MaterialTheme.appTypography.titleLarge + style = MaterialTheme.appTypography.titleLarge, + color = MaterialTheme.appColors.textDark ) } Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.profile_calendar_access_dialog_description), - style = MaterialTheme.appTypography.bodyMedium + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.textDark ) OpenEdXButton( modifier = Modifier.fillMaxWidth(), @@ -143,7 +145,8 @@ private fun CalendarAccessDialog( } } -@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun CalendarAccessDialogPreview() { OpenEdXTheme { @@ -152,4 +155,4 @@ private fun CalendarAccessDialogPreview() { onGrantCalendarAccessClick = { } ) } -} \ 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 new file mode 100644 index 000000000..361fa5776 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarColor.kt @@ -0,0 +1,19 @@ +package org.openedx.profile.presentation.calendar + +import androidx.annotation.StringRes +import org.openedx.profile.R + +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); +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index f41eb5548..debf7e798 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -67,7 +67,11 @@ class CalendarFragment : Fragment() { ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (!isGranted.containsValue(false)) { - //TODO + val dialog = NewCalendarDialogFragment.newInstance() + dialog.show( + requireActivity().supportFragmentManager, + NewCalendarDialogFragment::class.simpleName + ) } else { val dialog = CalendarAccessDialogFragment.newInstance() dialog.show( @@ -257,4 +261,4 @@ private fun VideoQualityScreenPreview() { onBackClick = {} ) } -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..060b328dc --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogFragment.kt @@ -0,0 +1,390 @@ +package org.openedx.profile.presentation.calendar + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.fragment.app.DialogFragment +import org.openedx.core.presentation.dialog.DefaultDialogBox +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.OpenEdXOutlinedButton +import org.openedx.core.ui.crop +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.theme.appTypography +import org.openedx.profile.R +import androidx.compose.ui.graphics.Color as ComposeColor +import org.openedx.core.R as CoreR + +class NewCalendarDialogFragment : DialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + if (dialog != null && dialog!!.window != null) { + dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + NewCalendarDialog( + onCancelClick = { + dismiss() + }, + onBeginSyncingClick = { calendarName, calendarColor -> + //TODO + } + ) + } + } + } + + companion object { + fun newInstance(): NewCalendarDialogFragment { + return NewCalendarDialogFragment() + } + + fun getDefaultCalendarName(context: Context): String { + return "${context.getString(CoreR.string.app_name)} ${context.getString(R.string.profile_course_dates)}" + } + } +} + +@Composable +private fun NewCalendarDialog( + modifier: Modifier = Modifier, + onCancelClick: () -> Unit, + onBeginSyncingClick: (calendarName: String, calendarColor: CalendarColor) -> Unit +) { + val context = LocalContext.current + var calendarName by rememberSaveable { + mutableStateOf("") + } + var calendarColor by rememberSaveable { + mutableStateOf(CalendarColor.ACCENT) + } + DefaultDialogBox( + modifier = modifier, + onDismissClick = onCancelClick + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.profile_new_calendar), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.titleLarge + ) + Icon( + modifier = Modifier + .size(24.dp) + .clickable { + onCancelClick() + }, + imageVector = Icons.Default.Close, + contentDescription = null, + tint = MaterialTheme.appColors.primary + ) + } + CalendarNameTextField( + onValueChanged = { + calendarName = it + } + ) + ColorDropdown( + onValueChanged = { + calendarColor = it + } + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_new_calendar_description), + style = MaterialTheme.appTypography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.appColors.textDark + ) + OpenEdXOutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = CoreR.string.core_cancel), + backgroundColor = MaterialTheme.appColors.background, + borderColor = MaterialTheme.appColors.buttonBackground, + textColor = MaterialTheme.appColors.buttonBackground, + onClick = { + onCancelClick() + } + ) + OpenEdXButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_begin_syncing), + onClick = { + onBeginSyncingClick( + calendarName.ifEmpty { NewCalendarDialogFragment.getDefaultCalendarName(context) }, + calendarColor + ) + } + ) + } + } +} + +@Composable +private fun CalendarNameTextField( + modifier: Modifier = Modifier, + onValueChanged: (String) -> Unit +) { + val focusManager = LocalFocusManager.current + var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) + } + + Column { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_calendar_name), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.labelLarge + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + modifier = modifier + .fillMaxWidth() + .height(48.dp), + value = textFieldValue, + onValueChange = { + textFieldValue = it + onValueChanged(it.text.trim()) + }, + colors = TextFieldDefaults.outlinedTextFieldColors( + unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder + ), + shape = MaterialTheme.appShapes.textFieldShape, + placeholder = { + Text( + text = NewCalendarDialogFragment.getDefaultCalendarName(LocalContext.current), + color = MaterialTheme.appColors.textFieldHint, + style = MaterialTheme.appTypography.bodyMedium + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions { + focusManager.clearFocus() + }, + textStyle = MaterialTheme.appTypography.bodyMedium, + singleLine = true + ) + } +} + +@Composable +private fun ColorDropdown( + modifier: Modifier = Modifier, + onValueChanged: (CalendarColor) -> Unit +) { + val density = LocalDensity.current + var expanded by remember { mutableStateOf(false) } + var currentValue by remember { mutableStateOf(CalendarColor.ACCENT) } + var dropdownWidth by remember { mutableStateOf(300.dp) } + val colorArrowRotation by animateFloatAsState( + targetValue = if (expanded) 180f else 0f, + label = "" + ) + + Column( + modifier = modifier + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_color), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.labelLarge + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clip(MaterialTheme.appShapes.textFieldShape) + .border( + 1.dp, + MaterialTheme.appColors.textFieldBorder, + MaterialTheme.appShapes.textFieldShape + ) + .onSizeChanged { + dropdownWidth = with(density) { it.width.toDp() } + } + .clickable { + expanded = true + }, + verticalAlignment = Alignment.CenterVertically + ) { + ColorCircle( + modifier = Modifier + .padding(start = 16.dp), + color = ComposeColor(currentValue.color) + ) + Text( + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + text = stringResource(id = currentValue.title), + color = MaterialTheme.appColors.textDark, + style = MaterialTheme.appTypography.bodyMedium + ) + Icon( + modifier = Modifier + .padding(end = 16.dp) + .rotate(colorArrowRotation), + imageVector = Icons.Default.ExpandMore, + tint = MaterialTheme.appColors.textDark, + contentDescription = null + ) + } + + MaterialTheme( + colors = MaterialTheme.colors.copy(surface = MaterialTheme.appColors.background), + shapes = MaterialTheme.shapes.copy(MaterialTheme.appShapes.textFieldShape) + ) { + Spacer(modifier = Modifier.padding(top = 4.dp)) + DropdownMenu( + modifier = Modifier + .crop(vertical = 8.dp) + .height(240.dp) + .width(dropdownWidth) + .border( + 1.dp, + MaterialTheme.appColors.textFieldBorder, + MaterialTheme.appShapes.textFieldShape + ) + .crop(vertical = 8.dp), + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + for ((index, calendarColor) in CalendarColor.entries.withIndex()) { + DropdownMenuItem( + modifier = Modifier + .background(MaterialTheme.appColors.background), + onClick = { + currentValue = calendarColor + expanded = false + onValueChanged(CalendarColor.entries[index]) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ColorCircle( + color = ComposeColor(calendarColor.color) + ) + Text( + text = stringResource(id = calendarColor.title), + style = MaterialTheme.appTypography.titleSmall, + color = MaterialTheme.appColors.textDark + ) + } + } + if (index < CalendarColor.entries.lastIndex) { + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.appColors.divider + ) + } + } + } + } + } +} + +@Composable +private fun ColorCircle( + modifier: Modifier = Modifier, + color: ComposeColor +) { + Box( + modifier = modifier + .size(18.dp) + .clip(CircleShape) + .background(color) + ) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewCalendarDialogPreview() { + OpenEdXTheme { + NewCalendarDialog( + onCancelClick = { }, + onBeginSyncingClick = { _, _ -> } + ) + } +} diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 56531fffd..60f0e4060 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -46,5 +46,19 @@ Calendar Access To show upcoming assignments and course milestones on your calendar, we need permission to access your calendar. Grant Calendar Access + New Calendar + Upcoming assignments for active courses will appear on this calendar + Begin Syncing + Calendar Name + Red + Orange + Yellow + Green + Blue + Purple + Brown + Accent + Course Dates + Color From 437febcdcf66529596914d460a5b30f7c74bcc0c Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 24 May 2024 13:35:48 +0300 Subject: [PATCH 04/26] fix: Fixes according to PR feedback --- core/src/main/java/org/openedx/core/config/Config.kt | 4 ++-- .../presentation/calendar/CalendarAccessDialogFragment.kt | 6 +++--- .../profile/presentation/calendar/CalendarFragment.kt | 4 ++-- .../presentation/calendar/NewCalendarDialogFragment.kt | 8 ++++---- .../profile/presentation/settings/SettingsViewModel.kt | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) 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 e73ed22f3..b0c3f211d 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -24,7 +24,7 @@ class Config(context: Context) { } fun getAppId(): String { - return getString(API_APP_ID, "") + return getString(APPLICATION_ID, "") } fun getApiHostURL(): String { @@ -150,7 +150,7 @@ class Config(context: Context) { } companion object { - private const val API_APP_ID = "APPLICATION_ID" + private const val APPLICATION_ID = "APPLICATION_ID" private const val API_HOST_URL = "API_HOST_URL" private const val URI_SCHEME = "URI_SCHEME" private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID" diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt index 9dc19e51a..53f955ec6 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt @@ -48,9 +48,7 @@ class CalendarAccessDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - if (dialog != null && dialog!!.window != null) { - dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -71,6 +69,8 @@ class CalendarAccessDialogFragment : DialogFragment() { } companion object { + const val DIALOG_TAG = "CalendarAccessDialogFragment" + fun newInstance(): CalendarAccessDialogFragment { return CalendarAccessDialogFragment() } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index debf7e798..46fe33e8d 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -70,13 +70,13 @@ class CalendarFragment : Fragment() { val dialog = NewCalendarDialogFragment.newInstance() dialog.show( requireActivity().supportFragmentManager, - NewCalendarDialogFragment::class.simpleName + NewCalendarDialogFragment.DIALOG_TAG ) } else { val dialog = CalendarAccessDialogFragment.newInstance() dialog.show( requireActivity().supportFragmentManager, - CalendarAccessDialogFragment::class.simpleName + CalendarAccessDialogFragment.DIALOG_TAG ) } } 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 060b328dc..bfd453f5c 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 @@ -78,9 +78,7 @@ class NewCalendarDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - if (dialog != null && dialog!!.window != null) { - dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -89,7 +87,7 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarName, calendarColor -> - //TODO + //TODO Create calendar and sync events } ) } @@ -97,6 +95,8 @@ class NewCalendarDialogFragment : DialogFragment() { } companion object { + const val DIALOG_TAG = "NewCalendarDialogFragment" + fun newInstance(): NewCalendarDialogFragment { return NewCalendarDialogFragment() } 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 545afabc9..9715eb774 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 @@ -189,7 +189,6 @@ class SettingsViewModel( router.navigateToCalendarSettings(fragmentManager) } - fun restartApp(fragmentManager: FragmentManager) { router.restartApp( fragmentManager, From 90d3f6b4e8092c550be102e979b26bd48ed85578 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 27 May 2024 12:02:05 +0300 Subject: [PATCH 05/26] feat: Create calendar --- .../main/java/org/openedx/app/di/ScreenModule.kt | 2 ++ .../java/org/openedx/core/system/CalendarManager.kt | 11 +++++++---- .../container/CourseContainerViewModel.kt | 2 +- .../calendar/NewCalendarDialogFragment.kt | 5 ++++- .../calendar/NewCalendarDialogViewModel.kt | 13 +++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt 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 3c99dbc0f..4e6bf1e30 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -54,6 +54,7 @@ import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.anothersaccount.AnothersProfileViewModel import org.openedx.profile.presentation.calendar.CalendarViewModel +import org.openedx.profile.presentation.calendar.NewCalendarDialogViewModel import org.openedx.profile.presentation.delete.DeleteProfileViewModel import org.openedx.profile.presentation.edit.EditProfileViewModel import org.openedx.profile.presentation.manageaccount.ManageAccountViewModel @@ -165,6 +166,7 @@ val screenModule = module { } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get()) } + viewModel { NewCalendarDialogViewModel(get()) } single { CourseRepository(get(), get(), get(), get(), get()) } factory { CourseInteractor(get()) } diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 53d7a1e1f..39bd8ceba 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -56,7 +56,8 @@ class CalendarManager( * Create or update the calendar if it is already existed in mobile calendar app */ fun createOrUpdateCalendar( - calendarTitle: String + calendarTitle: String, + calendarColor: Long ): Long { val calendarId = getCalendarId( calendarTitle = calendarTitle @@ -67,7 +68,8 @@ class CalendarManager( } return createCalendar( - calendarTitle = calendarTitle + calendarTitle = calendarTitle, + calendarColor = calendarColor ) } @@ -75,7 +77,8 @@ class CalendarManager( * Method to create a separate calendar based on course name in mobile calendar app */ private fun createCalendar( - calendarTitle: String + calendarTitle: String, + calendarColor: Long ): Long { val contentValues = ContentValues() contentValues.put(CalendarContract.Calendars.NAME, calendarTitle) @@ -94,7 +97,7 @@ class CalendarManager( contentValues.put(CalendarContract.Calendars.VISIBLE, 1) contentValues.put( CalendarContract.Calendars.CALENDAR_COLOR, - ContextCompat.getColor(context, org.openedx.core.R.color.primary) + calendarColor ) val creationUri: Uri? = asSyncAdapter( Uri.parse(CalendarContract.Calendars.CONTENT_URI.toString()), 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 1ec787e54..cee64227e 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 @@ -375,7 +375,7 @@ class CourseContainerViewModel( } private fun getCalendarId(): Long { - return calendarManager.createOrUpdateCalendar( + return calendarManager.getCalendarId( calendarTitle = _calendarSyncUIState.value.calendarTitle ) } 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 bfd453f5c..0cdd9e51f 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 @@ -59,6 +59,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.DialogFragment +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.presentation.dialog.DefaultDialogBox import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.OpenEdXOutlinedButton @@ -73,6 +74,8 @@ import org.openedx.core.R as CoreR class NewCalendarDialogFragment : DialogFragment() { + private val viewModel by viewModel() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -87,7 +90,7 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarName, calendarColor -> - //TODO Create calendar and sync events + viewModel.syncCalendar(calendarName, calendarColor) } ) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt new file mode 100644 index 000000000..253e0d61b --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -0,0 +1,13 @@ +package org.openedx.profile.presentation.calendar + +import org.openedx.core.BaseViewModel +import org.openedx.core.system.CalendarManager + +class NewCalendarDialogViewModel( + private val calendarManager: CalendarManager +) : BaseViewModel() { + + fun syncCalendar(calendarName: String, calendarColor: CalendarColor) { + calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) + } +} From f80afcc1c22eb9cbfd86721183a0d9e55e1983f1 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 24 May 2024 13:35:48 +0300 Subject: [PATCH 06/26] fix: Fixes according to PR feedback --- core/src/main/java/org/openedx/core/config/Config.kt | 4 ++-- .../main/java/org/openedx/core/ui/ComposeCommon.kt | 3 ++- core/src/main/res/drawable/core_ic_external_link.xml | 9 --------- .../calendar/CalendarAccessDialogFragment.kt | 11 +++++++---- .../profile/presentation/calendar/CalendarFragment.kt | 6 +++--- .../calendar/NewCalendarDialogFragment.kt | 8 ++++---- .../presentation/settings/SettingsViewModel.kt | 1 - 7 files changed, 18 insertions(+), 24 deletions(-) delete mode 100644 core/src/main/res/drawable/core_ic_external_link.xml 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 e73ed22f3..b0c3f211d 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -24,7 +24,7 @@ class Config(context: Context) { } fun getAppId(): String { - return getString(API_APP_ID, "") + return getString(APPLICATION_ID, "") } fun getApiHostURL(): String { @@ -150,7 +150,7 @@ class Config(context: Context) { } companion object { - private const val API_APP_ID = "APPLICATION_ID" + private const val APPLICATION_ID = "APPLICATION_ID" private const val API_HOST_URL = "API_HOST_URL" private const val URI_SCHEME = "URI_SCHEME" private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID" 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 3b97742f1..e6ce77829 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -937,6 +937,7 @@ fun TextIcon( icon: ImageVector, color: Color, textStyle: TextStyle = MaterialTheme.appTypography.bodySmall, + iconModifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { val modifier = if (onClick == null) { @@ -951,7 +952,7 @@ fun TextIcon( ) { Text(text = text, color = color, style = textStyle) Icon( - modifier = Modifier.size((textStyle.fontSize.value + 4).dp), + modifier = iconModifier.size((textStyle.fontSize.value + 4).dp), imageVector = icon, contentDescription = null, tint = color diff --git a/core/src/main/res/drawable/core_ic_external_link.xml b/core/src/main/res/drawable/core_ic_external_link.xml deleted file mode 100644 index 05d38676d..000000000 --- a/core/src/main/res/drawable/core_ic_external_link.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt index 9dc19e51a..a9094c67d 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt @@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,9 +50,7 @@ class CalendarAccessDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - if (dialog != null && dialog!!.window != null) { - dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -64,6 +64,7 @@ class CalendarAccessDialogFragment : DialogFragment() { Uri.parse("package:" + config.getAppId()) ) startActivity(intent) + dismiss() } ) } @@ -71,6 +72,8 @@ class CalendarAccessDialogFragment : DialogFragment() { } companion object { + const val DIALOG_TAG = "CalendarAccessDialogFragment" + fun newInstance(): CalendarAccessDialogFragment { return CalendarAccessDialogFragment() } @@ -124,7 +127,7 @@ private fun CalendarAccessDialog( content = { TextIcon( text = stringResource(id = R.string.profile_grant_access_calendar), - painter = painterResource(id = CoreR.drawable.core_ic_external_link), + icon = Icons.AutoMirrored.Filled.OpenInNew, color = MaterialTheme.appColors.buttonText, textStyle = MaterialTheme.appTypography.labelLarge, iconModifier = Modifier.padding(start = 4.dp) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index debf7e798..8a8794c94 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -70,13 +70,13 @@ class CalendarFragment : Fragment() { val dialog = NewCalendarDialogFragment.newInstance() dialog.show( requireActivity().supportFragmentManager, - NewCalendarDialogFragment::class.simpleName + NewCalendarDialogFragment.DIALOG_TAG ) } else { val dialog = CalendarAccessDialogFragment.newInstance() dialog.show( requireActivity().supportFragmentManager, - CalendarAccessDialogFragment::class.simpleName + CalendarAccessDialogFragment.DIALOG_TAG ) } } @@ -253,7 +253,7 @@ private fun CalendarScreen( @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun VideoQualityScreenPreview() { +private fun CalendarScreenPreview() { OpenEdXTheme { CalendarScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), 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 060b328dc..bfd453f5c 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 @@ -78,9 +78,7 @@ class NewCalendarDialogFragment : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { - if (dialog != null && dialog!!.window != null) { - dialog!!.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { @@ -89,7 +87,7 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarName, calendarColor -> - //TODO + //TODO Create calendar and sync events } ) } @@ -97,6 +95,8 @@ class NewCalendarDialogFragment : DialogFragment() { } companion object { + const val DIALOG_TAG = "NewCalendarDialogFragment" + fun newInstance(): NewCalendarDialogFragment { return NewCalendarDialogFragment() } 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 545afabc9..9715eb774 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 @@ -189,7 +189,6 @@ class SettingsViewModel( router.navigateToCalendarSettings(fragmentManager) } - fun restartApp(fragmentManager: FragmentManager) { router.restartApp( fragmentManager, From 267b9b422288fe31bf933245fc06631e8d05f2c5 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 28 May 2024 14:51:30 +0300 Subject: [PATCH 07/26] feat: Creating calendar --- .../org/openedx/core/system/CalendarManager.kt | 2 +- .../container/CourseContainerViewModel.kt | 3 +++ .../calendar/NewCalendarDialogFragment.kt | 1 + .../calendar/NewCalendarDialogViewModel.kt | 16 +++++++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 39bd8ceba..c90de7109 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -97,7 +97,7 @@ class CalendarManager( contentValues.put(CalendarContract.Calendars.VISIBLE, 1) contentValues.put( CalendarContract.Calendars.CALENDAR_COLOR, - calendarColor + calendarColor.toInt() ) val creationUri: Uri? = asSyncAdapter( Uri.parse(CalendarContract.Calendars.CONTENT_URI.toString()), 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 cee64227e..88f9a5913 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 @@ -375,6 +375,9 @@ class CourseContainerViewModel( } private fun getCalendarId(): Long { +// return calendarManager.createOrUpdateCalendar( +// calendarTitle = _calendarSyncUIState.value.calendarTitle +// ) return calendarManager.getCalendarId( calendarTitle = _calendarSyncUIState.value.calendarTitle ) 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 0cdd9e51f..4b201d688 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 @@ -91,6 +91,7 @@ class NewCalendarDialogFragment : DialogFragment() { }, onBeginSyncingClick = { calendarName, calendarColor -> viewModel.syncCalendar(calendarName, calendarColor) + dismiss() } ) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 253e0d61b..9baf5aefa 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -8,6 +8,20 @@ class NewCalendarDialogViewModel( ) : BaseViewModel() { fun syncCalendar(calendarName: String, calendarColor: CalendarColor) { - calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) + val calendarId = calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) +// if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { +// +// } +// val courseDateBlock = CourseDateBlock( +// title = "Homework 1: ABCD", +// description = "After this date, course content will be archived", +// date = Date(), +// ) +// calendarManager.addEventsIntoCalendar( +// calendarId = calendarId, +// courseId = "", +// courseName = "", +// courseDateBlock = courseDateBlock +// ) } } From 2f8c9f5d935a8109dd8ca78e16a213aeb5ac86d7 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 31 May 2024 16:39:54 +0300 Subject: [PATCH 08/26] feat: Creating a calendar, tryToSyncCalendar logic, CalendarUIState, start CalendarSyncService --- app/src/main/AndroidManifest.xml | 5 + .../main/java/org/openedx/app/AppActivity.kt | 14 ++ .../app/data/storage/PreferencesManager.kt | 17 +- .../main/java/org/openedx/app/di/AppModule.kt | 2 + .../java/org/openedx/app/di/ScreenModule.kt | 4 +- .../core/data/storage/CorePreferences.kt | 2 + .../core/service/CalendarSyncService.kt | 31 +++ .../openedx/core/system/CalendarManager.kt | 6 + .../system/notifier/AppUpgradeNotifier.kt | 3 +- .../container/CourseContainerViewModel.kt | 1 - .../presentation/calendar/CalendarCreated.kt | 3 + .../presentation/calendar/CalendarEvent.kt | 3 + .../presentation/calendar/CalendarFragment.kt | 216 ++---------------- .../presentation/calendar/CalendarNotifier.kt | 14 ++ .../calendar/CalendarSetUpView.kt | 208 +++++++++++++++++ .../calendar/CalendarSettingsView.kt | 129 +++++++++++ .../presentation/calendar/CalendarUIState.kt | 5 + .../calendar/CalendarViewModel.kt | 32 ++- .../calendar/NewCalendarDialogFragment.kt | 2 +- .../calendar/NewCalendarDialogViewModel.kt | 29 ++- 20 files changed, 504 insertions(+), 222 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/service/CalendarSyncService.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e8282acb..d6e830e72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -113,6 +113,11 @@ + + diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 5ab0d0b0e..c0c64279c 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -24,12 +24,15 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.requestApplyInsetsWhenAttached import org.openedx.core.presentation.global.InsetHolder import org.openedx.core.presentation.global.WindowSizeHolder +import org.openedx.core.system.CalendarManager import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.utils.Logger +import org.openedx.core.utils.isToday import org.openedx.profile.presentation.ProfileRouter import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment +import java.util.Date class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { @@ -48,6 +51,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private val whatsNewManager by inject() private val corePreferencesManager by inject() private val profileRouter by inject() + private val calendarManager by inject() private val branchLogger = Logger(BRANCH_TAG) @@ -139,6 +143,8 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { viewModel.logoutUser.observe(this) { profileRouter.restartApp(supportFragmentManager, viewModel.isLogistrationEnabled) } + + tryToSyncCalendar() } override fun onStart() { @@ -213,6 +219,14 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { } } + private fun tryToSyncCalendar() { + val isCalendarCreated = corePreferencesManager.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + val isCalendarSyncRequired = !Date(corePreferencesManager.lastCalendarSync).isToday() + if (isCalendarCreated && isCalendarSyncRequired) { + calendarManager.startSyncCalendarService() + } + } + companion object { const val TOP_INSET = "topInset" const val BOTTOM_INSET = "bottomInset" 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 603876d54..c1301117c 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 @@ -10,6 +10,7 @@ import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.VideoQuality import org.openedx.core.domain.model.VideoSettings import org.openedx.core.extension.replaceSpace +import org.openedx.core.system.CalendarManager import org.openedx.course.data.storage.CoursePreferences import org.openedx.profile.data.model.Account import org.openedx.profile.data.storage.ProfilePreferences @@ -37,7 +38,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences }.apply() } - private fun getLong(key: String): Long = sharedPreferences.getLong(key, 0L) + private fun getLong(key: String, defValue: Long = 0): Long = sharedPreferences.getLong(key, defValue) private fun saveBoolean(key: String, value: Boolean) { sharedPreferences.edit().apply { @@ -76,6 +77,18 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getLong(EXPIRES_IN) + override var calendarId: Long + set(value) { + saveLong(CALENDAR_ID, value) + } + get() = getLong(CALENDAR_ID, CalendarManager.CALENDAR_DOES_NOT_EXIST) + + override var lastCalendarSync: Long + set(value) { + saveLong(LAST_CALENDAR_SYNC, value) + } + get() = getLong(LAST_CALENDAR_SYNC) + override var user: User? set(value) { val userJson = Gson().toJson(value) @@ -172,5 +185,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val VIDEO_SETTINGS_STREAMING_QUALITY = "video_settings_streaming_quality" private const val VIDEO_SETTINGS_DOWNLOAD_QUALITY = "video_settings_download_quality" private const val APP_CONFIG = "app_config" + private const val CALENDAR_ID = "CALENDAR_ID" + private const val LAST_CALENDAR_SYNC = "LAST_CALENDAR_SYNC" } } 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 a5ec76b37..cb34ede7d 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -62,6 +62,7 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter +import org.openedx.profile.presentation.calendar.CalendarNotifier import org.openedx.profile.system.notifier.ProfileNotifier import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter @@ -99,6 +100,7 @@ val appModule = module { single { DownloadNotifier() } single { VideoNotifier() } single { DiscoveryNotifier() } + single { CalendarNotifier() } single { AppRouter() } single { get() } 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 969f104dd..f8c1bf72f 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -171,8 +171,8 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get()) } - viewModel { NewCalendarDialogViewModel(get()) } + viewModel { CalendarViewModel(get(), get(), get()) } + viewModel { NewCalendarDialogViewModel(get(), get(), get()) } single { CourseRepository(get(), get(), get(), get(), get()) } factory { CourseInteractor(get()) } diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index 48999ab4e..e0ffde5ee 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -11,6 +11,8 @@ interface CorePreferences { var user: User? var videoSettings: VideoSettings var appConfig: AppConfig + var calendarId: Long + var lastCalendarSync: Long fun clear() } diff --git a/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt b/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt new file mode 100644 index 000000000..6570a3420 --- /dev/null +++ b/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt @@ -0,0 +1,31 @@ +package org.openedx.core.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.openedx.core.system.CalendarManager + +class CalendarSyncService : Service() { + + private val calendarManager: CalendarManager by inject() + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + CoroutineScope(Dispatchers.IO).launch { + syncCalendar() + } + return START_STICKY + } + + private fun syncCalendar() { + Log.e("___", "syncCalendar") + } +} diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index c90de7109..2e3f14851 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.service.CalendarSyncService import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar import java.util.Calendar @@ -403,6 +404,11 @@ class CalendarManager( return "${resourceManager.getString(id = CoreR.string.platform_name)} - $courseName" } + fun startSyncCalendarService() { + val intent = Intent(context, CalendarSyncService::class.java) + context.startService(intent) + } + companion object { const val CALENDAR_DOES_NOT_EXIST = -1L private const val TAG = "CalendarManager" 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 index 0f5a274d5..176a3ba86 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt @@ -11,5 +11,4 @@ class AppUpgradeNotifier { val notifier: Flow = channel.asSharedFlow() suspend fun send(event: AppUpgradeEvent) = channel.emit(event) - -} \ No newline at end of file +} 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 127a3a32a..8961817d7 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 @@ -25,7 +25,6 @@ import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.exception.NoCachedDataException import org.openedx.core.extension.isInternetError -import org.openedx.core.presentation.course.CourseContainerTab import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState import org.openedx.core.system.CalendarManager diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt new file mode 100644 index 000000000..06a3c03fa --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.presentation.calendar + +object CalendarCreated : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt new file mode 100644 index 000000000..6073d86aa --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.presentation.calendar + +interface CalendarEvent \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index 8a8794c94..d825726ba 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -1,68 +1,22 @@ package org.openedx.profile.presentation.calendar -import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Autorenew -import androidx.compose.material.icons.rounded.CalendarToday -import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment -import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.ui.OpenEdXButton -import org.openedx.core.ui.Toolbar +import org.koin.androidx.compose.koinViewModel import org.openedx.core.ui.WindowSize -import org.openedx.core.ui.WindowType -import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.rememberWindowSize -import org.openedx.core.ui.settingsHeaderBackground -import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme -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.ui.windowSizeValue -import org.openedx.profile.R -import org.openedx.core.R as CoreR class CalendarFragment : Fragment() { - private val viewModel by viewModel() - private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> @@ -90,9 +44,12 @@ class CalendarFragment : Fragment() { setContent { OpenEdXTheme { val windowSize = rememberWindowSize() + val viewModel: CalendarViewModel = koinViewModel() + val uiState by viewModel.uiState.collectAsState() - CalendarScreen( + CalendarView( windowSize = windowSize, + uiState = uiState, setUpCalendarSync = { viewModel.setUpCalendarSync(permissionLauncher) }, @@ -105,160 +62,23 @@ class CalendarFragment : Fragment() { } } -@OptIn(ExperimentalComposeUiApi::class) @Composable -private fun CalendarScreen( +private fun CalendarView( windowSize: WindowSize, + uiState: CalendarUIState, setUpCalendarSync: () -> Unit, onBackClick: () -> Unit ) { - val scaffoldState = rememberScaffoldState() - - Scaffold( - modifier = Modifier - .fillMaxSize() - .semantics { - testTagsAsResourceId = true - }, - scaffoldState = scaffoldState - ) { paddingValues -> - - val contentWidth by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 420.dp), - compact = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - ) - ) - } - - val topBarWidth by remember(key1 = windowSize) { - mutableStateOf( - windowSize.windowSizeValue( - expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), - compact = Modifier - .fillMaxWidth() - ) - ) - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .settingsHeaderBackground() - .statusBarsInset(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Toolbar( - modifier = topBarWidth - .displayCutoutForLandscape(), - label = stringResource(id = R.string.profile_dates_and_calendar), - canShowBackBtn = true, - labelTint = MaterialTheme.appColors.settingsTitleContent, - iconTint = MaterialTheme.appColors.settingsTitleContent, - onBackClick = onBackClick - ) - - Box( - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.appShapes.screenBackgroundShape) - .background(MaterialTheme.appColors.background) - .displayCutoutForLandscape(), - contentAlignment = Alignment.TopCenter - ) { - Column( - modifier = contentWidth.padding(vertical = 28.dp), - ) { - Text( - modifier = Modifier.testTag("txt_settings"), - text = stringResource(id = CoreR.string.core_settings), - style = MaterialTheme.appTypography.labelLarge, - color = MaterialTheme.appColors.textSecondary - ) - Spacer(modifier = Modifier.height(14.dp)) - Card( - shape = MaterialTheme.appShapes.cardShape, - elevation = 0.dp, - backgroundColor = MaterialTheme.appColors.cardViewBackground - ) { - Column( - modifier = Modifier - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .padding(vertical = 28.dp), - contentAlignment = Alignment.Center - ) { - Icon( - modifier = Modifier - .fillMaxWidth() - .height(148.dp), - tint = MaterialTheme.appColors.textDark, - imageVector = Icons.Rounded.CalendarToday, - contentDescription = null - ) - Icon( - modifier = Modifier - .fillMaxWidth() - .padding(top = 30.dp) - .height(60.dp), - tint = MaterialTheme.appColors.textDark, - imageVector = Icons.Default.Autorenew, - contentDescription = null - ) - } - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.profile_calendar_sync), - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textDark - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.profile_calendar_sync_description), - style = MaterialTheme.appTypography.labelLarge, - color = MaterialTheme.appColors.textDark - ) - Spacer(modifier = Modifier.height(16.dp)) - OpenEdXButton( - modifier = Modifier.fillMaxWidth(0.75f), - text = stringResource(id = R.string.profile_set_up_calendar_sync), - onClick = { - setUpCalendarSync() - } - ) - Spacer(modifier = Modifier.height(24.dp)) - } - } - } - } - } - } - } -} - -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun CalendarScreenPreview() { - OpenEdXTheme { - CalendarScreen( - windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - setUpCalendarSync = {}, - onBackClick = {} + if (!uiState.isCalendarExist) { + CalendarSetUpView( + windowSize = windowSize, + setUpCalendarSync = setUpCalendarSync, + onBackClick = onBackClick + ) + } else { + CalendarSettingsView( + windowSize = windowSize, + onBackClick = onBackClick ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt new file mode 100644 index 000000000..ddac5e112 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt @@ -0,0 +1,14 @@ +package org.openedx.profile.presentation.calendar + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class CalendarNotifier { + + private val channel = MutableSharedFlow(replay = 0, extraBufferCapacity = 0) + + val notifier: Flow = channel.asSharedFlow() + + suspend fun send(event: CalendarEvent) = channel.emit(event) +} 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 new file mode 100644 index 000000000..9a1c39cf2 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSetUpView.kt @@ -0,0 +1,208 @@ +package org.openedx.profile.presentation.calendar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Autorenew +import androidx.compose.material.icons.rounded.CalendarToday +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.settingsHeaderBackground +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +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.ui.windowSizeValue +import org.openedx.profile.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun CalendarSetUpView( + windowSize: WindowSize, + setUpCalendarSync: () -> Unit, + onBackClick: () -> Unit +) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, + scaffoldState = scaffoldState + ) { paddingValues -> + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 420.dp), + compact = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + ) + } + + val topBarWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier + .fillMaxWidth() + ) + ) + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .settingsHeaderBackground() + .statusBarsInset(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Toolbar( + modifier = topBarWidth + .displayCutoutForLandscape(), + label = stringResource(id = R.string.profile_dates_and_calendar), + canShowBackBtn = true, + labelTint = MaterialTheme.appColors.settingsTitleContent, + iconTint = MaterialTheme.appColors.settingsTitleContent, + onBackClick = onBackClick + ) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.appShapes.screenBackgroundShape) + .background(MaterialTheme.appColors.background) + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = contentWidth.padding(vertical = 28.dp), + ) { + Text( + modifier = Modifier.testTag("txt_settings"), + text = stringResource(id = org.openedx.core.R.string.core_settings), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textSecondary + ) + Spacer(modifier = Modifier.height(14.dp)) + Card( + shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, + backgroundColor = MaterialTheme.appColors.cardViewBackground + ) { + Column( + modifier = Modifier + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .padding(vertical = 28.dp), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier + .fillMaxWidth() + .height(148.dp), + tint = MaterialTheme.appColors.textDark, + imageVector = Icons.Rounded.CalendarToday, + contentDescription = null + ) + Icon( + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp) + .height(60.dp), + tint = MaterialTheme.appColors.textDark, + imageVector = Icons.Default.Autorenew, + contentDescription = null + ) + } + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.profile_calendar_sync), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.profile_calendar_sync_description), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + Spacer(modifier = Modifier.height(16.dp)) + OpenEdXButton( + modifier = Modifier.fillMaxWidth(0.75f), + text = stringResource(id = R.string.profile_set_up_calendar_sync), + onClick = { + setUpCalendarSync() + } + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CalendarScreenPreview() { + OpenEdXTheme { + CalendarSetUpView( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + setUpCalendarSync = {}, + onBackClick = {} + ) + } +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt new file mode 100644 index 000000000..4fc6329aa --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -0,0 +1,129 @@ +package org.openedx.profile.presentation.calendar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.settingsHeaderBackground +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.windowSizeValue +import org.openedx.profile.R + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun CalendarSettingsView( + windowSize: WindowSize, + onBackClick: () -> Unit +) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + modifier = Modifier + .fillMaxSize() + .semantics { + testTagsAsResourceId = true + }, + scaffoldState = scaffoldState + ) { paddingValues -> + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 420.dp), + compact = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + ) + } + + val topBarWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier + .fillMaxWidth() + ) + ) + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .settingsHeaderBackground() + .statusBarsInset(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Toolbar( + modifier = topBarWidth + .displayCutoutForLandscape(), + label = stringResource(id = R.string.profile_dates_and_calendar), + canShowBackBtn = true, + labelTint = MaterialTheme.appColors.settingsTitleContent, + iconTint = MaterialTheme.appColors.settingsTitleContent, + onBackClick = onBackClick + ) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.appShapes.screenBackgroundShape) + .background(MaterialTheme.appColors.background) + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = contentWidth.padding(vertical = 28.dp), + ) { + //TODO + } + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun CalendarSettingsViewPreview() { + OpenEdXTheme { + CalendarSettingsView( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + onBackClick = {} + ) + } +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt new file mode 100644 index 000000000..fea208473 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -0,0 +1,5 @@ +package org.openedx.profile.presentation.calendar + +data class CalendarUIState( + val isCalendarExist: Boolean +) 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 316b689b4..24b2b4f37 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 @@ -1,13 +1,43 @@ package org.openedx.profile.presentation.calendar import androidx.activity.result.ActivityResultLauncher +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.CalendarManager class CalendarViewModel( - private val calendarManager: CalendarManager + private val calendarManager: CalendarManager, + private val corePreferences: CorePreferences, + private val calendarNotifier: CalendarNotifier ) : BaseViewModel() { + private val _uiState = MutableStateFlow( + CalendarUIState( + isCalendarExist = corePreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + ) + ) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + init { + viewModelScope.launch { + calendarNotifier.notifier.collect { calendarEvent -> + when (calendarEvent) { + CalendarCreated -> { + calendarManager.startSyncCalendarService() + _uiState.update { it.copy(isCalendarExist = true) } + } + } + } + } + } + fun setUpCalendarSync(permissionLauncher: ActivityResultLauncher>) { permissionLauncher.launch(calendarManager.permissions) } 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 4760abd49..892d67ffe 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 @@ -90,7 +90,7 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarName, calendarColor -> - viewModel.syncCalendar(calendarName, calendarColor) + viewModel.createCalendar(calendarName, calendarColor) dismiss() } ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 9baf5aefa..7ab07141b 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -1,27 +1,24 @@ package org.openedx.profile.presentation.calendar +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.CalendarManager class NewCalendarDialogViewModel( - private val calendarManager: CalendarManager + private val calendarManager: CalendarManager, + private val corePreferences: CorePreferences, + private val calendarNotifier: CalendarNotifier ) : BaseViewModel() { - fun syncCalendar(calendarName: String, calendarColor: CalendarColor) { + fun createCalendar(calendarName: String, calendarColor: CalendarColor) { val calendarId = calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) -// if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { -// -// } -// val courseDateBlock = CourseDateBlock( -// title = "Homework 1: ABCD", -// description = "After this date, course content will be archived", -// date = Date(), -// ) -// calendarManager.addEventsIntoCalendar( -// calendarId = calendarId, -// courseId = "", -// courseName = "", -// courseDateBlock = courseDateBlock -// ) + if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { + corePreferences.calendarId = calendarId + viewModelScope.launch { + calendarNotifier.send(CalendarCreated) + } + } } } From 6a4783d3c4f211e4d823061ac2b1deaeedea1ab4 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 3 Jun 2024 18:52:58 +0300 Subject: [PATCH 09/26] feat: CalendarSettingsView --- .../main/java/org/openedx/app/AppActivity.kt | 9 +- .../app/data/storage/PreferencesManager.kt | 17 +- .../main/java/org/openedx/app/di/AppModule.kt | 2 + .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../core/data/storage/CalendarPreferences.kt | 8 + .../core/data/storage/CorePreferences.kt | 2 - .../openedx/core/domain/model/CalendarData.kt | 6 + .../openedx/core/system/CalendarManager.kt | 31 +++ .../presentation/calendar/CalendarFragment.kt | 15 +- .../calendar/CalendarSettingsView.kt | 211 +++++++++++++++++- .../calendar/CalendarSyncState.kt | 35 +++ .../presentation/calendar/CalendarUIState.kt | 8 +- .../calendar/CalendarViewModel.kt | 31 ++- .../calendar/NewCalendarDialogViewModel.kt | 6 +- profile/src/main/res/values/strings.xml | 12 + 15 files changed, 376 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/CalendarData.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index c0c64279c..6a97a0f44 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -20,6 +20,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.ActivityAppBinding import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.signin.SignInFragment +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.requestApplyInsetsWhenAttached import org.openedx.core.presentation.global.InsetHolder @@ -50,6 +51,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private val viewModel by viewModel() private val whatsNewManager by inject() private val corePreferencesManager by inject() + private val calendarPreferencesManager by inject() private val profileRouter by inject() private val calendarManager by inject() @@ -220,9 +222,10 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { } private fun tryToSyncCalendar() { - val isCalendarCreated = corePreferencesManager.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST - val isCalendarSyncRequired = !Date(corePreferencesManager.lastCalendarSync).isToday() - if (isCalendarCreated && isCalendarSyncRequired) { + val isCalendarCreated = calendarPreferencesManager.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + val isCalendarSyncRequired = !Date(calendarPreferencesManager.lastCalendarSync).isToday() + val isCalendarSyncEnabled = calendarPreferencesManager.isCalendarSyncEnabled + if (isCalendarCreated && isCalendarSyncRequired && isCalendarSyncEnabled) { calendarManager.startSyncCalendarService() } } 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 f67db6d83..46df84c88 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 @@ -4,6 +4,7 @@ import android.content.Context import com.google.gson.Gson import org.openedx.app.BuildConfig import org.openedx.core.data.model.User +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.data.storage.InAppReviewPreferences import org.openedx.core.domain.model.AppConfig @@ -17,7 +18,7 @@ import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.whatsnew.data.storage.WhatsNewPreferences class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences, - WhatsNewPreferences, InAppReviewPreferences, CoursePreferences { + WhatsNewPreferences, InAppReviewPreferences, CoursePreferences, CalendarPreferences { private val sharedPreferences = context.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) @@ -171,6 +172,18 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getBoolean(RESET_APP_DIRECTORY, true) + override var isCalendarSyncEnabled: Boolean + set(value) { + saveBoolean(IS_CALENDAR_SYNC_ENABLED, value) + } + get() = getBoolean(IS_CALENDAR_SYNC_ENABLED, false) + + override var isRelativeDateEnabled: Boolean + set(value) { + saveBoolean(IS_RELATIVE_DATES_ENABLED, value) + } + get() = getBoolean(IS_RELATIVE_DATES_ENABLED, true) + override fun setCalendarSyncEventsDialogShown(courseName: String) { saveBoolean(courseName.replaceSpace("_"), true) } @@ -194,5 +207,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val CALENDAR_ID = "CALENDAR_ID" private const val LAST_CALENDAR_SYNC = "LAST_CALENDAR_SYNC" private const val RESET_APP_DIRECTORY = "reset_app_directory" + private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" + private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED" } } 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 cb34ede7d..1e423204a 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -28,6 +28,7 @@ import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.ImageProcessor import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.data.storage.InAppReviewPreferences import org.openedx.core.module.DownloadWorkerController @@ -78,6 +79,7 @@ val appModule = module { single { get() } single { get() } single { get() } + single { get() } single { ResourceManager(get()) } single { AppCookieManager(get(), get()) } 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 441c95ed0..e2c216654 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -171,7 +171,7 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get()) } + viewModel { CalendarViewModel(get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } single { CourseRepository(get(), get(), get(), get(), get()) } diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt new file mode 100644 index 000000000..75077aac1 --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -0,0 +1,8 @@ +package org.openedx.core.data.storage + +interface CalendarPreferences { + var calendarId: Long + var lastCalendarSync: Long + var isCalendarSyncEnabled: Boolean + var isRelativeDateEnabled: Boolean +} diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index 0f8b16a71..f9cacbd04 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -11,8 +11,6 @@ interface CorePreferences { var user: User? var videoSettings: VideoSettings var appConfig: AppConfig - var calendarId: Long - var lastCalendarSync: Long var canResetAppDirectory: Boolean fun clear() diff --git a/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt b/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt new file mode 100644 index 000000000..91f5b60c9 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt @@ -0,0 +1,6 @@ +package org.openedx.core.domain.model + +data class CalendarData( + val title: String, + val color: Int +) diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 2e3f14851..44469c796 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -12,6 +12,7 @@ import android.provider.CalendarContract import androidx.core.content.ContextCompat import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.CalendarData import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.service.CalendarSyncService import org.openedx.core.utils.Logger @@ -409,6 +410,36 @@ class CalendarManager( context.startService(intent) } + fun getCalendarData(calendarId: Long): CalendarData? { + val projection = arrayOf( + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + CalendarContract.Calendars.CALENDAR_COLOR + ) + val selection = "${CalendarContract.Calendars._ID} = ?" + val selectionArgs = arrayOf(calendarId.toString()) + + val cursor: Cursor? = context.contentResolver.query( + CalendarContract.Calendars.CONTENT_URI, + projection, + selection, + selectionArgs, + null + ) + + return cursor?.use { + if (it.moveToFirst()) { + val title = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME)) + val color = it.getInt(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_COLOR)) + CalendarData( + title = title, + color = color + ) + } else { + null + } + } + } + companion object { const val CALENDAR_DOES_NOT_EXIST = -1L private const val TAG = "CalendarManager" diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index d825726ba..4c2a432c3 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -55,6 +55,12 @@ class CalendarFragment : Fragment() { }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() + }, + onCalendarSyncSwitchClick = { + viewModel.setCalendarSyncEnabled(it) + }, + onRelativeDateSwitchClick = { + viewModel.setRelativeDateEnabled(it) } ) } @@ -67,7 +73,9 @@ private fun CalendarView( windowSize: WindowSize, uiState: CalendarUIState, setUpCalendarSync: () -> Unit, - onBackClick: () -> Unit + onBackClick: () -> Unit, + onCalendarSyncSwitchClick: (Boolean) -> Unit, + onRelativeDateSwitchClick: (Boolean) -> Unit ) { if (!uiState.isCalendarExist) { CalendarSetUpView( @@ -78,7 +86,10 @@ private fun CalendarView( } else { CalendarSettingsView( windowSize = windowSize, - onBackClick = onBackClick + uiState = uiState, + onBackClick = onBackClick, + onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, + onRelativeDateSwitchClick = onRelativeDateSwitchClick ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 4fc6329aa..455b27126 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -2,16 +2,29 @@ package org.openedx.profile.presentation.calendar import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -19,12 +32,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.openedx.core.domain.model.CalendarData +import org.openedx.core.ui.OpenEdXOutlinedButton import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -34,13 +50,18 @@ import org.openedx.core.ui.statusBarsInset import org.openedx.core.ui.theme.OpenEdXTheme 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.ui.windowSizeValue import org.openedx.profile.R +import org.openedx.profile.presentation.ui.SettingsItem @OptIn(ExperimentalComposeUiApi::class) @Composable fun CalendarSettingsView( windowSize: WindowSize, + uiState: CalendarUIState, + onCalendarSyncSwitchClick: (Boolean) -> Unit, + onRelativeDateSwitchClick: (Boolean) -> Unit, onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -108,7 +129,26 @@ fun CalendarSettingsView( Column( modifier = contentWidth.padding(vertical = 28.dp), ) { - //TODO + val coursesSynced = 5 + + if (uiState.calendarData != null) { + CalendarSyncSection( + isCourseCalendarSyncEnabled = uiState.isCalendarSyncEnabled, + calendarData = uiState.calendarData, + calendarSyncState = uiState.calendarSyncState, + onCalendarSyncSwitchClick = onCalendarSyncSwitchClick + ) + } + Spacer(modifier = Modifier.height(16.dp)) + CoursesToSyncSection( + coursesSynced = coursesSynced, + onSyncingCoursesClick = {} + ) + Spacer(modifier = Modifier.height(32.dp)) + OptionsSection( + isRelativeDatesEnabled = uiState.isRelativeDateEnabled, + onRelativeDateSwitchClick = onRelativeDateSwitchClick + ) } } } @@ -116,6 +156,164 @@ fun CalendarSettingsView( } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CalendarSyncSection( + isCourseCalendarSyncEnabled: Boolean, + calendarData: CalendarData, + calendarSyncState: CalendarSyncState, + onCalendarSyncSwitchClick: (Boolean) -> Unit +) { + Column { + SectionTitle(stringResource(id = R.string.profile_calendar_sync)) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.appShapes.cardShape) + .background(MaterialTheme.appColors.cardViewBackground) + .padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier + .size(18.dp) + .clip(CircleShape) + .background(Color(calendarData.color)) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = calendarData.title, + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + Text( + text = stringResource(id = calendarSyncState.title), + style = MaterialTheme.appTypography.labelSmall, + color = MaterialTheme.appColors.textFieldHint + ) + } + Icon( + imageVector = calendarSyncState.icon, + tint = calendarSyncState.tint, + contentDescription = null + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.profile_course_calendar_sync), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Switch( + modifier = Modifier + .padding(0.dp), + checked = isCourseCalendarSyncEnabled, + onCheckedChange = onCalendarSyncSwitchClick + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.profile_currently_syncing_events), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + Spacer(modifier = Modifier.height(16.dp)) + SyncOptionsButton() + } +} + +@Composable +fun SyncOptionsButton() { + OpenEdXOutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.profile_change_sync_options), + backgroundColor = MaterialTheme.appColors.background, + borderColor = MaterialTheme.appColors.primaryButtonBackground, + textColor = MaterialTheme.appColors.primaryButtonBackground, + onClick = { + // TODO + } + ) +} + +@Composable +fun CoursesToSyncSection( + coursesSynced: Int, + onSyncingCoursesClick: () -> Unit +) { + Column { + SectionTitle(stringResource(R.string.profile_courses_to_sync)) + Spacer(modifier = Modifier.height(8.dp)) + Card( + modifier = Modifier, + shape = MaterialTheme.appShapes.cardShape, + elevation = 0.dp, + backgroundColor = MaterialTheme.appColors.cardViewBackground + ) { + SettingsItem( + text = stringResource(R.string.profile_syncing_courses, coursesSynced), + onClick = onSyncingCoursesClick + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun OptionsSection( + isRelativeDatesEnabled: Boolean, + onRelativeDateSwitchClick: (Boolean) -> Unit +) { + Column { + SectionTitle(stringResource(R.string.profile_options)) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.profile_use_relative_dates), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Switch( + modifier = Modifier + .padding(0.dp), + checked = isRelativeDatesEnabled, + onCheckedChange = onRelativeDateSwitchClick + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.profile_show_relative_dates), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + } +} + +@Composable +fun SectionTitle(title: String) { + Text( + text = title, + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textSecondary + ) +} + @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -123,7 +321,16 @@ private fun CalendarSettingsViewPreview() { OpenEdXTheme { CalendarSettingsView( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - onBackClick = {} + uiState = CalendarUIState( + isCalendarExist = true, + calendarData = null, + calendarSyncState = CalendarSyncState.SYNCHRONIZATION, + isCalendarSyncEnabled = false, + isRelativeDateEnabled = true + ), + onBackClick = {}, + onCalendarSyncSwitchClick = {}, + onRelativeDateSwitchClick = {} ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt new file mode 100644 index 000000000..e4200f7d8 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt @@ -0,0 +1,35 @@ +package org.openedx.profile.presentation.calendar + +import androidx.annotation.StringRes +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudSync +import androidx.compose.material.icons.filled.SyncDisabled +import androidx.compose.material.icons.rounded.EventRepeat +import androidx.compose.material.icons.rounded.FreeCancellation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import org.openedx.core.ui.theme.appColors +import org.openedx.profile.R + +enum class CalendarSyncState( + @StringRes val title: Int, + val icon: ImageVector +) { + OFFLINE(R.string.profile_offline, Icons.Default.SyncDisabled), + SYNC_FAILED(R.string.profile_syncing_failed, Icons.Rounded.FreeCancellation), + SYNCED(R.string.profile_synced, Icons.Rounded.EventRepeat), + SYNCHRONIZATION(R.string.profile_synchronization, Icons.Default.CloudSync); + + val tint: Color + @Composable + @ReadOnlyComposable + get() = when (this) { + OFFLINE -> MaterialTheme.appColors.textFieldHint + SYNC_FAILED -> MaterialTheme.appColors.error + SYNCED -> MaterialTheme.appColors.accessGreen + SYNCHRONIZATION -> MaterialTheme.appColors.primary + } +} \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index fea208473..1f93c4968 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -1,5 +1,11 @@ package org.openedx.profile.presentation.calendar +import org.openedx.core.domain.model.CalendarData + data class CalendarUIState( - val isCalendarExist: Boolean + val isCalendarExist: Boolean, + val calendarData: CalendarData? = null, + val calendarSyncState: CalendarSyncState, + val isCalendarSyncEnabled: Boolean, + val isRelativeDateEnabled: Boolean, ) 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 24b2b4f37..5e51092b7 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 @@ -8,18 +8,24 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel -import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.system.CalendarManager +import org.openedx.core.system.connection.NetworkConnection class CalendarViewModel( private val calendarManager: CalendarManager, - private val corePreferences: CorePreferences, - private val calendarNotifier: CalendarNotifier + private val calendarPreferences: CalendarPreferences, + private val calendarNotifier: CalendarNotifier, + private val networkConnection: NetworkConnection ) : BaseViewModel() { private val _uiState = MutableStateFlow( CalendarUIState( - isCalendarExist = corePreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + isCalendarExist = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST, + calendarData = null, + calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCHRONIZATION else CalendarSyncState.OFFLINE, + isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, + isRelativeDateEnabled = calendarPreferences.isRelativeDateEnabled ) ) val uiState: StateFlow @@ -36,9 +42,26 @@ class CalendarViewModel( } } } + + getCalendarData() } fun setUpCalendarSync(permissionLauncher: ActivityResultLauncher>) { permissionLauncher.launch(calendarManager.permissions) } + + fun setCalendarSyncEnabled(isEnabled: Boolean) { + calendarPreferences.isCalendarSyncEnabled = isEnabled + _uiState.update { it.copy(isCalendarSyncEnabled = isEnabled) } + } + + fun setRelativeDateEnabled(isEnabled: Boolean) { + calendarPreferences.isRelativeDateEnabled = isEnabled + _uiState.update { it.copy(isRelativeDateEnabled = isEnabled) } + } + + private fun getCalendarData() { + val calendarData = calendarManager.getCalendarData(calendarId = calendarPreferences.calendarId) + _uiState.update { it.copy(calendarData = calendarData) } + } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 881a0cce7..46d5b2fb9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -4,14 +4,14 @@ import android.content.Context import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel -import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.extension.toastMessage import org.openedx.core.system.CalendarManager import org.openedx.core.R as coreR class NewCalendarDialogViewModel( private val calendarManager: CalendarManager, - private val corePreferences: CorePreferences, + private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier ) : BaseViewModel() { @@ -22,7 +22,7 @@ class NewCalendarDialogViewModel( ) { val calendarId = calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { - corePreferences.calendarId = calendarId + calendarPreferences.calendarId = calendarId viewModelScope.launch { calendarNotifier.send(CalendarCreated) } diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 60f0e4060..c04392eb4 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -60,5 +60,17 @@ Accent Course Dates Color + Course Calendar Sync + Currently syncing events to your calendar + Change Sync Options + Courses to Sync + Syncing %1$s Courses + Options + Use relative dates + Show relative dates like “Tomorrow” and “Yesterday” + Offline + Sync Failed + Synced + Synchronization From afbab8eb16b8293c5495eb4cb4030d353b87803b Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 3 Jun 2024 21:09:45 +0300 Subject: [PATCH 10/26] feat: Removed calendar logic from course home, edit calendar dialog --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/org/openedx/app/AppActivity.kt | 5 +- .../main/java/org/openedx/app/di/AppModule.kt | 4 +- .../java/org/openedx/app/di/ScreenModule.kt | 4 +- .../core/CalendarSyncServiceInitiator.kt | 12 ++ .../openedx/core/system/CalendarManager.kt | 11 +- .../CalendarSyncService.kt | 3 +- .../container/CourseContainerFragment.kt | 73 ----------- .../container/CourseContainerViewModel.kt | 113 ------------------ .../presentation/dates/CourseDatesScreen.kt | 26 +--- .../dates/CourseDatesViewModel.kt | 42 ------- .../container/CourseContainerViewModelTest.kt | 10 -- .../dates/CourseDatesViewModelTest.kt | 8 -- .../presentation/calendar/CalendarCreated.kt | 2 + .../presentation/calendar/CalendarEvent.kt | 3 - .../presentation/calendar/CalendarFragment.kt | 13 +- .../calendar/CalendarSettingsView.kt | 39 ++++-- .../calendar/CalendarSyncState.kt | 2 +- .../calendar/CalendarViewModel.kt | 7 +- .../calendar/NewCalendarDialogFragment.kt | 49 ++++++-- .../calendar/NewCalendarDialogViewModel.kt | 8 +- .../profile/system/notifier/CalendarEvent.kt | 3 + .../notifier}/CalendarNotifier.kt | 2 +- profile/src/main/res/values/strings.xml | 2 +- 24 files changed, 119 insertions(+), 324 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt rename core/src/main/java/org/openedx/core/{service => system}/CalendarSyncService.kt (90%) delete mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt rename profile/src/main/java/org/openedx/profile/{presentation/calendar => system/notifier}/CalendarNotifier.kt (88%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6e830e72..eae943206 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,7 +115,7 @@ diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 6a97a0f44..60d1a27a2 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -20,6 +20,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.ActivityAppBinding import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.signin.SignInFragment +import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.requestApplyInsetsWhenAttached @@ -53,7 +54,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private val corePreferencesManager by inject() private val calendarPreferencesManager by inject() private val profileRouter by inject() - private val calendarManager by inject() + private val calendarSyncServiceInitiator by inject() private val branchLogger = Logger(BRANCH_TAG) @@ -226,7 +227,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { val isCalendarSyncRequired = !Date(calendarPreferencesManager.lastCalendarSync).isToday() val isCalendarSyncEnabled = calendarPreferencesManager.isCalendarSyncEnabled if (isCalendarCreated && isCalendarSyncRequired && isCalendarSyncEnabled) { - calendarManager.startSyncCalendarService() + calendarSyncServiceInitiator.startSyncCalendarService() } } 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 1e423204a..39a9c1830 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -25,6 +25,7 @@ import org.openedx.auth.presentation.sso.FacebookAuthHelper import org.openedx.auth.presentation.sso.GoogleAuthHelper import org.openedx.auth.presentation.sso.MicrosoftAuthHelper import org.openedx.auth.presentation.sso.OAuthHelper +import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.ImageProcessor import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments @@ -63,7 +64,7 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.presentation.calendar.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.profile.system.notifier.ProfileNotifier import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter @@ -85,6 +86,7 @@ val appModule = module { single { AppCookieManager(get(), get()) } single { ReviewManagerFactory.create(get()) } single { CalendarManager(get(), get(), get()) } + single { CalendarSyncServiceInitiator(get()) } single { ImageProcessor(get()) } 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 e2c216654..95411e2fd 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -171,7 +171,7 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get(), get()) } + viewModel { CalendarViewModel(get(), get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } single { CourseRepository(get(), get(), get(), get(), get()) } @@ -217,7 +217,6 @@ val screenModule = module { get(), get(), get(), - get(), get() ) } @@ -316,7 +315,6 @@ val screenModule = module { get(), get(), get(), - get(), get() ) } diff --git a/core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt b/core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt new file mode 100644 index 000000000..c141e615b --- /dev/null +++ b/core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt @@ -0,0 +1,12 @@ +package org.openedx.core + +import android.content.Context +import android.content.Intent +import org.openedx.core.system.CalendarSyncService + +class CalendarSyncServiceInitiator(private val context: Context) { + fun startSyncCalendarService() { + val intent = Intent(context, CalendarSyncService::class.java) + context.startService(intent) + } +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 44469c796..460ee8db6 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -14,7 +14,6 @@ import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CalendarData import org.openedx.core.domain.model.CourseDateBlock -import org.openedx.core.service.CalendarSyncService import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar import java.util.Calendar @@ -58,13 +57,10 @@ class CalendarManager( * Create or update the calendar if it is already existed in mobile calendar app */ fun createOrUpdateCalendar( + calendarId: Long = CALENDAR_DOES_NOT_EXIST, calendarTitle: String, calendarColor: Long ): Long { - val calendarId = getCalendarId( - calendarTitle = calendarTitle - ) - if (calendarId != CALENDAR_DOES_NOT_EXIST) { deleteCalendar(calendarId = calendarId) } @@ -405,11 +401,6 @@ class CalendarManager( return "${resourceManager.getString(id = CoreR.string.platform_name)} - $courseName" } - fun startSyncCalendarService() { - val intent = Intent(context, CalendarSyncService::class.java) - context.startService(intent) - } - fun getCalendarData(calendarId: Long): CalendarData? { val projection = arrayOf( CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, diff --git a/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt b/core/src/main/java/org/openedx/core/system/CalendarSyncService.kt similarity index 90% rename from core/src/main/java/org/openedx/core/service/CalendarSyncService.kt rename to core/src/main/java/org/openedx/core/system/CalendarSyncService.kt index 6570a3420..599cecd7d 100644 --- a/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarSyncService.kt @@ -1,4 +1,4 @@ -package org.openedx.core.service +package org.openedx.core.system import android.app.Service import android.content.Intent @@ -8,7 +8,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject -import org.openedx.core.system.CalendarManager class CalendarSyncService : Service() { 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 a55ca6bc9..4a8f026ea 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 @@ -55,7 +55,6 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.global.viewBinding -import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialog import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog @@ -169,84 +168,12 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { OpenEdXTheme { val syncState by viewModel.calendarSyncUIState.collectAsState() - LaunchedEffect(key1 = syncState.checkForOutOfSync) { - if (syncState.isCalendarSyncEnabled && syncState.checkForOutOfSync.get()) { - viewModel.checkIfCalendarOutOfDate() - } - } - LaunchedEffect(syncState.uiMessage.get()) { syncState.uiMessage.get().takeIfNotEmpty()?.let { Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show() syncState.uiMessage.set("") } } - - CalendarSyncDialog( - syncDialogType = syncState.dialogType, - calendarTitle = syncState.calendarTitle, - syncDialogPosAction = { dialog -> - when (dialog) { - CalendarSyncDialogType.SYNC_DIALOG -> { - viewModel.logCalendarAddDates(true) - viewModel.addOrUpdateEventsInCalendar( - updatedEvent = false, - ) - } - - CalendarSyncDialogType.UN_SYNC_DIALOG -> { - viewModel.logCalendarRemoveDates(true) - viewModel.deleteCourseCalendar() - } - - CalendarSyncDialogType.PERMISSION_DIALOG -> { - permissionLauncher.launch(viewModel.calendarPermissions) - } - - CalendarSyncDialogType.OUT_OF_SYNC_DIALOG -> { - viewModel.logCalendarSyncUpdate(true) - viewModel.addOrUpdateEventsInCalendar( - updatedEvent = true, - ) - } - - CalendarSyncDialogType.EVENTS_DIALOG -> { - viewModel.logCalendarSyncedConfirmation(true) - viewModel.openCalendarApp() - } - - else -> {} - } - }, - syncDialogNegAction = { dialog -> - when (dialog) { - CalendarSyncDialogType.SYNC_DIALOG -> - viewModel.logCalendarAddDates(false) - - CalendarSyncDialogType.UN_SYNC_DIALOG -> - viewModel.logCalendarRemoveDates(false) - - CalendarSyncDialogType.OUT_OF_SYNC_DIALOG -> { - viewModel.logCalendarSyncUpdate(false) - viewModel.deleteCourseCalendar() - } - - CalendarSyncDialogType.EVENTS_DIALOG -> - viewModel.logCalendarSyncedConfirmation(false) - - CalendarSyncDialogType.LOADING_DIALOG, - CalendarSyncDialogType.PERMISSION_DIALOG, - CalendarSyncDialogType.NONE, - -> { - } - } - - viewModel.setCalendarSyncDialogType(CalendarSyncDialogType.NONE) - }, - dismissSyncDialog = { - viewModel.setCalendarSyncDialogType(CalendarSyncDialogType.NONE) - } - ) } } } 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 8961817d7..fb6b80bfb 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 @@ -7,7 +7,6 @@ import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,10 +26,8 @@ import org.openedx.core.exception.NoCachedDataException import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState -import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseCompletionSet import org.openedx.core.system.notifier.CourseDatesShifted @@ -40,7 +37,6 @@ import org.openedx.core.system.notifier.CourseOpenBlock import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.RefreshDates import org.openedx.core.system.notifier.RefreshDiscussions -import org.openedx.core.utils.TimeUtils import org.openedx.course.DatesShiftedSnackBar import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.domain.interactor.CourseInteractor @@ -61,7 +57,6 @@ class CourseContainerViewModel( private val enrollmentMode: String, private val config: Config, private val interactor: CourseInteractor, - private val calendarManager: CalendarManager, private val resourceManager: ResourceManager, private val courseNotifier: CourseNotifier, private val networkConnection: NetworkConnection, @@ -104,13 +99,9 @@ class CourseContainerViewModel( val organization: String get() = _organization - val calendarPermissions: Array - get() = calendarManager.permissions - private val _calendarSyncUIState = MutableStateFlow( CalendarSyncUIState( isCalendarSyncEnabled = isCalendarSyncEnabled(), - calendarTitle = calendarManager.getCourseCalendarTitle(courseName), courseDates = emptyList(), dialogType = CalendarSyncDialogType.NONE, checkForOutOfSync = AtomicReference(false), @@ -278,116 +269,12 @@ class CourseContainerViewModel( } } - fun addOrUpdateEventsInCalendar( - updatedEvent: Boolean, - ) { - setCalendarSyncDialogType(CalendarSyncDialogType.LOADING_DIALOG) - - val startSyncTime = TimeUtils.getCurrentTime() - val calendarId = getCalendarId() - - if (calendarId == CalendarManager.CALENDAR_DOES_NOT_EXIST) { - setUiMessage(CoreR.string.core_snackbar_course_calendar_error) - setCalendarSyncDialogType(CalendarSyncDialogType.NONE) - - return - } - - viewModelScope.launch(Dispatchers.IO) { - val courseDates = _calendarSyncUIState.value.courseDates - if (courseDates.isNotEmpty()) { - courseDates.forEach { courseDateBlock -> - calendarManager.addEventsIntoCalendar( - calendarId = calendarId, - courseId = courseId, - courseName = courseName, - courseDateBlock = courseDateBlock - ) - } - } - val elapsedSyncTime = TimeUtils.getCurrentTime() - startSyncTime - val delayRemaining = maxOf(0, 1000 - elapsedSyncTime) - - // Ensure minimum 1s delay to prevent flicker for rapid event creation - if (delayRemaining > 0) { - delay(delayRemaining) - } - - setCalendarSyncDialogType(CalendarSyncDialogType.NONE) - updateCalendarSyncState() - - if (updatedEvent) { - logCalendarSyncSnackbar(CalendarSyncSnackbar.UPDATED) - setUiMessage(CoreR.string.core_snackbar_course_calendar_updated) - } else if (coursePreferences.isCalendarSyncEventsDialogShown(courseName)) { - logCalendarSyncSnackbar(CalendarSyncSnackbar.ADDED) - setUiMessage(CoreR.string.core_snackbar_course_calendar_added) - } else { - coursePreferences.setCalendarSyncEventsDialogShown(courseName) - setCalendarSyncDialogType(CalendarSyncDialogType.EVENTS_DIALOG) - } - } - } - - private fun updateCalendarSyncState() { - viewModelScope.launch { - val isCalendarSynced = calendarManager.isCalendarExists( - calendarTitle = _calendarSyncUIState.value.calendarTitle - ) - courseNotifier.send(CheckCalendarSyncEvent(isSynced = isCalendarSynced)) - } - } - - fun checkIfCalendarOutOfDate() { - val courseDates = _calendarSyncUIState.value.courseDates - if (courseDates.isNotEmpty()) { - _calendarSyncUIState.value.checkForOutOfSync.set(false) - val outdatedCalendarId = calendarManager.isCalendarOutOfDate( - calendarTitle = _calendarSyncUIState.value.calendarTitle, - courseDateBlocks = courseDates - ) - if (outdatedCalendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { - setCalendarSyncDialogType(CalendarSyncDialogType.OUT_OF_SYNC_DIALOG) - } - } - } - - fun deleteCourseCalendar() { - if (calendarManager.hasPermissions()) { - viewModelScope.launch(Dispatchers.IO) { - val calendarId = getCalendarId() - if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { - calendarManager.deleteCalendar( - calendarId = calendarId, - ) - } - updateCalendarSyncState() - - } - logCalendarSyncSnackbar(CalendarSyncSnackbar.REMOVED) - setUiMessage(CoreR.string.core_snackbar_course_calendar_removed) - } - } - - fun openCalendarApp() { - calendarManager.openCalendarApp() - } - private fun setUiMessage(@StringRes stringResId: Int) { _calendarSyncUIState.update { it.copy(uiMessage = AtomicReference(resourceManager.getString(stringResId))) } } - private fun getCalendarId(): Long { -// return calendarManager.createOrUpdateCalendar( -// calendarTitle = _calendarSyncUIState.value.calendarTitle -// ) - return calendarManager.getCalendarId( - calendarTitle = _calendarSyncUIState.value.calendarTitle - ) - } - private fun isCalendarSyncEnabled(): Boolean { val calendarSync = corePreferences.appConfig.courseDatesCalendarSync return calendarSync.isEnabled && ((calendarSync.isSelfPacedEnabled && isSelfPaced) || 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 9672c8d82..dda18519a 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 @@ -86,7 +86,6 @@ import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.core.utils.clearTime import org.openedx.course.R -import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.CourseDatesBanner import org.openedx.course.presentation.ui.CourseDatesBannerTablet import java.util.concurrent.atomic.AtomicReference @@ -102,7 +101,6 @@ fun CourseDatesScreen( ) { val uiState by viewModel.uiState.observeAsState(DatesUIState.Loading) val uiMessage by viewModel.uiMessage.collectAsState(null) - val calendarSyncUIState by viewModel.calendarSyncUIState.collectAsState() val context = LocalContext.current CourseDatesUI( @@ -110,7 +108,6 @@ fun CourseDatesScreen( uiState = uiState, uiMessage = uiMessage, isSelfPaced = viewModel.isSelfPaced, - calendarSyncUIState = calendarSyncUIState, onItemClick = { block -> if (block.blockId.isNotEmpty()) { viewModel.getVerticalBlock(block.blockId) @@ -167,10 +164,7 @@ fun CourseDatesScreen( updateCourseStructure() } } - }, - onCalendarSyncSwitch = { isChecked -> - viewModel.handleCalendarSyncState(isChecked) - }, + } ) } @@ -180,11 +174,9 @@ private fun CourseDatesUI( uiState: DatesUIState, uiMessage: UIMessage?, isSelfPaced: Boolean, - calendarSyncUIState: CalendarSyncUIState, onItemClick: (CourseDateBlock) -> Unit, onPLSBannerViewed: () -> Unit, - onSyncDates: () -> Unit, - onCalendarSyncSwitch: (Boolean) -> Unit = {}, + onSyncDates: () -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -238,16 +230,6 @@ private fun CourseDatesUI( val courseBanner = uiState.courseDatesResult.courseBanner val datesSection = uiState.courseDatesResult.datesSection - if (calendarSyncUIState.isCalendarSyncEnabled) { - item { - CalendarSyncCard( - modifier = Modifier.padding(top = 24.dp), - checked = calendarSyncUIState.isSynced, - onCalendarSync = onCalendarSyncSwitch - ) - } - } - if (courseBanner.isBannerAvailableForUserType(isSelfPaced)) { item { onPLSBannerViewed() @@ -643,11 +625,9 @@ private fun CourseDatesScreenPreview() { uiState = DatesUIState.Dates(CourseDatesResult(mockedResponse, mockedCourseBannerInfo)), uiMessage = null, isSelfPaced = true, - calendarSyncUIState = mockCalendarSyncUIState, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, - onCalendarSyncSwitch = {}, ) } } @@ -662,11 +642,9 @@ private fun CourseDatesScreenTabletPreview() { uiState = DatesUIState.Dates(CourseDatesResult(mockedResponse, mockedCourseBannerInfo)), uiMessage = null, isSelfPaced = true, - calendarSyncUIState = mockCalendarSyncUIState, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, - onCalendarSyncSwitch = {}, ) } } 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 a6a78cb72..8485fcb5a 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 @@ -4,12 +4,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R @@ -24,10 +20,7 @@ import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType -import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState -import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseDatesShifted import org.openedx.core.system.notifier.CourseLoading @@ -46,7 +39,6 @@ class CourseDatesViewModel( private val enrollmentMode: String, private val courseNotifier: CourseNotifier, private val interactor: CourseInteractor, - private val calendarManager: CalendarManager, private val resourceManager: ResourceManager, private val corePreferences: CorePreferences, private val courseAnalytics: CourseAnalytics, @@ -64,16 +56,6 @@ class CourseDatesViewModel( val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() - private val _calendarSyncUIState = MutableStateFlow( - CalendarSyncUIState( - isCalendarSyncEnabled = isCalendarSyncEnabled(), - calendarTitle = calendarManager.getCourseCalendarTitle(courseTitle), - isSynced = false, - ) - ) - val calendarSyncUIState: StateFlow = - _calendarSyncUIState.asStateFlow() - private var courseBannerType: CourseBannerType = CourseBannerType.BLANK private var courseStructure: CourseStructure? = null @@ -83,10 +65,6 @@ class CourseDatesViewModel( viewModelScope.launch { courseNotifier.notifier.collect { event -> when (event) { - is CheckCalendarSyncEvent -> { - _calendarSyncUIState.update { it.copy(isSynced = event.isSynced) } - } - is RefreshDates -> { loadingCourseDatesInternal() } @@ -95,7 +73,6 @@ class CourseDatesViewModel( } loadingCourseDatesInternal() - updateAndFetchCalendarSyncState() } private fun loadingCourseDatesInternal() { @@ -159,25 +136,6 @@ class CourseDatesViewModel( } } - fun handleCalendarSyncState(isChecked: Boolean) { - logCalendarSyncToggle(isChecked) - setCalendarSyncDialogType( - when { - isChecked && calendarManager.hasPermissions() -> CalendarSyncDialogType.SYNC_DIALOG - isChecked -> CalendarSyncDialogType.PERMISSION_DIALOG - else -> CalendarSyncDialogType.UN_SYNC_DIALOG - } - ) - } - - private fun updateAndFetchCalendarSyncState(): Boolean { - val isCalendarSynced = calendarManager.isCalendarExists( - calendarTitle = _calendarSyncUIState.value.calendarTitle - ) - _calendarSyncUIState.update { it.copy(isSynced = isCalendarSynced) } - return isCalendarSynced - } - private fun setCalendarSyncDialogType(dialog: CalendarSyncDialogType) { val value = _uiState.value if (value is DatesUIState.Dates) { 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 c20bb07be..52d6c6cd3 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 @@ -33,7 +33,6 @@ import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.CourseDatesCalendarSync import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess -import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier @@ -57,7 +56,6 @@ class CourseContainerViewModelTest { private val resourceManager = mockk() private val config = mockk() private val interactor = mockk() - private val calendarManager = mockk() private val networkConnection = mockk() private val notifier = spyk() private val analytics = mockk() @@ -137,7 +135,6 @@ class CourseContainerViewModelTest { every { corePreferences.user } returns user every { corePreferences.appConfig } returns appConfig every { notifier.notifier } returns emptyFlow() - every { calendarManager.getCourseCalendarTitle(any()) } returns calendarTitle every { config.getApiHostURL() } returns "baseUrl" every { imageProcessor.loadImage(any(), any(), any()) } returns Unit every { imageProcessor.applyBlur(any(), any()) } returns mockBitmap @@ -157,7 +154,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -191,7 +187,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -225,7 +220,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -258,7 +252,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -294,7 +287,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -325,7 +317,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, @@ -356,7 +347,6 @@ class CourseContainerViewModelTest { "", config, interactor, - calendarManager, resourceManager, notifier, networkConnection, diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index 5e2ed50a4..f772c85b1 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -37,7 +37,6 @@ import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.domain.model.DatesSection -import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseLoading @@ -58,7 +57,6 @@ class CourseDatesViewModelTest { private val resourceManager = mockk() private val notifier = mockk() private val interactor = mockk() - private val calendarManager = mockk() private val corePreferences = mockk() private val analytics = mockk() private val config = mockk() @@ -148,8 +146,6 @@ class CourseDatesViewModelTest { every { corePreferences.user } returns user every { corePreferences.appConfig } returns appConfig every { notifier.notifier } returns flowOf(CourseLoading(false)) - every { calendarManager.getCourseCalendarTitle(any()) } returns calendarTitle - every { calendarManager.isCalendarExists(any()) } returns true coEvery { notifier.send(any()) } returns Unit coEvery { notifier.send(any()) } returns Unit coEvery { notifier.send(any()) } returns Unit @@ -168,7 +164,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - calendarManager, resourceManager, corePreferences, analytics, @@ -197,7 +192,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - calendarManager, resourceManager, corePreferences, analytics, @@ -226,7 +220,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - calendarManager, resourceManager, corePreferences, analytics, @@ -255,7 +248,6 @@ class CourseDatesViewModelTest { "", notifier, interactor, - calendarManager, resourceManager, corePreferences, analytics, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt index 06a3c03fa..becb787b7 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt @@ -1,3 +1,5 @@ package org.openedx.profile.presentation.calendar +import org.openedx.profile.system.notifier.CalendarEvent + object CalendarCreated : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt deleted file mode 100644 index 6073d86aa..000000000 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.presentation.calendar - -interface CalendarEvent \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index 4c2a432c3..6703452af 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -21,7 +21,7 @@ class CalendarFragment : Fragment() { ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (!isGranted.containsValue(false)) { - val dialog = NewCalendarDialogFragment.newInstance() + val dialog = NewCalendarDialogFragment.newInstance(DialogType.CREATE_NEW) dialog.show( requireActivity().supportFragmentManager, NewCalendarDialogFragment.DIALOG_TAG @@ -61,6 +61,13 @@ class CalendarFragment : Fragment() { }, onRelativeDateSwitchClick = { viewModel.setRelativeDateEnabled(it) + }, + onChangeSyncOptionClick = { + val dialog = NewCalendarDialogFragment.newInstance(DialogType.UPDATE) + dialog.show( + requireActivity().supportFragmentManager, + NewCalendarDialogFragment.DIALOG_TAG + ) } ) } @@ -74,6 +81,7 @@ private fun CalendarView( uiState: CalendarUIState, setUpCalendarSync: () -> Unit, onBackClick: () -> Unit, + onChangeSyncOptionClick: () -> Unit, onCalendarSyncSwitchClick: (Boolean) -> Unit, onRelativeDateSwitchClick: (Boolean) -> Unit ) { @@ -89,7 +97,8 @@ private fun CalendarView( uiState = uiState, onBackClick = onBackClick, onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, - onRelativeDateSwitchClick = onRelativeDateSwitchClick + onRelativeDateSwitchClick = onRelativeDateSwitchClick, + onChangeSyncOptionClick = onChangeSyncOptionClick ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 455b27126..8cc305497 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement @@ -62,6 +63,7 @@ fun CalendarSettingsView( uiState: CalendarUIState, onCalendarSyncSwitchClick: (Boolean) -> Unit, onRelativeDateSwitchClick: (Boolean) -> Unit, + onChangeSyncOptionClick: () -> Unit, onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -136,7 +138,8 @@ fun CalendarSettingsView( isCourseCalendarSyncEnabled = uiState.isCalendarSyncEnabled, calendarData = uiState.calendarData, calendarSyncState = uiState.calendarSyncState, - onCalendarSyncSwitchClick = onCalendarSyncSwitchClick + onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, + onChangeSyncOptionClick = onChangeSyncOptionClick ) } Spacer(modifier = Modifier.height(16.dp)) @@ -162,7 +165,8 @@ fun CalendarSyncSection( isCourseCalendarSyncEnabled: Boolean, calendarData: CalendarData, calendarSyncState: CalendarSyncState, - onCalendarSyncSwitchClick: (Boolean) -> Unit + onCalendarSyncSwitchClick: (Boolean) -> Unit, + onChangeSyncOptionClick: () -> Unit ) { Column { SectionTitle(stringResource(id = R.string.profile_calendar_sync)) @@ -197,11 +201,19 @@ fun CalendarSyncSection( color = MaterialTheme.appColors.textFieldHint ) } - Icon( - imageVector = calendarSyncState.icon, - tint = calendarSyncState.tint, - contentDescription = null - ) + if (calendarSyncState == CalendarSyncState.SYNCHRONIZATION) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.appColors.primary + ) + } else { + Icon( + imageVector = calendarSyncState.icon, + tint = calendarSyncState.tint, + contentDescription = null + ) + } } Spacer(modifier = Modifier.height(16.dp)) Row( @@ -229,12 +241,16 @@ fun CalendarSyncSection( color = MaterialTheme.appColors.textPrimaryVariant ) Spacer(modifier = Modifier.height(16.dp)) - SyncOptionsButton() + SyncOptionsButton( + onChangeSyncOptionClick = onChangeSyncOptionClick + ) } } @Composable -fun SyncOptionsButton() { +fun SyncOptionsButton( + onChangeSyncOptionClick: () -> Unit +) { OpenEdXOutlinedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.profile_change_sync_options), @@ -242,7 +258,7 @@ fun SyncOptionsButton() { borderColor = MaterialTheme.appColors.primaryButtonBackground, textColor = MaterialTheme.appColors.primaryButtonBackground, onClick = { - // TODO + onChangeSyncOptionClick() } ) } @@ -330,7 +346,8 @@ private fun CalendarSettingsViewPreview() { ), onBackClick = {}, onCalendarSyncSwitchClick = {}, - onRelativeDateSwitchClick = {} + onRelativeDateSwitchClick = {}, + onChangeSyncOptionClick = {} ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt index e4200f7d8..bb2e8102f 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt @@ -21,7 +21,7 @@ enum class CalendarSyncState( OFFLINE(R.string.profile_offline, Icons.Default.SyncDisabled), SYNC_FAILED(R.string.profile_syncing_failed, Icons.Rounded.FreeCancellation), SYNCED(R.string.profile_synced, Icons.Rounded.EventRepeat), - SYNCHRONIZATION(R.string.profile_synchronization, Icons.Default.CloudSync); + SYNCHRONIZATION(R.string.profile_syncing_to_calendar, Icons.Default.CloudSync); val tint: Color @Composable 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 5e51092b7..cbe0a1081 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 @@ -8,11 +8,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.system.CalendarManager import org.openedx.core.system.connection.NetworkConnection +import org.openedx.profile.system.notifier.CalendarNotifier class CalendarViewModel( + private val calendarSyncServiceInitiator: CalendarSyncServiceInitiator, private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, @@ -36,8 +39,9 @@ class CalendarViewModel( calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { CalendarCreated -> { - calendarManager.startSyncCalendarService() + calendarSyncServiceInitiator.startSyncCalendarService() _uiState.update { it.copy(isCalendarExist = true) } + getCalendarData() } } } @@ -53,6 +57,7 @@ class CalendarViewModel( fun setCalendarSyncEnabled(isEnabled: Boolean) { calendarPreferences.isCalendarSyncEnabled = isEnabled _uiState.update { it.copy(isCalendarSyncEnabled = isEnabled) } + calendarSyncServiceInitiator.startSyncCalendarService() } fun setRelativeDateEnabled(isEnabled: Boolean) { 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 3d8a0d119..5de80e8af 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 @@ -5,6 +5,7 @@ import android.content.res.Configuration import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.animation.core.animateFloatAsState @@ -58,8 +59,11 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment +import kotlinx.parcelize.Parcelize import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.extension.parcelable import org.openedx.core.presentation.dialog.DefaultDialogBox import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.OpenEdXOutlinedButton @@ -86,11 +90,12 @@ class NewCalendarDialogFragment : DialogFragment() { setContent { OpenEdXTheme { NewCalendarDialog( + dialogType = requireArguments().parcelable(ARG_DIALOG_TYPE) ?: DialogType.CREATE_NEW, onCancelClick = { dismiss() }, - onBeginSyncingClick = { calendarName, calendarColor -> - viewModel.createCalendar(requireContext(), calendarName, calendarColor) + onBeginSyncingClick = { calendarTitle, calendarColor -> + viewModel.createCalendar(requireContext(), calendarTitle, calendarColor) dismiss() } ) @@ -100,25 +105,42 @@ class NewCalendarDialogFragment : DialogFragment() { companion object { const val DIALOG_TAG = "NewCalendarDialogFragment" + const val ARG_DIALOG_TYPE = "ARG_DIALOG_TYPE" - fun newInstance(): NewCalendarDialogFragment { - return NewCalendarDialogFragment() + fun newInstance( + dialogType: DialogType + ): NewCalendarDialogFragment { + val fragment = NewCalendarDialogFragment() + fragment.arguments = bundleOf( + ARG_DIALOG_TYPE to dialogType + ) + return fragment } - fun getDefaultCalendarName(context: Context): String { + fun getDefaultCalendarTitle(context: Context): String { return "${context.getString(CoreR.string.app_name)} ${context.getString(R.string.profile_course_dates)}" } } } +@Parcelize +enum class DialogType : Parcelable { + CREATE_NEW, UPDATE +} + @Composable private fun NewCalendarDialog( modifier: Modifier = Modifier, + dialogType: DialogType, onCancelClick: () -> Unit, - onBeginSyncingClick: (calendarName: String, calendarColor: CalendarColor) -> Unit + onBeginSyncingClick: (calendarTitle: String, calendarColor: CalendarColor) -> Unit ) { val context = LocalContext.current - var calendarName by rememberSaveable { + val title = when (dialogType) { + DialogType.CREATE_NEW -> stringResource(id = R.string.profile_new_calendar) + DialogType.UPDATE -> stringResource(id = R.string.profile_change_sync_options) + } + var calendarTitle by rememberSaveable { mutableStateOf("") } var calendarColor by rememberSaveable { @@ -140,7 +162,7 @@ private fun NewCalendarDialog( ) { Text( modifier = Modifier.weight(1f), - text = stringResource(id = R.string.profile_new_calendar), + text = title, color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.titleLarge ) @@ -155,9 +177,9 @@ private fun NewCalendarDialog( tint = MaterialTheme.appColors.primary ) } - CalendarNameTextField( + CalendarTitleTextField( onValueChanged = { - calendarName = it + calendarTitle = it } ) ColorDropdown( @@ -187,7 +209,7 @@ private fun NewCalendarDialog( text = stringResource(id = R.string.profile_begin_syncing), onClick = { onBeginSyncingClick( - calendarName.ifEmpty { NewCalendarDialogFragment.getDefaultCalendarName(context) }, + calendarTitle.ifEmpty { NewCalendarDialogFragment.getDefaultCalendarTitle(context) }, calendarColor ) } @@ -197,7 +219,7 @@ private fun NewCalendarDialog( } @Composable -private fun CalendarNameTextField( +private fun CalendarTitleTextField( modifier: Modifier = Modifier, onValueChanged: (String) -> Unit ) { @@ -231,7 +253,7 @@ private fun CalendarNameTextField( shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( - text = NewCalendarDialogFragment.getDefaultCalendarName(LocalContext.current), + text = NewCalendarDialogFragment.getDefaultCalendarTitle(LocalContext.current), color = MaterialTheme.appColors.textFieldHint, style = MaterialTheme.appTypography.bodyMedium ) @@ -387,6 +409,7 @@ private fun ColorCircle( private fun NewCalendarDialogPreview() { OpenEdXTheme { NewCalendarDialog( + dialogType = DialogType.CREATE_NEW, onCancelClick = { }, onBeginSyncingClick = { _, _ -> } ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 46d5b2fb9..b6e3923d1 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -7,6 +7,7 @@ import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.extension.toastMessage import org.openedx.core.system.CalendarManager +import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.core.R as coreR class NewCalendarDialogViewModel( @@ -17,10 +18,13 @@ class NewCalendarDialogViewModel( fun createCalendar( context: Context, - calendarName: String, + calendarTitle: String, calendarColor: CalendarColor, ) { - val calendarId = calendarManager.createOrUpdateCalendar(calendarName, calendarColor.color) + val calendarId = calendarManager.createOrUpdateCalendar( + calendarTitle = calendarTitle, + calendarColor = calendarColor.color + ) if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { calendarPreferences.calendarId = calendarId viewModelScope.launch { diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt new file mode 100644 index 000000000..b85e4db66 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +interface CalendarEvent \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt similarity index 88% rename from profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt rename to profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt index ddac5e112..3d9f054d3 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarNotifier.kt +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt @@ -1,4 +1,4 @@ -package org.openedx.profile.presentation.calendar +package org.openedx.profile.system.notifier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index c04392eb4..971c00cad 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -71,6 +71,6 @@ Offline Sync Failed Synced - Synchronization + Syncing to calendar From 98045424ca85304fd24a36acebc598c07e12799c Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 3 Jun 2024 23:21:11 +0300 Subject: [PATCH 11/26] feat: Add events to calendar --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/org/openedx/app/AppActivity.kt | 4 +- .../app/data/storage/PreferencesManager.kt | 2 +- .../main/java/org/openedx/app/di/AppModule.kt | 4 +- .../java/org/openedx/app/di/ScreenModule.kt | 5 ++ .../org/openedx/core/data/api/CourseApi.kt | 6 ++ .../core/data/model/EnrollmentStatus.kt | 19 +++++ .../domain/interactor/CalendarInteractor.kt | 12 ++++ .../core/domain/model/EnrollmentStatus.kt | 7 ++ .../core/repository/CalendarRepository.kt | 19 +++++ .../core/system/CalendarSyncService.kt | 30 -------- .../presentation/calendar/CalendarCreated.kt | 5 -- .../calendar/CalendarViewModel.kt | 26 +++++-- .../calendar/NewCalendarDialogViewModel.kt | 3 +- .../profile/service/CalendarSyncService.kt | 71 +++++++++++++++++++ .../profile}/system/CalendarManager.kt | 3 +- .../system}/CalendarSyncServiceInitiator.kt | 7 +- .../system/notifier/CalendarCreated.kt | 3 + .../system/notifier/CalendarSyncFailed.kt | 3 + .../profile/system/notifier/CalendarSynced.kt | 3 + .../system/notifier/CalendarSyncing.kt | 3 + 21 files changed, 187 insertions(+), 50 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/model/EnrollmentStatus.kt create mode 100644 core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/EnrollmentStatus.kt create mode 100644 core/src/main/java/org/openedx/core/repository/CalendarRepository.kt delete mode 100644 core/src/main/java/org/openedx/core/system/CalendarSyncService.kt delete mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt create mode 100644 profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt rename {core/src/main/java/org/openedx/core => profile/src/main/java/org/openedx/profile}/system/CalendarManager.kt (99%) rename {core/src/main/java/org/openedx/core => profile/src/main/java/org/openedx/profile/system}/CalendarSyncServiceInitiator.kt (75%) create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eae943206..db069bf3f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,7 +115,7 @@ diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 60d1a27a2..d91d021a7 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -20,18 +20,18 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.ActivityAppBinding import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.signin.SignInFragment -import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.requestApplyInsetsWhenAttached import org.openedx.core.presentation.global.InsetHolder import org.openedx.core.presentation.global.WindowSizeHolder -import org.openedx.core.system.CalendarManager import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.utils.Logger import org.openedx.core.utils.isToday import org.openedx.profile.presentation.ProfileRouter +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.CalendarSyncServiceInitiator import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment import java.util.Date 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 46df84c88..3180c8a24 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 @@ -11,10 +11,10 @@ import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.VideoQuality import org.openedx.core.domain.model.VideoSettings import org.openedx.core.extension.replaceSpace -import org.openedx.core.system.CalendarManager import org.openedx.course.data.storage.CoursePreferences import org.openedx.profile.data.model.Account import org.openedx.profile.data.storage.ProfilePreferences +import org.openedx.profile.system.CalendarManager import org.openedx.whatsnew.data.storage.WhatsNewPreferences class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences, 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 39a9c1830..223ea961c 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -25,7 +25,6 @@ import org.openedx.auth.presentation.sso.FacebookAuthHelper import org.openedx.auth.presentation.sso.GoogleAuthHelper import org.openedx.auth.presentation.sso.MicrosoftAuthHelper import org.openedx.auth.presentation.sso.OAuthHelper -import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.ImageProcessor import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments @@ -42,7 +41,6 @@ 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.system.AppCookieManager -import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.AppUpgradeNotifier @@ -64,6 +62,8 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.CalendarSyncServiceInitiator import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.profile.system.notifier.ProfileNotifier import org.openedx.whatsnew.WhatsNewManager 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 95411e2fd..502278a92 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -12,8 +12,10 @@ import org.openedx.auth.presentation.restore.RestorePasswordViewModel import org.openedx.auth.presentation.signin.SignInViewModel import org.openedx.auth.presentation.signup.SignUpViewModel import org.openedx.core.Validator +import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel import org.openedx.core.presentation.settings.video.VideoQualityViewModel +import org.openedx.core.repository.CalendarRepository import org.openedx.course.data.repository.CourseRepository import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.container.CourseContainerViewModel @@ -173,9 +175,12 @@ val screenModule = module { viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get(), get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } + single { CalendarRepository(get(), get()) } + factory { CalendarInteractor(get()) } single { CourseRepository(get(), get(), get(), get(), get()) } factory { CourseInteractor(get()) } + viewModel { (pathId: String, infoType: String) -> CourseInfoViewModel( pathId, 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 6d30a9044..fab5d924b 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 @@ -7,6 +7,7 @@ import org.openedx.core.data.model.CourseDates import org.openedx.core.data.model.CourseDatesBannerInfo import org.openedx.core.data.model.CourseEnrollments import org.openedx.core.data.model.CourseStructureModel +import org.openedx.core.data.model.EnrollmentStatus import org.openedx.core.data.model.HandoutsModel import org.openedx.core.data.model.ResetCourseDates import retrofit2.http.Body @@ -76,4 +77,9 @@ interface CourseApi { @Query("status") status: String? = null, @Query("requested_fields") fields: List = emptyList() ): CourseEnrollments + + @GET("/api/mobile/v1/users/{username}/enrollments_status/") + suspend fun getEnrollmentsStatus( + @Path("username") username: String + ): List } diff --git a/core/src/main/java/org/openedx/core/data/model/EnrollmentStatus.kt b/core/src/main/java/org/openedx/core/data/model/EnrollmentStatus.kt new file mode 100644 index 000000000..f5535879e --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/EnrollmentStatus.kt @@ -0,0 +1,19 @@ +package org.openedx.core.data.model + +import com.google.gson.annotations.SerializedName +import org.openedx.core.domain.model.EnrollmentStatus + +data class EnrollmentStatus( + @SerializedName("course_id") + val courseId: String?, + @SerializedName("course_name") + val courseName: String?, + @SerializedName("is_active") + val isActive: Boolean? +) { + fun mapToDomain() = EnrollmentStatus( + courseId = courseId ?: "", + courseName = courseName ?: "", + isActive = isActive ?: false + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt new file mode 100644 index 000000000..c84f02f02 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -0,0 +1,12 @@ +package org.openedx.core.domain.interactor + +import org.openedx.core.repository.CalendarRepository + +class CalendarInteractor( + private val repository: CalendarRepository +) { + + suspend fun getEnrollmentsStatus() = repository.getEnrollmentsStatus() + + suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId) +} diff --git a/core/src/main/java/org/openedx/core/domain/model/EnrollmentStatus.kt b/core/src/main/java/org/openedx/core/domain/model/EnrollmentStatus.kt new file mode 100644 index 000000000..8d40ea71d --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/EnrollmentStatus.kt @@ -0,0 +1,7 @@ +package org.openedx.core.domain.model + +data class EnrollmentStatus( + val courseId: String, + val courseName: String, + val isActive: Boolean +) diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt new file mode 100644 index 000000000..95b028f6c --- /dev/null +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -0,0 +1,19 @@ +package org.openedx.core.repository + +import org.openedx.core.data.api.CourseApi +import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.EnrollmentStatus + +class CalendarRepository( + private val api: CourseApi, + private val corePreferences: CorePreferences +) { + + suspend fun getEnrollmentsStatus(): List { + val response = api.getEnrollmentsStatus(corePreferences.user?.username ?: "") + return response.map { it.mapToDomain() } + } + + suspend fun getCourseDates(courseId: String) = api.getCourseDates(courseId) + +} diff --git a/core/src/main/java/org/openedx/core/system/CalendarSyncService.kt b/core/src/main/java/org/openedx/core/system/CalendarSyncService.kt deleted file mode 100644 index 599cecd7d..000000000 --- a/core/src/main/java/org/openedx/core/system/CalendarSyncService.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.openedx.core.system - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject - -class CalendarSyncService : Service() { - - private val calendarManager: CalendarManager by inject() - - override fun onBind(intent: Intent?): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - CoroutineScope(Dispatchers.IO).launch { - syncCalendar() - } - return START_STICKY - } - - private fun syncCalendar() { - Log.e("___", "syncCalendar") - } -} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt deleted file mode 100644 index becb787b7..000000000 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarCreated.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.openedx.profile.presentation.calendar - -import org.openedx.profile.system.notifier.CalendarEvent - -object CalendarCreated : CalendarEvent 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 cbe0a1081..cfd155f26 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 @@ -8,11 +8,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel -import org.openedx.core.CalendarSyncServiceInitiator import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.core.system.CalendarManager import org.openedx.core.system.connection.NetworkConnection +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.CalendarSyncServiceInitiator +import org.openedx.profile.system.notifier.CalendarCreated import org.openedx.profile.system.notifier.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarSyncFailed +import org.openedx.profile.system.notifier.CalendarSynced +import org.openedx.profile.system.notifier.CalendarSyncing class CalendarViewModel( private val calendarSyncServiceInitiator: CalendarSyncServiceInitiator, @@ -26,7 +30,7 @@ class CalendarViewModel( CalendarUIState( isCalendarExist = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST, calendarData = null, - calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCHRONIZATION else CalendarSyncState.OFFLINE, + calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, isRelativeDateEnabled = calendarPreferences.isRelativeDateEnabled ) @@ -43,6 +47,18 @@ class CalendarViewModel( _uiState.update { it.copy(isCalendarExist = true) } getCalendarData() } + + CalendarSyncing -> { + _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNCHRONIZATION) } + } + + CalendarSynced -> { + _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNCED) } + } + + CalendarSyncFailed -> { + _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNC_FAILED) } + } } } } @@ -57,7 +73,9 @@ class CalendarViewModel( fun setCalendarSyncEnabled(isEnabled: Boolean) { calendarPreferences.isCalendarSyncEnabled = isEnabled _uiState.update { it.copy(isCalendarSyncEnabled = isEnabled) } - calendarSyncServiceInitiator.startSyncCalendarService() + if (isEnabled) { + calendarSyncServiceInitiator.startSyncCalendarService() + } } fun setRelativeDateEnabled(isEnabled: Boolean) { diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index b6e3923d1..272414b58 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -6,7 +6,8 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.extension.toastMessage -import org.openedx.core.system.CalendarManager +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.notifier.CalendarCreated import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.core.R as coreR diff --git a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt new file mode 100644 index 000000000..90186e2c9 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt @@ -0,0 +1,71 @@ +package org.openedx.profile.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.notifier.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarSyncFailed +import org.openedx.profile.system.notifier.CalendarSynced +import org.openedx.profile.system.notifier.CalendarSyncing +import java.util.Date + +class CalendarSyncService : Service() { + + private val calendarManager: CalendarManager by inject() + private val calendarRepository: CalendarInteractor by inject() + private val calendarNotifier: CalendarNotifier by inject() + private val calendarPreferences: CalendarPreferences by inject() + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + syncCalendar() + return START_STICKY + } + + private fun syncCalendar() { + CoroutineScope(Dispatchers.IO).launch { + calendarNotifier.send(CalendarSyncing) + try { + val courseDatesDeferredResults = calendarRepository.getEnrollmentsStatus() + .filter { it.isActive } + .map { enrollmentStatus -> + async(Dispatchers.IO) { + Pair(enrollmentStatus, calendarRepository.getCourseDates(enrollmentStatus.courseId)) + } + } + val courseDates = courseDatesDeferredResults.awaitAll().filterNotNull() + val calendarSyncDeferred = courseDates.map { (enrollmentStatus, courseDatesResult) -> + async { + courseDatesResult.courseDateBlocks.forEach { courseDateBlock -> + courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> + calendarManager.addEventsIntoCalendar( + calendarId = calendarPreferences.calendarId, + courseId = enrollmentStatus.courseId, + courseName = enrollmentStatus.courseName, + courseDateBlock = domainCourseDateBlock + ) + } + } + } + } + calendarSyncDeferred.awaitAll() + calendarPreferences.lastCalendarSync = Date().time + calendarNotifier.send(CalendarSynced) + } catch (e: Exception) { + calendarNotifier.send(CalendarSyncFailed) + } + } + } +} diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt similarity index 99% rename from core/src/main/java/org/openedx/core/system/CalendarManager.kt rename to profile/src/main/java/org/openedx/profile/system/CalendarManager.kt index 460ee8db6..e9291f338 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt @@ -1,4 +1,4 @@ -package org.openedx.core.system +package org.openedx.profile.system import android.annotation.SuppressLint import android.content.ContentUris @@ -14,6 +14,7 @@ import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CalendarData import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.system.ResourceManager import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar import java.util.Calendar diff --git a/core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt b/profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt similarity index 75% rename from core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt rename to profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt index c141e615b..4810868b5 100644 --- a/core/src/main/java/org/openedx/core/CalendarSyncServiceInitiator.kt +++ b/profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt @@ -1,12 +1,13 @@ -package org.openedx.core +package org.openedx.profile.system import android.content.Context import android.content.Intent -import org.openedx.core.system.CalendarSyncService +import org.openedx.profile.service.CalendarSyncService class CalendarSyncServiceInitiator(private val context: Context) { + fun startSyncCalendarService() { val intent = Intent(context, CalendarSyncService::class.java) context.startService(intent) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt new file mode 100644 index 000000000..ba605a4a1 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +object CalendarCreated : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt new file mode 100644 index 000000000..63ffd0fd9 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +object CalendarSyncFailed : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt new file mode 100644 index 000000000..00d65b7a5 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +object CalendarSynced : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt new file mode 100644 index 000000000..bb92163af --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +object CalendarSyncing : CalendarEvent From c774d3b8285a358dc9876b3a399cd54018f2aaed Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 4 Jun 2024 16:02:08 +0300 Subject: [PATCH 12/26] fix: Save CourseCalendarEventEntity to DB --- .../main/java/org/openedx/app/di/AppModule.kt | 5 +++ .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../java/org/openedx/app/room/AppDatabase.kt | 6 ++- .../data/storage/CourseCalendarEventEntity.kt | 26 ++++++++++++ .../domain/interactor/CalendarInteractor.kt | 9 ++++ .../core/domain/model/CourseCalendarEvent.kt | 7 ++++ .../org/openedx/core/module/db/CalendarDao.kt | 20 +++++++++ .../core/repository/CalendarRepository.kt | 12 +++++- .../calendar/CalendarViewModel.kt | 6 ++- .../profile/service/CalendarSyncService.kt | 42 ++++++++++++------- .../openedx/profile/system/CalendarManager.kt | 5 ++- 11 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt create mode 100644 core/src/main/java/org/openedx/core/module/db/CalendarDao.kt 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 223ea961c..db2ceaa63 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -156,6 +156,11 @@ val appModule = module { room.downloadDao() } + single { + val room = get() + room.calendarDao() + } + single { FileDownloader() } 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 502278a92..07f5b538e 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -175,7 +175,7 @@ val screenModule = module { viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get(), get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } - single { CalendarRepository(get(), get()) } + single { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } single { CourseRepository(get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index be320bae7..a3fd4d387 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -5,6 +5,8 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import org.openedx.core.data.model.room.CourseStructureEntity import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity +import org.openedx.core.data.storage.CourseCalendarEventEntity +import org.openedx.core.module.db.CalendarDao import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.db.DownloadModelEntity import org.openedx.course.data.storage.CourseConverter @@ -22,7 +24,8 @@ const val DATABASE_NAME = "OpenEdX_db" CourseEntity::class, EnrolledCourseEntity::class, CourseStructureEntity::class, - DownloadModelEntity::class + DownloadModelEntity::class, + CourseCalendarEventEntity::class ], version = DATABASE_VERSION, exportSchema = false @@ -33,4 +36,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun courseDao(): CourseDao abstract fun dashboardDao(): DashboardDao abstract fun downloadDao(): DownloadDao + abstract fun calendarDao(): CalendarDao } diff --git a/core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt b/core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt new file mode 100644 index 000000000..8a4eace5d --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt @@ -0,0 +1,26 @@ +package org.openedx.core.data.storage + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.openedx.core.data.model.room.discovery.CourseDateBlockDb +import org.openedx.core.domain.model.CourseCalendarEvent + +@Entity(tableName = "course_calendar_event_table") +data class CourseCalendarEventEntity( + @PrimaryKey + @ColumnInfo("event_id") + val eventId: Long, + @ColumnInfo("course_id") + val courseId: String, + @Embedded + val courseDateBlockDb: CourseDateBlockDb, +) { + + fun mapToDomain() = CourseCalendarEvent( + courseId = courseId, + eventId = eventId, + courseDateBlock = courseDateBlockDb.mapToDomain() + ) +} diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index c84f02f02..15591c99d 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -1,5 +1,6 @@ package org.openedx.core.domain.interactor +import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.repository.CalendarRepository class CalendarInteractor( @@ -9,4 +10,12 @@ class CalendarInteractor( suspend fun getEnrollmentsStatus() = repository.getEnrollmentsStatus() suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId) + + suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) { + repository.insertCourseCalendarEntity(*courseCalendarEntity) + } + + suspend fun readAllData(): List { + return repository.readAllData() + } } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt new file mode 100644 index 000000000..2b841969c --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt @@ -0,0 +1,7 @@ +package org.openedx.core.domain.model + +data class CourseCalendarEvent( + val courseId: String, + val eventId: Long, + val courseDateBlock: CourseDateBlock +) diff --git a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt new file mode 100644 index 000000000..5e01b8688 --- /dev/null +++ b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt @@ -0,0 +1,20 @@ +package org.openedx.core.module.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.openedx.core.data.storage.CourseCalendarEventEntity + +@Dao +interface CalendarDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) + + @Query("DELETE FROM course_calendar_event_table") + suspend fun clearCachedData() + + @Query("SELECT * FROM course_calendar_event_table") + suspend fun readAllData(): List +} diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index 95b028f6c..7ca7c5fc9 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -2,11 +2,14 @@ package org.openedx.core.repository import org.openedx.core.data.api.CourseApi import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.domain.model.EnrollmentStatus +import org.openedx.core.module.db.CalendarDao class CalendarRepository( private val api: CourseApi, - private val corePreferences: CorePreferences + private val corePreferences: CorePreferences, + private val calendarDao: CalendarDao ) { suspend fun getEnrollmentsStatus(): List { @@ -16,4 +19,11 @@ class CalendarRepository( suspend fun getCourseDates(courseId: String) = api.getCourseDates(courseId) + suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) { + calendarDao.insertCourseCalendarEntity(*courseCalendarEntity) + } + + suspend fun readAllData(): List { + return calendarDao.readAllData() + } } 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 cfd155f26..cea7cd7ed 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 @@ -84,7 +84,9 @@ class CalendarViewModel( } private fun getCalendarData() { - val calendarData = calendarManager.getCalendarData(calendarId = calendarPreferences.calendarId) - _uiState.update { it.copy(calendarData = calendarData) } + if (calendarManager.hasPermissions()) { + val calendarData = calendarManager.getCalendarData(calendarId = calendarPreferences.calendarId) + _uiState.update { it.copy(calendarData = calendarData) } + } } } diff --git a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt index 90186e2c9..208018934 100644 --- a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt +++ b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt @@ -3,6 +3,7 @@ package org.openedx.profile.service import android.app.Service import android.content.Intent import android.os.IBinder +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -10,6 +11,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.profile.system.CalendarManager import org.openedx.profile.system.notifier.CalendarNotifier @@ -21,7 +23,7 @@ import java.util.Date class CalendarSyncService : Service() { private val calendarManager: CalendarManager by inject() - private val calendarRepository: CalendarInteractor by inject() + private val calendarInteractor: CalendarInteractor by inject() private val calendarNotifier: CalendarNotifier by inject() private val calendarPreferences: CalendarPreferences by inject() @@ -38,32 +40,42 @@ class CalendarSyncService : Service() { CoroutineScope(Dispatchers.IO).launch { calendarNotifier.send(CalendarSyncing) try { - val courseDatesDeferredResults = calendarRepository.getEnrollmentsStatus() + val courseDatesDeferredResults = calendarInteractor.getEnrollmentsStatus() .filter { it.isActive } .map { enrollmentStatus -> async(Dispatchers.IO) { - Pair(enrollmentStatus, calendarRepository.getCourseDates(enrollmentStatus.courseId)) + Pair(enrollmentStatus, calendarInteractor.getCourseDates(enrollmentStatus.courseId)) } } - val courseDates = courseDatesDeferredResults.awaitAll().filterNotNull() - val calendarSyncDeferred = courseDates.map { (enrollmentStatus, courseDatesResult) -> - async { - courseDatesResult.courseDateBlocks.forEach { courseDateBlock -> - courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> - calendarManager.addEventsIntoCalendar( - calendarId = calendarPreferences.calendarId, - courseId = enrollmentStatus.courseId, - courseName = enrollmentStatus.courseName, - courseDateBlock = domainCourseDateBlock - ) + val calendarSyncDeferred = + courseDatesDeferredResults.awaitAll().map { (enrollmentStatus, courseDatesResult) -> + async { + courseDatesResult.courseDateBlocks.forEach { courseDateBlock -> + courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> + val eventId = calendarManager.addEventsIntoCalendar( + calendarId = calendarPreferences.calendarId, + courseId = enrollmentStatus.courseId, + courseName = enrollmentStatus.courseName, + courseDateBlock = domainCourseDateBlock + ) + courseDateBlock.mapToRoomEntity()?.let { courseDateBlockDb -> + val courseCalendarEventEntity = CourseCalendarEventEntity( + courseId = enrollmentStatus.courseId, + eventId = eventId, + courseDateBlockDb = courseDateBlockDb + ) + calendarInteractor.insertCourseCalendarEntity(courseCalendarEventEntity) + } + } } } } - } calendarSyncDeferred.awaitAll() calendarPreferences.lastCalendarSync = Date().time calendarNotifier.send(CalendarSynced) + Log.e("___", "${calendarInteractor.readAllData()}") } catch (e: Exception) { + Log.e("___", "$e") calendarNotifier.send(CalendarSyncFailed) } } diff --git a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt index e9291f338..eeba219db 100644 --- a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt +++ b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt @@ -154,7 +154,7 @@ class CalendarManager( courseId: String, courseName: String, courseDateBlock: CourseDateBlock - ) { + ): Long { val date = courseDateBlock.date.toCalendar() // start time of the event, adjusted 1 hour earlier for a 1-hour duration val startMillis: Long = date.timeInMillis - TimeUnit.HOURS.toMillis(1) @@ -181,6 +181,8 @@ class CalendarManager( } val uri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, values) uri?.let { addReminderToEvent(uri = it) } + val eventId = uri?.lastPathSegment?.toLong() ?: EVENT_DOES_NOT_EXIST + return eventId } /** @@ -434,6 +436,7 @@ class CalendarManager( companion object { const val CALENDAR_DOES_NOT_EXIST = -1L + const val EVENT_DOES_NOT_EXIST = -1L private const val TAG = "CalendarManager" private const val LOCAL_USER = "local_user" } From 3a4b34e37e7918d521ea06ea3de08434e6683bb3 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 4 Jun 2024 18:04:34 +0300 Subject: [PATCH 13/26] feat: CoursesToSyncFragment UI --- .../main/java/org/openedx/app/AppRouter.kt | 5 + .../app/data/storage/PreferencesManager.kt | 7 + .../java/org/openedx/app/di/ScreenModule.kt | 4 +- .../java/org/openedx/app/room/AppDatabase.kt | 2 +- .../room}/CourseCalendarEventEntity.kt | 2 +- .../core/data/storage/CalendarPreferences.kt | 1 + .../domain/interactor/CalendarInteractor.kt | 6 +- .../org/openedx/core/module/db/CalendarDao.kt | 2 +- .../core/repository/CalendarRepository.kt | 4 +- .../profile/presentation/ProfileRouter.kt | 2 + .../presentation/calendar/CalendarFragment.kt | 7 +- .../calendar/CalendarSettingsView.kt | 12 +- .../calendar/CalendarViewModel.kt | 9 +- .../calendar/CoursesToSyncFragment.kt | 337 ++++++++++++++++++ .../calendar/CoursesToSyncUIState.kt | 8 + .../calendar/CoursesToSyncViewModel.kt | 42 +++ .../presentation/calendar/SyncCourseTab.kt | 12 + .../profile/service/CalendarSyncService.kt | 4 +- profile/src/main/res/values/strings.xml | 5 + 19 files changed, 453 insertions(+), 18 deletions(-) rename core/src/main/java/org/openedx/core/data/{storage => model/room}/CourseCalendarEventEntity.kt (94%) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 17b47d11d..4abf912fe 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -46,6 +46,7 @@ import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.presentation.anothersaccount.AnothersProfileFragment import org.openedx.profile.presentation.calendar.CalendarFragment +import org.openedx.profile.presentation.calendar.CoursesToSyncFragment import org.openedx.profile.presentation.delete.DeleteProfileFragment import org.openedx.profile.presentation.edit.EditProfileFragment import org.openedx.profile.presentation.manageaccount.ManageAccountFragment @@ -398,6 +399,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToCalendarSettings(fm: FragmentManager) { replaceFragmentWithBackStack(fm, CalendarFragment()) } + + override fun navigateToCoursesToSync(fm: FragmentManager) { + replaceFragmentWithBackStack(fm, CoursesToSyncFragment()) + } //endregion private fun replaceFragmentWithBackStack(fm: FragmentManager, fragment: Fragment) { 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 3180c8a24..da797d36d 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 @@ -184,6 +184,12 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getBoolean(IS_RELATIVE_DATES_ENABLED, true) + override var isHideInactiveCourses: Boolean + set(value) { + saveBoolean(HIDE_INACTIVE_COURSES, value) + } + get() = getBoolean(HIDE_INACTIVE_COURSES, true) + override fun setCalendarSyncEventsDialogShown(courseName: String) { saveBoolean(courseName.replaceSpace("_"), true) } @@ -209,5 +215,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val RESET_APP_DIRECTORY = "reset_app_directory" private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED" + private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES" } } 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 07f5b538e..f81920d09 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -59,6 +59,7 @@ import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.anothersaccount.AnothersProfileViewModel import org.openedx.profile.presentation.calendar.CalendarViewModel +import org.openedx.profile.presentation.calendar.CoursesToSyncViewModel import org.openedx.profile.presentation.calendar.NewCalendarDialogViewModel import org.openedx.profile.presentation.delete.DeleteProfileViewModel import org.openedx.profile.presentation.edit.EditProfileViewModel @@ -173,7 +174,8 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get(), get(), get()) } + viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { CoursesToSyncViewModel(get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } single { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index a3fd4d387..eafb40d7a 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -3,9 +3,9 @@ package org.openedx.app.room import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseStructureEntity import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity -import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.module.db.CalendarDao import org.openedx.core.module.db.DownloadDao import org.openedx.core.module.db.DownloadModelEntity diff --git a/core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt similarity index 94% rename from core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt rename to core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt index 8a4eace5d..e78194049 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CourseCalendarEventEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt @@ -1,4 +1,4 @@ -package org.openedx.core.data.storage +package org.openedx.core.data.model.room import androidx.room.ColumnInfo import androidx.room.Embedded diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt index 75077aac1..6d133f443 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -5,4 +5,5 @@ interface CalendarPreferences { var lastCalendarSync: Long var isCalendarSyncEnabled: Boolean var isRelativeDateEnabled: Boolean + var isHideInactiveCourses: Boolean } diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index 15591c99d..9afae97cc 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -1,6 +1,6 @@ package org.openedx.core.domain.interactor -import org.openedx.core.data.storage.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.repository.CalendarRepository class CalendarInteractor( @@ -15,7 +15,7 @@ class CalendarInteractor( repository.insertCourseCalendarEntity(*courseCalendarEntity) } - suspend fun readAllData(): List { - return repository.readAllData() + suspend fun getCourseCalendarEventEntityFromCache(): List { + return repository.getCourseCalendarEventEntityFromCache() } } diff --git a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt index 5e01b8688..46b5acca5 100644 --- a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt +++ b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt @@ -4,7 +4,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import org.openedx.core.data.storage.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarEventEntity @Dao interface CalendarDao { diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index 7ca7c5fc9..f3a067c35 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -1,8 +1,8 @@ package org.openedx.core.repository import org.openedx.core.data.api.CourseApi +import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.storage.CorePreferences -import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.domain.model.EnrollmentStatus import org.openedx.core.module.db.CalendarDao @@ -23,7 +23,7 @@ class CalendarRepository( calendarDao.insertCourseCalendarEntity(*courseCalendarEntity) } - suspend fun readAllData(): List { + suspend fun getCourseCalendarEventEntityFromCache(): List { return calendarDao.readAllData() } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt index fd7514bd5..7526c886b 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt @@ -23,4 +23,6 @@ interface ProfileRouter { fun navigateToManageAccount(fm: FragmentManager) fun navigateToCalendarSettings(fm: FragmentManager) + + fun navigateToCoursesToSync(fm: FragmentManager) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index 6703452af..c87d12b98 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -68,6 +68,9 @@ class CalendarFragment : Fragment() { requireActivity().supportFragmentManager, NewCalendarDialogFragment.DIALOG_TAG ) + }, + onCourseToSyncClick = { + viewModel.navigateToCoursesToSync(requireActivity().supportFragmentManager) } ) } @@ -82,6 +85,7 @@ private fun CalendarView( setUpCalendarSync: () -> Unit, onBackClick: () -> Unit, onChangeSyncOptionClick: () -> Unit, + onCourseToSyncClick: () -> Unit, onCalendarSyncSwitchClick: (Boolean) -> Unit, onRelativeDateSwitchClick: (Boolean) -> Unit ) { @@ -98,7 +102,8 @@ private fun CalendarView( onBackClick = onBackClick, onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, onRelativeDateSwitchClick = onRelativeDateSwitchClick, - onChangeSyncOptionClick = onChangeSyncOptionClick + onChangeSyncOptionClick = onChangeSyncOptionClick, + onCourseToSyncClick = onCourseToSyncClick ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 8cc305497..0271fbb62 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -64,6 +64,7 @@ fun CalendarSettingsView( onCalendarSyncSwitchClick: (Boolean) -> Unit, onRelativeDateSwitchClick: (Boolean) -> Unit, onChangeSyncOptionClick: () -> Unit, + onCourseToSyncClick: () -> Unit, onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -142,10 +143,10 @@ fun CalendarSettingsView( onChangeSyncOptionClick = onChangeSyncOptionClick ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(20.dp)) CoursesToSyncSection( coursesSynced = coursesSynced, - onSyncingCoursesClick = {} + onCourseToSyncClick = onCourseToSyncClick ) Spacer(modifier = Modifier.height(32.dp)) OptionsSection( @@ -266,7 +267,7 @@ fun SyncOptionsButton( @Composable fun CoursesToSyncSection( coursesSynced: Int, - onSyncingCoursesClick: () -> Unit + onCourseToSyncClick: () -> Unit ) { Column { SectionTitle(stringResource(R.string.profile_courses_to_sync)) @@ -279,7 +280,7 @@ fun CoursesToSyncSection( ) { SettingsItem( text = stringResource(R.string.profile_syncing_courses, coursesSynced), - onClick = onSyncingCoursesClick + onClick = onCourseToSyncClick ) } } @@ -347,7 +348,8 @@ private fun CalendarSettingsViewPreview() { onBackClick = {}, onCalendarSyncSwitchClick = {}, onRelativeDateSwitchClick = {}, - onChangeSyncOptionClick = {} + onChangeSyncOptionClick = {}, + onCourseToSyncClick = {} ) } } 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 cea7cd7ed..7d0e8b29e 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 @@ -1,6 +1,7 @@ package org.openedx.profile.presentation.calendar import androidx.activity.result.ActivityResultLauncher +import androidx.fragment.app.FragmentManager import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -10,6 +11,7 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.system.connection.NetworkConnection +import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.CalendarManager import org.openedx.profile.system.CalendarSyncServiceInitiator import org.openedx.profile.system.notifier.CalendarCreated @@ -23,7 +25,8 @@ class CalendarViewModel( private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, - private val networkConnection: NetworkConnection + private val networkConnection: NetworkConnection, + private val profileRouter: ProfileRouter ) : BaseViewModel() { private val _uiState = MutableStateFlow( @@ -83,6 +86,10 @@ class CalendarViewModel( _uiState.update { it.copy(isRelativeDateEnabled = isEnabled) } } + fun navigateToCoursesToSync(fragmentManager: FragmentManager) { + profileRouter.navigateToCoursesToSync(fragmentManager) + } + private fun getCalendarData() { if (calendarManager.hasPermissions()) { val calendarData = calendarManager.getCalendarData(calendarId = calendarPreferences.calendarId) 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 new file mode 100644 index 000000000..9798fa6e0 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncFragment.kt @@ -0,0 +1,337 @@ +package org.openedx.profile.presentation.calendar + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Switch +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.fragment.app.Fragment +import org.koin.androidx.compose.koinViewModel +import org.openedx.core.domain.model.EnrollmentStatus +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.settingsHeaderBackground +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +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.ui.theme.fontFamily +import org.openedx.core.ui.windowSizeValue +import org.openedx.profile.R + +class CoursesToSyncFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + val viewModel: CoursesToSyncViewModel = koinViewModel() + val uiState by viewModel.uiState.collectAsState() + + CoursesToSyncView( + windowSize = windowSize, + uiState = uiState, + onHideInactiveCoursesSwitchClick = { + viewModel.setHideInactiveCoursesEnabled(it) + }, + onBackClick = { + requireActivity().supportFragmentManager.popBackStack() + } + ) + } + } + } +} + +@Composable +fun CoursesToSyncView( + windowSize: WindowSize, + onBackClick: () -> Unit, + uiState: CoursesToSyncUIState, + onHideInactiveCoursesSwitchClick: (Boolean) -> Unit +) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + modifier = Modifier + .fillMaxSize(), + scaffoldState = scaffoldState + ) { paddingValues -> + + val contentWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 420.dp), + compact = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + ) + } + + val topBarWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier + .fillMaxWidth() + ) + ) + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .settingsHeaderBackground() + .statusBarsInset(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Toolbar( + modifier = topBarWidth + .displayCutoutForLandscape(), + label = stringResource(id = R.string.profile_courses_to_sync), + canShowBackBtn = true, + labelTint = MaterialTheme.appColors.settingsTitleContent, + iconTint = MaterialTheme.appColors.settingsTitleContent, + onBackClick = onBackClick + ) + + Box( + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.appShapes.screenBackgroundShape) + .background(MaterialTheme.appColors.background) + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = contentWidth.padding(vertical = 28.dp), + ) { + Text( + text = stringResource(R.string.profile_courses_to_sync_title), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + Spacer(modifier = Modifier.height(20.dp)) + HideInactiveCoursesView( + isHideInactiveCourses = uiState.isHideInactiveCourses, + onHideInactiveCoursesSwitchClick = onHideInactiveCoursesSwitchClick + ) + Spacer(modifier = Modifier.height(20.dp)) + SyncCourseTabRow(uiState.enrollmentsStatus) + } + } + } + } + } +} + +@Composable +fun SyncCourseTabRow( + enrollmentsStatus: List +) { + var selectedTab by remember { mutableStateOf(SyncCourseTab.SYNCED) } + val selectedTabIndex = SyncCourseTab.entries.indexOf(selectedTab) + + Column { + TabRow( + modifier = Modifier + .clip(MaterialTheme.appShapes.buttonShape) + .border( + 1.dp, + MaterialTheme.appColors.textAccent, + MaterialTheme.appShapes.buttonShape + ), + selectedTabIndex = selectedTabIndex, + backgroundColor = MaterialTheme.appColors.background, + indicator = {} + ) { + SyncCourseTab.entries.forEachIndexed { index, tab -> + val backgroundColor = if (selectedTabIndex == index) { + MaterialTheme.appColors.textAccent + } else { + MaterialTheme.appColors.background + } + Tab( + modifier = Modifier + .background(backgroundColor), + text = { Text(stringResource(id = tab.title)) }, + selected = selectedTabIndex == index, + onClick = { selectedTab = SyncCourseTab.entries[index] }, + unselectedContentColor = MaterialTheme.appColors.textAccent, + selectedContentColor = MaterialTheme.appColors.background + ) + } + } + + CourseCheckboxList( + selectedTab = selectedTab, + enrollmentsStatus = enrollmentsStatus + ) + } +} + + +@Composable +private fun CourseCheckboxList( + selectedTab: SyncCourseTab, + enrollmentsStatus: List +) { + LazyColumn( + modifier = Modifier.padding(8.dp), + ) { + items(enrollmentsStatus) { course -> + val annotatedString = buildAnnotatedString { + append(course.courseName) + if (!course.isActive) { + append(" ") + withStyle( + style = SpanStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + fontFamily = fontFamily, + color = MaterialTheme.appColors.textFieldHint, + ) + ) { + append(stringResource(R.string.profile_inactive)) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier.size(24.dp), + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.appColors.primary, + uncheckedColor = MaterialTheme.appColors.textFieldText + ), + checked = true, + enabled = course.isActive, + onCheckedChange = { + //TODO + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = annotatedString, + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun HideInactiveCoursesView( + isHideInactiveCourses: Boolean, + onHideInactiveCoursesSwitchClick: (Boolean) -> Unit +) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.profile_hide_inactive_courses), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Switch( + modifier = Modifier + .padding(0.dp), + checked = isHideInactiveCourses, + onCheckedChange = onHideInactiveCoursesSwitchClick + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.profile_automatically_remove_events), + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textPrimaryVariant + ) + } +} + +@Preview +@Composable +private fun CoursesToSyncViewPreview() { + OpenEdXTheme { + CoursesToSyncView( + windowSize = rememberWindowSize(), + uiState = CoursesToSyncUIState( + enrollmentsStatus = emptyList(), + isHideInactiveCourses = true + ), + onHideInactiveCoursesSwitchClick = {}, + onBackClick = {} + ) + } +} \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt new file mode 100644 index 000000000..9e9b09b49 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt @@ -0,0 +1,8 @@ +package org.openedx.profile.presentation.calendar + +import org.openedx.core.domain.model.EnrollmentStatus + +data class CoursesToSyncUIState( + val enrollmentsStatus: List, + val isHideInactiveCourses: Boolean +) 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 new file mode 100644 index 000000000..ee004794f --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncViewModel.kt @@ -0,0 +1,42 @@ +package org.openedx.profile.presentation.calendar + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.domain.interactor.CalendarInteractor + +class CoursesToSyncViewModel( + private val calendarInteractor: CalendarInteractor, + private val calendarPreferences: CalendarPreferences +) : BaseViewModel() { + + private val _uiState = MutableStateFlow( + CoursesToSyncUIState( + enrollmentsStatus = emptyList(), + isHideInactiveCourses = calendarPreferences.isHideInactiveCourses + ) + ) + val uiState: StateFlow + get() = _uiState.asStateFlow() + + init { + getEnrollmentsStatus() + } + + fun setHideInactiveCoursesEnabled(isEnabled: Boolean) { + calendarPreferences.isHideInactiveCourses = isEnabled + _uiState.update { it.copy(isHideInactiveCourses = isEnabled) } + } + + private fun getEnrollmentsStatus() { + viewModelScope.launch { + val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() + _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus) } + } + } +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt new file mode 100644 index 000000000..a3d2e7d55 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt @@ -0,0 +1,12 @@ +package org.openedx.profile.presentation.calendar + +import androidx.annotation.StringRes +import org.openedx.profile.R + +enum class SyncCourseTab( + @StringRes + val title: Int +) { + SYNCED(R.string.profile_synced), + NOT_SYNCED(R.string.profile_not_synced) +} \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt index 208018934..e5d03e3a4 100644 --- a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt +++ b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import org.koin.android.ext.android.inject +import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.core.data.storage.CourseCalendarEventEntity import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.profile.system.CalendarManager import org.openedx.profile.system.notifier.CalendarNotifier @@ -73,7 +73,7 @@ class CalendarSyncService : Service() { calendarSyncDeferred.awaitAll() calendarPreferences.lastCalendarSync = Date().time calendarNotifier.send(CalendarSynced) - Log.e("___", "${calendarInteractor.readAllData()}") + Log.e("___", "${calendarInteractor.getCourseCalendarEventEntityFromCache()}") } catch (e: Exception) { Log.e("___", "$e") calendarNotifier.send(CalendarSyncFailed) diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 971c00cad..deafb879a 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -72,5 +72,10 @@ Sync Failed Synced Syncing to calendar + Disabling sync for a course will remove all events connected to the course from your synced calendar. + Automatically remove events from courses you haven’t viewed in the last month + Not Synced + Inactive + Hide Inactive Courses From b44b9a6e3fa2bf64d4da2bd1dde9a0e9d61ec80c Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 5 Jun 2024 17:38:03 +0300 Subject: [PATCH 14/26] feat: All Calendar sync logic --- app/src/main/AndroidManifest.xml | 4 - .../main/java/org/openedx/app/AppActivity.kt | 20 +-- .../app/data/storage/PreferencesManager.kt | 9 +- .../main/java/org/openedx/app/di/AppModule.kt | 5 +- .../java/org/openedx/app/di/ScreenModule.kt | 4 +- .../java/org/openedx/app/room/AppDatabase.kt | 4 +- .../model/room/CourseCalendarEventEntity.kt | 9 +- .../model/room/CourseCalendarStateEntity.kt | 24 +++ .../core/data/storage/CalendarPreferences.kt | 1 - .../domain/interactor/CalendarInteractor.kt | 29 +++- .../core/domain/model/CourseCalendarEvent.kt | 3 +- .../core/domain/model/CourseCalendarState.kt | 7 + .../core/domain/model/CourseDateBlock.kt | 20 +++ .../org/openedx/core/module/db/CalendarDao.kt | 34 +++- .../core/repository/CalendarRepository.kt | 29 +++- .../calendar/CalendarSettingsView.kt | 5 +- .../calendar/CalendarSyncState.kt | 2 +- .../presentation/calendar/CalendarUIState.kt | 1 + .../calendar/CalendarViewModel.kt | 30 +++- .../calendar/CoursesToSyncFragment.kt | 47 ++++-- .../calendar/CoursesToSyncUIState.kt | 2 + .../calendar/CoursesToSyncViewModel.kt | 24 ++- .../presentation/calendar/SyncCourseTab.kt | 2 +- .../profile/service/CalendarSyncService.kt | 83 ---------- .../openedx/profile/system/CalendarManager.kt | 12 +- .../system/CalendarSyncServiceInitiator.kt | 13 -- .../profile/worker/CalendarSyncScheduler.kt | 39 +++++ .../profile/worker/CalendarSyncWorker.kt | 153 ++++++++++++++++++ 28 files changed, 440 insertions(+), 175 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/data/model/room/CourseCalendarStateEntity.kt create mode 100644 core/src/main/java/org/openedx/core/domain/model/CourseCalendarState.kt delete mode 100644 profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt create mode 100644 profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt create mode 100644 profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index db069bf3f..7142d9190 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -114,10 +114,6 @@ - diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index d91d021a7..88460e828 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -20,7 +20,6 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.app.databinding.ActivityAppBinding import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.signin.SignInFragment -import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.requestApplyInsetsWhenAttached import org.openedx.core.presentation.global.InsetHolder @@ -28,13 +27,10 @@ import org.openedx.core.presentation.global.WindowSizeHolder import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.utils.Logger -import org.openedx.core.utils.isToday import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.CalendarSyncServiceInitiator +import org.openedx.profile.worker.CalendarSyncScheduler import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment -import java.util.Date class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { @@ -52,9 +48,8 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private val viewModel by viewModel() private val whatsNewManager by inject() private val corePreferencesManager by inject() - private val calendarPreferencesManager by inject() private val profileRouter by inject() - private val calendarSyncServiceInitiator by inject() + private val calendarSyncScheduler by inject() private val branchLogger = Logger(BRANCH_TAG) @@ -147,7 +142,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { profileRouter.restartApp(supportFragmentManager, viewModel.isLogistrationEnabled) } - tryToSyncCalendar() + calendarSyncScheduler.scheduleDailySync() } override fun onStart() { @@ -222,15 +217,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { } } - private fun tryToSyncCalendar() { - val isCalendarCreated = calendarPreferencesManager.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST - val isCalendarSyncRequired = !Date(calendarPreferencesManager.lastCalendarSync).isToday() - val isCalendarSyncEnabled = calendarPreferencesManager.isCalendarSyncEnabled - if (isCalendarCreated && isCalendarSyncRequired && isCalendarSyncEnabled) { - calendarSyncServiceInitiator.startSyncCalendarService() - } - } - companion object { const val TOP_INSET = "topInset" const val BOTTOM_INSET = "bottomInset" 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 da797d36d..d5e15c405 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 @@ -84,12 +84,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getLong(CALENDAR_ID, CalendarManager.CALENDAR_DOES_NOT_EXIST) - override var lastCalendarSync: Long - set(value) { - saveLong(LAST_CALENDAR_SYNC, value) - } - get() = getLong(LAST_CALENDAR_SYNC) - override var user: User? set(value) { val userJson = Gson().toJson(value) @@ -176,7 +170,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences set(value) { saveBoolean(IS_CALENDAR_SYNC_ENABLED, value) } - get() = getBoolean(IS_CALENDAR_SYNC_ENABLED, false) + get() = getBoolean(IS_CALENDAR_SYNC_ENABLED, true) override var isRelativeDateEnabled: Boolean set(value) { @@ -211,7 +205,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val VIDEO_SETTINGS_DOWNLOAD_QUALITY = "video_settings_download_quality" private const val APP_CONFIG = "app_config" private const val CALENDAR_ID = "CALENDAR_ID" - private const val LAST_CALENDAR_SYNC = "LAST_CALENDAR_SYNC" private const val RESET_APP_DIRECTORY = "reset_app_directory" private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED" 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 db2ceaa63..67deb6641 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -63,9 +63,9 @@ import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.CalendarSyncServiceInitiator import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.worker.CalendarSyncScheduler import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.data.storage.WhatsNewPreferences @@ -86,7 +86,6 @@ val appModule = module { single { AppCookieManager(get(), get()) } single { ReviewManagerFactory.create(get()) } single { CalendarManager(get(), get(), get()) } - single { CalendarSyncServiceInitiator(get()) } single { ImageProcessor(get()) } @@ -195,4 +194,6 @@ val appModule = module { factory { OAuthHelper(get(), get(), get()) } factory { FileUtil(get()) } + + single { CalendarSyncScheduler(get()) } } 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 f81920d09..e06a650be 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -174,8 +174,8 @@ val screenModule = module { ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } - viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get()) } - viewModel { CoursesToSyncViewModel(get(), get()) } + viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { CoursesToSyncViewModel(get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } single { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } diff --git a/app/src/main/java/org/openedx/app/room/AppDatabase.kt b/app/src/main/java/org/openedx/app/room/AppDatabase.kt index eafb40d7a..1728dfe9b 100644 --- a/app/src/main/java/org/openedx/app/room/AppDatabase.kt +++ b/app/src/main/java/org/openedx/app/room/AppDatabase.kt @@ -4,6 +4,7 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity import org.openedx.core.data.model.room.CourseStructureEntity import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity import org.openedx.core.module.db.CalendarDao @@ -25,7 +26,8 @@ const val DATABASE_NAME = "OpenEdX_db" EnrolledCourseEntity::class, CourseStructureEntity::class, DownloadModelEntity::class, - CourseCalendarEventEntity::class + CourseCalendarEventEntity::class, + CourseCalendarStateEntity::class ], version = DATABASE_VERSION, exportSchema = false diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt index e78194049..62f3c30b4 100644 --- a/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarEventEntity.kt @@ -1,10 +1,8 @@ package org.openedx.core.data.model.room import androidx.room.ColumnInfo -import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey -import org.openedx.core.data.model.room.discovery.CourseDateBlockDb import org.openedx.core.domain.model.CourseCalendarEvent @Entity(tableName = "course_calendar_event_table") @@ -13,14 +11,11 @@ data class CourseCalendarEventEntity( @ColumnInfo("event_id") val eventId: Long, @ColumnInfo("course_id") - val courseId: String, - @Embedded - val courseDateBlockDb: CourseDateBlockDb, + val courseId: String ) { fun mapToDomain() = CourseCalendarEvent( courseId = courseId, - eventId = eventId, - courseDateBlock = courseDateBlockDb.mapToDomain() + eventId = eventId ) } diff --git a/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarStateEntity.kt b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarStateEntity.kt new file mode 100644 index 000000000..e2c39991c --- /dev/null +++ b/core/src/main/java/org/openedx/core/data/model/room/CourseCalendarStateEntity.kt @@ -0,0 +1,24 @@ +package org.openedx.core.data.model.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.openedx.core.domain.model.CourseCalendarState + +@Entity(tableName = "course_calendar_state_table") +data class CourseCalendarStateEntity( + @PrimaryKey + @ColumnInfo("course_id") + val courseId: String, + @ColumnInfo("checksum") + val checksum: Int = 0, + @ColumnInfo("is_course_sync_enabled") + val isCourseSyncEnabled: Boolean, +) { + + fun mapToDomain() = CourseCalendarState( + checksum = checksum, + courseId = courseId, + isCourseSyncEnabled = isCourseSyncEnabled + ) +} diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt index 6d133f443..67cdf2e19 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -2,7 +2,6 @@ package org.openedx.core.data.storage interface CalendarPreferences { var calendarId: Long - var lastCalendarSync: Long var isCalendarSyncEnabled: Boolean var isRelativeDateEnabled: Boolean var isHideInactiveCourses: Boolean diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index 9afae97cc..e6aa8e3fb 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -1,6 +1,7 @@ package org.openedx.core.domain.interactor import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity import org.openedx.core.repository.CalendarRepository class CalendarInteractor( @@ -15,7 +16,31 @@ class CalendarInteractor( repository.insertCourseCalendarEntity(*courseCalendarEntity) } - suspend fun getCourseCalendarEventEntityFromCache(): List { - return repository.getCourseCalendarEventEntityFromCache() + suspend fun getCourseCalendarEventsById(courseId: String): List { + return repository.getCourseCalendarEventsById(courseId) + } + + suspend fun deleteCourseCalendarEntitiesById(courseId: String) { + repository.deleteCourseCalendarEntitiesById(courseId) + } + + suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { + repository.insertCourseCalendarStateEntity(*courseCalendarStateEntity) + } + + suspend fun getCourseCalendarStateById(courseId: String): CourseCalendarStateEntity? { + return repository.getCourseCalendarStateById(courseId) + } + + suspend fun getAllCourseCalendarState(): List { + return repository.getAllCourseCalendarState() + } + + suspend fun updateCourseCalendarStateById( + courseId: String, + checksum: Int? = null, + isCourseSyncEnabled: Boolean? = null + ) { + return repository.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) } } diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt index 2b841969c..abda712cc 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt @@ -2,6 +2,5 @@ package org.openedx.core.domain.model data class CourseCalendarEvent( val courseId: String, - val eventId: Long, - val courseDateBlock: CourseDateBlock + val eventId: Long ) diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarState.kt b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarState.kt new file mode 100644 index 000000000..fefad4d82 --- /dev/null +++ b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarState.kt @@ -0,0 +1,7 @@ +package org.openedx.core.domain.model + +data class CourseCalendarState( + val checksum: Int, + val courseId: String, + val isCourseSyncEnabled: Boolean +) 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 394ebdd56..97f8612bf 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 @@ -32,4 +32,24 @@ data class CourseDateBlock( fun isTimeDifferenceLessThan24Hours(): Boolean { return (date.isToday() && date.before(Date())) || date.isTimeLessThan24Hours() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CourseDateBlock + + if (blockId != other.blockId) return false + if (date != other.date) return false + if (assignmentType != other.assignmentType) return false + + return true + } + + override fun hashCode(): Int { + var result = blockId.hashCode() + result = 31 * result + date.hashCode() + result = 31 * result + (assignmentType?.hashCode() ?: 0) + return result + } } diff --git a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt index 46b5acca5..c0bf134db 100644 --- a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt +++ b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt @@ -5,16 +5,42 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity @Dao interface CalendarDao { + // region CourseCalendarEventEntity @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) - @Query("DELETE FROM course_calendar_event_table") - suspend fun clearCachedData() + @Query("DELETE FROM course_calendar_event_table WHERE course_id = :courseId") + suspend fun deleteCourseCalendarEntitiesById(courseId: String) - @Query("SELECT * FROM course_calendar_event_table") - suspend fun readAllData(): List + @Query("SELECT * FROM course_calendar_event_table WHERE course_id=:courseId") + suspend fun readCourseCalendarEventsById(courseId: String): List + + // region CourseCalendarStateEntity + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) + + @Query("SELECT * FROM course_calendar_state_table WHERE course_id=:courseId") + suspend fun readCourseCalendarStateById(courseId: String): CourseCalendarStateEntity? + + @Query("SELECT * FROM course_calendar_state_table") + suspend fun readAllCourseCalendarState(): List + + @Query( + """ + UPDATE course_calendar_state_table + SET + checksum = COALESCE(:checksum, checksum), + is_course_sync_enabled = COALESCE(:isCourseSyncEnabled, is_course_sync_enabled) + WHERE course_id = :courseId""" + ) + suspend fun updateCourseCalendarStateById( + courseId: String, + checksum: Int? = null, + isCourseSyncEnabled: Boolean? = null + ) } diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index f3a067c35..f4ed68811 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -2,6 +2,7 @@ package org.openedx.core.repository import org.openedx.core.data.api.CourseApi import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.EnrollmentStatus import org.openedx.core.module.db.CalendarDao @@ -23,7 +24,31 @@ class CalendarRepository( calendarDao.insertCourseCalendarEntity(*courseCalendarEntity) } - suspend fun getCourseCalendarEventEntityFromCache(): List { - return calendarDao.readAllData() + suspend fun getCourseCalendarEventsById(courseId: String): List { + return calendarDao.readCourseCalendarEventsById(courseId) + } + + suspend fun deleteCourseCalendarEntitiesById(courseId: String) { + calendarDao.deleteCourseCalendarEntitiesById(courseId) + } + + suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { + calendarDao.insertCourseCalendarStateEntity(*courseCalendarStateEntity) + } + + suspend fun getCourseCalendarStateById(courseId: String): CourseCalendarStateEntity? { + return calendarDao.readCourseCalendarStateById(courseId) + } + + suspend fun getAllCourseCalendarState(): List { + return calendarDao.readAllCourseCalendarState() + } + + suspend fun updateCourseCalendarStateById( + courseId: String, + checksum: Int? = null, + isCourseSyncEnabled: Boolean? = null + ) { + return calendarDao.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 0271fbb62..c067cafc4 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -132,7 +132,7 @@ fun CalendarSettingsView( Column( modifier = contentWidth.padding(vertical = 28.dp), ) { - val coursesSynced = 5 + val coursesSynced = uiState.coursesSynced if (uiState.calendarData != null) { CalendarSyncSection( @@ -343,7 +343,8 @@ private fun CalendarSettingsViewPreview() { calendarData = null, calendarSyncState = CalendarSyncState.SYNCHRONIZATION, isCalendarSyncEnabled = false, - isRelativeDateEnabled = true + isRelativeDateEnabled = true, + coursesSynced = 5 ), onBackClick = {}, onCalendarSyncSwitchClick = {}, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt index bb2e8102f..47be25677 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt @@ -32,4 +32,4 @@ enum class CalendarSyncState( SYNCED -> MaterialTheme.appColors.accessGreen SYNCHRONIZATION -> MaterialTheme.appColors.primary } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index 1f93c4968..a13b36468 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -8,4 +8,5 @@ data class CalendarUIState( val calendarSyncState: CalendarSyncState, val isCalendarSyncEnabled: Boolean, val isRelativeDateEnabled: Boolean, + val coursesSynced: Int ) 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 7d0e8b29e..005ca99e1 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 @@ -10,23 +10,25 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.system.connection.NetworkConnection import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.CalendarSyncServiceInitiator import org.openedx.profile.system.notifier.CalendarCreated import org.openedx.profile.system.notifier.CalendarNotifier import org.openedx.profile.system.notifier.CalendarSyncFailed import org.openedx.profile.system.notifier.CalendarSynced import org.openedx.profile.system.notifier.CalendarSyncing +import org.openedx.profile.worker.CalendarSyncScheduler class CalendarViewModel( - private val calendarSyncServiceInitiator: CalendarSyncServiceInitiator, + private val calendarSyncScheduler: CalendarSyncScheduler, private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, - private val networkConnection: NetworkConnection, - private val profileRouter: ProfileRouter + private val calendarInteractor: CalendarInteractor, + private val profileRouter: ProfileRouter, + networkConnection: NetworkConnection, ) : BaseViewModel() { private val _uiState = MutableStateFlow( @@ -35,7 +37,8 @@ class CalendarViewModel( calendarData = null, calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, - isRelativeDateEnabled = calendarPreferences.isRelativeDateEnabled + isRelativeDateEnabled = calendarPreferences.isRelativeDateEnabled, + coursesSynced = 0 ) ) val uiState: StateFlow @@ -46,7 +49,8 @@ class CalendarViewModel( calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { CalendarCreated -> { - calendarSyncServiceInitiator.startSyncCalendarService() + calendarSyncScheduler.scheduleDailySync() + calendarSyncScheduler.requestImmediateSync() _uiState.update { it.copy(isCalendarExist = true) } getCalendarData() } @@ -57,6 +61,7 @@ class CalendarViewModel( CalendarSynced -> { _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNCED) } + updateSyncedCoursesCount() } CalendarSyncFailed -> { @@ -67,6 +72,7 @@ class CalendarViewModel( } getCalendarData() + updateSyncedCoursesCount() } fun setUpCalendarSync(permissionLauncher: ActivityResultLauncher>) { @@ -77,7 +83,7 @@ class CalendarViewModel( calendarPreferences.isCalendarSyncEnabled = isEnabled _uiState.update { it.copy(isCalendarSyncEnabled = isEnabled) } if (isEnabled) { - calendarSyncServiceInitiator.startSyncCalendarService() + calendarSyncScheduler.requestImmediateSync() } } @@ -96,4 +102,14 @@ class CalendarViewModel( _uiState.update { it.copy(calendarData = calendarData) } } } + + private fun updateSyncedCoursesCount() { + viewModelScope.launch { + calendarInteractor.getAllCourseCalendarState() + .count { it.isCourseSyncEnabled } + .let { coursesSynced -> + _uiState.update { it.copy(coursesSynced = coursesSynced) } + } + } + } } 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 9798fa6e0..4e06a46e5 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 @@ -52,7 +52,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import org.koin.androidx.compose.koinViewModel -import org.openedx.core.domain.model.EnrollmentStatus import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.displayCutoutForLandscape @@ -87,6 +86,9 @@ class CoursesToSyncFragment : Fragment() { onHideInactiveCoursesSwitchClick = { viewModel.setHideInactiveCoursesEnabled(it) }, + onCourseSyncCheckChange = { isEnabled, courseId -> + viewModel.setCourseSyncEnabled(isEnabled, courseId) + }, onBackClick = { requireActivity().supportFragmentManager.popBackStack() } @@ -97,11 +99,12 @@ class CoursesToSyncFragment : Fragment() { } @Composable -fun CoursesToSyncView( +private fun CoursesToSyncView( windowSize: WindowSize, onBackClick: () -> Unit, uiState: CoursesToSyncUIState, - onHideInactiveCoursesSwitchClick: (Boolean) -> Unit + onHideInactiveCoursesSwitchClick: (Boolean) -> Unit, + onCourseSyncCheckChange: (Boolean, String) -> Unit ) { val scaffoldState = rememberScaffoldState() @@ -176,7 +179,10 @@ fun CoursesToSyncView( onHideInactiveCoursesSwitchClick = onHideInactiveCoursesSwitchClick ) Spacer(modifier = Modifier.height(20.dp)) - SyncCourseTabRow(uiState.enrollmentsStatus) + SyncCourseTabRow( + uiState = uiState, + onCourseSyncCheckChange = onCourseSyncCheckChange + ) } } } @@ -185,8 +191,9 @@ fun CoursesToSyncView( } @Composable -fun SyncCourseTabRow( - enrollmentsStatus: List +private fun SyncCourseTabRow( + uiState: CoursesToSyncUIState, + onCourseSyncCheckChange: (Boolean, String) -> Unit ) { var selectedTab by remember { mutableStateOf(SyncCourseTab.SYNCED) } val selectedTabIndex = SyncCourseTab.entries.indexOf(selectedTab) @@ -224,7 +231,8 @@ fun SyncCourseTabRow( CourseCheckboxList( selectedTab = selectedTab, - enrollmentsStatus = enrollmentsStatus + uiState = uiState, + onCourseSyncCheckChange = onCourseSyncCheckChange ) } } @@ -233,12 +241,20 @@ fun SyncCourseTabRow( @Composable private fun CourseCheckboxList( selectedTab: SyncCourseTab, - enrollmentsStatus: List + uiState: CoursesToSyncUIState, + onCourseSyncCheckChange: (Boolean, String) -> Unit ) { LazyColumn( modifier = Modifier.padding(8.dp), ) { - items(enrollmentsStatus) { course -> + val courseList = uiState.run { + val isSyncEnabled = selectedTab == SyncCourseTab.SYNCED + val courseIds = coursesCalendarState.filter { it.isCourseSyncEnabled == isSyncEnabled }.map { it.courseId } + enrollmentsStatus.filter { it.courseId in courseIds } + } + items(courseList) { course -> + val isCourseSyncEnabled = + uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled ?: false val annotatedString = buildAnnotatedString { append(course.courseName) if (!course.isActive) { @@ -257,6 +273,7 @@ private fun CourseCheckboxList( } } + if (uiState.isHideInactiveCourses && !course.isActive) return@items Row( modifier = Modifier .fillMaxWidth() @@ -269,10 +286,10 @@ private fun CourseCheckboxList( checkedColor = MaterialTheme.appColors.primary, uncheckedColor = MaterialTheme.appColors.textFieldText ), - checked = true, + checked = isCourseSyncEnabled, enabled = course.isActive, - onCheckedChange = { - //TODO + onCheckedChange = { isEnabled -> + onCourseSyncCheckChange(isEnabled, course.courseId) } ) Spacer(modifier = Modifier.width(8.dp)) @@ -288,7 +305,7 @@ private fun CourseCheckboxList( @OptIn(ExperimentalMaterialApi::class) @Composable -fun HideInactiveCoursesView( +private fun HideInactiveCoursesView( isHideInactiveCourses: Boolean, onHideInactiveCoursesSwitchClick: (Boolean) -> Unit ) { @@ -328,10 +345,12 @@ private fun CoursesToSyncViewPreview() { windowSize = rememberWindowSize(), uiState = CoursesToSyncUIState( enrollmentsStatus = emptyList(), + coursesCalendarState = emptyList(), isHideInactiveCourses = true ), onHideInactiveCoursesSwitchClick = {}, + onCourseSyncCheckChange = { _, _ -> }, onBackClick = {} ) } -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt index 9e9b09b49..275343848 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt @@ -1,8 +1,10 @@ package org.openedx.profile.presentation.calendar +import org.openedx.core.domain.model.CourseCalendarState import org.openedx.core.domain.model.EnrollmentStatus data class CoursesToSyncUIState( val enrollmentsStatus: List, + val coursesCalendarState: List, val isHideInactiveCourses: Boolean ) 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 ee004794f..5ba286169 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 @@ -9,15 +9,18 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.profile.worker.CalendarSyncScheduler class CoursesToSyncViewModel( private val calendarInteractor: CalendarInteractor, - private val calendarPreferences: CalendarPreferences + private val calendarPreferences: CalendarPreferences, + private val calendarSyncScheduler: CalendarSyncScheduler, ) : BaseViewModel() { private val _uiState = MutableStateFlow( CoursesToSyncUIState( enrollmentsStatus = emptyList(), + coursesCalendarState = emptyList(), isHideInactiveCourses = calendarPreferences.isHideInactiveCourses ) ) @@ -26,6 +29,7 @@ class CoursesToSyncViewModel( init { getEnrollmentsStatus() + getCourseCalendarState() } fun setHideInactiveCoursesEnabled(isEnabled: Boolean) { @@ -33,6 +37,24 @@ class CoursesToSyncViewModel( _uiState.update { it.copy(isHideInactiveCourses = isEnabled) } } + fun setCourseSyncEnabled(isEnabled: Boolean, courseId: String) { + viewModelScope.launch { + calendarInteractor.updateCourseCalendarStateById( + courseId = courseId, + isCourseSyncEnabled = isEnabled + ) + getCourseCalendarState() + calendarSyncScheduler.requestImmediateSync(courseId) + } + } + + private fun getCourseCalendarState() { + viewModelScope.launch { + val coursesCalendarState = calendarInteractor.getAllCourseCalendarState().map { it.mapToDomain() } + _uiState.update { it.copy(coursesCalendarState = coursesCalendarState) } + } + } + private fun getEnrollmentsStatus() { viewModelScope.launch { val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt index a3d2e7d55..d0645cb54 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt @@ -9,4 +9,4 @@ enum class SyncCourseTab( ) { SYNCED(R.string.profile_synced), NOT_SYNCED(R.string.profile_not_synced) -} \ No newline at end of file +} diff --git a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt b/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt deleted file mode 100644 index e5d03e3a4..000000000 --- a/profile/src/main/java/org/openedx/profile/service/CalendarSyncService.kt +++ /dev/null @@ -1,83 +0,0 @@ -package org.openedx.profile.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject -import org.openedx.core.data.model.room.CourseCalendarEventEntity -import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.core.domain.interactor.CalendarInteractor -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.profile.system.notifier.CalendarSyncFailed -import org.openedx.profile.system.notifier.CalendarSynced -import org.openedx.profile.system.notifier.CalendarSyncing -import java.util.Date - -class CalendarSyncService : Service() { - - private val calendarManager: CalendarManager by inject() - private val calendarInteractor: CalendarInteractor by inject() - private val calendarNotifier: CalendarNotifier by inject() - private val calendarPreferences: CalendarPreferences by inject() - - override fun onBind(intent: Intent?): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - syncCalendar() - return START_STICKY - } - - private fun syncCalendar() { - CoroutineScope(Dispatchers.IO).launch { - calendarNotifier.send(CalendarSyncing) - try { - val courseDatesDeferredResults = calendarInteractor.getEnrollmentsStatus() - .filter { it.isActive } - .map { enrollmentStatus -> - async(Dispatchers.IO) { - Pair(enrollmentStatus, calendarInteractor.getCourseDates(enrollmentStatus.courseId)) - } - } - val calendarSyncDeferred = - courseDatesDeferredResults.awaitAll().map { (enrollmentStatus, courseDatesResult) -> - async { - courseDatesResult.courseDateBlocks.forEach { courseDateBlock -> - courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> - val eventId = calendarManager.addEventsIntoCalendar( - calendarId = calendarPreferences.calendarId, - courseId = enrollmentStatus.courseId, - courseName = enrollmentStatus.courseName, - courseDateBlock = domainCourseDateBlock - ) - courseDateBlock.mapToRoomEntity()?.let { courseDateBlockDb -> - val courseCalendarEventEntity = CourseCalendarEventEntity( - courseId = enrollmentStatus.courseId, - eventId = eventId, - courseDateBlockDb = courseDateBlockDb - ) - calendarInteractor.insertCourseCalendarEntity(courseCalendarEventEntity) - } - } - } - } - } - calendarSyncDeferred.awaitAll() - calendarPreferences.lastCalendarSync = Date().time - calendarNotifier.send(CalendarSynced) - Log.e("___", "${calendarInteractor.getCourseCalendarEventEntityFromCache()}") - } catch (e: Exception) { - Log.e("___", "$e") - calendarNotifier.send(CalendarSyncFailed) - } - } - } -} diff --git a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt index eeba219db..f82b60c66 100644 --- a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt +++ b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt @@ -326,7 +326,7 @@ class CalendarManager( /** * Method to delete the course calendar from the mobile calendar app */ - fun deleteCalendar(calendarId: Long) { + private fun deleteCalendar(calendarId: Long) { context.contentResolver.delete( Uri.parse("content://com.android.calendar/calendars/$calendarId"), null, @@ -434,6 +434,16 @@ class CalendarManager( } } + fun deleteEvent(eventId: Long) { + val deleteUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId) + val rows = context.contentResolver.delete(deleteUri, null, null) + if (rows > 0) { + logger.d { "Event deleted successfully" } + } else { + logger.d { "Event deletion failed" } + } + } + companion object { const val CALENDAR_DOES_NOT_EXIST = -1L const val EVENT_DOES_NOT_EXIST = -1L diff --git a/profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt b/profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt deleted file mode 100644 index 4810868b5..000000000 --- a/profile/src/main/java/org/openedx/profile/system/CalendarSyncServiceInitiator.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.openedx.profile.system - -import android.content.Context -import android.content.Intent -import org.openedx.profile.service.CalendarSyncService - -class CalendarSyncServiceInitiator(private val context: Context) { - - fun startSyncCalendarService() { - val intent = Intent(context, CalendarSyncService::class.java) - context.startService(intent) - } -} diff --git a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt new file mode 100644 index 000000000..3e7ff251d --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt @@ -0,0 +1,39 @@ +package org.openedx.profile.worker + +import android.content.Context +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit + +class CalendarSyncScheduler(private val context: Context) { + + fun scheduleDailySync() { + val periodicWorkRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .addTag(CalendarSyncWorker.WORKER_TAG) + .build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + CalendarSyncWorker.WORKER_TAG, + ExistingPeriodicWorkPolicy.KEEP, + periodicWorkRequest + ) + } + + fun requestImmediateSync() { + val syncWorkRequest = OneTimeWorkRequestBuilder().build() + WorkManager.getInstance(context).enqueue(syncWorkRequest) + } + + fun requestImmediateSync(courseId: String) { + val inputData = Data.Builder() + .putString(CalendarSyncWorker.ARG_COURSE_ID, courseId) + .build() + val syncWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + WorkManager.getInstance(context).enqueue(syncWorkRequest) + } +} diff --git a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt new file mode 100644 index 000000000..6b49b9bdd --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt @@ -0,0 +1,153 @@ +package org.openedx.profile.worker + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.openedx.core.data.model.CourseDates +import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity +import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.domain.model.EnrollmentStatus +import org.openedx.profile.system.CalendarManager +import org.openedx.profile.system.notifier.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarSyncFailed +import org.openedx.profile.system.notifier.CalendarSynced +import org.openedx.profile.system.notifier.CalendarSyncing + +class CalendarSyncWorker( + context: Context, + workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams), KoinComponent { + + private val calendarManager: CalendarManager by inject() + private val calendarInteractor: CalendarInteractor by inject() + private val calendarNotifier: CalendarNotifier by inject() + private val calendarPreferences: CalendarPreferences by inject() + + override suspend fun doWork(): Result { + return try { + val courseId = inputData.getString(ARG_COURSE_ID) + tryToSyncCalendar(courseId) + Result.success() + } catch (e: Exception) { + calendarNotifier.send(CalendarSyncFailed) + Result.failure() + } + } + + private suspend fun tryToSyncCalendar(courseId: String?) { + val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled + if (isCalendarCreated && isCalendarSyncEnabled) { + calendarNotifier.send(CalendarSyncing) + if (courseId.isNullOrEmpty()) { + syncCalendar() + } else { + syncCalendar(courseId) + } + calendarNotifier.send(CalendarSynced) + } + } + + private suspend fun syncCalendar(courseId: String) { + calendarInteractor.getEnrollmentsStatus() + .find { it.courseId == courseId } + ?.let { enrollmentStatus -> + syncCourseEvents(enrollmentStatus) + } + } + + private suspend fun syncCalendar() { + calendarInteractor.getEnrollmentsStatus() + .forEach { enrollmentStatus -> + syncCourseEvents(enrollmentStatus) + } + } + + private suspend fun syncCourseEvents(enrollmentStatus: EnrollmentStatus) { + createCalendarState(enrollmentStatus) + val courseId = enrollmentStatus.courseId + if (enrollmentStatus.isActive && isCourseSyncEnabled(courseId)) { + val courseDates = calendarInteractor.getCourseDates(courseId) + val isCourseCalendarUpToDate = isCourseCalendarUpToDate(courseId, courseDates) + if (!isCourseCalendarUpToDate) { + removeCalendarEvents(courseId) + updateCourseEvents(courseDates, enrollmentStatus) + } + } else { + removeCalendarEvents(courseId) + } + } + + private suspend fun updateCourseEvents(courseDates: CourseDates, enrollmentStatus: EnrollmentStatus) { + courseDates.courseDateBlocks.forEach { courseDateBlock -> + courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> + createEvent(domainCourseDateBlock, enrollmentStatus) + } + } + calendarInteractor.updateCourseCalendarStateById( + courseId = enrollmentStatus.courseId, + checksum = getCourseChecksum(courseDates) + ) + } + + private suspend fun removeCalendarEvents(courseId: String) { + calendarInteractor.getCourseCalendarEventsById(courseId).forEach { + calendarManager.deleteEvent(it.eventId) + } + calendarInteractor.deleteCourseCalendarEntitiesById(courseId) + calendarInteractor.updateCourseCalendarStateById(courseId = courseId, checksum = 0) + } + + private suspend fun createEvent(courseDateBlock: CourseDateBlock, enrollmentStatus: EnrollmentStatus) { + val eventId = calendarManager.addEventsIntoCalendar( + calendarId = calendarPreferences.calendarId, + courseId = enrollmentStatus.courseId, + courseName = enrollmentStatus.courseName, + courseDateBlock = courseDateBlock + ) + val courseCalendarEventEntity = CourseCalendarEventEntity( + courseId = enrollmentStatus.courseId, + eventId = eventId + ) + calendarInteractor.insertCourseCalendarEntity(courseCalendarEventEntity) + } + + private suspend fun createCalendarState(enrollmentStatus: EnrollmentStatus) { + val courseCalendarStateChecksum = getCourseCalendarStateChecksum(enrollmentStatus.courseId) + if (courseCalendarStateChecksum == null) { + val courseCalendarStateEntity = CourseCalendarStateEntity( + courseId = enrollmentStatus.courseId, + isCourseSyncEnabled = enrollmentStatus.isActive + ) + calendarInteractor.insertCourseCalendarStateEntity(courseCalendarStateEntity) + } + } + + private suspend fun isCourseCalendarUpToDate(courseId: String, courseDates: CourseDates): Boolean { + val oldChecksum = getCourseCalendarStateChecksum(courseId) + val newChecksum = getCourseChecksum(courseDates) + return newChecksum == oldChecksum + } + + private suspend fun isCourseSyncEnabled(courseId: String): Boolean { + return calendarInteractor.getCourseCalendarStateById(courseId)?.mapToDomain()?.isCourseSyncEnabled ?: true + } + + private fun getCourseChecksum(courseDates: CourseDates): Int { + return courseDates.courseDateBlocks.sumOf { it.mapToDomain().hashCode() } + } + + private suspend fun getCourseCalendarStateChecksum(courseId: String): Int? { + return calendarInteractor.getCourseCalendarStateById(courseId)?.mapToDomain()?.checksum + } + + companion object { + const val ARG_COURSE_ID = "ARG_COURSE_ID" + const val WORKER_TAG = "calendar_sync_worker_tag" + } +} From 02b0a63db6fb781e3897cc0d5e9f9d2d13c32e5b Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 5 Jun 2024 18:50:52 +0300 Subject: [PATCH 15/26] feat: DisableCalendarSyncDialogFragment --- .../java/org/openedx/app/di/ScreenModule.kt | 2 + .../openedx/core/domain/model/CalendarData.kt | 6 +- core/src/main/res/values/strings.xml | 1 + .../presentation/calendar/CalendarFragment.kt | 2 +- .../calendar/CalendarSettingsView.kt | 5 +- .../calendar/CalendarViewModel.kt | 21 +- .../DisableCalendarSyncDialogFragment.kt | 188 ++++++++++++++++++ .../DisableCalendarSyncDialogViewModel.kt | 21 ++ .../system/notifier/CalendarSyncDisabled.kt | 3 + profile/src/main/res/values/strings.xml | 2 + 10 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt 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 e06a650be..932869ae4 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -60,6 +60,7 @@ import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.anothersaccount.AnothersProfileViewModel import org.openedx.profile.presentation.calendar.CalendarViewModel import org.openedx.profile.presentation.calendar.CoursesToSyncViewModel +import org.openedx.profile.presentation.calendar.DisableCalendarSyncDialogViewModel import org.openedx.profile.presentation.calendar.NewCalendarDialogViewModel import org.openedx.profile.presentation.delete.DeleteProfileViewModel import org.openedx.profile.presentation.edit.EditProfileViewModel @@ -177,6 +178,7 @@ val screenModule = module { viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { CoursesToSyncViewModel(get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get()) } + viewModel { DisableCalendarSyncDialogViewModel(get(), get()) } single { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } diff --git a/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt b/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt index 91f5b60c9..849d2f303 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CalendarData.kt @@ -1,6 +1,10 @@ package org.openedx.core.domain.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize data class CalendarData( val title: String, val color: Int -) +) : Parcelable diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index afbc28243..551c671c6 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -171,4 +171,5 @@ Discussions More Dates + Disable Syncing diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index c87d12b98..c9b0700ca 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -57,7 +57,7 @@ class CalendarFragment : Fragment() { requireActivity().supportFragmentManager.popBackStack() }, onCalendarSyncSwitchClick = { - viewModel.setCalendarSyncEnabled(it) + viewModel.setCalendarSyncEnabled(it, requireActivity().supportFragmentManager) }, onRelativeDateSwitchClick = { viewModel.setRelativeDateEnabled(it) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index c067cafc4..a1a091e6a 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -340,8 +341,8 @@ private fun CalendarSettingsViewPreview() { windowSize = WindowSize(WindowType.Compact, WindowType.Compact), uiState = CalendarUIState( isCalendarExist = true, - calendarData = null, - calendarSyncState = CalendarSyncState.SYNCHRONIZATION, + calendarData = CalendarData("calendar", Color.Red.toArgb()), + calendarSyncState = CalendarSyncState.SYNCED, isCalendarSyncEnabled = false, isRelativeDateEnabled = true, coursesSynced = 5 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 005ca99e1..6b6a67420 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 @@ -16,6 +16,7 @@ import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.CalendarManager import org.openedx.profile.system.notifier.CalendarCreated import org.openedx.profile.system.notifier.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarSyncDisabled import org.openedx.profile.system.notifier.CalendarSyncFailed import org.openedx.profile.system.notifier.CalendarSynced import org.openedx.profile.system.notifier.CalendarSyncing @@ -67,6 +68,10 @@ class CalendarViewModel( CalendarSyncFailed -> { _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNC_FAILED) } } + + CalendarSyncDisabled -> { + _uiState.update { it.copy(isCalendarSyncEnabled = false) } + } } } } @@ -79,10 +84,18 @@ class CalendarViewModel( permissionLauncher.launch(calendarManager.permissions) } - fun setCalendarSyncEnabled(isEnabled: Boolean) { - calendarPreferences.isCalendarSyncEnabled = isEnabled - _uiState.update { it.copy(isCalendarSyncEnabled = isEnabled) } - if (isEnabled) { + fun setCalendarSyncEnabled(isEnabled: Boolean, fragmentManager: FragmentManager) { + if (!isEnabled) { + _uiState.value.calendarData?.let { + val dialog = DisableCalendarSyncDialogFragment.newInstance(it) + dialog.show( + fragmentManager, + DisableCalendarSyncDialogFragment.DIALOG_TAG + ) + } + } else { + calendarPreferences.isCalendarSyncEnabled = true + _uiState.update { it.copy(isCalendarSyncEnabled = true) } calendarSyncScheduler.requestImmediateSync() } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt new file mode 100644 index 000000000..1fd51721f --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt @@ -0,0 +1,188 @@ +package org.openedx.profile.presentation.calendar + +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.R +import org.openedx.core.domain.model.CalendarData +import org.openedx.core.extension.parcelable +import org.openedx.core.presentation.dialog.DefaultDialogBox +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.OpenEdXOutlinedButton +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.theme.appTypography +import androidx.compose.ui.graphics.Color as ComposeColor + +class DisableCalendarSyncDialogFragment : DialogFragment() { + + private val viewModel by viewModel() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + DisableCalendarSyncDialogView( + calendarData = requireArguments().parcelable(ARG_CALENDAR_DATA), + onCancelClick = { + dismiss() + }, + onDisableSyncingClick = { + viewModel.disableSyncingClick() + dismiss() + } + ) + } + } + } + + companion object { + const val DIALOG_TAG = "DisableCalendarSyncDialogFragment" + const val ARG_CALENDAR_DATA = "ARG_CALENDAR_DATA" + + fun newInstance( + calendarData: CalendarData + ): DisableCalendarSyncDialogFragment { + val fragment = DisableCalendarSyncDialogFragment() + fragment.arguments = bundleOf( + ARG_CALENDAR_DATA to calendarData + ) + return fragment + } + } +} + +@Composable +fun DisableCalendarSyncDialogView( + modifier: Modifier = Modifier, + calendarData: CalendarData?, + onCancelClick: () -> Unit, + onDisableSyncingClick: () -> Unit +) { + DefaultDialogBox( + modifier = modifier, + onDismissClick = onCancelClick + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.core_ic_warning), + contentDescription = null + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = org.openedx.profile.R.string.profile_disable_calendar_dialog_title), + style = MaterialTheme.appTypography.titleLarge, + color = MaterialTheme.appColors.textDark + ) + } + calendarData?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.appShapes.cardShape) + .background(MaterialTheme.appColors.cardViewBackground) + .padding(vertical = 16.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier + .size(18.dp) + .clip(CircleShape) + .background(ComposeColor(calendarData.color)) + ) + Text( + text = calendarData.title, + style = MaterialTheme.appTypography.bodyMedium.copy( + textDecoration = TextDecoration.LineThrough + ), + color = MaterialTheme.appColors.textDark + ) + } + } + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = org.openedx.profile.R.string.profile_disable_calendar_dialog_description), + style = MaterialTheme.appTypography.bodyMedium, + color = MaterialTheme.appColors.textDark + ) + + OpenEdXOutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.profile_disable_syncing), + backgroundColor = MaterialTheme.appColors.background, + borderColor = MaterialTheme.appColors.primaryButtonBackground, + textColor = MaterialTheme.appColors.primaryButtonBackground, + onClick = { + onDisableSyncingClick() + } + ) + OpenEdXButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.core_cancel), + onClick = { + onCancelClick() + } + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewCalendarDialogPreview() { + OpenEdXTheme { + DisableCalendarSyncDialogView( + calendarData = CalendarData("calendar", Color.GREEN), + onCancelClick = { }, + onDisableSyncingClick = { } + ) + } +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt new file mode 100644 index 000000000..10bcf08a5 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt @@ -0,0 +1,21 @@ +package org.openedx.profile.presentation.calendar + +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.profile.system.notifier.CalendarNotifier +import org.openedx.profile.system.notifier.CalendarSyncDisabled + +class DisableCalendarSyncDialogViewModel( + private val calendarPreferences: CalendarPreferences, + private val calendarNotifier: CalendarNotifier +) : BaseViewModel() { + + fun disableSyncingClick() { + calendarPreferences.isCalendarSyncEnabled = false + viewModelScope.launch { + calendarNotifier.send(CalendarSyncDisabled) + } + } +} diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt new file mode 100644 index 000000000..c2290f3b9 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier + +object CalendarSyncDisabled : CalendarEvent diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index deafb879a..f5aa71051 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -77,5 +77,7 @@ Not Synced Inactive Hide Inactive Courses + Disable Calendar Sync + Disabling calendar sync will delete the calendar “My Assignments.” You can turn calendar sync back on at any time. From f371e98bb3a94e48b6be5bab05091937a433660e Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 5 Jun 2024 19:15:15 +0300 Subject: [PATCH 16/26] refactor: Calendar repository refactoring, minor UI fixes --- .../app/data/storage/PreferencesManager.kt | 7 - .../core/data/storage/CalendarPreferences.kt | 1 - .../domain/interactor/CalendarInteractor.kt | 30 +++-- .../core/repository/CalendarRepository.kt | 24 ++-- .../presentation/calendar/CalendarFragment.kt | 9 +- .../calendar/CalendarSettingsView.kt | 50 +------- .../presentation/calendar/CalendarUIState.kt | 1 - .../calendar/CalendarViewModel.kt | 8 +- .../calendar/CoursesToSyncFragment.kt | 121 ++++++++++-------- .../calendar/CoursesToSyncUIState.kt | 3 +- .../calendar/CoursesToSyncViewModel.kt | 9 +- .../DisableCalendarSyncDialogFragment.kt | 4 +- .../calendar/NewCalendarDialogFragment.kt | 24 ++-- .../calendar/NewCalendarDialogType.kt | 9 ++ .../profile/worker/CalendarSyncWorker.kt | 16 +-- 15 files changed, 141 insertions(+), 175 deletions(-) create mode 100644 profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogType.kt 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 d5e15c405..7578d799c 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 @@ -172,12 +172,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getBoolean(IS_CALENDAR_SYNC_ENABLED, true) - override var isRelativeDateEnabled: Boolean - set(value) { - saveBoolean(IS_RELATIVE_DATES_ENABLED, value) - } - get() = getBoolean(IS_RELATIVE_DATES_ENABLED, true) - override var isHideInactiveCourses: Boolean set(value) { saveBoolean(HIDE_INACTIVE_COURSES, value) @@ -207,7 +201,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val CALENDAR_ID = "CALENDAR_ID" private const val RESET_APP_DIRECTORY = "reset_app_directory" private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" - private const val IS_RELATIVE_DATES_ENABLED = "IS_RELATIVE_DATES_ENABLED" private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES" } } diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt index 67cdf2e19..4efd79881 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -3,6 +3,5 @@ package org.openedx.core.data.storage interface CalendarPreferences { var calendarId: Long var isCalendarSyncEnabled: Boolean - var isRelativeDateEnabled: Boolean var isHideInactiveCourses: Boolean } diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index e6aa8e3fb..0154bd373 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -2,6 +2,8 @@ package org.openedx.core.domain.interactor import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseCalendarStateEntity +import org.openedx.core.domain.model.CourseCalendarEvent +import org.openedx.core.domain.model.CourseCalendarState import org.openedx.core.repository.CalendarRepository class CalendarInteractor( @@ -12,35 +14,35 @@ class CalendarInteractor( suspend fun getCourseDates(courseId: String) = repository.getCourseDates(courseId) - suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) { - repository.insertCourseCalendarEntity(*courseCalendarEntity) + suspend fun insertCourseCalendarEntityToCache(vararg courseCalendarEntity: CourseCalendarEventEntity) { + repository.insertCourseCalendarEntityToCache(*courseCalendarEntity) } - suspend fun getCourseCalendarEventsById(courseId: String): List { - return repository.getCourseCalendarEventsById(courseId) + suspend fun getCourseCalendarEventsByIdFromCache(courseId: String): List { + return repository.getCourseCalendarEventsByIdFromCache(courseId) } - suspend fun deleteCourseCalendarEntitiesById(courseId: String) { - repository.deleteCourseCalendarEntitiesById(courseId) + suspend fun deleteCourseCalendarEntitiesByIdFromCache(courseId: String) { + repository.deleteCourseCalendarEntitiesByIdFromCache(courseId) } - suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { - repository.insertCourseCalendarStateEntity(*courseCalendarStateEntity) + suspend fun insertCourseCalendarStateEntityToCache(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { + repository.insertCourseCalendarStateEntityToCache(*courseCalendarStateEntity) } - suspend fun getCourseCalendarStateById(courseId: String): CourseCalendarStateEntity? { - return repository.getCourseCalendarStateById(courseId) + suspend fun getCourseCalendarStateByIdFromCache(courseId: String): CourseCalendarState? { + return repository.getCourseCalendarStateByIdFromCache(courseId) } - suspend fun getAllCourseCalendarState(): List { - return repository.getAllCourseCalendarState() + suspend fun getAllCourseCalendarStateFromCache(): List { + return repository.getAllCourseCalendarStateFromCache() } - suspend fun updateCourseCalendarStateById( + suspend fun updateCourseCalendarStateByIdInCache( courseId: String, checksum: Int? = null, isCourseSyncEnabled: Boolean? = null ) { - return repository.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) + repository.updateCourseCalendarStateByIdInCache(courseId, checksum, isCourseSyncEnabled) } } diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index f4ed68811..e822ca237 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -4,6 +4,8 @@ import org.openedx.core.data.api.CourseApi import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseCalendarStateEntity import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.model.CourseCalendarEvent +import org.openedx.core.domain.model.CourseCalendarState import org.openedx.core.domain.model.EnrollmentStatus import org.openedx.core.module.db.CalendarDao @@ -20,35 +22,35 @@ class CalendarRepository( suspend fun getCourseDates(courseId: String) = api.getCourseDates(courseId) - suspend fun insertCourseCalendarEntity(vararg courseCalendarEntity: CourseCalendarEventEntity) { + suspend fun insertCourseCalendarEntityToCache(vararg courseCalendarEntity: CourseCalendarEventEntity) { calendarDao.insertCourseCalendarEntity(*courseCalendarEntity) } - suspend fun getCourseCalendarEventsById(courseId: String): List { - return calendarDao.readCourseCalendarEventsById(courseId) + suspend fun getCourseCalendarEventsByIdFromCache(courseId: String): List { + return calendarDao.readCourseCalendarEventsById(courseId).map { it.mapToDomain() } } - suspend fun deleteCourseCalendarEntitiesById(courseId: String) { + suspend fun deleteCourseCalendarEntitiesByIdFromCache(courseId: String) { calendarDao.deleteCourseCalendarEntitiesById(courseId) } - suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { + suspend fun insertCourseCalendarStateEntityToCache(vararg courseCalendarStateEntity: CourseCalendarStateEntity) { calendarDao.insertCourseCalendarStateEntity(*courseCalendarStateEntity) } - suspend fun getCourseCalendarStateById(courseId: String): CourseCalendarStateEntity? { - return calendarDao.readCourseCalendarStateById(courseId) + suspend fun getCourseCalendarStateByIdFromCache(courseId: String): CourseCalendarState? { + return calendarDao.readCourseCalendarStateById(courseId)?.mapToDomain() } - suspend fun getAllCourseCalendarState(): List { - return calendarDao.readAllCourseCalendarState() + suspend fun getAllCourseCalendarStateFromCache(): List { + return calendarDao.readAllCourseCalendarState().map { it.mapToDomain() } } - suspend fun updateCourseCalendarStateById( + suspend fun updateCourseCalendarStateByIdInCache( courseId: String, checksum: Int? = null, isCourseSyncEnabled: Boolean? = null ) { - return calendarDao.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) + calendarDao.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt index c9b0700ca..112a4e774 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarFragment.kt @@ -21,7 +21,7 @@ class CalendarFragment : Fragment() { ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (!isGranted.containsValue(false)) { - val dialog = NewCalendarDialogFragment.newInstance(DialogType.CREATE_NEW) + val dialog = NewCalendarDialogFragment.newInstance(NewCalendarDialogType.CREATE_NEW) dialog.show( requireActivity().supportFragmentManager, NewCalendarDialogFragment.DIALOG_TAG @@ -59,11 +59,8 @@ class CalendarFragment : Fragment() { onCalendarSyncSwitchClick = { viewModel.setCalendarSyncEnabled(it, requireActivity().supportFragmentManager) }, - onRelativeDateSwitchClick = { - viewModel.setRelativeDateEnabled(it) - }, onChangeSyncOptionClick = { - val dialog = NewCalendarDialogFragment.newInstance(DialogType.UPDATE) + val dialog = NewCalendarDialogFragment.newInstance(NewCalendarDialogType.UPDATE) dialog.show( requireActivity().supportFragmentManager, NewCalendarDialogFragment.DIALOG_TAG @@ -87,7 +84,6 @@ private fun CalendarView( onChangeSyncOptionClick: () -> Unit, onCourseToSyncClick: () -> Unit, onCalendarSyncSwitchClick: (Boolean) -> Unit, - onRelativeDateSwitchClick: (Boolean) -> Unit ) { if (!uiState.isCalendarExist) { CalendarSetUpView( @@ -101,7 +97,6 @@ private fun CalendarView( uiState = uiState, onBackClick = onBackClick, onCalendarSyncSwitchClick = onCalendarSyncSwitchClick, - onRelativeDateSwitchClick = onRelativeDateSwitchClick, onChangeSyncOptionClick = onChangeSyncOptionClick, onCourseToSyncClick = onCourseToSyncClick ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index a1a091e6a..33ba5df38 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -22,6 +22,7 @@ import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable @@ -63,7 +64,6 @@ fun CalendarSettingsView( windowSize: WindowSize, uiState: CalendarUIState, onCalendarSyncSwitchClick: (Boolean) -> Unit, - onRelativeDateSwitchClick: (Boolean) -> Unit, onChangeSyncOptionClick: () -> Unit, onCourseToSyncClick: () -> Unit, onBackClick: () -> Unit @@ -149,11 +149,6 @@ fun CalendarSettingsView( coursesSynced = coursesSynced, onCourseToSyncClick = onCourseToSyncClick ) - Spacer(modifier = Modifier.height(32.dp)) - OptionsSection( - isRelativeDatesEnabled = uiState.isRelativeDateEnabled, - onRelativeDateSwitchClick = onRelativeDateSwitchClick - ) } } } @@ -232,7 +227,10 @@ fun CalendarSyncSection( modifier = Modifier .padding(0.dp), checked = isCourseCalendarSyncEnabled, - onCheckedChange = onCalendarSyncSwitchClick + onCheckedChange = onCalendarSyncSwitchClick, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.appColors.textAccent + ) ) } } @@ -287,42 +285,6 @@ fun CoursesToSyncSection( } } -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun OptionsSection( - isRelativeDatesEnabled: Boolean, - onRelativeDateSwitchClick: (Boolean) -> Unit -) { - Column { - SectionTitle(stringResource(R.string.profile_options)) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.profile_use_relative_dates), - style = MaterialTheme.appTypography.titleMedium, - color = MaterialTheme.appColors.textDark - ) - CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { - Switch( - modifier = Modifier - .padding(0.dp), - checked = isRelativeDatesEnabled, - onCheckedChange = onRelativeDateSwitchClick - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(R.string.profile_show_relative_dates), - style = MaterialTheme.appTypography.labelMedium, - color = MaterialTheme.appColors.textPrimaryVariant - ) - } -} - @Composable fun SectionTitle(title: String) { Text( @@ -344,12 +306,10 @@ private fun CalendarSettingsViewPreview() { calendarData = CalendarData("calendar", Color.Red.toArgb()), calendarSyncState = CalendarSyncState.SYNCED, isCalendarSyncEnabled = false, - isRelativeDateEnabled = true, coursesSynced = 5 ), onBackClick = {}, onCalendarSyncSwitchClick = {}, - onRelativeDateSwitchClick = {}, onChangeSyncOptionClick = {}, onCourseToSyncClick = {} ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index a13b36468..e111fd4be 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -7,6 +7,5 @@ data class CalendarUIState( val calendarData: CalendarData? = null, val calendarSyncState: CalendarSyncState, val isCalendarSyncEnabled: Boolean, - val isRelativeDateEnabled: Boolean, val coursesSynced: Int ) 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 6b6a67420..9a9f54372 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 @@ -38,7 +38,6 @@ class CalendarViewModel( calendarData = null, calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, - isRelativeDateEnabled = calendarPreferences.isRelativeDateEnabled, coursesSynced = 0 ) ) @@ -100,11 +99,6 @@ class CalendarViewModel( } } - fun setRelativeDateEnabled(isEnabled: Boolean) { - calendarPreferences.isRelativeDateEnabled = isEnabled - _uiState.update { it.copy(isRelativeDateEnabled = isEnabled) } - } - fun navigateToCoursesToSync(fragmentManager: FragmentManager) { profileRouter.navigateToCoursesToSync(fragmentManager) } @@ -118,7 +112,7 @@ class CalendarViewModel( private fun updateSyncedCoursesCount() { viewModelScope.launch { - calendarInteractor.getAllCourseCalendarState() + calendarInteractor.getAllCourseCalendarStateFromCache() .count { it.isCourseSyncEnabled } .let { coursesSynced -> _uiState.update { it.copy(coursesSynced = coursesSynced) } 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 4e06a46e5..0901ee8b2 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 @@ -20,11 +20,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults import androidx.compose.material.Tab import androidx.compose.material.TabRow import androidx.compose.material.Text @@ -244,60 +246,71 @@ private fun CourseCheckboxList( uiState: CoursesToSyncUIState, onCourseSyncCheckChange: (Boolean, String) -> Unit ) { - LazyColumn( - modifier = Modifier.padding(8.dp), - ) { - val courseList = uiState.run { - val isSyncEnabled = selectedTab == SyncCourseTab.SYNCED - val courseIds = coursesCalendarState.filter { it.isCourseSyncEnabled == isSyncEnabled }.map { it.courseId } - enrollmentsStatus.filter { it.courseId in courseIds } + if (uiState.isLoading) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } - items(courseList) { course -> - val isCourseSyncEnabled = - uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled ?: false - val annotatedString = buildAnnotatedString { - append(course.courseName) - if (!course.isActive) { - append(" ") - withStyle( - style = SpanStyle( - fontSize = 10.sp, - fontWeight = FontWeight.Normal, - letterSpacing = 0.sp, - fontFamily = fontFamily, - color = MaterialTheme.appColors.textFieldHint, - ) - ) { - append(stringResource(R.string.profile_inactive)) + } else { + LazyColumn( + modifier = Modifier.padding(8.dp), + ) { + val courseList = uiState.run { + val isSyncEnabled = selectedTab == SyncCourseTab.SYNCED + val courseIds = + coursesCalendarState.filter { it.isCourseSyncEnabled == isSyncEnabled }.map { it.courseId } + enrollmentsStatus.filter { it.courseId in courseIds } + } + items(courseList) { course -> + val isCourseSyncEnabled = + uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled ?: false + val annotatedString = buildAnnotatedString { + append(course.courseName) + if (!course.isActive) { + append(" ") + withStyle( + style = SpanStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + fontFamily = fontFamily, + color = MaterialTheme.appColors.textFieldHint, + ) + ) { + append(stringResource(R.string.profile_inactive)) + } } } - } - if (uiState.isHideInactiveCourses && !course.isActive) return@items - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier.size(24.dp), - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.appColors.primary, - uncheckedColor = MaterialTheme.appColors.textFieldText - ), - checked = isCourseSyncEnabled, - enabled = course.isActive, - onCheckedChange = { isEnabled -> - onCourseSyncCheckChange(isEnabled, course.courseId) - } - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = annotatedString, - style = MaterialTheme.appTypography.labelLarge, - color = MaterialTheme.appColors.textDark - ) + if (uiState.isHideInactiveCourses && !course.isActive) return@items + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier.size(24.dp), + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.appColors.primary, + uncheckedColor = MaterialTheme.appColors.textFieldText + ), + checked = isCourseSyncEnabled, + enabled = course.isActive, + onCheckedChange = { isEnabled -> + onCourseSyncCheckChange(isEnabled, course.courseId) + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = annotatedString, + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + } } } } @@ -324,7 +337,10 @@ private fun HideInactiveCoursesView( modifier = Modifier .padding(0.dp), checked = isHideInactiveCourses, - onCheckedChange = onHideInactiveCoursesSwitchClick + onCheckedChange = onHideInactiveCoursesSwitchClick, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.appColors.textAccent + ) ) } } @@ -346,7 +362,8 @@ private fun CoursesToSyncViewPreview() { uiState = CoursesToSyncUIState( enrollmentsStatus = emptyList(), coursesCalendarState = emptyList(), - isHideInactiveCourses = true + isHideInactiveCourses = true, + isLoading = false ), onHideInactiveCoursesSwitchClick = {}, onCourseSyncCheckChange = { _, _ -> }, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt index 275343848..e43988d2f 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CoursesToSyncUIState.kt @@ -6,5 +6,6 @@ import org.openedx.core.domain.model.EnrollmentStatus data class CoursesToSyncUIState( val enrollmentsStatus: List, val coursesCalendarState: List, - val isHideInactiveCourses: Boolean + val isHideInactiveCourses: Boolean, + val isLoading: Boolean ) 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 5ba286169..0ed28f058 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 @@ -21,7 +21,8 @@ class CoursesToSyncViewModel( CoursesToSyncUIState( enrollmentsStatus = emptyList(), coursesCalendarState = emptyList(), - isHideInactiveCourses = calendarPreferences.isHideInactiveCourses + isHideInactiveCourses = calendarPreferences.isHideInactiveCourses, + isLoading = true ) ) val uiState: StateFlow @@ -39,7 +40,7 @@ class CoursesToSyncViewModel( fun setCourseSyncEnabled(isEnabled: Boolean, courseId: String) { viewModelScope.launch { - calendarInteractor.updateCourseCalendarStateById( + calendarInteractor.updateCourseCalendarStateByIdInCache( courseId = courseId, isCourseSyncEnabled = isEnabled ) @@ -50,7 +51,7 @@ class CoursesToSyncViewModel( private fun getCourseCalendarState() { viewModelScope.launch { - val coursesCalendarState = calendarInteractor.getAllCourseCalendarState().map { it.mapToDomain() } + val coursesCalendarState = calendarInteractor.getAllCourseCalendarStateFromCache() _uiState.update { it.copy(coursesCalendarState = coursesCalendarState) } } } @@ -58,7 +59,7 @@ class CoursesToSyncViewModel( private fun getEnrollmentsStatus() { viewModelScope.launch { val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() - _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus) } + _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus, isLoading = false) } } } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt index 1fd51721f..f25051181 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt @@ -88,7 +88,7 @@ class DisableCalendarSyncDialogFragment : DialogFragment() { } @Composable -fun DisableCalendarSyncDialogView( +private fun DisableCalendarSyncDialogView( modifier: Modifier = Modifier, calendarData: CalendarData?, onCancelClick: () -> Unit, @@ -177,7 +177,7 @@ fun DisableCalendarSyncDialogView( @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun NewCalendarDialogPreview() { +private fun DisableCalendarSyncDialogPreview() { OpenEdXTheme { DisableCalendarSyncDialogView( calendarData = CalendarData("calendar", Color.GREEN), 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 5de80e8af..7f0103789 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 @@ -5,7 +5,6 @@ import android.content.res.Configuration import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle -import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.animation.core.animateFloatAsState @@ -61,7 +60,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment -import kotlinx.parcelize.Parcelize import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.extension.parcelable import org.openedx.core.presentation.dialog.DefaultDialogBox @@ -90,7 +88,8 @@ class NewCalendarDialogFragment : DialogFragment() { setContent { OpenEdXTheme { NewCalendarDialog( - dialogType = requireArguments().parcelable(ARG_DIALOG_TYPE) ?: DialogType.CREATE_NEW, + newCalendarDialogType = requireArguments().parcelable(ARG_DIALOG_TYPE) + ?: NewCalendarDialogType.CREATE_NEW, onCancelClick = { dismiss() }, @@ -108,11 +107,11 @@ class NewCalendarDialogFragment : DialogFragment() { const val ARG_DIALOG_TYPE = "ARG_DIALOG_TYPE" fun newInstance( - dialogType: DialogType + newCalendarDialogType: NewCalendarDialogType ): NewCalendarDialogFragment { val fragment = NewCalendarDialogFragment() fragment.arguments = bundleOf( - ARG_DIALOG_TYPE to dialogType + ARG_DIALOG_TYPE to newCalendarDialogType ) return fragment } @@ -123,22 +122,17 @@ class NewCalendarDialogFragment : DialogFragment() { } } -@Parcelize -enum class DialogType : Parcelable { - CREATE_NEW, UPDATE -} - @Composable private fun NewCalendarDialog( modifier: Modifier = Modifier, - dialogType: DialogType, + newCalendarDialogType: NewCalendarDialogType, onCancelClick: () -> Unit, onBeginSyncingClick: (calendarTitle: String, calendarColor: CalendarColor) -> Unit ) { val context = LocalContext.current - val title = when (dialogType) { - DialogType.CREATE_NEW -> stringResource(id = R.string.profile_new_calendar) - DialogType.UPDATE -> stringResource(id = R.string.profile_change_sync_options) + val title = when (newCalendarDialogType) { + NewCalendarDialogType.CREATE_NEW -> stringResource(id = R.string.profile_new_calendar) + NewCalendarDialogType.UPDATE -> stringResource(id = R.string.profile_change_sync_options) } var calendarTitle by rememberSaveable { mutableStateOf("") @@ -409,7 +403,7 @@ private fun ColorCircle( private fun NewCalendarDialogPreview() { OpenEdXTheme { NewCalendarDialog( - dialogType = DialogType.CREATE_NEW, + newCalendarDialogType = NewCalendarDialogType.CREATE_NEW, onCancelClick = { }, onBeginSyncingClick = { _, _ -> } ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogType.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogType.kt new file mode 100644 index 000000000..1905b8faa --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogType.kt @@ -0,0 +1,9 @@ +package org.openedx.profile.presentation.calendar + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class NewCalendarDialogType : Parcelable { + CREATE_NEW, UPDATE +} diff --git a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt index 6b49b9bdd..a2f8399eb 100644 --- a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt +++ b/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt @@ -89,18 +89,18 @@ class CalendarSyncWorker( createEvent(domainCourseDateBlock, enrollmentStatus) } } - calendarInteractor.updateCourseCalendarStateById( + calendarInteractor.updateCourseCalendarStateByIdInCache( courseId = enrollmentStatus.courseId, checksum = getCourseChecksum(courseDates) ) } private suspend fun removeCalendarEvents(courseId: String) { - calendarInteractor.getCourseCalendarEventsById(courseId).forEach { + calendarInteractor.getCourseCalendarEventsByIdFromCache(courseId).forEach { calendarManager.deleteEvent(it.eventId) } - calendarInteractor.deleteCourseCalendarEntitiesById(courseId) - calendarInteractor.updateCourseCalendarStateById(courseId = courseId, checksum = 0) + calendarInteractor.deleteCourseCalendarEntitiesByIdFromCache(courseId) + calendarInteractor.updateCourseCalendarStateByIdInCache(courseId = courseId, checksum = 0) } private suspend fun createEvent(courseDateBlock: CourseDateBlock, enrollmentStatus: EnrollmentStatus) { @@ -114,7 +114,7 @@ class CalendarSyncWorker( courseId = enrollmentStatus.courseId, eventId = eventId ) - calendarInteractor.insertCourseCalendarEntity(courseCalendarEventEntity) + calendarInteractor.insertCourseCalendarEntityToCache(courseCalendarEventEntity) } private suspend fun createCalendarState(enrollmentStatus: EnrollmentStatus) { @@ -124,7 +124,7 @@ class CalendarSyncWorker( courseId = enrollmentStatus.courseId, isCourseSyncEnabled = enrollmentStatus.isActive ) - calendarInteractor.insertCourseCalendarStateEntity(courseCalendarStateEntity) + calendarInteractor.insertCourseCalendarStateEntityToCache(courseCalendarStateEntity) } } @@ -135,7 +135,7 @@ class CalendarSyncWorker( } private suspend fun isCourseSyncEnabled(courseId: String): Boolean { - return calendarInteractor.getCourseCalendarStateById(courseId)?.mapToDomain()?.isCourseSyncEnabled ?: true + return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.isCourseSyncEnabled ?: true } private fun getCourseChecksum(courseDates: CourseDates): Int { @@ -143,7 +143,7 @@ class CalendarSyncWorker( } private suspend fun getCourseCalendarStateChecksum(courseId: String): Int? { - return calendarInteractor.getCourseCalendarStateById(courseId)?.mapToDomain()?.checksum + return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.checksum } companion object { From fe0687019f3b93f8f9dd5e04e47b0ed916ae7b0a Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 5 Jun 2024 20:46:02 +0300 Subject: [PATCH 17/26] refactor: Calendar Manager refactor --- .../calendar/CalendarViewModel.kt | 7 +- .../calendar/CoursesToSyncFragment.kt | 13 +- .../openedx/profile/system/CalendarManager.kt | 177 ++---------------- 3 files changed, 29 insertions(+), 168 deletions(-) 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 9a9f54372..79c459071 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 @@ -34,7 +34,7 @@ class CalendarViewModel( private val _uiState = MutableStateFlow( CalendarUIState( - isCalendarExist = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST, + isCalendarExist = isCalendarExist(), calendarData = null, calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, @@ -119,4 +119,9 @@ class CalendarViewModel( } } } + + private fun isCalendarExist(): Boolean { + return calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && + calendarManager.isCalendarExist(calendarPreferences.calendarId) + } } 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 0901ee8b2..93b6d12be 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 @@ -258,13 +258,12 @@ private fun CourseCheckboxList( LazyColumn( modifier = Modifier.padding(8.dp), ) { - val courseList = uiState.run { - val isSyncEnabled = selectedTab == SyncCourseTab.SYNCED - val courseIds = - coursesCalendarState.filter { it.isCourseSyncEnabled == isSyncEnabled }.map { it.courseId } - enrollmentsStatus.filter { it.courseId in courseIds } - } - items(courseList) { course -> + val courseIds = uiState.coursesCalendarState + .filter { it.isCourseSyncEnabled == (selectedTab == SyncCourseTab.SYNCED) } + .map { it.courseId } + val filteredEnrollments = uiState.enrollmentsStatus + .filter { it.courseId in courseIds } + items(filteredEnrollments) { course -> val isCourseSyncEnabled = uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled ?: false val annotatedString = buildAnnotatedString { diff --git a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt index f82b60c66..7e60a3981 100644 --- a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt +++ b/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt @@ -1,10 +1,8 @@ package org.openedx.profile.system -import android.annotation.SuppressLint import android.content.ContentUris import android.content.ContentValues import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.database.Cursor import android.net.Uri @@ -17,10 +15,8 @@ import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.system.ResourceManager import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar -import java.util.Calendar import java.util.TimeZone import java.util.concurrent.TimeUnit -import org.openedx.core.R as CoreR class CalendarManager( private val context: Context, @@ -47,11 +43,23 @@ class CalendarManager( /** * Check if the calendar is already existed in mobile calendar app or not */ - fun isCalendarExists(calendarTitle: String): Boolean { - if (hasPermissions()) { - return getCalendarId(calendarTitle) != CALENDAR_DOES_NOT_EXIST - } - return false + fun isCalendarExist(calendarId: Long): Boolean { + val projection = arrayOf(CalendarContract.Calendars._ID) + val selection = "${CalendarContract.Calendars._ID} = ?" + val selectionArgs = arrayOf(calendarId.toString()) + + val cursor = context.contentResolver.query( + CalendarContract.Calendars.CONTENT_URI, + projection, + selection, + selectionArgs, + null + ) + + val exists = cursor != null && cursor.count > 0 + cursor?.close() + + return exists } /** @@ -113,39 +121,6 @@ class CalendarManager( return CALENDAR_DOES_NOT_EXIST } - /** - * Method to check if the calendar with the course name exist in the mobile calendar app or not - */ - @SuppressLint("Range") - fun getCalendarId(calendarTitle: String): Long { - var calendarId = CALENDAR_DOES_NOT_EXIST - val projection = arrayOf( - CalendarContract.Calendars._ID, - CalendarContract.Calendars.ACCOUNT_NAME, - CalendarContract.Calendars.NAME - ) - val calendarContentResolver = context.contentResolver - val cursor = calendarContentResolver.query( - CalendarContract.Calendars.CONTENT_URI, projection, - CalendarContract.Calendars.ACCOUNT_NAME + "=? and (" + - CalendarContract.Calendars.NAME + "=? or " + - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME + "=?)", arrayOf( - accountName, calendarTitle, - calendarTitle - ), null - ) - if (cursor?.moveToFirst() == true) { - if (cursor.getString(cursor.getColumnIndex(CalendarContract.Calendars.NAME)) - .equals(calendarTitle) - ) { - calendarId = - cursor.getInt(cursor.getColumnIndex(CalendarContract.Calendars._ID)).toLong() - } - } - cursor?.close() - return calendarId - } - /** * Method to add important dates of course as calendar event into calendar of mobile app */ @@ -247,82 +222,6 @@ class CalendarManager( context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, eventValues) } - /** - * Method to query the events for the given calendar id - * - * @param calendarId calendarId to query the events - * - * @return [Cursor] - * - * */ - private fun getCalendarEvents(calendarId: Long): Cursor? { - val calendarContentResolver = context.contentResolver - val projection = arrayOf( - CalendarContract.Events._ID, - CalendarContract.Events.DTEND, - CalendarContract.Events.DESCRIPTION - ) - val selection = CalendarContract.Events.CALENDAR_ID + "=?" - return calendarContentResolver.query( - CalendarContract.Events.CONTENT_URI, - projection, - selection, - arrayOf(calendarId.toString()), - null - ) - } - - /** - * Method to compare the calendar events with course dates - * @return true if the events are the same as calendar dates otherwise false - */ - @SuppressLint("Range") - private fun compareEvents( - calendarId: Long, - courseDateBlocks: List - ): Boolean { - val cursor = getCalendarEvents(calendarId) ?: return false - - val datesList = ArrayList(courseDateBlocks) - val dueDateColumnIndex = cursor.getColumnIndex(CalendarContract.Events.DTEND) - val descriptionColumnIndex = cursor.getColumnIndex(CalendarContract.Events.DESCRIPTION) - - while (cursor.moveToNext()) { - val dueDateInMillis = cursor.getLong(dueDateColumnIndex) - - val description = cursor.getString(descriptionColumnIndex) - if (description != null) { - val matchedDate = datesList.find { unit -> - description.contains(unit.title, ignoreCase = true) - } - - matchedDate?.let { unit -> - val dueDateCalendar = Calendar.getInstance().apply { - timeInMillis = dueDateInMillis - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - - val unitDateCalendar = unit.date.toCalendar().apply { - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - } - - if (dueDateCalendar == unitDateCalendar) { - datesList.remove(unit) - } else { - // If any single value isn't matched, return false - cursor.close() - return false - } - } - } - } - - cursor.close() - return datesList.isEmpty() - } - /** * Method to delete the course calendar from the mobile calendar app */ @@ -353,37 +252,6 @@ class CalendarManager( ).build() } - fun openCalendarApp() { - val builder: Uri.Builder = CalendarContract.CONTENT_URI.buildUpon() - .appendPath("time") - ContentUris.appendId(builder, Calendar.getInstance().timeInMillis) - val intent = Intent(Intent.ACTION_VIEW).setData(builder.build()) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - } - - /** - * Helper method used to check that the calendar if outdated for the course or not - * - * @param calendarTitle Title for the course Calendar - * @param courseDateBlocks Course dates events - * - * @return Calendar Id if Calendar is outdated otherwise -1 or CALENDAR_DOES_NOT_EXIST - * - */ - fun isCalendarOutOfDate( - calendarTitle: String, - courseDateBlocks: List - ): Long { - if (isCalendarExists(calendarTitle)) { - val calendarId = getCalendarId(calendarTitle) - if (compareEvents(calendarId, courseDateBlocks).not()) { - return calendarId - } - } - return CALENDAR_DOES_NOT_EXIST - } - /** * Method to get the current user account as the Calendar owner * @@ -393,17 +261,6 @@ class CalendarManager( return corePreferences.user?.email ?: LOCAL_USER } - /** - * Method to create the Calendar title for the platform against the course - * - * @param courseName Name of the course for that creating the Calendar events. - * - * @return title of the Calendar against the course - */ - fun getCourseCalendarTitle(courseName: String): String { - return "${resourceManager.getString(id = CoreR.string.platform_name)} - $courseName" - } - fun getCalendarData(calendarId: Long): CalendarData? { val projection = arrayOf( CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, From 0fec040eb041a82555a0a7ceedb9cd6010be793f Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Thu, 6 Jun 2024 14:28:03 +0300 Subject: [PATCH 18/26] fix: Fixes according to code review feedback --- app/src/main/AndroidManifest.xml | 4 + .../main/java/org/openedx/app/AppActivity.kt | 2 +- .../main/java/org/openedx/app/AppViewModel.kt | 2 +- .../app/data/storage/PreferencesManager.kt | 12 ++- .../main/java/org/openedx/app/di/AppModule.kt | 8 +- .../java/org/openedx/app/di/ScreenModule.kt | 12 +-- .../test/java/org/openedx/AppViewModelTest.kt | 6 +- .../core/data/storage/CalendarPreferences.kt | 2 + .../core/data/storage/CorePreferences.kt | 2 +- .../domain/interactor/CalendarInteractor.kt | 8 ++ .../org/openedx/core/module/db/CalendarDao.kt | 9 ++ .../core/repository/CalendarRepository.kt | 17 ++++ .../core/service/CalendarSyncService.kt | 99 +++++++++++++------ .../openedx/core}/system/CalendarManager.kt | 5 +- .../notifier/calendar/CalendarCreated.kt | 3 + .../system/notifier/calendar/CalendarEvent.kt | 3 + .../notifier/calendar}/CalendarNotifier.kt | 2 +- .../notifier/calendar/CalendarSyncDisabled.kt | 3 + .../notifier/calendar/CalendarSyncFailed.kt | 3 + .../notifier/calendar/CalendarSynced.kt | 3 + .../notifier/calendar/CalendarSyncing.kt | 3 + .../core}/worker/CalendarSyncScheduler.kt | 2 +- .../openedx/core/worker/CalendarSyncWorker.kt | 37 +++++++ core/src/main/res/values/strings.xml | 1 + .../container/CourseContainerViewModel.kt | 48 +-------- .../dates/CourseDatesViewModel.kt | 36 ------- .../data/repository/ProfileRepository.kt | 8 +- .../calendar/CalendarAccessDialogFragment.kt | 4 + .../calendar/CalendarSetUpView.kt | 11 ++- .../calendar/CalendarSettingsView.kt | 19 ++-- .../presentation/calendar/CalendarUIState.kt | 2 +- .../calendar/CalendarViewModel.kt | 41 ++++---- .../calendar/CoursesToSyncFragment.kt | 3 +- .../calendar/CoursesToSyncViewModel.kt | 2 +- .../DisableCalendarSyncDialogFragment.kt | 10 +- .../DisableCalendarSyncDialogViewModel.kt | 14 ++- .../calendar/NewCalendarDialogFragment.kt | 13 ++- .../calendar/NewCalendarDialogViewModel.kt | 24 ++--- .../delete/DeleteProfileViewModel.kt | 4 +- .../presentation/edit/EditProfileViewModel.kt | 4 +- .../manageaccount/ManageAccountViewModel.kt | 4 +- .../presentation/profile/ProfileViewModel.kt | 4 +- .../settings/SettingsViewModel.kt | 4 +- .../system/notifier/AccountDeactivated.kt | 3 - .../profile/system/notifier/AccountUpdated.kt | 3 - .../system/notifier/CalendarCreated.kt | 3 - .../profile/system/notifier/CalendarEvent.kt | 3 - .../system/notifier/CalendarSyncDisabled.kt | 3 - .../system/notifier/CalendarSyncFailed.kt | 3 - .../profile/system/notifier/CalendarSynced.kt | 3 - .../system/notifier/CalendarSyncing.kt | 3 - .../profile/system/notifier/ProfileEvent.kt | 3 - .../notifier/account/AccountDeactivated.kt | 5 + .../system/notifier/account/AccountUpdated.kt | 5 + .../system/notifier/profile/ProfileEvent.kt | 3 + .../notifier/{ => profile}/ProfileNotifier.kt | 5 +- profile/src/main/res/values/strings.xml | 4 +- .../edit/EditProfileViewModelTest.kt | 30 +++--- .../profile/ProfileViewModelTest.kt | 4 +- 59 files changed, 344 insertions(+), 242 deletions(-) rename profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt => core/src/main/java/org/openedx/core/service/CalendarSyncService.kt (62%) rename {profile/src/main/java/org/openedx/profile => core/src/main/java/org/openedx/core}/system/CalendarManager.kt (98%) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarCreated.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarEvent.kt rename {profile/src/main/java/org/openedx/profile/system/notifier => core/src/main/java/org/openedx/core/system/notifier/calendar}/CalendarNotifier.kt (88%) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncDisabled.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncFailed.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSynced.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncing.kt rename {profile/src/main/java/org/openedx/profile => core/src/main/java/org/openedx/core}/worker/CalendarSyncScheduler.kt (97%) create mode 100644 core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/AccountDeactivated.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/AccountUpdated.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt delete mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/ProfileEvent.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/account/AccountDeactivated.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/account/AccountUpdated.kt create mode 100644 profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileEvent.kt rename profile/src/main/java/org/openedx/profile/system/notifier/{ => profile}/ProfileNotifier.kt (70%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7142d9190..9bf8b635a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -114,6 +114,10 @@ + + diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 88460e828..6c0d6d05d 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -27,8 +27,8 @@ import org.openedx.core.presentation.global.WindowSizeHolder import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.utils.Logger +import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.worker.CalendarSyncScheduler import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index c18e48026..755b9726f 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -46,7 +46,7 @@ class AppViewModel( notifier.notifier.collect { event -> if (event is LogoutEvent && System.currentTimeMillis() - logoutHandledAt > 5000) { logoutHandledAt = System.currentTimeMillis() - preferencesManager.clear() + preferencesManager.clearCorePreferences() withContext(dispatcher) { room.clearAllTables() } 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 7578d799c..b4b21bdfe 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 @@ -11,10 +11,10 @@ import org.openedx.core.domain.model.AppConfig import org.openedx.core.domain.model.VideoQuality import org.openedx.core.domain.model.VideoSettings import org.openedx.core.extension.replaceSpace +import org.openedx.core.system.CalendarManager import org.openedx.course.data.storage.CoursePreferences import org.openedx.profile.data.model.Account import org.openedx.profile.data.storage.ProfilePreferences -import org.openedx.profile.system.CalendarManager import org.openedx.whatsnew.data.storage.WhatsNewPreferences class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences, @@ -51,7 +51,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences return sharedPreferences.getBoolean(key, defValue) } - override fun clear() { + override fun clearCorePreferences() { sharedPreferences.edit().apply { remove(ACCESS_TOKEN) remove(REFRESH_TOKEN) @@ -60,6 +60,14 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences }.apply() } + override fun clearCalendarPreferences() { + sharedPreferences.edit().apply { + remove(CALENDAR_ID) + remove(IS_CALENDAR_SYNC_ENABLED) + remove(HIDE_INACTIVE_COURSES) + }.apply() + } + override var accessToken: String set(value) { saveString(ACCESS_TOKEN, value) 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 67deb6641..01f8e076a 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -41,6 +41,7 @@ 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.system.AppCookieManager +import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.AppUpgradeNotifier @@ -48,7 +49,9 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.VideoNotifier +import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.core.utils.FileUtil +import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseRouter @@ -62,10 +65,7 @@ import org.openedx.discussion.system.notifier.DiscussionNotifier import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.profile.system.notifier.ProfileNotifier -import org.openedx.profile.worker.CalendarSyncScheduler +import org.openedx.profile.system.notifier.profile.ProfileNotifier import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.data.storage.WhatsNewPreferences 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 932869ae4..8cbc8b64c 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -144,7 +144,7 @@ val screenModule = module { ) } - factory { ProfileRepository(get(), get(), get(), get(), get()) } + factory { ProfileRepository(get(), get(), get(), get(), get(), get(), get()) } factory { ProfileInteractor(get()) } viewModel { ProfileViewModel( @@ -177,9 +177,9 @@ val screenModule = module { viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { CoursesToSyncViewModel(get(), get(), get()) } - viewModel { NewCalendarDialogViewModel(get(), get(), get()) } - viewModel { DisableCalendarSyncDialogViewModel(get(), get()) } - single { CalendarRepository(get(), get(), get()) } + viewModel { NewCalendarDialogViewModel(get(), get(), get(), get()) } + viewModel { DisableCalendarSyncDialogViewModel(get(), get(), get(), get()) } + factory { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } single { CourseRepository(get(), get(), get(), get(), get()) } @@ -313,17 +313,15 @@ val screenModule = module { get(), ) } - viewModel { (courseId: String, courseTitle: String, enrollmentMode: String) -> + viewModel { (courseId: String, enrollmentMode: String) -> CourseDatesViewModel( courseId, - courseTitle, enrollmentMode, get(), get(), get(), get(), get(), - get(), get() ) } diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index c81c9c2e5..80a312ffb 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -88,7 +88,7 @@ class AppViewModelTest { every { notifier.notifier } returns flow { emit(LogoutEvent()) } - every { preferencesManager.clear() } returns Unit + every { preferencesManager.clearCorePreferences() } returns Unit every { analytics.setUserIdForSession(any()) } returns Unit every { preferencesManager.user } returns user every { room.clearAllTables() } returns Unit @@ -121,7 +121,7 @@ class AppViewModelTest { emit(LogoutEvent()) emit(LogoutEvent()) } - every { preferencesManager.clear() } returns Unit + every { preferencesManager.clearCorePreferences() } returns Unit every { analytics.setUserIdForSession(any()) } returns Unit every { preferencesManager.user } returns user every { room.clearAllTables() } returns Unit @@ -145,7 +145,7 @@ class AppViewModelTest { advanceUntilIdle() verify(exactly = 1) { analytics.logoutEvent(true) } - verify(exactly = 1) { preferencesManager.clear() } + verify(exactly = 1) { preferencesManager.clearCorePreferences() } verify(exactly = 1) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { preferencesManager.user } verify(exactly = 1) { room.clearAllTables() } diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt index 4efd79881..8d202d186 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -4,4 +4,6 @@ interface CalendarPreferences { var calendarId: Long var isCalendarSyncEnabled: Boolean var isHideInactiveCourses: Boolean + + fun clearCalendarPreferences() } diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index f9cacbd04..b2da6255d 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -13,5 +13,5 @@ interface CorePreferences { var appConfig: AppConfig var canResetAppDirectory: Boolean - fun clear() + fun clearCorePreferences() } diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index 0154bd373..653dca298 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -38,6 +38,14 @@ class CalendarInteractor( return repository.getAllCourseCalendarStateFromCache() } + suspend fun clearCalendarCachedData() { + repository.clearCalendarCachedData() + } + + suspend fun resetChecksums() { + repository.resetChecksums() + } + suspend fun updateCourseCalendarStateByIdInCache( courseId: String, checksum: Int? = null, diff --git a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt index c0bf134db..3b8fa0a74 100644 --- a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt +++ b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt @@ -20,6 +20,9 @@ interface CalendarDao { @Query("SELECT * FROM course_calendar_event_table WHERE course_id=:courseId") suspend fun readCourseCalendarEventsById(courseId: String): List + @Query("DELETE FROM course_calendar_event_table") + suspend fun clearCourseCalendarEventsCachedData() + // region CourseCalendarStateEntity @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCourseCalendarStateEntity(vararg courseCalendarStateEntity: CourseCalendarStateEntity) @@ -30,6 +33,12 @@ interface CalendarDao { @Query("SELECT * FROM course_calendar_state_table") suspend fun readAllCourseCalendarState(): List + @Query("DELETE FROM course_calendar_state_table") + suspend fun clearCourseCalendarStateCachedData() + + @Query("UPDATE course_calendar_state_table SET checksum = 0") + suspend fun resetChecksums() + @Query( """ UPDATE course_calendar_state_table diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index e822ca237..c59d85ccc 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -1,5 +1,9 @@ package org.openedx.core.repository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import org.openedx.core.data.api.CourseApi import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseCalendarStateEntity @@ -46,6 +50,19 @@ class CalendarRepository( return calendarDao.readAllCourseCalendarState().map { it.mapToDomain() } } + suspend fun resetChecksums() { + calendarDao.resetChecksums() + } + + suspend fun clearCalendarCachedData() { + CoroutineScope(Dispatchers.Main).launch { + val clearCourseCalendarStateDeferred = async { calendarDao.clearCourseCalendarStateCachedData() } + val clearCourseCalendarEventsDeferred = async { calendarDao.clearCourseCalendarEventsCachedData() } + clearCourseCalendarStateDeferred.await() + clearCourseCalendarEventsDeferred.await() + } + } + suspend fun updateCourseCalendarStateByIdInCache( courseId: String, checksum: Int? = null, diff --git a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt b/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt similarity index 62% rename from profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt rename to core/src/main/java/org/openedx/core/service/CalendarSyncService.kt index a2f8399eb..f40ca2528 100644 --- a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt @@ -1,10 +1,21 @@ -package org.openedx.profile.worker +package org.openedx.core.service +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters +import android.content.Intent +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.openedx.core.R import org.openedx.core.data.model.CourseDates import org.openedx.core.data.model.room.CourseCalendarEventEntity import org.openedx.core.data.model.room.CourseCalendarStateEntity @@ -12,44 +23,71 @@ import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.EnrollmentStatus -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.profile.system.notifier.CalendarSyncFailed -import org.openedx.profile.system.notifier.CalendarSynced -import org.openedx.profile.system.notifier.CalendarSyncing +import org.openedx.core.system.CalendarManager +import org.openedx.core.system.notifier.calendar.CalendarNotifier +import org.openedx.core.system.notifier.calendar.CalendarSyncFailed +import org.openedx.core.system.notifier.calendar.CalendarSynced +import org.openedx.core.system.notifier.calendar.CalendarSyncing -class CalendarSyncWorker( - context: Context, - workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams), KoinComponent { +class CalendarSyncForegroundService : Service(), KoinComponent { private val calendarManager: CalendarManager by inject() private val calendarInteractor: CalendarInteractor by inject() private val calendarNotifier: CalendarNotifier by inject() private val calendarPreferences: CalendarPreferences by inject() - override suspend fun doWork(): Result { - return try { - val courseId = inputData.getString(ARG_COURSE_ID) + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + startForeground(NOTIFICATION_ID, createNotification()) + } else { + startForeground(NOTIFICATION_ID, createNotification(), FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } + val courseId = intent?.getStringExtra(ARG_COURSE_ID) + CoroutineScope(Dispatchers.Main).launch { tryToSyncCalendar(courseId) - Result.success() - } catch (e: Exception) { - calendarNotifier.send(CalendarSyncFailed) - Result.failure() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() } + return START_NOT_STICKY + } + + private fun createNotification(): Notification { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + NOTIFICATION_CHANEL_ID, + getString(R.string.core_calendar_notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + + return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID) + .setSmallIcon(R.drawable.core_ic_calendar) + .setContentTitle(getString(R.string.core_calendar_notification_channel_name)) + .setContentText(getString(R.string.core_title_syncing_calendar)) + .build() } private suspend fun tryToSyncCalendar(courseId: String?) { - val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST - val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled - if (isCalendarCreated && isCalendarSyncEnabled) { - calendarNotifier.send(CalendarSyncing) - if (courseId.isNullOrEmpty()) { - syncCalendar() - } else { - syncCalendar(courseId) + try { + val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled + if (isCalendarCreated && isCalendarSyncEnabled) { + calendarNotifier.send(CalendarSyncing) + if (courseId.isNullOrEmpty()) { + syncCalendar() + } else { + syncCalendar(courseId) + } + calendarNotifier.send(CalendarSynced) } - calendarNotifier.send(CalendarSynced) + } catch (e: Exception) { + calendarNotifier.send(CalendarSyncFailed) } } @@ -147,7 +185,8 @@ class CalendarSyncWorker( } companion object { + const val NOTIFICATION_ID = 1234 const val ARG_COURSE_ID = "ARG_COURSE_ID" - const val WORKER_TAG = "calendar_sync_worker_tag" + const val NOTIFICATION_CHANEL_ID = "calendar_sync_channel" } -} +} \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt similarity index 98% rename from profile/src/main/java/org/openedx/profile/system/CalendarManager.kt rename to core/src/main/java/org/openedx/core/system/CalendarManager.kt index 7e60a3981..380f16c88 100644 --- a/profile/src/main/java/org/openedx/profile/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -1,4 +1,4 @@ -package org.openedx.profile.system +package org.openedx.core.system import android.content.ContentUris import android.content.ContentValues @@ -12,7 +12,6 @@ import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CalendarData import org.openedx.core.domain.model.CourseDateBlock -import org.openedx.core.system.ResourceManager import org.openedx.core.utils.Logger import org.openedx.core.utils.toCalendar import java.util.TimeZone @@ -225,7 +224,7 @@ class CalendarManager( /** * Method to delete the course calendar from the mobile calendar app */ - private fun deleteCalendar(calendarId: Long) { + fun deleteCalendar(calendarId: Long) { context.contentResolver.delete( Uri.parse("content://com.android.calendar/calendars/$calendarId"), null, diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarCreated.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarCreated.kt new file mode 100644 index 000000000..028b0d3e3 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarCreated.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarCreated : CalendarEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarEvent.kt new file mode 100644 index 000000000..1bdf92dca --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +interface CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarNotifier.kt similarity index 88% rename from profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt rename to core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarNotifier.kt index 3d9f054d3..b0baa674b 100644 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarNotifier.kt @@ -1,4 +1,4 @@ -package org.openedx.profile.system.notifier +package org.openedx.core.system.notifier.calendar import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncDisabled.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncDisabled.kt new file mode 100644 index 000000000..ec9d61e84 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncDisabled.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarSyncDisabled : CalendarEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncFailed.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncFailed.kt new file mode 100644 index 000000000..af7f507ea --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncFailed.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarSyncFailed : CalendarEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSynced.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSynced.kt new file mode 100644 index 000000000..71bfed3ef --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSynced.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarSynced : CalendarEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncing.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncing.kt new file mode 100644 index 000000000..edfe066a9 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncing.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarSyncing : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt b/core/src/main/java/org/openedx/core/worker/CalendarSyncScheduler.kt similarity index 97% rename from profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt rename to core/src/main/java/org/openedx/core/worker/CalendarSyncScheduler.kt index 3e7ff251d..b74d7c9da 100644 --- a/profile/src/main/java/org/openedx/profile/worker/CalendarSyncScheduler.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncScheduler.kt @@ -1,4 +1,4 @@ -package org.openedx.profile.worker +package org.openedx.core.worker import android.content.Context import androidx.work.Data diff --git a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt new file mode 100644 index 000000000..6f80c2850 --- /dev/null +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -0,0 +1,37 @@ +package org.openedx.core.worker + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import org.koin.core.component.KoinComponent +import org.openedx.core.service.CalendarSyncForegroundService + +class CalendarSyncWorker( + context: Context, + workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams), KoinComponent { + + override suspend fun doWork(): Result { + return try { + val courseId = inputData.getString(ARG_COURSE_ID) + startForegroundService(courseId) + Result.success() + } catch (e: Exception) { + Result.failure() + } + } + + private fun startForegroundService(courseId: String?) { + val serviceIntent = Intent(applicationContext, CalendarSyncForegroundService::class.java).apply { + putExtra(CalendarSyncForegroundService.ARG_COURSE_ID, courseId) + } + ContextCompat.startForegroundService(applicationContext, serviceIntent) + } + + companion object { + const val ARG_COURSE_ID = "ARG_COURSE_ID" + const val WORKER_TAG = "calendar_sync_worker_tag" + } +} diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 551c671c6..466f80196 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -172,4 +172,5 @@ More Dates Disable Syncing + Calendar Sync 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 fb6b80bfb..d429b4044 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 @@ -3,7 +3,6 @@ package org.openedx.course.presentation.container import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.os.Build -import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -37,11 +36,10 @@ import org.openedx.core.system.notifier.CourseOpenBlock import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.core.system.notifier.RefreshDates import org.openedx.core.system.notifier.RefreshDiscussions +import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.course.DatesShiftedSnackBar -import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CalendarSyncDialog -import org.openedx.course.presentation.CalendarSyncSnackbar import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent import org.openedx.course.presentation.CourseAnalyticsKey @@ -61,9 +59,9 @@ class CourseContainerViewModel( private val courseNotifier: CourseNotifier, private val networkConnection: NetworkConnection, private val corePreferences: CorePreferences, - private val coursePreferences: CoursePreferences, private val courseAnalytics: CourseAnalytics, private val imageProcessor: ImageProcessor, + private val calendarSyncScheduler: CalendarSyncScheduler, val courseRouter: CourseRouter, ) : BaseViewModel() { @@ -137,6 +135,7 @@ class CourseContainerViewModel( } is CourseDatesShifted -> { + calendarSyncScheduler.requestImmediateSync(courseId) _uiMessage.emit(DatesShiftedSnackBar()) } @@ -269,12 +268,6 @@ class CourseContainerViewModel( } } - private fun setUiMessage(@StringRes stringResId: Int) { - _calendarSyncUIState.update { - it.copy(uiMessage = AtomicReference(resourceManager.getString(stringResId))) - } - } - private fun isCalendarSyncEnabled(): Boolean { val calendarSync = corePreferences.appConfig.courseDatesCalendarSync return calendarSync.isEnabled && ((calendarSync.isSelfPacedEnabled && isSelfPaced) || @@ -323,41 +316,6 @@ class CourseContainerViewModel( ) } - fun logCalendarAddDates(action: Boolean) { - logCalendarSyncEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_DIALOG_ACTION, - CalendarSyncDialog.ADD.getBuildMap(action) - ) - } - - fun logCalendarRemoveDates(action: Boolean) { - logCalendarSyncEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_DIALOG_ACTION, - CalendarSyncDialog.REMOVE.getBuildMap(action) - ) - } - - fun logCalendarSyncedConfirmation(action: Boolean) { - logCalendarSyncEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_DIALOG_ACTION, - CalendarSyncDialog.CONFIRMED.getBuildMap(action) - ) - } - - fun logCalendarSyncUpdate(action: Boolean) { - logCalendarSyncEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_DIALOG_ACTION, - CalendarSyncDialog.UPDATE.getBuildMap(action) - ) - } - - private fun logCalendarSyncSnackbar(snackbar: CalendarSyncSnackbar) { - logCalendarSyncEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_SNACKBAR, - snackbar.getBuildMap() - ) - } - private fun logCalendarSyncEvent( event: CourseAnalyticsEvent, param: Map = emptyMap(), 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 8485fcb5a..6b52b0c7b 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 @@ -11,7 +11,6 @@ import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config -import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseBannerType import org.openedx.core.domain.model.CourseDateBlock @@ -35,12 +34,10 @@ import org.openedx.core.R as CoreR class CourseDatesViewModel( val courseId: String, - courseTitle: String, private val enrollmentMode: String, private val courseNotifier: CourseNotifier, private val interactor: CourseInteractor, private val resourceManager: ResourceManager, - private val corePreferences: CorePreferences, private val courseAnalytics: CourseAnalytics, private val config: Config, val courseRouter: CourseRouter @@ -136,21 +133,6 @@ class CourseDatesViewModel( } } - private fun setCalendarSyncDialogType(dialog: CalendarSyncDialogType) { - val value = _uiState.value - if (value is DatesUIState.Dates) { - viewModelScope.launch { - courseNotifier.send( - CreateCalendarSyncEvent( - courseDates = value.courseDatesResult.datesSection.values.flatten(), - dialogType = dialog.name, - checkOutOfSync = false, - ) - ) - } - } - } - private fun checkIfCalendarOutOfDate() { val value = _uiState.value if (value is DatesUIState.Dates) { @@ -166,12 +148,6 @@ class CourseDatesViewModel( } } - private fun isCalendarSyncEnabled(): Boolean { - val calendarSync = corePreferences.appConfig.courseDatesCalendarSync - return calendarSync.isEnabled && ((calendarSync.isSelfPacedEnabled && isSelfPaced) || - (calendarSync.isInstructorPacedEnabled && !isSelfPaced)) - } - fun logPlsBannerViewed() { logPLSBannerEvent(CourseAnalyticsEvent.PLS_BANNER_VIEWED) } @@ -195,18 +171,6 @@ class CourseDatesViewModel( logDatesEvent(CourseAnalyticsEvent.DATES_COURSE_COMPONENT_CLICKED, params) } - private fun logCalendarSyncToggle(isChecked: Boolean) { - logDatesEvent( - CourseAnalyticsEvent.DATES_CALENDAR_SYNC_TOGGLE, - buildMap { - put( - CourseAnalyticsKey.ACTION.key, - if (isChecked) CourseAnalyticsKey.ON.key else CourseAnalyticsKey.OFF.key - ) - } - ) - } - private fun logDatesEvent( event: CourseAnalyticsEvent, param: Map = emptyMap(), diff --git a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt index ce5580a45..e0a1044ff 100644 --- a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt +++ b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt @@ -5,7 +5,9 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import org.openedx.core.ApiConstants import org.openedx.core.config.Config +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.CalendarManager import org.openedx.profile.data.api.ProfileApi import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.domain.model.Account @@ -17,6 +19,8 @@ class ProfileRepository( private val room: RoomDatabase, private val profilePreferences: ProfilePreferences, private val corePreferences: CorePreferences, + private val calendarPreferences: CalendarPreferences, + private val calendarManager: CalendarManager ) { suspend fun getAccount(): Account { @@ -61,7 +65,9 @@ class ProfileRepository( ApiConstants.TOKEN_TYPE_REFRESH ) } finally { - corePreferences.clear() + corePreferences.clearCorePreferences() + calendarManager.deleteCalendar(calendarPreferences.calendarId) + calendarPreferences.clearCalendarPreferences() room.clearAllTables() } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt index 8d49fb8ec..c1dc22df2 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarAccessDialogFragment.kt @@ -16,6 +16,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -86,6 +88,7 @@ private fun CalendarAccessDialog( onCancelClick: () -> Unit, onGrantCalendarAccessClick: () -> Unit ) { + val scrollState = rememberScrollState() DefaultDialogBox( modifier = modifier, onDismissClick = onCancelClick @@ -93,6 +96,7 @@ private fun CalendarAccessDialog( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(scrollState) .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) 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 9a1c39cf2..06a842630 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 @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme @@ -57,6 +59,7 @@ fun CalendarSetUpView( onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() + val scrollState = rememberScrollState() Scaffold( modifier = Modifier @@ -119,11 +122,13 @@ fun CalendarSetUpView( contentAlignment = Alignment.TopCenter ) { Column( - modifier = contentWidth.padding(vertical = 28.dp), + modifier = contentWidth + .verticalScroll(scrollState) + .padding(vertical = 28.dp), ) { Text( - modifier = Modifier.testTag("txt_settings"), - text = stringResource(id = org.openedx.core.R.string.core_settings), + modifier = Modifier.testTag("txt_calendar_sync"), + text = stringResource(id = R.string.profile_calendar_sync), style = MaterialTheme.appTypography.labelLarge, color = MaterialTheme.appColors.textSecondary ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 33ba5df38..866ab2f86 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi @@ -69,6 +71,7 @@ fun CalendarSettingsView( onBackClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() + val scrollState = rememberScrollState() Scaffold( modifier = Modifier @@ -131,10 +134,10 @@ fun CalendarSettingsView( contentAlignment = Alignment.TopCenter ) { Column( - modifier = contentWidth.padding(vertical = 28.dp), + modifier = contentWidth + .verticalScroll(scrollState) + .padding(vertical = 28.dp), ) { - val coursesSynced = uiState.coursesSynced - if (uiState.calendarData != null) { CalendarSyncSection( isCourseCalendarSyncEnabled = uiState.isCalendarSyncEnabled, @@ -145,10 +148,12 @@ fun CalendarSettingsView( ) } Spacer(modifier = Modifier.height(20.dp)) - CoursesToSyncSection( - coursesSynced = coursesSynced, - onCourseToSyncClick = onCourseToSyncClick - ) + if (uiState.coursesSynced != null) { + CoursesToSyncSection( + coursesSynced = uiState.coursesSynced, + onCourseToSyncClick = onCourseToSyncClick + ) + } } } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index e111fd4be..294f8e00d 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -7,5 +7,5 @@ data class CalendarUIState( val calendarData: CalendarData? = null, val calendarSyncState: CalendarSyncState, val isCalendarSyncEnabled: Boolean, - val coursesSynced: Int + val coursesSynced: Int? ) 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 79c459071..f522cd4c6 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 @@ -11,16 +11,16 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.system.CalendarManager import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.calendar.CalendarCreated +import org.openedx.core.system.notifier.calendar.CalendarNotifier +import org.openedx.core.system.notifier.calendar.CalendarSyncDisabled +import org.openedx.core.system.notifier.calendar.CalendarSyncFailed +import org.openedx.core.system.notifier.calendar.CalendarSynced +import org.openedx.core.system.notifier.calendar.CalendarSyncing +import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.notifier.CalendarCreated -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.profile.system.notifier.CalendarSyncDisabled -import org.openedx.profile.system.notifier.CalendarSyncFailed -import org.openedx.profile.system.notifier.CalendarSynced -import org.openedx.profile.system.notifier.CalendarSyncing -import org.openedx.profile.worker.CalendarSyncScheduler class CalendarViewModel( private val calendarSyncScheduler: CalendarSyncScheduler, @@ -29,18 +29,19 @@ class CalendarViewModel( private val calendarNotifier: CalendarNotifier, private val calendarInteractor: CalendarInteractor, private val profileRouter: ProfileRouter, - networkConnection: NetworkConnection, + private val networkConnection: NetworkConnection, ) : BaseViewModel() { - private val _uiState = MutableStateFlow( - CalendarUIState( + private val calendarInitState: CalendarUIState + get() = CalendarUIState( isCalendarExist = isCalendarExist(), calendarData = null, calendarSyncState = if (networkConnection.isOnline()) CalendarSyncState.SYNCED else CalendarSyncState.OFFLINE, isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled, - coursesSynced = 0 + coursesSynced = null ) - ) + + private val _uiState = MutableStateFlow(calendarInitState) val uiState: StateFlow get() = _uiState.asStateFlow() @@ -49,7 +50,6 @@ class CalendarViewModel( calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { CalendarCreated -> { - calendarSyncScheduler.scheduleDailySync() calendarSyncScheduler.requestImmediateSync() _uiState.update { it.copy(isCalendarExist = true) } getCalendarData() @@ -69,7 +69,7 @@ class CalendarViewModel( } CalendarSyncDisabled -> { - _uiState.update { it.copy(isCalendarSyncEnabled = false) } + _uiState.update { calendarInitState } } } } @@ -112,14 +112,15 @@ class CalendarViewModel( private fun updateSyncedCoursesCount() { viewModelScope.launch { - calendarInteractor.getAllCourseCalendarStateFromCache() - .count { it.isCourseSyncEnabled } - .let { coursesSynced -> - _uiState.update { it.copy(coursesSynced = coursesSynced) } - } + val courseStates = calendarInteractor.getAllCourseCalendarStateFromCache() + if (courseStates.isNotEmpty()) { + val syncedCoursesCount = courseStates.count { it.isCourseSyncEnabled } + _uiState.update { it.copy(coursesSynced = syncedCoursesCount) } + } } } + private fun isCalendarExist(): Boolean { return calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && calendarManager.isCalendarExist(calendarPreferences.calendarId) 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 93b6d12be..6dcffa1b9 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 @@ -168,7 +168,8 @@ private fun CoursesToSyncView( contentAlignment = Alignment.TopCenter ) { Column( - modifier = contentWidth.padding(vertical = 28.dp), + modifier = contentWidth + .padding(vertical = 28.dp), ) { Text( text = stringResource(R.string.profile_courses_to_sync_title), 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 0ed28f058..cab784d30 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 @@ -9,7 +9,7 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor -import org.openedx.profile.worker.CalendarSyncScheduler +import org.openedx.core.worker.CalendarSyncScheduler class CoursesToSyncViewModel( private val calendarInteractor: CalendarInteractor, diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt index f25051181..a3074e78e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -94,6 +96,7 @@ private fun DisableCalendarSyncDialogView( onCancelClick: () -> Unit, onDisableSyncingClick: () -> Unit ) { + val scrollState = rememberScrollState() DefaultDialogBox( modifier = modifier, onDismissClick = onCancelClick @@ -101,6 +104,7 @@ private fun DisableCalendarSyncDialogView( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(scrollState) .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) @@ -148,11 +152,13 @@ private fun DisableCalendarSyncDialogView( } Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_disable_calendar_dialog_description), + text = stringResource( + id = org.openedx.profile.R.string.profile_disable_calendar_dialog_description, + calendarData?.title ?: "" + ), style = MaterialTheme.appTypography.bodyMedium, color = MaterialTheme.appColors.textDark ) - OpenEdXOutlinedButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.profile_disable_syncing), diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt index 10bcf08a5..303dd2a40 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogViewModel.kt @@ -4,17 +4,23 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.profile.system.notifier.CalendarSyncDisabled +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.system.CalendarManager +import org.openedx.core.system.notifier.calendar.CalendarNotifier +import org.openedx.core.system.notifier.calendar.CalendarSyncDisabled class DisableCalendarSyncDialogViewModel( + private val calendarNotifier: CalendarNotifier, + private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, - private val calendarNotifier: CalendarNotifier + private val calendarInteractor: CalendarInteractor, ) : BaseViewModel() { fun disableSyncingClick() { - calendarPreferences.isCalendarSyncEnabled = false viewModelScope.launch { + calendarInteractor.clearCalendarCachedData() + calendarManager.deleteCalendar(calendarPreferences.calendarId) + calendarPreferences.clearCalendarPreferences() calendarNotifier.send(CalendarSyncDisabled) } } 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 7f0103789..77d365ccc 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 @@ -21,9 +21,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -62,6 +64,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.extension.parcelable +import org.openedx.core.extension.toastMessage import org.openedx.core.presentation.dialog.DefaultDialogBox import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.OpenEdXOutlinedButton @@ -94,7 +97,10 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarTitle, calendarColor -> - viewModel.createCalendar(requireContext(), calendarTitle, calendarColor) + val isCalendarCreated = viewModel.createCalendar(calendarTitle, calendarColor) + if (!isCalendarCreated) { + requireContext().toastMessage(context.getString(CoreR.string.core_error_unknown_error)) + } dismiss() } ) @@ -130,6 +136,7 @@ private fun NewCalendarDialog( onBeginSyncingClick: (calendarTitle: String, calendarColor: CalendarColor) -> Unit ) { val context = LocalContext.current + val scrollState = rememberScrollState() val title = when (newCalendarDialogType) { NewCalendarDialogType.CREATE_NEW -> stringResource(id = R.string.profile_new_calendar) NewCalendarDialogType.UPDATE -> stringResource(id = R.string.profile_change_sync_options) @@ -147,6 +154,7 @@ private fun NewCalendarDialog( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(scrollState) .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) @@ -218,6 +226,7 @@ private fun CalendarTitleTextField( onValueChanged: (String) -> Unit ) { val focusManager = LocalFocusManager.current + val maxChar = 40 var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue("") @@ -238,7 +247,7 @@ private fun CalendarTitleTextField( .height(48.dp), value = textFieldValue, onValueChange = { - textFieldValue = it + if (it.text.length <= maxChar) textFieldValue = it onValueChanged(it.text.trim()) }, colors = TextFieldDefaults.outlinedTextFieldColors( diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 272414b58..9722f1244 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -1,38 +1,40 @@ package org.openedx.profile.presentation.calendar -import android.content.Context import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.core.extension.toastMessage -import org.openedx.profile.system.CalendarManager -import org.openedx.profile.system.notifier.CalendarCreated -import org.openedx.profile.system.notifier.CalendarNotifier -import org.openedx.core.R as coreR +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.system.CalendarManager +import org.openedx.core.system.notifier.calendar.CalendarCreated +import org.openedx.core.system.notifier.calendar.CalendarNotifier class NewCalendarDialogViewModel( private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, - private val calendarNotifier: CalendarNotifier + private val calendarNotifier: CalendarNotifier, + private val calendarInteractor: CalendarInteractor ) : BaseViewModel() { fun createCalendar( - context: Context, calendarTitle: String, calendarColor: CalendarColor, - ) { + ): Boolean { + viewModelScope.launch { + calendarInteractor.resetChecksums() + } val calendarId = calendarManager.createOrUpdateCalendar( calendarTitle = calendarTitle, calendarColor = calendarColor.color ) - if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { + return if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { calendarPreferences.calendarId = calendarId viewModelScope.launch { calendarNotifier.send(CalendarCreated) } + true } else { - context.toastMessage(context.getString(coreR.string.core_error_unknown_error)) + false } } } 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 c4477ef28..79fff00d1 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 @@ -15,8 +15,8 @@ import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey -import org.openedx.profile.system.notifier.AccountDeactivated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountDeactivated +import org.openedx.profile.system.notifier.profile.ProfileNotifier class DeleteProfileViewModel( private val resourceManager: ResourceManager, 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 64cf9789f..ee179c146 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 @@ -15,8 +15,8 @@ import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey -import org.openedx.profile.system.notifier.AccountUpdated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountUpdated +import org.openedx.profile.system.notifier.profile.ProfileNotifier import java.io.File class EditProfileViewModel( 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 2370e0508..972426d2e 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 @@ -20,8 +20,8 @@ import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.notifier.AccountUpdated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountUpdated +import org.openedx.profile.system.notifier.profile.ProfileNotifier class ManageAccountViewModel( private val interactor: ProfileInteractor, diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt index d8fc19715..f02e09c22 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt @@ -19,8 +19,8 @@ import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.notifier.AccountUpdated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountUpdated +import org.openedx.profile.system.notifier.profile.ProfileNotifier class ProfileViewModel( private val interactor: ProfileInteractor, 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 9715eb774..3e9492a21 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 @@ -32,8 +32,8 @@ import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileAnalyticsEvent import org.openedx.profile.presentation.ProfileAnalyticsKey import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.notifier.AccountDeactivated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountDeactivated +import org.openedx.profile.system.notifier.profile.ProfileNotifier class SettingsViewModel( private val appData: AppData, diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/AccountDeactivated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/AccountDeactivated.kt deleted file mode 100644 index ff09cbf72..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/AccountDeactivated.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -class AccountDeactivated : ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/AccountUpdated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/AccountUpdated.kt deleted file mode 100644 index 2870235f2..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/AccountUpdated.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -class AccountUpdated : ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt deleted file mode 100644 index ba605a4a1..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarCreated.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -object CalendarCreated : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt deleted file mode 100644 index b85e4db66..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -interface CalendarEvent \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt deleted file mode 100644 index c2290f3b9..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncDisabled.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -object CalendarSyncDisabled : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt deleted file mode 100644 index 63ffd0fd9..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncFailed.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -object CalendarSyncFailed : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt deleted file mode 100644 index 00d65b7a5..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSynced.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -object CalendarSynced : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt b/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt deleted file mode 100644 index bb92163af..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/CalendarSyncing.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -object CalendarSyncing : CalendarEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/ProfileEvent.kt b/profile/src/main/java/org/openedx/profile/system/notifier/ProfileEvent.kt deleted file mode 100644 index dbe877081..000000000 --- a/profile/src/main/java/org/openedx/profile/system/notifier/ProfileEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.profile.system.notifier - -interface ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountDeactivated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountDeactivated.kt new file mode 100644 index 000000000..68f68e58f --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountDeactivated.kt @@ -0,0 +1,5 @@ +package org.openedx.profile.system.notifier.account + +import org.openedx.profile.system.notifier.profile.ProfileEvent + +class AccountDeactivated : ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountUpdated.kt b/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountUpdated.kt new file mode 100644 index 000000000..f43d6c329 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/account/AccountUpdated.kt @@ -0,0 +1,5 @@ +package org.openedx.profile.system.notifier.account + +import org.openedx.profile.system.notifier.profile.ProfileEvent + +class AccountUpdated : ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileEvent.kt b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileEvent.kt new file mode 100644 index 000000000..c978a78d3 --- /dev/null +++ b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.profile.system.notifier.profile + +interface ProfileEvent diff --git a/profile/src/main/java/org/openedx/profile/system/notifier/ProfileNotifier.kt b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt similarity index 70% rename from profile/src/main/java/org/openedx/profile/system/notifier/ProfileNotifier.kt rename to profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt index c51d82340..71e2dbf1d 100644 --- a/profile/src/main/java/org/openedx/profile/system/notifier/ProfileNotifier.kt +++ b/profile/src/main/java/org/openedx/profile/system/notifier/profile/ProfileNotifier.kt @@ -1,9 +1,10 @@ -package org.openedx.profile.system.notifier +package org.openedx.profile.system.notifier.profile import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import org.openedx.core.system.notifier.VideoQualityChanged +import org.openedx.profile.system.notifier.account.AccountDeactivated +import org.openedx.profile.system.notifier.account.AccountUpdated class ProfileNotifier { diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index f5aa71051..422f7aa31 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -71,13 +71,13 @@ Offline Sync Failed Synced - Syncing to calendar + Syncing to calendar… Disabling sync for a course will remove all events connected to the course from your synced calendar. Automatically remove events from courses you haven’t viewed in the last month Not Synced Inactive Hide Inactive Courses Disable Calendar Sync - Disabling calendar sync will delete the calendar “My Assignments.” You can turn calendar sync back on at any time. + Disabling calendar sync will delete the calendar “%1$s.” You can turn calendar sync back on at any time. 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 bfe6bb0b3..2322f8485 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 @@ -1,25 +1,33 @@ package org.openedx.profile.presentation.edit import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import org.openedx.core.R -import org.openedx.core.UIMessage -import org.openedx.profile.domain.model.Account -import org.openedx.core.domain.model.ProfileImage -import org.openedx.core.system.ResourceManager -import org.openedx.profile.domain.interactor.ProfileInteractor -import org.openedx.profile.presentation.ProfileAnalytics -import org.openedx.profile.system.notifier.AccountUpdated -import org.openedx.profile.system.notifier.ProfileNotifier -import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import org.openedx.core.R +import org.openedx.core.UIMessage +import org.openedx.core.domain.model.ProfileImage +import org.openedx.core.system.ResourceManager +import org.openedx.profile.domain.interactor.ProfileInteractor +import org.openedx.profile.domain.model.Account +import org.openedx.profile.presentation.ProfileAnalytics +import org.openedx.profile.system.notifier.account.AccountUpdated +import org.openedx.profile.system.notifier.profile.ProfileNotifier import java.io.File import java.net.UnknownHostException diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt index ca2ffd9bb..d33f24fb9 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt @@ -32,8 +32,8 @@ import org.openedx.core.system.ResourceManager import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter -import org.openedx.profile.system.notifier.AccountUpdated -import org.openedx.profile.system.notifier.ProfileNotifier +import org.openedx.profile.system.notifier.account.AccountUpdated +import org.openedx.profile.system.notifier.profile.ProfileNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) From 8c684e323ead4075c5675339c1c02610bdb43ec9 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 7 Jun 2024 12:35:14 +0300 Subject: [PATCH 19/26] feat: Save calendar on logout, remove on different user login --- .../app/data/storage/PreferencesManager.kt | 7 +++++ .../main/java/org/openedx/app/di/AppModule.kt | 4 +++ .../java/org/openedx/app/di/ScreenModule.kt | 4 ++- .../org/openedx/app/room/DatabaseManager.kt | 26 +++++++++++++++++++ .../presentation/signin/SignInViewModel.kt | 8 ++++++ .../java/org/openedx/core/DatabaseManager.kt | 5 ++++ .../core/data/storage/CalendarPreferences.kt | 1 + .../org/openedx/core/module/db/DownloadDao.kt | 9 ++++++- .../openedx/core/system/CalendarManager.kt | 2 +- .../openedx/course/data/storage/CourseDao.kt | 7 +++-- .../data/repository/ProfileRepository.kt | 12 +++------ .../calendar/CalendarSyncState.kt | 2 +- .../calendar/NewCalendarDialogViewModel.kt | 3 ++- 13 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/room/DatabaseManager.kt create mode 100644 core/src/main/java/org/openedx/core/DatabaseManager.kt 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 b4b21bdfe..18a1d79c3 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 @@ -180,6 +180,12 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getBoolean(IS_CALENDAR_SYNC_ENABLED, true) + override var calendarUser: String + set(value) { + saveString(CALENDAR_USER, value) + } + get() = getString(CALENDAR_USER) + override var isHideInactiveCourses: Boolean set(value) { saveBoolean(HIDE_INACTIVE_COURSES, value) @@ -210,5 +216,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences private const val RESET_APP_DIRECTORY = "reset_app_directory" private const val IS_CALENDAR_SYNC_ENABLED = "IS_CALENDAR_SYNC_ENABLED" private const val HIDE_INACTIVE_COURSES = "HIDE_INACTIVE_COURSES" + private const val CALENDAR_USER = "CALENDAR_USER" } } 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 01f8e076a..78d5c5a0d 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -17,6 +17,7 @@ import org.openedx.app.BuildConfig import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME +import org.openedx.app.room.DatabaseManager import org.openedx.app.system.notifier.AppNotifier import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics @@ -70,6 +71,7 @@ import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.data.storage.WhatsNewPreferences import org.openedx.whatsnew.presentation.WhatsNewAnalytics +import org.openedx.core.DatabaseManager as IDatabaseManager val appModule = module { @@ -86,6 +88,8 @@ val appModule = module { single { AppCookieManager(get(), get()) } single { ReviewManagerFactory.create(get()) } single { CalendarManager(get(), get(), get()) } + single { DatabaseManager(get(), get(), get(), get()) } + single { get() } single { ImageProcessor(get()) } 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 8cbc8b64c..5dd91cd5a 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -101,6 +101,8 @@ val screenModule = module { get(), get(), get(), + get(), + get(), courseId, infoType, ) @@ -144,7 +146,7 @@ val screenModule = module { ) } - factory { ProfileRepository(get(), get(), get(), get(), get(), get(), get()) } + factory { ProfileRepository(get(), get(), get(), get(), get()) } factory { ProfileInteractor(get()) } viewModel { ProfileViewModel( diff --git a/app/src/main/java/org/openedx/app/room/DatabaseManager.kt b/app/src/main/java/org/openedx/app/room/DatabaseManager.kt new file mode 100644 index 000000000..f373e0e42 --- /dev/null +++ b/app/src/main/java/org/openedx/app/room/DatabaseManager.kt @@ -0,0 +1,26 @@ +package org.openedx.app.room + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.openedx.core.DatabaseManager +import org.openedx.core.module.db.DownloadDao +import org.openedx.course.data.storage.CourseDao +import org.openedx.dashboard.data.DashboardDao +import org.openedx.discovery.data.storage.DiscoveryDao + +class DatabaseManager( + private val courseDao: CourseDao, + private val dashboardDao: DashboardDao, + private val downloadDao: DownloadDao, + private val discoveryDao: DiscoveryDao +) : DatabaseManager { + override fun clearTables() { + CoroutineScope(Dispatchers.Main).launch { + courseDao.clearCachedData() + dashboardDao.clearCachedData() + downloadDao.clearCachedData() + discoveryDao.clearCachedData() + } + } +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index 7ebc5a569..752b0d1cb 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -26,7 +26,9 @@ import org.openedx.core.SingleEventLiveData import org.openedx.core.UIMessage import org.openedx.core.Validator import org.openedx.core.config.Config +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.createHonorCodeField import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.global.WhatsNewGlobalManager @@ -47,6 +49,8 @@ class SignInViewModel( private val oAuthHelper: OAuthHelper, private val router: AuthRouter, private val whatsNewGlobalManager: WhatsNewGlobalManager, + private val calendarPreferences: CalendarPreferences, + private val calendarInteractor: CalendarInteractor, agreementProvider: AgreementProvider, config: Config, val courseId: String?, @@ -107,6 +111,10 @@ class SignInViewModel( ) } ) + if (calendarPreferences.calendarUser != username) { + calendarPreferences.clearCalendarPreferences() + calendarInteractor.clearCalendarCachedData() + } } catch (e: Exception) { if (e is EdxError.InvalidGrantException) { _uiMessage.value = diff --git a/core/src/main/java/org/openedx/core/DatabaseManager.kt b/core/src/main/java/org/openedx/core/DatabaseManager.kt new file mode 100644 index 000000000..d7bc7d025 --- /dev/null +++ b/core/src/main/java/org/openedx/core/DatabaseManager.kt @@ -0,0 +1,5 @@ +package org.openedx.core + +interface DatabaseManager { + fun clearTables() +} diff --git a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt index 8d202d186..91e38b35c 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CalendarPreferences.kt @@ -2,6 +2,7 @@ package org.openedx.core.data.storage interface CalendarPreferences { var calendarId: Long + var calendarUser: String var isCalendarSyncEnabled: Boolean var isHideInactiveCourses: Boolean diff --git a/core/src/main/java/org/openedx/core/module/db/DownloadDao.kt b/core/src/main/java/org/openedx/core/module/db/DownloadDao.kt index 5bdfc637b..8005a4b95 100644 --- a/core/src/main/java/org/openedx/core/module/db/DownloadDao.kt +++ b/core/src/main/java/org/openedx/core/module/db/DownloadDao.kt @@ -1,6 +1,10 @@ package org.openedx.core.module.db -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update import kotlinx.coroutines.flow.Flow @Dao @@ -23,4 +27,7 @@ interface DownloadDao { @Query("DELETE FROM download_model WHERE id in (:ids)") suspend fun removeAllDownloadModels(ids: List) + + @Query("DELETE FROM download_model") + suspend fun clearCachedData() } diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 380f16c88..611de6a25 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -29,7 +29,7 @@ class CalendarManager( android.Manifest.permission.READ_CALENDAR ) - private val accountName: String + val accountName: String get() = getUserAccountForSync() /** diff --git a/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt b/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt index ca7286e48..63bd1c4d9 100644 --- a/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt +++ b/course/src/main/java/org/openedx/course/data/storage/CourseDao.kt @@ -5,17 +5,16 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import org.openedx.core.data.model.room.CourseStructureEntity -import org.openedx.core.data.model.room.discovery.EnrolledCourseEntity @Dao interface CourseDao { - @Query("SELECT * FROM course_enrolled_table WHERE id=:id") - suspend fun getEnrolledCourseById(id: String): EnrolledCourseEntity? - @Query("SELECT * FROM course_structure_table WHERE id=:id") suspend fun getCourseStructureById(id: String): CourseStructureEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCourseStructureEntity(vararg courseStructureEntity: CourseStructureEntity) + + @Query("DELETE FROM course_structure_table") + suspend fun clearCachedData() } diff --git a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt index e0a1044ff..561d73c05 100644 --- a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt +++ b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt @@ -1,13 +1,11 @@ package org.openedx.profile.data.repository -import androidx.room.RoomDatabase import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import org.openedx.core.ApiConstants +import org.openedx.core.DatabaseManager import org.openedx.core.config.Config -import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences -import org.openedx.core.system.CalendarManager import org.openedx.profile.data.api.ProfileApi import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.profile.domain.model.Account @@ -16,11 +14,9 @@ import java.io.File class ProfileRepository( private val config: Config, private val api: ProfileApi, - private val room: RoomDatabase, private val profilePreferences: ProfilePreferences, private val corePreferences: CorePreferences, - private val calendarPreferences: CalendarPreferences, - private val calendarManager: CalendarManager + private val databaseManager: DatabaseManager ) { suspend fun getAccount(): Account { @@ -66,9 +62,7 @@ class ProfileRepository( ) } finally { corePreferences.clearCorePreferences() - calendarManager.deleteCalendar(calendarPreferences.calendarId) - calendarPreferences.clearCalendarPreferences() - room.clearAllTables() + databaseManager.clearTables() } } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt index 47be25677..29dbda632 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt @@ -29,7 +29,7 @@ enum class CalendarSyncState( get() = when (this) { OFFLINE -> MaterialTheme.appColors.textFieldHint SYNC_FAILED -> MaterialTheme.appColors.error - SYNCED -> MaterialTheme.appColors.accessGreen + SYNCED -> MaterialTheme.appColors.successGreen SYNCHRONIZATION -> MaterialTheme.appColors.primary } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 9722f1244..39b853251 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -13,7 +13,7 @@ class NewCalendarDialogViewModel( private val calendarManager: CalendarManager, private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, - private val calendarInteractor: CalendarInteractor + private val calendarInteractor: CalendarInteractor, ) : BaseViewModel() { fun createCalendar( @@ -29,6 +29,7 @@ class NewCalendarDialogViewModel( ) return if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { calendarPreferences.calendarId = calendarId + calendarPreferences.calendarUser = calendarManager.accountName viewModelScope.launch { calendarNotifier.send(CalendarCreated) } From 2b25e475c167f35fad9bf3390477bb2150392bd2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 7 Jun 2024 13:32:38 +0300 Subject: [PATCH 20/26] feat: Notification for CalendarSyncWorker, junit test fix --- app/src/main/AndroidManifest.xml | 4 - .../presentation/signin/SignInViewModel.kt | 8 +- .../signin/SignInViewModelTest.kt | 23 +++ .../core/service/CalendarSyncService.kt | 192 ------------------ .../openedx/core/worker/CalendarSyncWorker.kt | 178 +++++++++++++++- core/src/main/res/values/strings.xml | 2 - .../container/CourseContainerFragment.kt | 11 - .../DisableCalendarSyncDialogFragment.kt | 11 +- profile/src/main/res/values/strings.xml | 1 + 9 files changed, 203 insertions(+), 227 deletions(-) delete mode 100644 core/src/main/java/org/openedx/core/service/CalendarSyncService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9bf8b635a..7142d9190 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -114,10 +114,6 @@ - - diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index 752b0d1cb..27a52aff8 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -102,6 +102,10 @@ class SignInViewModel( interactor.login(username, password) _uiState.update { it.copy(loginSuccess = true) } setUserId() + if (calendarPreferences.calendarUser != username) { + calendarPreferences.clearCalendarPreferences() + calendarInteractor.clearCalendarCachedData() + } logEvent( AuthAnalyticsEvent.SIGN_IN_SUCCESS, buildMap { @@ -111,10 +115,6 @@ class SignInViewModel( ) } ) - if (calendarPreferences.calendarUser != username) { - calendarPreferences.clearCalendarPreferences() - calendarInteractor.clearCalendarCachedData() - } } catch (e: Exception) { if (e is EdxError.InvalidGrantException) { _uiMessage.value = diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index b36aabb10..cdfc6ecb1 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -34,7 +34,9 @@ import org.openedx.core.config.FacebookConfig import org.openedx.core.config.GoogleConfig import org.openedx.core.config.MicrosoftConfig import org.openedx.core.data.model.User +import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager @@ -61,6 +63,8 @@ class SignInViewModelTest { private val oAuthHelper = mockk() private val router = mockk() private val whatsNewGlobalManager = mockk() + private val calendarInteractor = mockk() + private val calendarPreferences = mockk() private val invalidCredential = "Invalid credentials" private val noInternet = "Slow or no internet connection" @@ -85,6 +89,9 @@ class SignInViewModelTest { every { config.getFacebookConfig() } returns FacebookConfig() every { config.getGoogleConfig() } returns GoogleConfig() every { config.getMicrosoftConfig() } returns MicrosoftConfig() + every { calendarPreferences.calendarUser } returns "" + every { calendarPreferences.clearCalendarPreferences() } returns Unit + coEvery { calendarInteractor.clearCalendarCachedData() } returns Unit } @After @@ -112,6 +119,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) viewModel.login("", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -145,6 +154,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.o", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -179,6 +190,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.org", "") @@ -212,6 +225,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.org", "ed") @@ -247,6 +262,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } returns Unit viewModel.login("acc@test.org", "edx") @@ -283,6 +300,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException() viewModel.login("acc@test.org", "edx") @@ -321,6 +340,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException() viewModel.login("acc@test.org", "edx") @@ -359,6 +380,8 @@ class SignInViewModelTest { whatsNewGlobalManager = whatsNewGlobalManager, courseId = "", infoType = "", + calendarInteractor = calendarInteractor, + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException() viewModel.login("acc@test.org", "edx") diff --git a/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt b/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt deleted file mode 100644 index f40ca2528..000000000 --- a/core/src/main/java/org/openedx/core/service/CalendarSyncService.kt +++ /dev/null @@ -1,192 +0,0 @@ -package org.openedx.core.service - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC -import android.os.Build -import android.os.IBinder -import androidx.core.app.NotificationCompat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.openedx.core.R -import org.openedx.core.data.model.CourseDates -import org.openedx.core.data.model.room.CourseCalendarEventEntity -import org.openedx.core.data.model.room.CourseCalendarStateEntity -import org.openedx.core.data.storage.CalendarPreferences -import org.openedx.core.domain.interactor.CalendarInteractor -import org.openedx.core.domain.model.CourseDateBlock -import org.openedx.core.domain.model.EnrollmentStatus -import org.openedx.core.system.CalendarManager -import org.openedx.core.system.notifier.calendar.CalendarNotifier -import org.openedx.core.system.notifier.calendar.CalendarSyncFailed -import org.openedx.core.system.notifier.calendar.CalendarSynced -import org.openedx.core.system.notifier.calendar.CalendarSyncing - -class CalendarSyncForegroundService : Service(), KoinComponent { - - private val calendarManager: CalendarManager by inject() - private val calendarInteractor: CalendarInteractor by inject() - private val calendarNotifier: CalendarNotifier by inject() - private val calendarPreferences: CalendarPreferences by inject() - - override fun onBind(intent: Intent?): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - startForeground(NOTIFICATION_ID, createNotification()) - } else { - startForeground(NOTIFICATION_ID, createNotification(), FOREGROUND_SERVICE_TYPE_DATA_SYNC) - } - val courseId = intent?.getStringExtra(ARG_COURSE_ID) - CoroutineScope(Dispatchers.Main).launch { - tryToSyncCalendar(courseId) - stopForeground(STOP_FOREGROUND_REMOVE) - stopSelf() - } - return START_NOT_STICKY - } - - private fun createNotification(): Notification { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - NOTIFICATION_CHANEL_ID, - getString(R.string.core_calendar_notification_channel_name), - NotificationManager.IMPORTANCE_DEFAULT - ) - notificationManager.createNotificationChannel(channel) - } - - return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID) - .setSmallIcon(R.drawable.core_ic_calendar) - .setContentTitle(getString(R.string.core_calendar_notification_channel_name)) - .setContentText(getString(R.string.core_title_syncing_calendar)) - .build() - } - - private suspend fun tryToSyncCalendar(courseId: String?) { - try { - val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST - val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled - if (isCalendarCreated && isCalendarSyncEnabled) { - calendarNotifier.send(CalendarSyncing) - if (courseId.isNullOrEmpty()) { - syncCalendar() - } else { - syncCalendar(courseId) - } - calendarNotifier.send(CalendarSynced) - } - } catch (e: Exception) { - calendarNotifier.send(CalendarSyncFailed) - } - } - - private suspend fun syncCalendar(courseId: String) { - calendarInteractor.getEnrollmentsStatus() - .find { it.courseId == courseId } - ?.let { enrollmentStatus -> - syncCourseEvents(enrollmentStatus) - } - } - - private suspend fun syncCalendar() { - calendarInteractor.getEnrollmentsStatus() - .forEach { enrollmentStatus -> - syncCourseEvents(enrollmentStatus) - } - } - - private suspend fun syncCourseEvents(enrollmentStatus: EnrollmentStatus) { - createCalendarState(enrollmentStatus) - val courseId = enrollmentStatus.courseId - if (enrollmentStatus.isActive && isCourseSyncEnabled(courseId)) { - val courseDates = calendarInteractor.getCourseDates(courseId) - val isCourseCalendarUpToDate = isCourseCalendarUpToDate(courseId, courseDates) - if (!isCourseCalendarUpToDate) { - removeCalendarEvents(courseId) - updateCourseEvents(courseDates, enrollmentStatus) - } - } else { - removeCalendarEvents(courseId) - } - } - - private suspend fun updateCourseEvents(courseDates: CourseDates, enrollmentStatus: EnrollmentStatus) { - courseDates.courseDateBlocks.forEach { courseDateBlock -> - courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> - createEvent(domainCourseDateBlock, enrollmentStatus) - } - } - calendarInteractor.updateCourseCalendarStateByIdInCache( - courseId = enrollmentStatus.courseId, - checksum = getCourseChecksum(courseDates) - ) - } - - private suspend fun removeCalendarEvents(courseId: String) { - calendarInteractor.getCourseCalendarEventsByIdFromCache(courseId).forEach { - calendarManager.deleteEvent(it.eventId) - } - calendarInteractor.deleteCourseCalendarEntitiesByIdFromCache(courseId) - calendarInteractor.updateCourseCalendarStateByIdInCache(courseId = courseId, checksum = 0) - } - - private suspend fun createEvent(courseDateBlock: CourseDateBlock, enrollmentStatus: EnrollmentStatus) { - val eventId = calendarManager.addEventsIntoCalendar( - calendarId = calendarPreferences.calendarId, - courseId = enrollmentStatus.courseId, - courseName = enrollmentStatus.courseName, - courseDateBlock = courseDateBlock - ) - val courseCalendarEventEntity = CourseCalendarEventEntity( - courseId = enrollmentStatus.courseId, - eventId = eventId - ) - calendarInteractor.insertCourseCalendarEntityToCache(courseCalendarEventEntity) - } - - private suspend fun createCalendarState(enrollmentStatus: EnrollmentStatus) { - val courseCalendarStateChecksum = getCourseCalendarStateChecksum(enrollmentStatus.courseId) - if (courseCalendarStateChecksum == null) { - val courseCalendarStateEntity = CourseCalendarStateEntity( - courseId = enrollmentStatus.courseId, - isCourseSyncEnabled = enrollmentStatus.isActive - ) - calendarInteractor.insertCourseCalendarStateEntityToCache(courseCalendarStateEntity) - } - } - - private suspend fun isCourseCalendarUpToDate(courseId: String, courseDates: CourseDates): Boolean { - val oldChecksum = getCourseCalendarStateChecksum(courseId) - val newChecksum = getCourseChecksum(courseDates) - return newChecksum == oldChecksum - } - - private suspend fun isCourseSyncEnabled(courseId: String): Boolean { - return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.isCourseSyncEnabled ?: true - } - - private fun getCourseChecksum(courseDates: CourseDates): Int { - return courseDates.courseDateBlocks.sumOf { it.mapToDomain().hashCode() } - } - - private suspend fun getCourseCalendarStateChecksum(courseId: String): Int? { - return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.checksum - } - - companion object { - const val NOTIFICATION_ID = 1234 - const val ARG_COURSE_ID = "ARG_COURSE_ID" - const val NOTIFICATION_CHANEL_ID = "calendar_sync_channel" - } -} \ 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 6f80c2850..abd072606 100644 --- a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -1,37 +1,197 @@ package org.openedx.core.worker +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context -import android.content.Intent -import androidx.core.content.ContextCompat +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import org.koin.core.component.KoinComponent -import org.openedx.core.service.CalendarSyncForegroundService +import org.koin.core.component.inject +import org.openedx.core.R +import org.openedx.core.data.model.CourseDates +import org.openedx.core.data.model.room.CourseCalendarEventEntity +import org.openedx.core.data.model.room.CourseCalendarStateEntity +import org.openedx.core.data.storage.CalendarPreferences +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.domain.model.CourseDateBlock +import org.openedx.core.domain.model.EnrollmentStatus +import org.openedx.core.system.CalendarManager +import org.openedx.core.system.notifier.calendar.CalendarNotifier +import org.openedx.core.system.notifier.calendar.CalendarSyncFailed +import org.openedx.core.system.notifier.calendar.CalendarSynced +import org.openedx.core.system.notifier.calendar.CalendarSyncing class CalendarSyncWorker( - context: Context, + private val context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams), KoinComponent { + private val calendarManager: CalendarManager by inject() + private val calendarInteractor: CalendarInteractor by inject() + private val calendarNotifier: CalendarNotifier by inject() + private val calendarPreferences: CalendarPreferences by inject() + + private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANEL_ID) + override suspend fun doWork(): Result { return try { + setForeground(createForegroundInfo()) val courseId = inputData.getString(ARG_COURSE_ID) - startForegroundService(courseId) + tryToSyncCalendar(courseId) Result.success() } catch (e: Exception) { + calendarNotifier.send(CalendarSyncFailed) Result.failure() } } - private fun startForegroundService(courseId: String?) { - val serviceIntent = Intent(applicationContext, CalendarSyncForegroundService::class.java).apply { - putExtra(CalendarSyncForegroundService.ARG_COURSE_ID, courseId) + private fun createForegroundInfo(): ForegroundInfo { + 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 + + return ForegroundInfo( + NOTIFICATION_ID, + notificationBuilder + .setSmallIcon(R.drawable.core_ic_calendar) + .setContentText(context.getString(R.string.core_title_syncing_calendar)) + .setContentTitle("") + .build(), + serviceType + ) + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createChannel() { + val notificationChannel = + NotificationChannel( + NOTIFICATION_CHANEL_ID, + context.getString(R.string.core_header_sync_to_calendar), + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(notificationChannel) + } + + private suspend fun tryToSyncCalendar(courseId: String?) { + val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST + val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled + if (isCalendarCreated && isCalendarSyncEnabled) { + calendarNotifier.send(CalendarSyncing) + if (courseId.isNullOrEmpty()) { + syncCalendar() + } else { + syncCalendar(courseId) + } + calendarNotifier.send(CalendarSynced) + } + } + + private suspend fun syncCalendar(courseId: String) { + calendarInteractor.getEnrollmentsStatus() + .find { it.courseId == courseId } + ?.let { enrollmentStatus -> + syncCourseEvents(enrollmentStatus) + } + } + + private suspend fun syncCalendar() { + calendarInteractor.getEnrollmentsStatus() + .forEach { enrollmentStatus -> + syncCourseEvents(enrollmentStatus) + } + } + + private suspend fun syncCourseEvents(enrollmentStatus: EnrollmentStatus) { + createCalendarState(enrollmentStatus) + val courseId = enrollmentStatus.courseId + if (enrollmentStatus.isActive && isCourseSyncEnabled(courseId)) { + val courseDates = calendarInteractor.getCourseDates(courseId) + val isCourseCalendarUpToDate = isCourseCalendarUpToDate(courseId, courseDates) + if (!isCourseCalendarUpToDate) { + removeCalendarEvents(courseId) + updateCourseEvents(courseDates, enrollmentStatus) + } + } else { + removeCalendarEvents(courseId) + } + } + + private suspend fun updateCourseEvents(courseDates: CourseDates, enrollmentStatus: EnrollmentStatus) { + courseDates.courseDateBlocks.forEach { courseDateBlock -> + courseDateBlock.mapToDomain()?.let { domainCourseDateBlock -> + createEvent(domainCourseDateBlock, enrollmentStatus) + } + } + calendarInteractor.updateCourseCalendarStateByIdInCache( + courseId = enrollmentStatus.courseId, + checksum = getCourseChecksum(courseDates) + ) + } + + private suspend fun removeCalendarEvents(courseId: String) { + calendarInteractor.getCourseCalendarEventsByIdFromCache(courseId).forEach { + calendarManager.deleteEvent(it.eventId) + } + calendarInteractor.deleteCourseCalendarEntitiesByIdFromCache(courseId) + calendarInteractor.updateCourseCalendarStateByIdInCache(courseId = courseId, checksum = 0) + } + + private suspend fun createEvent(courseDateBlock: CourseDateBlock, enrollmentStatus: EnrollmentStatus) { + val eventId = calendarManager.addEventsIntoCalendar( + calendarId = calendarPreferences.calendarId, + courseId = enrollmentStatus.courseId, + courseName = enrollmentStatus.courseName, + courseDateBlock = courseDateBlock + ) + val courseCalendarEventEntity = CourseCalendarEventEntity( + courseId = enrollmentStatus.courseId, + eventId = eventId + ) + calendarInteractor.insertCourseCalendarEntityToCache(courseCalendarEventEntity) + } + + private suspend fun createCalendarState(enrollmentStatus: EnrollmentStatus) { + val courseCalendarStateChecksum = getCourseCalendarStateChecksum(enrollmentStatus.courseId) + if (courseCalendarStateChecksum == null) { + val courseCalendarStateEntity = CourseCalendarStateEntity( + courseId = enrollmentStatus.courseId, + isCourseSyncEnabled = enrollmentStatus.isActive + ) + calendarInteractor.insertCourseCalendarStateEntityToCache(courseCalendarStateEntity) } - ContextCompat.startForegroundService(applicationContext, serviceIntent) + } + + private suspend fun isCourseCalendarUpToDate(courseId: String, courseDates: CourseDates): Boolean { + val oldChecksum = getCourseCalendarStateChecksum(courseId) + val newChecksum = getCourseChecksum(courseDates) + return newChecksum == oldChecksum + } + + private suspend fun isCourseSyncEnabled(courseId: String): Boolean { + return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.isCourseSyncEnabled ?: true + } + + private fun getCourseChecksum(courseDates: CourseDates): Int { + return courseDates.courseDateBlocks.sumOf { it.mapToDomain().hashCode() } + } + + private suspend fun getCourseCalendarStateChecksum(courseId: String): Int? { + return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.checksum } companion object { const val ARG_COURSE_ID = "ARG_COURSE_ID" const val WORKER_TAG = "calendar_sync_worker_tag" + const val NOTIFICATION_ID = 1234 + const val NOTIFICATION_CHANEL_ID = "calendar_sync_channel" } } diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 8e8426f6d..580d262ac 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -190,6 +190,4 @@ Discussions More Dates - Disable Syncing - Calendar Sync 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 4e1efc249..946185a93 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 @@ -2,7 +2,6 @@ package org.openedx.course.presentation.container import android.os.Bundle import android.view.View -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -55,7 +54,6 @@ import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.global.viewBinding -import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.RoundTabsBar @@ -87,15 +85,6 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { ) } - private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { isGranted -> - viewModel.logCalendarPermissionAccess(!isGranted.containsValue(false)) - if (!isGranted.containsValue(false)) { - viewModel.setCalendarSyncDialogType(CalendarSyncDialogType.SYNC_DIALOG) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.preloadCourseStructure() diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt index a3074e78e..43c805831 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.R import org.openedx.core.domain.model.CalendarData import org.openedx.core.extension.parcelable import org.openedx.core.presentation.dialog.DefaultDialogBox @@ -44,7 +43,9 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.profile.R import androidx.compose.ui.graphics.Color as ComposeColor +import org.openedx.core.R as coreR class DisableCalendarSyncDialogFragment : DialogFragment() { @@ -115,12 +116,12 @@ private fun DisableCalendarSyncDialogView( ) { Image( modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.core_ic_warning), + painter = painterResource(id = coreR.drawable.core_ic_warning), contentDescription = null ) Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = org.openedx.profile.R.string.profile_disable_calendar_dialog_title), + text = stringResource(id = R.string.profile_disable_calendar_dialog_title), style = MaterialTheme.appTypography.titleLarge, color = MaterialTheme.appColors.textDark ) @@ -153,7 +154,7 @@ private fun DisableCalendarSyncDialogView( Text( modifier = Modifier.fillMaxWidth(), text = stringResource( - id = org.openedx.profile.R.string.profile_disable_calendar_dialog_description, + id = R.string.profile_disable_calendar_dialog_description, calendarData?.title ?: "" ), style = MaterialTheme.appTypography.bodyMedium, @@ -171,7 +172,7 @@ private fun DisableCalendarSyncDialogView( ) OpenEdXButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.core_cancel), + text = stringResource(id = coreR.string.core_cancel), onClick = { onCancelClick() } diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 422f7aa31..548bea1ce 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -79,5 +79,6 @@ Hide Inactive Courses Disable Calendar Sync Disabling calendar sync will delete the calendar “%1$s.” You can turn calendar sync back on at any time. + Disable Syncing From 87c23c9f674a0ab00dd8f5d5188a6f01d74fea43 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Sat, 8 Jun 2024 13:36:53 +0300 Subject: [PATCH 21/26] feat: removeUnenrolledCourseEvents --- .../domain/interactor/CalendarInteractor.kt | 4 +++ .../org/openedx/core/module/db/CalendarDao.kt | 3 ++ .../core/repository/CalendarRepository.kt | 4 +++ .../openedx/core/worker/CalendarSyncWorker.kt | 29 +++++++++++++------ .../calendar/CalendarViewModel.kt | 1 + 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt index 653dca298..da84dba1a 100644 --- a/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt +++ b/core/src/main/java/org/openedx/core/domain/interactor/CalendarInteractor.kt @@ -53,4 +53,8 @@ class CalendarInteractor( ) { repository.updateCourseCalendarStateByIdInCache(courseId, checksum, isCourseSyncEnabled) } + + suspend fun deleteCourseCalendarStateByIdFromCache(courseId: String) { + repository.deleteCourseCalendarStateByIdFromCache(courseId) + } } diff --git a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt index 3b8fa0a74..0dcef5006 100644 --- a/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt +++ b/core/src/main/java/org/openedx/core/module/db/CalendarDao.kt @@ -36,6 +36,9 @@ interface CalendarDao { @Query("DELETE FROM course_calendar_state_table") suspend fun clearCourseCalendarStateCachedData() + @Query("DELETE FROM course_calendar_state_table WHERE course_id = :courseId") + suspend fun deleteCourseCalendarStateById(courseId: String) + @Query("UPDATE course_calendar_state_table SET checksum = 0") suspend fun resetChecksums() diff --git a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt index c59d85ccc..e46922605 100644 --- a/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt +++ b/core/src/main/java/org/openedx/core/repository/CalendarRepository.kt @@ -70,4 +70,8 @@ class CalendarRepository( ) { calendarDao.updateCourseCalendarStateById(courseId, checksum, isCourseSyncEnabled) } + + suspend fun deleteCourseCalendarStateByIdFromCache(courseId: String) { + calendarDao.deleteCourseCalendarStateById(courseId) + } } 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 abd072606..752c60b1d 100644 --- a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -86,28 +86,39 @@ class CalendarSyncWorker( val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled if (isCalendarCreated && isCalendarSyncEnabled) { calendarNotifier.send(CalendarSyncing) + val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() if (courseId.isNullOrEmpty()) { - syncCalendar() + syncCalendar(enrollmentsStatus) } else { - syncCalendar(courseId) + syncCalendar(enrollmentsStatus, courseId) } + removeUnenrolledCourseEvents(enrollmentsStatus) calendarNotifier.send(CalendarSynced) } } - private suspend fun syncCalendar(courseId: String) { - calendarInteractor.getEnrollmentsStatus() + private suspend fun removeUnenrolledCourseEvents(enrollmentStatus: List) { + val enrolledCourseIds = enrollmentStatus.map { it.courseId } + val cachedCourseIds = calendarInteractor.getAllCourseCalendarStateFromCache().map { it.courseId } + val unenrolledCourseIds = cachedCourseIds.filter { it !in enrolledCourseIds } + unenrolledCourseIds.forEach { courseId -> + removeCalendarEvents(courseId) + calendarInteractor.deleteCourseCalendarStateByIdFromCache(courseId) + } + } + + private suspend fun syncCalendar(enrollmentsStatus: List, courseId: String) { + enrollmentsStatus .find { it.courseId == courseId } ?.let { enrollmentStatus -> syncCourseEvents(enrollmentStatus) } } - private suspend fun syncCalendar() { - calendarInteractor.getEnrollmentsStatus() - .forEach { enrollmentStatus -> - syncCourseEvents(enrollmentStatus) - } + private suspend fun syncCalendar(enrollmentsStatus: List) { + enrollmentsStatus.forEach { enrollmentStatus -> + syncCourseEvents(enrollmentStatus) + } } private suspend fun syncCourseEvents(enrollmentStatus: EnrollmentStatus) { 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 f522cd4c6..cb67fd3e9 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 @@ -47,6 +47,7 @@ class CalendarViewModel( init { viewModelScope.launch { + calendarSyncScheduler.requestImmediateSync() calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { CalendarCreated -> { From 70106396513a8ad5ffc7353b5dc68e444b33ecaa Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Mon, 10 Jun 2024 15:02:56 +0300 Subject: [PATCH 22/26] feat: Calendar sync state on CourseDatesScreen --- .../main/java/org/openedx/app/AppRouter.kt | 3 +- .../main/java/org/openedx/app/di/AppModule.kt | 4 +- .../java/org/openedx/app/di/ScreenModule.kt | 8 ++- .../java/org/openedx/core/CalendarRouter.kt | 8 +++ .../calendarsync}/CalendarSyncState.kt | 29 +++++++-- core/src/main/res/values/strings.xml | 7 +++ .../presentation/dates/CourseDatesScreen.kt | 62 +++++++++++++++++-- .../dates/CourseDatesViewModel.kt | 58 ++++++++++++++--- .../presentation/dates/DashboardUIState.kt | 2 + .../dates/CourseDatesViewModelTest.kt | 37 +++++++++-- .../profile/presentation/ProfileRouter.kt | 2 - .../calendar/CalendarSettingsView.kt | 1 + .../presentation/calendar/CalendarUIState.kt | 1 + .../calendar/CalendarViewModel.kt | 2 +- .../presentation/calendar/SyncCourseTab.kt | 6 +- .../settings/SettingsViewModel.kt | 20 +++--- profile/src/main/res/values/strings.xml | 5 -- 17 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/CalendarRouter.kt rename {profile/src/main/java/org/openedx/profile/presentation/calendar => core/src/main/java/org/openedx/core/presentation/settings/calendarsync}/CalendarSyncState.kt (61%) diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index cbf7c30e9..0b64fb94f 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -8,6 +8,7 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.restore.RestorePasswordFragment import org.openedx.auth.presentation.signin.SignInFragment 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 @@ -57,7 +58,7 @@ import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, - ProfileRouter, AppUpgradeRouter, WhatsNewRouter { + ProfileRouter, AppUpgradeRouter, WhatsNewRouter, CalendarRouter { //region AuthRouter override fun navigateToMain( 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 59f628e2a..e9f272848 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -12,10 +12,10 @@ import org.koin.core.qualifier.named import org.koin.dsl.module import org.openedx.app.AnalyticsManager import org.openedx.app.AppAnalytics -import org.openedx.app.deeplink.DeepLinkRouter import org.openedx.app.AppRouter import org.openedx.app.BuildConfig import org.openedx.app.data.storage.PreferencesManager +import org.openedx.app.deeplink.DeepLinkRouter import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME import org.openedx.app.room.DatabaseManager @@ -27,6 +27,7 @@ import org.openedx.auth.presentation.sso.FacebookAuthHelper import org.openedx.auth.presentation.sso.GoogleAuthHelper import org.openedx.auth.presentation.sso.MicrosoftAuthHelper import org.openedx.auth.presentation.sso.OAuthHelper +import org.openedx.core.CalendarRouter import org.openedx.core.ImageProcessor import org.openedx.core.config.Config import org.openedx.core.data.model.CourseEnrollments @@ -119,6 +120,7 @@ val appModule = module { single { get() } single { get() } single { get() } + single { get() } single { DeepLinkRouter(get(), get(), get(), get(), get()) } single { NetworkConnection(get()) } 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 5aec83a35..155c7d15b 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -173,7 +173,8 @@ val screenModule = module { get(), get(), get(), - get() + get(), + get(), ) } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } @@ -324,7 +325,10 @@ val screenModule = module { get(), get(), get(), - get() + get(), + get(), + get(), + get(), ) } viewModel { (courseId: String, handoutsType: String) -> diff --git a/core/src/main/java/org/openedx/core/CalendarRouter.kt b/core/src/main/java/org/openedx/core/CalendarRouter.kt new file mode 100644 index 000000000..1969ca860 --- /dev/null +++ b/core/src/main/java/org/openedx/core/CalendarRouter.kt @@ -0,0 +1,8 @@ +package org.openedx.core + +import androidx.fragment.app.FragmentManager + +interface CalendarRouter { + + fun navigateToCalendarSettings(fm: FragmentManager) +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt similarity index 61% rename from profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt rename to core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt index 29dbda632..3e6188ed1 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSyncState.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt @@ -1,4 +1,4 @@ -package org.openedx.profile.presentation.calendar +package org.openedx.core.presentation.settings.calendarsync import androidx.annotation.StringRes import androidx.compose.material.MaterialTheme @@ -11,17 +11,34 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import org.openedx.core.R import org.openedx.core.ui.theme.appColors -import org.openedx.profile.R enum class CalendarSyncState( @StringRes val title: Int, + @StringRes val longTitle: Int, val icon: ImageVector ) { - OFFLINE(R.string.profile_offline, Icons.Default.SyncDisabled), - SYNC_FAILED(R.string.profile_syncing_failed, Icons.Rounded.FreeCancellation), - SYNCED(R.string.profile_synced, Icons.Rounded.EventRepeat), - SYNCHRONIZATION(R.string.profile_syncing_to_calendar, Icons.Default.CloudSync); + OFFLINE( + R.string.core_offline, + R.string.core_offline, + Icons.Default.SyncDisabled + ), + SYNC_FAILED( + R.string.core_syncing_failed, + R.string.core_calendar_sync_failed, + Icons.Rounded.FreeCancellation + ), + SYNCED( + R.string.core_synced, + R.string.core_synced_to_calendar, + Icons.Rounded.EventRepeat + ), + SYNCHRONIZATION( + R.string.core_syncing_to_calendar, + R.string.core_syncing_to_calendar, + Icons.Default.CloudSync + ); val tint: Color @Composable diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 580d262ac..accb31946 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -190,4 +190,11 @@ Discussions More Dates + + Calendar Sync Failed + Synced to Calendar + Sync Failed + Synced + Not Synced + Syncing to calendar… 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 dda18519a..8d3f1536a 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 @@ -43,7 +43,6 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -73,6 +72,7 @@ import org.openedx.core.extension.isNotEmptyThenLet import org.openedx.core.presentation.CoreAnalyticsScreen import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.dialog.alert.ActionDialogFragment +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState import org.openedx.core.presentation.settings.calendarsync.CalendarSyncUIState import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.WindowSize @@ -99,7 +99,7 @@ fun CourseDatesScreen( isFragmentResumed: Boolean, updateCourseStructure: () -> Unit ) { - val uiState by viewModel.uiState.observeAsState(DatesUIState.Loading) + val uiState by viewModel.uiState.collectAsState(DatesUIState.Loading) val uiMessage by viewModel.uiMessage.collectAsState(null) val context = LocalContext.current @@ -164,6 +164,9 @@ fun CourseDatesScreen( updateCourseStructure() } } + }, + onCalendarSyncStateClick = { + viewModel.calendarRouter.navigateToCalendarSettings(fragmentManager) } ) } @@ -176,7 +179,8 @@ private fun CourseDatesUI( isSelfPaced: Boolean, onItemClick: (CourseDateBlock) -> Unit, onPLSBannerViewed: () -> Unit, - onSyncDates: () -> Unit + onSyncDates: () -> Unit, + onCalendarSyncStateClick: () -> Unit, ) { val scaffoldState = rememberScaffoldState() @@ -249,6 +253,46 @@ private fun CourseDatesUI( } } + // Handle calendar sync state + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .background( + MaterialTheme.appColors.cardViewBackground, + MaterialTheme.shapes.medium + ) + .border( + 0.75.dp, + MaterialTheme.appColors.cardViewBorder, + MaterialTheme.shapes.medium + ) + .clickable { + onCalendarSyncStateClick() + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, start = 16.dp, end = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = uiState.calendarSyncState.icon, + tint = uiState.calendarSyncState.tint, + contentDescription = null + ) + Text( + text = stringResource(uiState.calendarSyncState.longTitle), + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + } + } + } + // Handle DatesSection.COMPLETED separately datesSection[DatesSection.COMPLETED]?.isNotEmptyThenLet { section -> item { @@ -622,12 +666,16 @@ private fun CourseDatesScreenPreview() { OpenEdXTheme { CourseDatesUI( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - uiState = DatesUIState.Dates(CourseDatesResult(mockedResponse, mockedCourseBannerInfo)), + uiState = DatesUIState.Dates( + CourseDatesResult(mockedResponse, mockedCourseBannerInfo), + CalendarSyncState.SYNCED + ), uiMessage = null, isSelfPaced = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, + onCalendarSyncStateClick = {}, ) } } @@ -639,12 +687,16 @@ private fun CourseDatesScreenTabletPreview() { OpenEdXTheme { CourseDatesUI( windowSize = WindowSize(WindowType.Medium, WindowType.Medium), - uiState = DatesUIState.Dates(CourseDatesResult(mockedResponse, mockedCourseBannerInfo)), + uiState = DatesUIState.Dates( + CourseDatesResult(mockedResponse, mockedCourseBannerInfo), + CalendarSyncState.SYNCED + ), uiMessage = null, isSelfPaced = true, onItemClick = {}, onPLSBannerViewed = {}, onSyncDates = {}, + onCalendarSyncStateClick = {}, ) } } 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 cc149f799..addad3199 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 @@ -1,16 +1,20 @@ package org.openedx.course.presentation.dates -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.CalendarRouter import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config +import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseBannerType import org.openedx.core.domain.model.CourseDateBlock @@ -19,12 +23,14 @@ import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.settings.calendarsync.CalendarSyncDialogType +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState import org.openedx.core.system.ResourceManager 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.RefreshDates +import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseAnalyticsEvent @@ -40,14 +46,17 @@ class CourseDatesViewModel( private val resourceManager: ResourceManager, private val courseAnalytics: CourseAnalytics, private val config: Config, - val courseRouter: CourseRouter + private val calendarInteractor: CalendarInteractor, + private val calendarNotifier: CalendarNotifier, + val courseRouter: CourseRouter, + val calendarRouter: CalendarRouter ) : BaseViewModel() { var isSelfPaced = true - private val _uiState = MutableLiveData(DatesUIState.Loading) - val uiState: LiveData - get() = _uiState + private val _uiState = MutableStateFlow(DatesUIState.Loading) + val uiState: StateFlow + get() = _uiState.asStateFlow() private val _uiMessage = MutableSharedFlow() val uiMessage: SharedFlow @@ -68,6 +77,16 @@ class CourseDatesViewModel( } } } + viewModelScope.launch { + calendarNotifier.notifier.collect { + (_uiState.value as? DatesUIState.Dates)?.let { currentUiState -> + val courseDates = currentUiState.courseDatesResult.datesSection.values.flatten() + _uiState.update { + (it as DatesUIState.Dates).copy(calendarSyncState = getCalendarState(courseDates)) + } + } + } + } loadingCourseDatesInternal() } @@ -81,7 +100,9 @@ class CourseDatesViewModel( if (datesResponse.datesSection.isEmpty()) { _uiState.value = DatesUIState.Empty } else { - _uiState.value = DatesUIState.Dates(datesResponse) + val courseDates = datesResponse.datesSection.values.flatten() + val calendarState = getCalendarState(courseDates) + _uiState.value = DatesUIState.Dates(datesResponse, calendarState) courseBannerType = datesResponse.courseBanner.bannerType checkIfCalendarOutOfDate() } @@ -148,6 +169,29 @@ class CourseDatesViewModel( } } + private suspend fun getCalendarState(courseDates: List): CalendarSyncState { + val courseCalendarState = calendarInteractor.getCourseCalendarStateByIdFromCache(courseId) + return when { + courseCalendarState?.isCourseSyncEnabled != true -> CalendarSyncState.OFFLINE + !isCourseCalendarUpToDate(courseDates) -> CalendarSyncState.SYNC_FAILED + else -> CalendarSyncState.SYNCED + } + } + + private suspend fun isCourseCalendarUpToDate(courseDateBlocks: List): Boolean { + val oldChecksum = getCourseCalendarStateChecksum() + val newChecksum = getCourseChecksum(courseDateBlocks) + return newChecksum == oldChecksum + } + + private fun getCourseChecksum(courseDateBlocks: List): Int { + return courseDateBlocks.sumOf { it.hashCode() } + } + + private suspend fun getCourseCalendarStateChecksum(): Int? { + return calendarInteractor.getCourseCalendarStateByIdFromCache(courseId)?.checksum + } + fun logPlsBannerViewed() { logPLSBannerEvent(CourseAnalyticsEvent.PLS_BANNER_VIEWED) } diff --git a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt index 8ff75239f..43fc63f58 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt @@ -1,10 +1,12 @@ package org.openedx.course.presentation.dates import org.openedx.core.domain.model.CourseDatesResult +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState sealed class DatesUIState { data class Dates( val courseDatesResult: CourseDatesResult, + val calendarSyncState: CalendarSyncState ) : DatesUIState() object Empty : DatesUIState() diff --git a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt index ed062bac3..ca0c18c79 100644 --- a/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/dates/CourseDatesViewModelTest.kt @@ -23,10 +23,13 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import org.openedx.core.CalendarRouter import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config import org.openedx.core.data.model.DateType +import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.domain.model.CourseCalendarState import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.CourseDatesBannerInfo import org.openedx.core.domain.model.CourseDatesResult @@ -37,6 +40,9 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEvent import org.openedx.core.system.notifier.CourseLoading import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.core.system.notifier.calendar.CalendarEvent +import org.openedx.core.system.notifier.calendar.CalendarNotifier +import org.openedx.core.system.notifier.calendar.CalendarSynced import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseRouter @@ -56,6 +62,9 @@ class CourseDatesViewModelTest { private val analytics = mockk() private val config = mockk() private val courseRouter = mockk() + private val calendarRouter = mockk() + private val calendarNotifier = mockk() + private val calendarInteractor = mockk() private val openEdx = "OpenEdx" private val noInternet = "Slow or no internet connection" @@ -127,7 +136,13 @@ class CourseDatesViewModelTest { every { notifier.notifier } returns flowOf(CourseLoading(false)) coEvery { notifier.send(any()) } returns Unit coEvery { notifier.send(any()) } returns Unit - coEvery { notifier.send(any()) } returns Unit + every { calendarNotifier.notifier } returns flowOf(CalendarSynced) + coEvery { calendarNotifier.send(any()) } returns Unit + coEvery { calendarInteractor.getCourseCalendarStateByIdFromCache(any()) } returns CourseCalendarState( + 0, + "", + true + ) } @After @@ -145,7 +160,10 @@ class CourseDatesViewModelTest { resourceManager, analytics, config, - courseRouter + calendarInteractor, + calendarNotifier, + courseRouter, + calendarRouter, ) coEvery { interactor.getCourseDates(any()) } throws UnknownHostException() val message = async { @@ -171,7 +189,10 @@ class CourseDatesViewModelTest { resourceManager, analytics, config, - courseRouter + calendarInteractor, + calendarNotifier, + courseRouter, + calendarRouter, ) coEvery { interactor.getCourseDates(any()) } throws Exception() val message = async { @@ -197,7 +218,10 @@ class CourseDatesViewModelTest { resourceManager, analytics, config, - courseRouter + calendarInteractor, + calendarNotifier, + courseRouter, + calendarRouter, ) coEvery { interactor.getCourseDates(any()) } returns mockedCourseDatesResult val message = async { @@ -223,7 +247,10 @@ class CourseDatesViewModelTest { resourceManager, analytics, config, - courseRouter + calendarInteractor, + calendarNotifier, + courseRouter, + calendarRouter, ) coEvery { interactor.getCourseDates(any()) } returns CourseDatesResult( datesSection = linkedMapOf(), diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt index 7526c886b..e9f67ad48 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileRouter.kt @@ -22,7 +22,5 @@ interface ProfileRouter { fun navigateToManageAccount(fm: FragmentManager) - fun navigateToCalendarSettings(fm: FragmentManager) - fun navigateToCoursesToSync(fm: FragmentManager) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt index 866ab2f86..bce3ede77 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarSettingsView.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.openedx.core.domain.model.CalendarData +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState import org.openedx.core.ui.OpenEdXOutlinedButton import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt index 294f8e00d..cf99e0fa2 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/CalendarUIState.kt @@ -1,6 +1,7 @@ package org.openedx.profile.presentation.calendar import org.openedx.core.domain.model.CalendarData +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState data class CalendarUIState( val isCalendarExist: Boolean, 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 cb67fd3e9..a66f5607e 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 @@ -11,6 +11,7 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState import org.openedx.core.system.CalendarManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.calendar.CalendarCreated @@ -47,7 +48,6 @@ class CalendarViewModel( init { viewModelScope.launch { - calendarSyncScheduler.requestImmediateSync() calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { CalendarCreated -> { diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt index d0645cb54..a68fedc3e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt @@ -1,12 +1,12 @@ package org.openedx.profile.presentation.calendar import androidx.annotation.StringRes -import org.openedx.profile.R +import org.openedx.core.R enum class SyncCourseTab( @StringRes val title: Int ) { - SYNCED(R.string.profile_synced), - NOT_SYNCED(R.string.profile_not_synced) + SYNCED(R.string.core_synced), + NOT_SYNCED(R.string.core_not_synced) } 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 3e9492a21..1465b4176 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 @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.openedx.core.AppUpdateState import org.openedx.core.BaseViewModel +import org.openedx.core.CalendarRouter import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.config.Config @@ -43,7 +44,8 @@ class SettingsViewModel( private val cookieManager: AppCookieManager, private val workerController: DownloadWorkerController, private val analytics: ProfileAnalytics, - private val router: ProfileRouter, + private val profileRouter: ProfileRouter, + private val calendarRouter: CalendarRouter, private val appUpgradeNotifier: AppUpgradeNotifier, private val profileNotifier: ProfileNotifier, ) : BaseViewModel() { @@ -124,12 +126,12 @@ class SettingsViewModel( } fun videoSettingsClicked(fragmentManager: FragmentManager) { - router.navigateToVideoSettings(fragmentManager) + profileRouter.navigateToVideoSettings(fragmentManager) logProfileEvent(ProfileAnalyticsEvent.VIDEO_SETTING_CLICKED) } fun privacyPolicyClicked(fragmentManager: FragmentManager) { - router.navigateToWebContent( + profileRouter.navigateToWebContent( fm = fragmentManager, title = resourceManager.getString(R.string.core_privacy_policy), url = configuration.agreementUrls.privacyPolicyUrl, @@ -138,7 +140,7 @@ class SettingsViewModel( } fun cookiePolicyClicked(fragmentManager: FragmentManager) { - router.navigateToWebContent( + profileRouter.navigateToWebContent( fm = fragmentManager, title = resourceManager.getString(R.string.core_cookie_policy), url = configuration.agreementUrls.cookiePolicyUrl, @@ -147,7 +149,7 @@ class SettingsViewModel( } fun dataSellClicked(fragmentManager: FragmentManager) { - router.navigateToWebContent( + profileRouter.navigateToWebContent( fm = fragmentManager, title = resourceManager.getString(R.string.core_data_sell), url = configuration.agreementUrls.dataSellConsentUrl, @@ -160,7 +162,7 @@ class SettingsViewModel( } fun termsOfUseClicked(fragmentManager: FragmentManager) { - router.navigateToWebContent( + profileRouter.navigateToWebContent( fm = fragmentManager, title = resourceManager.getString(R.string.core_terms_of_use), url = configuration.agreementUrls.tosUrl, @@ -182,15 +184,15 @@ class SettingsViewModel( } fun manageAccountClicked(fragmentManager: FragmentManager) { - router.navigateToManageAccount(fragmentManager) + profileRouter.navigateToManageAccount(fragmentManager) } fun calendarSettingsClicked(fragmentManager: FragmentManager) { - router.navigateToCalendarSettings(fragmentManager) + calendarRouter.navigateToCalendarSettings(fragmentManager) } fun restartApp(fragmentManager: FragmentManager) { - router.restartApp( + profileRouter.restartApp( fragmentManager, isLogistrationEnabled ) diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 548bea1ce..a36595f71 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -68,13 +68,8 @@ Options Use relative dates Show relative dates like “Tomorrow” and “Yesterday” - Offline - Sync Failed - Synced - Syncing to calendar… Disabling sync for a course will remove all events connected to the course from your synced calendar. Automatically remove events from courses you haven’t viewed in the last month - Not Synced Inactive Hide Inactive Courses Disable Calendar Sync From f55f2cd7a1616c7622bc338040386ee875ab6b5a Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Tue, 11 Jun 2024 18:20:12 +0300 Subject: [PATCH 23/26] fix: Crash CoursesToSyncFragment in offline mode --- .../java/org/openedx/app/di/ScreenModule.kt | 2 +- .../calendar/CoursesToSyncFragment.kt | 11 ++++++ .../calendar/CoursesToSyncViewModel.kt | 35 ++++++++++++++++--- 3 files changed, 43 insertions(+), 5 deletions(-) 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 155c7d15b..8aa41e0f1 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -179,7 +179,7 @@ val screenModule = module { } viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { CoursesToSyncViewModel(get(), get(), get()) } + viewModel { CoursesToSyncViewModel(get(), get(), get(), get()) } viewModel { NewCalendarDialogViewModel(get(), get(), get(), get()) } viewModel { DisableCalendarSyncDialogViewModel(get(), get(), get(), get()) } factory { CalendarRepository(get(), get(), get()) } 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 6dcffa1b9..b47d47d11 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 @@ -54,6 +54,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import org.koin.androidx.compose.koinViewModel +import org.openedx.core.UIMessage +import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.displayCutoutForLandscape @@ -81,10 +83,12 @@ class CoursesToSyncFragment : Fragment() { val windowSize = rememberWindowSize() val viewModel: CoursesToSyncViewModel = koinViewModel() val uiState by viewModel.uiState.collectAsState() + val uiMessage by viewModel.uiMessage.collectAsState(initial = null) CoursesToSyncView( windowSize = windowSize, uiState = uiState, + uiMessage = uiMessage, onHideInactiveCoursesSwitchClick = { viewModel.setHideInactiveCoursesEnabled(it) }, @@ -105,6 +109,7 @@ private fun CoursesToSyncView( windowSize: WindowSize, onBackClick: () -> Unit, uiState: CoursesToSyncUIState, + uiMessage: UIMessage?, onHideInactiveCoursesSwitchClick: (Boolean) -> Unit, onCourseSyncCheckChange: (Boolean, String) -> Unit ) { @@ -137,6 +142,11 @@ private fun CoursesToSyncView( ) } + HandleUIMessage( + uiMessage = uiMessage, + scaffoldState = scaffoldState + ) + Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter @@ -365,6 +375,7 @@ private fun CoursesToSyncViewPreview() { isHideInactiveCourses = true, isLoading = false ), + uiMessage = null, onHideInactiveCoursesSwitchClick = {}, onCourseSyncCheckChange = { _, _ -> }, onBackClick = {} 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 cab784d30..5e54363e6 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 @@ -1,20 +1,28 @@ package org.openedx.profile.presentation.calendar import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.UIMessage import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor +import org.openedx.core.extension.isInternetError +import org.openedx.core.system.ResourceManager import org.openedx.core.worker.CalendarSyncScheduler class CoursesToSyncViewModel( private val calendarInteractor: CalendarInteractor, private val calendarPreferences: CalendarPreferences, private val calendarSyncScheduler: CalendarSyncScheduler, + private val resourceManager: ResourceManager, ) : BaseViewModel() { private val _uiState = MutableStateFlow( @@ -25,6 +33,11 @@ class CoursesToSyncViewModel( isLoading = true ) ) + + private val _uiMessage = MutableSharedFlow() + val uiMessage: SharedFlow + get() = _uiMessage.asSharedFlow() + val uiState: StateFlow get() = _uiState.asStateFlow() @@ -51,15 +64,29 @@ class CoursesToSyncViewModel( private fun getCourseCalendarState() { viewModelScope.launch { - val coursesCalendarState = calendarInteractor.getAllCourseCalendarStateFromCache() - _uiState.update { it.copy(coursesCalendarState = coursesCalendarState) } + try { + 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))) + } } } private fun getEnrollmentsStatus() { viewModelScope.launch { - val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() - _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus, isLoading = false) } + try { + val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() + _uiState.update { it.copy(enrollmentsStatus = enrollmentsStatus) } + } catch (e: Exception) { + 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))) + } + } finally { + _uiState.update { it.copy(isLoading = false) } + } } } } From 6508b203bc17b2c01d304d71df76831b43e3c954 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 12 Jun 2024 16:34:40 +0300 Subject: [PATCH 24/26] fix: Fixes according to QA feedback --- .../main/java/org/openedx/app/di/AppModule.kt | 2 +- .../java/org/openedx/app/di/ScreenModule.kt | 5 +- .../core/domain/model/CourseCalendarEvent.kt | 2 +- .../openedx/core/system/CalendarManager.kt | 6 +-- .../notifier/calendar/CalendarSyncOffline.kt | 3 ++ .../openedx/core/worker/CalendarSyncWorker.kt | 7 ++- .../presentation/dates/DashboardUIState.kt | 8 +-- .../detail/CourseDetailsViewModel.kt | 3 ++ .../calendar/CalendarViewModel.kt | 7 ++- .../DisableCalendarSyncDialogFragment.kt | 5 +- .../calendar/NewCalendarDialogFragment.kt | 29 ++++++++--- .../calendar/NewCalendarDialogViewModel.kt | 50 +++++++++++++------ 12 files changed, 87 insertions(+), 40 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncOffline.kt 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 e9f272848..9265d449e 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -89,7 +89,7 @@ val appModule = module { single { ResourceManager(get()) } single { AppCookieManager(get(), get()) } single { ReviewManagerFactory.create(get()) } - single { CalendarManager(get(), get(), get()) } + single { CalendarManager(get(), get()) } single { DatabaseManager(get(), get(), get(), get()) } single { get() } 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 8aa41e0f1..089e1e538 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -180,7 +180,7 @@ val screenModule = module { viewModel { ManageAccountViewModel(get(), get(), get(), get(), get()) } viewModel { CalendarViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { CoursesToSyncViewModel(get(), get(), get(), get()) } - viewModel { NewCalendarDialogViewModel(get(), get(), get(), get()) } + viewModel { NewCalendarDialogViewModel(get(), get(), get(), get(), get(), get()) } viewModel { DisableCalendarSyncDialogViewModel(get(), get(), get(), get()) } factory { CalendarRepository(get(), get(), get()) } factory { CalendarInteractor(get()) } @@ -211,7 +211,8 @@ val screenModule = module { get(), get(), get(), - get() + get(), + get(), ) } viewModel { (courseId: String, courseTitle: String, enrollmentMode: String, resumeBlockId: String) -> diff --git a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt index abda712cc..bdf676c7f 100644 --- a/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt +++ b/core/src/main/java/org/openedx/core/domain/model/CourseCalendarEvent.kt @@ -2,5 +2,5 @@ package org.openedx.core.domain.model data class CourseCalendarEvent( val courseId: String, - val eventId: Long + val eventId: Long, ) diff --git a/core/src/main/java/org/openedx/core/system/CalendarManager.kt b/core/src/main/java/org/openedx/core/system/CalendarManager.kt index 611de6a25..8f3132dcd 100644 --- a/core/src/main/java/org/openedx/core/system/CalendarManager.kt +++ b/core/src/main/java/org/openedx/core/system/CalendarManager.kt @@ -8,7 +8,6 @@ import android.database.Cursor import android.net.Uri import android.provider.CalendarContract import androidx.core.content.ContextCompat -import org.openedx.core.R import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.CalendarData import org.openedx.core.domain.model.CourseDateBlock @@ -20,7 +19,6 @@ import java.util.concurrent.TimeUnit class CalendarManager( private val context: Context, private val corePreferences: CorePreferences, - private val resourceManager: ResourceManager, ) { private val logger = Logger(TAG) @@ -140,7 +138,7 @@ class CalendarManager( put(CalendarContract.Events.DTEND, endMillis) put( CalendarContract.Events.TITLE, - "${resourceManager.getString(R.string.core_assignment_due_tag)} : $courseName" + "${courseDateBlock.title} : $courseName" ) put( CalendarContract.Events.DESCRIPTION, @@ -169,7 +167,7 @@ class CalendarManager( courseDateBlock: CourseDateBlock, isDeeplinkEnabled: Boolean ): String { - val eventDescription = courseDateBlock.title + val eventDescription = courseDateBlock.description // The following code for branch and deep links will be enabled after implementation /* if (isDeeplinkEnabled && !TextUtils.isEmpty(courseDateBlock.blockId)) { diff --git a/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncOffline.kt b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncOffline.kt new file mode 100644 index 000000000..ac78a4a4c --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/calendar/CalendarSyncOffline.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.calendar + +object CalendarSyncOffline : CalendarEvent 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 752c60b1d..986eb9b9d 100644 --- a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -21,8 +21,10 @@ import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.domain.model.CourseDateBlock import org.openedx.core.domain.model.EnrollmentStatus import org.openedx.core.system.CalendarManager +import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.core.system.notifier.calendar.CalendarSyncFailed +import org.openedx.core.system.notifier.calendar.CalendarSyncOffline import org.openedx.core.system.notifier.calendar.CalendarSynced import org.openedx.core.system.notifier.calendar.CalendarSyncing @@ -35,6 +37,7 @@ class CalendarSyncWorker( private val calendarInteractor: CalendarInteractor by inject() private val calendarNotifier: CalendarNotifier by inject() private val calendarPreferences: CalendarPreferences by inject() + private val networkConnection: NetworkConnection by inject() private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANEL_ID) @@ -84,7 +87,9 @@ class CalendarSyncWorker( private suspend fun tryToSyncCalendar(courseId: String?) { val isCalendarCreated = calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST val isCalendarSyncEnabled = calendarPreferences.isCalendarSyncEnabled - if (isCalendarCreated && isCalendarSyncEnabled) { + if (!networkConnection.isOnline()) { + calendarNotifier.send(CalendarSyncOffline) + } else if (isCalendarCreated && isCalendarSyncEnabled) { calendarNotifier.send(CalendarSyncing) val enrollmentsStatus = calendarInteractor.getEnrollmentsStatus() if (courseId.isNullOrEmpty()) { diff --git a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt index 43fc63f58..18aebac3f 100644 --- a/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/dates/DashboardUIState.kt @@ -3,12 +3,12 @@ package org.openedx.course.presentation.dates import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.presentation.settings.calendarsync.CalendarSyncState -sealed class DatesUIState { +sealed interface DatesUIState { data class Dates( val courseDatesResult: CourseDatesResult, val calendarSyncState: CalendarSyncState - ) : DatesUIState() + ) : DatesUIState - object Empty : DatesUIState() - object Loading : DatesUIState() + data object Empty : DatesUIState + data object Loading : DatesUIState } 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 c68dd1c47..817363fa3 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 @@ -15,6 +15,7 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.worker.CalendarSyncScheduler import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.Course import org.openedx.discovery.presentation.DiscoveryAnalytics @@ -30,6 +31,7 @@ class CourseDetailsViewModel( private val resourceManager: ResourceManager, private val notifier: DiscoveryNotifier, private val analytics: DiscoveryAnalytics, + private val calendarSyncScheduler: CalendarSyncScheduler, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() val isUserLoggedIn get() = corePreferences.user != null @@ -92,6 +94,7 @@ class CourseDetailsViewModel( if (courseData is CourseDetailsUIState.CourseData) { _uiState.value = courseData.copy(course = course) courseEnrollSuccessEvent(id, title) + calendarSyncScheduler.requestImmediateSync(id) notifier.send(CourseDashboardUpdate()) } } catch (e: Exception) { 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 a66f5607e..359308b68 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 @@ -18,6 +18,7 @@ import org.openedx.core.system.notifier.calendar.CalendarCreated import org.openedx.core.system.notifier.calendar.CalendarNotifier import org.openedx.core.system.notifier.calendar.CalendarSyncDisabled import org.openedx.core.system.notifier.calendar.CalendarSyncFailed +import org.openedx.core.system.notifier.calendar.CalendarSyncOffline import org.openedx.core.system.notifier.calendar.CalendarSynced import org.openedx.core.system.notifier.calendar.CalendarSyncing import org.openedx.core.worker.CalendarSyncScheduler @@ -47,6 +48,7 @@ class CalendarViewModel( get() = _uiState.asStateFlow() init { + calendarSyncScheduler.requestImmediateSync() viewModelScope.launch { calendarNotifier.notifier.collect { calendarEvent -> when (calendarEvent) { @@ -69,6 +71,10 @@ class CalendarViewModel( _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNC_FAILED) } } + CalendarSyncOffline -> { + _uiState.update { it.copy(calendarSyncState = CalendarSyncState.OFFLINE) } + } + CalendarSyncDisabled -> { _uiState.update { calendarInitState } } @@ -121,7 +127,6 @@ class CalendarViewModel( } } - private fun isCalendarExist(): Boolean { return calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && calendarManager.isCalendarExist(calendarPreferences.calendarId) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt index 43c805831..e6a196a8c 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/DisableCalendarSyncDialogFragment.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.androidx.compose.koinViewModel import org.openedx.core.domain.model.CalendarData import org.openedx.core.extension.parcelable import org.openedx.core.presentation.dialog.DefaultDialogBox @@ -49,8 +49,6 @@ import org.openedx.core.R as coreR class DisableCalendarSyncDialogFragment : DialogFragment() { - private val viewModel by viewModel() - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -60,6 +58,7 @@ class DisableCalendarSyncDialogFragment : DialogFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { + val viewModel: DisableCalendarSyncDialogViewModel = koinViewModel() DisableCalendarSyncDialogView( calendarData = requireArguments().parcelable(ARG_CALENDAR_DATA), onCancelClick = { 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 77d365ccc..af09b3ea3 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 @@ -38,6 +38,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -62,7 +63,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.androidx.compose.koinViewModel import org.openedx.core.extension.parcelable import org.openedx.core.extension.toastMessage import org.openedx.core.presentation.dialog.DefaultDialogBox @@ -79,8 +80,6 @@ import org.openedx.core.R as CoreR class NewCalendarDialogFragment : DialogFragment() { - private val viewModel by viewModel() - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -90,6 +89,24 @@ class NewCalendarDialogFragment : DialogFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { + val viewModel: NewCalendarDialogViewModel = koinViewModel() + + LaunchedEffect(Unit) { + viewModel.uiMessage.collect { message -> + if (message.isNotEmpty()) { + context.toastMessage(message) + } + } + } + + LaunchedEffect(Unit) { + viewModel.isSuccess.collect { isSuccess -> + if (isSuccess) { + dismiss() + } + } + } + NewCalendarDialog( newCalendarDialogType = requireArguments().parcelable(ARG_DIALOG_TYPE) ?: NewCalendarDialogType.CREATE_NEW, @@ -97,11 +114,7 @@ class NewCalendarDialogFragment : DialogFragment() { dismiss() }, onBeginSyncingClick = { calendarTitle, calendarColor -> - val isCalendarCreated = viewModel.createCalendar(calendarTitle, calendarColor) - if (!isCalendarCreated) { - requireContext().toastMessage(context.getString(CoreR.string.core_error_unknown_error)) - } - dismiss() + viewModel.createCalendar(calendarTitle, calendarColor) } ) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 39b853251..3085f7db9 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -1,11 +1,17 @@ package org.openedx.profile.presentation.calendar import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel +import org.openedx.core.R import org.openedx.core.data.storage.CalendarPreferences import org.openedx.core.domain.interactor.CalendarInteractor import org.openedx.core.system.CalendarManager +import org.openedx.core.system.ResourceManager +import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.calendar.CalendarCreated import org.openedx.core.system.notifier.calendar.CalendarNotifier @@ -14,28 +20,42 @@ class NewCalendarDialogViewModel( private val calendarPreferences: CalendarPreferences, private val calendarNotifier: CalendarNotifier, private val calendarInteractor: CalendarInteractor, + private val networkConnection: NetworkConnection, + private val resourceManager: ResourceManager, ) : BaseViewModel() { + private val _uiMessage = MutableSharedFlow() + val uiMessage: SharedFlow + get() = _uiMessage.asSharedFlow() + + private val _isSuccess = MutableSharedFlow() + val isSuccess: SharedFlow + get() = _isSuccess.asSharedFlow() + fun createCalendar( calendarTitle: String, calendarColor: CalendarColor, - ): Boolean { + ) { viewModelScope.launch { - calendarInteractor.resetChecksums() - } - val calendarId = calendarManager.createOrUpdateCalendar( - calendarTitle = calendarTitle, - calendarColor = calendarColor.color - ) - return if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { - calendarPreferences.calendarId = calendarId - calendarPreferences.calendarUser = calendarManager.accountName - viewModelScope.launch { - calendarNotifier.send(CalendarCreated) + if (networkConnection.isOnline()) { + calendarInteractor.resetChecksums() + val calendarId = calendarManager.createOrUpdateCalendar( + calendarTitle = calendarTitle, + calendarColor = calendarColor.color + ) + if (calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST) { + calendarPreferences.calendarId = calendarId + calendarPreferences.calendarUser = calendarManager.accountName + viewModelScope.launch { + calendarNotifier.send(CalendarCreated) + } + _isSuccess.emit(true) + } else { + _uiMessage.emit(resourceManager.getString(R.string.core_error_unknown_error)) + } + } else { + _uiMessage.emit(resourceManager.getString(R.string.core_error_no_connection)) } - true - } else { - false } } } From 71ab3603baadfddbd3a63f35a0d1d645f8de26c8 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Fri, 12 Jul 2024 13:34:36 +0300 Subject: [PATCH 25/26] fix: Calendar Permission crash --- .../profile/presentation/calendar/CalendarViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 359308b68..29cb017a6 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 @@ -128,7 +128,11 @@ class CalendarViewModel( } private fun isCalendarExist(): Boolean { - return calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && - calendarManager.isCalendarExist(calendarPreferences.calendarId) + return try { + calendarPreferences.calendarId != CalendarManager.CALENDAR_DOES_NOT_EXIST && + calendarManager.isCalendarExist(calendarPreferences.calendarId) + } catch (e: SecurityException) { + false + } } } From 5cd96fbb97a405a6d378d7c7bc5a8d1e487073a2 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk Date: Wed, 17 Jul 2024 13:51:05 +0300 Subject: [PATCH 26/26] fix: Fixes according to PR feedback --- .../calendarsync/CalendarSyncState.kt | 2 +- .../openedx/core/worker/CalendarSyncWorker.kt | 29 ++-- .../src/main/res/drawable/core_ic_book.xml | 0 core/src/main/res/values/strings.xml | 2 +- .../presentation/AllEnrolledCoursesView.kt | 2 +- .../presentation/DashboardGalleryView.kt | 4 +- .../calendar/CalendarViewModel.kt | 1 + .../calendar/CoursesToSyncFragment.kt | 145 ++++++++++++------ .../calendar/NewCalendarDialogViewModel.kt | 1 + .../presentation/calendar/SyncCourseTab.kt | 2 +- profile/src/main/res/values/strings.xml | 3 + 11 files changed, 133 insertions(+), 58 deletions(-) rename dashboard/src/main/res/drawable/dashboard_ic_book.xml => core/src/main/res/drawable/core_ic_book.xml (100%) diff --git a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt index 3e6188ed1..95a851442 100644 --- a/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt +++ b/core/src/main/java/org/openedx/core/presentation/settings/calendarsync/CalendarSyncState.kt @@ -30,7 +30,7 @@ enum class CalendarSyncState( Icons.Rounded.FreeCancellation ), SYNCED( - R.string.core_synced, + R.string.core_to_sync, R.string.core_synced_to_calendar, Icons.Rounded.EventRepeat ), 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 986eb9b9d..2c36f075b 100644 --- a/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt +++ b/core/src/main/java/org/openedx/core/worker/CalendarSyncWorker.kt @@ -42,6 +42,8 @@ class CalendarSyncWorker( private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANEL_ID) + private val failedCoursesSync = mutableSetOf() + override suspend fun doWork(): Result { return try { setForeground(createForegroundInfo()) @@ -98,7 +100,11 @@ class CalendarSyncWorker( syncCalendar(enrollmentsStatus, courseId) } removeUnenrolledCourseEvents(enrollmentsStatus) - calendarNotifier.send(CalendarSynced) + if (failedCoursesSync.isEmpty()) { + calendarNotifier.send(CalendarSynced) + } else { + calendarNotifier.send(CalendarSyncFailed) + } } } @@ -127,17 +133,22 @@ class CalendarSyncWorker( } private suspend fun syncCourseEvents(enrollmentStatus: EnrollmentStatus) { - createCalendarState(enrollmentStatus) val courseId = enrollmentStatus.courseId - if (enrollmentStatus.isActive && isCourseSyncEnabled(courseId)) { - val courseDates = calendarInteractor.getCourseDates(courseId) - val isCourseCalendarUpToDate = isCourseCalendarUpToDate(courseId, courseDates) - if (!isCourseCalendarUpToDate) { + try { + createCalendarState(enrollmentStatus) + if (enrollmentStatus.isActive && isCourseSyncEnabled(courseId)) { + val courseDates = calendarInteractor.getCourseDates(courseId) + val isCourseCalendarUpToDate = isCourseCalendarUpToDate(courseId, courseDates) + if (!isCourseCalendarUpToDate) { + removeCalendarEvents(courseId) + updateCourseEvents(courseDates, enrollmentStatus) + } + } else { removeCalendarEvents(courseId) - updateCourseEvents(courseDates, enrollmentStatus) } - } else { - removeCalendarEvents(courseId) + } catch (e: Exception) { + failedCoursesSync.add(courseId) + e.printStackTrace() } } diff --git a/dashboard/src/main/res/drawable/dashboard_ic_book.xml b/core/src/main/res/drawable/core_ic_book.xml similarity index 100% rename from dashboard/src/main/res/drawable/dashboard_ic_book.xml rename to core/src/main/res/drawable/core_ic_book.xml diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index dc055138e..9aded8c31 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -186,7 +186,7 @@ Calendar Sync Failed Synced to Calendar Sync Failed - Synced + To Sync Not Synced Syncing to calendar… 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 3392ed7bd..c2668f766 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/AllEnrolledCoursesView.kt @@ -527,7 +527,7 @@ fun EmptyState( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - painter = painterResource(id = org.openedx.dashboard.R.drawable.dashboard_ic_book), + painter = painterResource(id = R.drawable.core_ic_book), tint = MaterialTheme.appColors.textFieldBorder, contentDescription = null ) 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 7401f6304..a6d375569 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryView.kt @@ -384,7 +384,7 @@ private fun ViewAllItem( ) { Icon( modifier = Modifier.size(48.dp), - painter = painterResource(id = R.drawable.dashboard_ic_book), + painter = painterResource(id = CoreR.drawable.core_ic_book), tint = MaterialTheme.appColors.textFieldBorder, contentDescription = null ) @@ -749,7 +749,7 @@ private fun NoCoursesInfo( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - painter = painterResource(id = R.drawable.dashboard_ic_book), + painter = painterResource(id = CoreR.drawable.core_ic_book), tint = MaterialTheme.appColors.textFieldBorder, contentDescription = null ) 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 29cb017a6..658d7ca8e 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 @@ -69,6 +69,7 @@ class CalendarViewModel( CalendarSyncFailed -> { _uiState.update { it.copy(calendarSyncState = CalendarSyncState.SYNC_FAILED) } + updateSyncedCoursesCount() } CalendarSyncOffline -> { 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 b47d47d11..7b4d1d9d0 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 @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,6 +23,7 @@ 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.LocalMinimumInteractiveComponentEnforcement import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold @@ -43,10 +45,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -69,6 +73,7 @@ import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.theme.fontFamily import org.openedx.core.ui.windowSizeValue import org.openedx.profile.R +import org.openedx.core.R as coreR class CoursesToSyncFragment : Fragment() { @@ -274,58 +279,112 @@ private fun CourseCheckboxList( .map { it.courseId } val filteredEnrollments = uiState.enrollmentsStatus .filter { it.courseId in courseIds } - items(filteredEnrollments) { course -> - val isCourseSyncEnabled = - uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled ?: false - val annotatedString = buildAnnotatedString { - append(course.courseName) - if (!course.isActive) { - append(" ") - withStyle( - style = SpanStyle( - fontSize = 10.sp, - fontWeight = FontWeight.Normal, - letterSpacing = 0.sp, - fontFamily = fontFamily, - color = MaterialTheme.appColors.textFieldHint, - ) - ) { - append(stringResource(R.string.profile_inactive)) - } + .let { enrollments -> + if (uiState.isHideInactiveCourses) { + enrollments.filter { it.isActive } + } else { + enrollments } } - - if (uiState.isHideInactiveCourses && !course.isActive) return@items - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - modifier = Modifier.size(24.dp), - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.appColors.primary, - uncheckedColor = MaterialTheme.appColors.textFieldText - ), - checked = isCourseSyncEnabled, - enabled = course.isActive, - onCheckedChange = { isEnabled -> - onCourseSyncCheckChange(isEnabled, course.courseId) - } - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = annotatedString, - style = MaterialTheme.appTypography.labelLarge, - color = MaterialTheme.appColors.textDark + if (filteredEnrollments.isEmpty()) { + item { + EmptyListState( + selectedTab = selectedTab ) } + } else { + items(filteredEnrollments) { course -> + val isCourseSyncEnabled = + uiState.coursesCalendarState.find { it.courseId == course.courseId }?.isCourseSyncEnabled + ?: false + val annotatedString = buildAnnotatedString { + append(course.courseName) + if (!course.isActive) { + append(" ") + withStyle( + style = SpanStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + letterSpacing = 0.sp, + fontFamily = fontFamily, + color = MaterialTheme.appColors.textFieldHint, + ) + ) { + append(stringResource(R.string.profile_inactive)) + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier.size(24.dp), + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.appColors.primary, + uncheckedColor = MaterialTheme.appColors.textFieldText + ), + checked = isCourseSyncEnabled, + enabled = course.isActive, + onCheckedChange = { isEnabled -> + onCourseSyncCheckChange(isEnabled, course.courseId) + } + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = annotatedString, + style = MaterialTheme.appTypography.labelLarge, + color = MaterialTheme.appColors.textDark + ) + } + } } } } } +@Composable +private fun EmptyListState( + modifier: Modifier = Modifier, + selectedTab: SyncCourseTab, +) { + val description = if (selectedTab == SyncCourseTab.SYNCED) { + stringResource(id = R.string.profile_no_sync_courses) + } else { + stringResource(id = R.string.profile_no_courses_with_current_filter) + } + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 40.dp, vertical = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + modifier = Modifier.size(96.dp), + painter = painterResource(id = coreR.drawable.core_ic_book), + tint = MaterialTheme.appColors.divider, + contentDescription = null + ) + Text( + text = stringResource( + id = R.string.profile_no_courses, + stringResource(id = selectedTab.title) + ), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textDark + ) + Text( + text = description, + style = MaterialTheme.appTypography.labelMedium, + color = MaterialTheme.appColors.textDark, + textAlign = TextAlign.Center + ) + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun HideInactiveCoursesView( diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt index 3085f7db9..e43f1b989 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/NewCalendarDialogViewModel.kt @@ -40,6 +40,7 @@ class NewCalendarDialogViewModel( if (networkConnection.isOnline()) { calendarInteractor.resetChecksums() val calendarId = calendarManager.createOrUpdateCalendar( + calendarId = calendarPreferences.calendarId, calendarTitle = calendarTitle, calendarColor = calendarColor.color ) diff --git a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt index a68fedc3e..ef65db249 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/calendar/SyncCourseTab.kt @@ -7,6 +7,6 @@ enum class SyncCourseTab( @StringRes val title: Int ) { - SYNCED(R.string.core_synced), + SYNCED(R.string.core_to_sync), NOT_SYNCED(R.string.core_not_synced) } diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index a36595f71..41535240c 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -75,5 +75,8 @@ Disable Calendar Sync Disabling calendar sync will delete the calendar “%1$s.” You can turn calendar sync back on at any time. Disable Syncing + No %1$s Courses + No courses are currently being synced to your calendar. + No courses match the current filter.