From d388acb0555df2d1b6a94377e431b3c8b8c7969f Mon Sep 17 00:00:00 2001 From: omerhabib26 Date: Wed, 13 Mar 2024 01:37:13 +0500 Subject: [PATCH] feat: Add Analytics events - Add analytics for following modules - AppReview, Profile, WhatsNew, Course, Discussion, Logistration, MainDashboard, Discovery - Restructure the analytics implementation to module level - Update the event names accordingly fix: LEARNER-9876 --- .../java/org/openedx/app/AnalyticsManager.kt | 216 ++---------------- .../main/java/org/openedx/app/AppAnalytics.kt | 23 +- .../main/java/org/openedx/app/AppRouter.kt | 5 +- .../main/java/org/openedx/app/MainFragment.kt | 8 +- .../java/org/openedx/app/MainViewModel.kt | 24 ++ .../main/java/org/openedx/app/di/AppModule.kt | 4 + .../java/org/openedx/app/di/ScreenModule.kt | 45 +++- .../auth/presentation/AuthAnalytics.kt | 45 +++- .../logistration/LogistrationFragment.kt | 28 +-- .../logistration/LogistrationViewModel.kt | 71 ++++++ .../restore/RestorePasswordViewModel.kt | 30 ++- .../presentation/signin/SignInViewModel.kt | 38 ++- .../presentation/signup/SignUpViewModel.kt | 47 +++- .../dialog/appreview/AppReviewAnalytics.kt | 36 +++ .../appreview/BaseAppReviewDialogFragment.kt | 109 ++++++++- .../appreview/FeedbackDialogFragment.kt | 9 +- .../dialog/appreview/RateDialogFragment.kt | 14 +- .../appreview/ThankYouDialogFragment.kt | 9 +- .../java/org/openedx/core/ui/ComposeCommon.kt | 5 +- .../course/presentation/CourseAnalytics.kt | 173 ++++++++++++-- .../course/presentation/CourseRouter.kt | 2 +- .../calendarsync/CalendarSyncDialog.kt | 10 +- .../container/CourseContainerFragment.kt | 37 ++- .../container/CourseContainerViewModel.kt | 143 +++++++++++- .../presentation/dates/CourseDatesFragment.kt | 51 ++++- .../dates/CourseDatesViewModel.kt | 107 ++++++++- .../detail/CourseDetailsFragment.kt | 4 +- .../detail/CourseDetailsViewModel.kt | 39 +++- .../handouts/HandoutsViewModel.kt | 24 +- .../presentation/info/CourseInfoViewModel.kt | 3 +- .../outline/CourseOutlineFragment.kt | 14 +- .../outline/CourseOutlineViewModel.kt | 36 ++- .../section/CourseSectionFragment.kt | 2 +- .../section/CourseSectionViewModel.kt | 14 +- .../course/presentation/ui/CourseUI.kt | 9 +- .../container/CourseUnitContainerFragment.kt | 14 +- .../container/CourseUnitContainerViewModel.kt | 19 +- .../unit/video/BaseVideoViewModel.kt | 114 +++++++++ .../unit/video/EncodedVideoUnitViewModel.kt | 51 ++++- .../unit/video/VideoFullScreenFragment.kt | 33 ++- .../unit/video/VideoUnitFragment.kt | 16 +- .../unit/video/VideoUnitViewModel.kt | 9 +- .../presentation/unit/video/VideoViewModel.kt | 24 +- .../video/YoutubeVideoFullScreenFragment.kt | 14 +- .../unit/video/YoutubeVideoUnitFragment.kt | 34 ++- .../dashboard/presentation/DashboardRouter.kt | 3 +- .../dashboard/DashboardFragment.kt | 3 +- .../presentation/program/ProgramViewModel.kt | 3 +- .../presentation/DiscoveryAnalytics.kt | 1 + .../presentation/DiscussionAnalytics.kt | 32 ++- .../profile/presentation/ProfileAnalytics.kt | 62 ++++- .../presentation/edit/EditProfileViewModel.kt | 42 +++- .../presentation/profile/ProfileViewModel.kt | 47 +++- .../settings/video/VideoSettingsFragment.kt | 12 +- .../settings/video/VideoSettingsViewModel.kt | 48 +++- .../presentation/WhatsNewAnalytics.kt | 22 ++ .../presentation/whatsnew/WhatsNewFragment.kt | 2 + .../whatsnew/WhatsNewViewModel.kt | 29 ++- 58 files changed, 1637 insertions(+), 431 deletions(-) create mode 100644 auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt create mode 100644 core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewAnalytics.kt create mode 100644 course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt create mode 100644 whatsnew/src/main/java/org/openedx/whatsnew/presentation/WhatsNewAnalytics.kt diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index e474416dd..a1cda1047 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -6,17 +6,19 @@ import org.openedx.app.analytics.FirebaseAnalytics import org.openedx.app.analytics.SegmentAnalytics import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.config.Config +import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics import org.openedx.course.presentation.CourseAnalytics import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics import org.openedx.discovery.presentation.DiscoveryAnalytics import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.profile.presentation.ProfileAnalytics +import org.openedx.whatsnew.presentation.WhatsNewAnalytics class AnalyticsManager( context: Context, config: Config, -) : DashboardAnalytics, AuthAnalytics, AppAnalytics, DiscoveryAnalytics, ProfileAnalytics, - CourseAnalytics, DiscussionAnalytics { +) : DashboardAnalytics, AuthAnalytics, AppAnalytics, DiscoveryAnalytics, + CourseAnalytics, DiscussionAnalytics, AppReviewAnalytics, WhatsNewAnalytics, ProfileAnalytics { private val services: ArrayList = arrayListOf() @@ -41,6 +43,12 @@ class AnalyticsManager( } } + override fun logEvent(event: String, params: Map) { + services.forEach { analytics -> + analytics.logEvent(event, params) + } + } + private fun setUserId(userId: Long) { services.forEach { analytics -> analytics.logUserId(userId) @@ -54,56 +62,12 @@ class AnalyticsManager( }) } - override fun userLoginEvent(method: String) { - logEvent(Event.USER_LOGIN, buildMap { - put(Key.METHOD.keyName, method) - }) - } - - override fun signUpClickedEvent() { - logEvent(Event.SIGN_UP_CLICKED) - } - - override fun createAccountClickedEvent(provider: String) { - logEvent(Event.CREATE_ACCOUNT_CLICKED, buildMap { - put(Key.PROVIDER.keyName, provider) - }) - } - - override fun registrationSuccessEvent(provider: String) { - logEvent(Event.REGISTRATION_SUCCESS, buildMap { put(Key.PROVIDER.keyName, provider) }) - } - - override fun forgotPasswordClickedEvent() { - logEvent(Event.FORGOT_PASSWORD_CLICKED) - } - - override fun resetPasswordClickedEvent(success: Boolean) { - logEvent(Event.RESET_PASSWORD_CLICKED, buildMap { put(Key.SUCCESS.keyName, success) }) - } - override fun logoutEvent(force: Boolean) { logEvent(Event.USER_LOGOUT, buildMap { put(Key.FORCE.keyName, force) }) } - override fun discoveryTabClickedEvent() { - logEvent(Event.DISCOVERY_TAB_CLICKED) - } - - override fun dashboardTabClickedEvent() { - logEvent(Event.DASHBOARD_TAB_CLICKED) - } - - override fun programsTabClickedEvent() { - logEvent(Event.PROGRAMS_TAB_CLICKED) - } - - override fun profileTabClickedEvent() { - logEvent(Event.PROFILE_TAB_CLICKED) - } - override fun setUserIdForSession(userId: Long) { setUserId(userId) } @@ -126,77 +90,8 @@ class AnalyticsManager( }) } - override fun profileEditClickedEvent() { - logEvent(Event.PROFILE_EDIT_CLICKED) - } - - override fun profileEditDoneClickedEvent() { - logEvent(Event.PROFILE_EDIT_DONE_CLICKED) - } - - override fun profileDeleteAccountClickedEvent() { - logEvent(Event.PROFILE_DELETE_ACCOUNT_CLICKED) - } - - override fun profileVideoSettingsClickedEvent() { - logEvent(Event.PROFILE_VIDEO_SETTINGS_CLICKED) - } - - override fun privacyPolicyClickedEvent() { - logEvent(Event.PRIVACY_POLICY_CLICKED) - } - - override fun termsOfUseClickedEvent() { - logEvent(Event.TERMS_OF_USE_CLICKED) - } - - override fun cookiePolicyClickedEvent() { - logEvent(Event.COOKIE_POLICY_CLICKED) - } - - override fun dataSellClickedEvent() { - logEvent(Event.DATE_SELL_CLICKED) - } - - override fun faqClickedEvent() { - logEvent(Event.FAQ_CLICKED) - } - - override fun emailSupportClickedEvent() { - logEvent(Event.EMAIL_SUPPORT_CLICKED) - } - - override fun courseEnrollClickedEvent(courseId: String, courseName: String) { - logEvent(Event.COURSE_ENROLL_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun courseEnrollSuccessEvent(courseId: String, courseName: String) { - logEvent(Event.COURSE_ENROLL_SUCCESS, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun viewCourseClickedEvent(courseId: String, courseName: String) { - logEvent(Event.VIEW_COURSE_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun resumeCourseTappedEvent(courseId: String, courseName: String, blockId: String) { - logEvent(Event.RESUME_COURSE_TAPPED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - }) - } - override fun sequentialClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String + courseId: String, courseName: String, blockId: String, blockName: String, ) { logEvent(Event.SEQUENTIAL_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -206,19 +101,8 @@ class AnalyticsManager( }) } - override fun verticalClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String - ) { - logEvent(Event.VERTICAL_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - put(Key.BLOCK_ID.keyName, blockId) - put(Key.BLOCK_NAME.keyName, blockName) - }) - } - override fun nextBlockClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String + courseId: String, courseName: String, blockId: String, blockName: String, ) { logEvent(Event.NEXT_BLOCK_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -229,7 +113,7 @@ class AnalyticsManager( } override fun prevBlockClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String + courseId: String, courseName: String, blockId: String, blockName: String, ) { logEvent(Event.PREV_BLOCK_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -240,7 +124,7 @@ class AnalyticsManager( } override fun finishVerticalClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String + courseId: String, courseName: String, blockId: String, blockName: String, ) { logEvent(Event.FINISH_VERTICAL_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -251,7 +135,7 @@ class AnalyticsManager( } override fun finishVerticalNextClickedEvent( - courseId: String, courseName: String, blockId: String, blockName: String + courseId: String, courseName: String, blockId: String, blockName: String, ) { logEvent(Event.FINISH_VERTICAL_NEXT_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -268,41 +152,6 @@ class AnalyticsManager( }) } - override fun courseTabClickedEvent(courseId: String, courseName: String) { - logEvent(Event.COURSE_TAB_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun videoTabClickedEvent(courseId: String, courseName: String) { - logEvent(Event.VIDEO_TAB_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun discussionTabClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DISCUSSION_TAB_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun datesTabClickedEvent(courseId: String, courseName: String) { - logEvent(Event.DATES_TAB_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - - override fun handoutsTabClickedEvent(courseId: String, courseName: String) { - logEvent(Event.HANDOUTS_TAB_CLICKED, buildMap { - put(Key.COURSE_ID.keyName, courseId) - put(Key.COURSE_NAME.keyName, courseName) - }) - } - override fun discussionAllPostsClickedEvent(courseId: String, courseName: String) { logEvent(Event.DISCUSSION_ALL_POSTS_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -318,7 +167,7 @@ class AnalyticsManager( } override fun discussionTopicClickedEvent( - courseId: String, courseName: String, topicId: String, topicName: String + courseId: String, courseName: String, topicId: String, topicName: String, ) { logEvent(Event.DISCUSSION_TOPIC_CLICKED, buildMap { put(Key.COURSE_ID.keyName, courseId) @@ -329,48 +178,19 @@ class AnalyticsManager( } } -private enum class Event(val eventName: String) { - USER_LOGIN("User_Login"), - SIGN_UP_CLICKED("Sign_up_Clicked"), - CREATE_ACCOUNT_CLICKED("Create_Account_Clicked"), - REGISTRATION_SUCCESS("Registration_Success"), +enum class Event(val eventName: String) { USER_LOGOUT("User_Logout"), - FORGOT_PASSWORD_CLICKED("Forgot_password_Clicked"), - RESET_PASSWORD_CLICKED("Reset_password_Clicked"), - DISCOVERY_TAB_CLICKED("Main_Discovery_tab_Clicked"), - DASHBOARD_TAB_CLICKED("Main_Dashboard_tab_Clicked"), - PROGRAMS_TAB_CLICKED("Main_Programs_tab_Clicked"), - PROFILE_TAB_CLICKED("Main_Profile_tab_Clicked"), DISCOVERY_SEARCH_BAR_CLICKED("Discovery_Search_Bar_Clicked"), DISCOVERY_COURSE_SEARCH("Discovery_Courses_Search"), DISCOVERY_COURSE_CLICKED("Discovery_Course_Clicked"), DASHBOARD_COURSE_CLICKED("Dashboard_Course_Clicked"), - PROFILE_EDIT_CLICKED("Profile_Edit_Clicked"), - PROFILE_EDIT_DONE_CLICKED("Profile_Edit_Done_Clicked"), - PROFILE_DELETE_ACCOUNT_CLICKED("Profile_Delete_Account_Clicked"), - PROFILE_VIDEO_SETTINGS_CLICKED("Profile_Video_settings_Clicked"), - PRIVACY_POLICY_CLICKED("Privacy_Policy_Clicked"), - TERMS_OF_USE_CLICKED("Terms_Of_Use_Clicked"), - COOKIE_POLICY_CLICKED("Cookie_Policy_Clicked"), - DATE_SELL_CLICKED("Data_Sell_Clicked"), - FAQ_CLICKED("FAQ_Clicked"), - EMAIL_SUPPORT_CLICKED("Email_Support_Clicked"), - COURSE_ENROLL_CLICKED("Course_Enroll_Clicked"), - COURSE_ENROLL_SUCCESS("Course_Enroll_Success"), - VIEW_COURSE_CLICKED("View_Course_Clicked"), - RESUME_COURSE_TAPPED("Resume_Course_Tapped"), + SEQUENTIAL_CLICKED("Sequential_Clicked"), - VERTICAL_CLICKED("Vertical_Clicked"), NEXT_BLOCK_CLICKED("Next_Block_Clicked"), PREV_BLOCK_CLICKED("Prev_Block_Clicked"), FINISH_VERTICAL_CLICKED("Finish_Vertical_Clicked"), FINISH_VERTICAL_NEXT_CLICKED("Finish_Vertical_Next_section_Clicked"), FINISH_VERTICAL_BACK_CLICKED("Finish_Vertical_Back_to_outline_Clicked"), - COURSE_TAB_CLICKED("Course_Outline_Course_tab_Clicked"), - VIDEO_TAB_CLICKED("Course_Outline_Videos_tab_Clicked"), - DISCUSSION_TAB_CLICKED("Course_Outline_Discussion_tab_Clicked"), - DATES_TAB_CLICKED("Course_Outline_Dates_tab_Clicked"), - HANDOUTS_TAB_CLICKED("Course_Outline_Handouts_tab_Clicked"), DISCUSSION_ALL_POSTS_CLICKED("Discussion_All_Posts_Clicked"), DISCUSSION_FOLLOWING_CLICKED("Discussion_Following_Clicked"), DISCUSSION_TOPIC_CLICKED("Discussion_Topic_Clicked"), diff --git a/app/src/main/java/org/openedx/app/AppAnalytics.kt b/app/src/main/java/org/openedx/app/AppAnalytics.kt index 1114ccd8e..a29301ebf 100644 --- a/app/src/main/java/org/openedx/app/AppAnalytics.kt +++ b/app/src/main/java/org/openedx/app/AppAnalytics.kt @@ -2,9 +2,22 @@ package org.openedx.app interface AppAnalytics { fun logoutEvent(force: Boolean) - fun discoveryTabClickedEvent() - fun dashboardTabClickedEvent() - fun programsTabClickedEvent() - fun profileTabClickedEvent() fun setUserIdForSession(userId: Long) -} \ No newline at end of file + fun logEvent(event: String, params: Map) +} + + +enum class AppAnalyticEvent(val event: String) { + DISCOVER("MainDashboard:Discover"), + MY_COURSES("MainDashboard:My Courses"), + MY_PROGRAMS("MainDashboard:My Programs"), + PROFILE("MainDashboard:Profile"), +} + +enum class AppAnalyticValues(val value: String) { + SCREEN_NAVIGATION("edx.bi.app.navigation.screen"), +} + +enum class AppAnalyticKey(val key: String) { + NAME("name"), +} diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index daf3662f0..1e6875b4e 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -134,11 +134,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToCourseOutline( fm: FragmentManager, courseId: String, - courseTitle: String + courseTitle: String, + enrollmentMode: String, ) { replaceFragmentWithBackStack( fm, - CourseContainerFragment.newInstance(courseId, courseTitle) + CourseContainerFragment.newInstance(courseId, courseTitle, enrollmentMode) ) } diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 8534eaffe..d87b761fb 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -49,22 +49,22 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.bottomNavView.setOnItemSelectedListener { when (it.itemId) { R.id.fragmentHome -> { - analytics.discoveryTabClickedEvent() + viewModel.logDiscoveryTabClickedEvent() binding.viewPager.setCurrentItem(0, false) } R.id.fragmentDashboard -> { - analytics.dashboardTabClickedEvent() + viewModel.logMyCoursesTabClickedEvent() binding.viewPager.setCurrentItem(1, false) } R.id.fragmentPrograms -> { - analytics.programsTabClickedEvent() + viewModel.logMyProgramsTabClickedEvent() binding.viewPager.setCurrentItem(2, false) } R.id.fragmentProfile -> { - analytics.profileTabClickedEvent() + viewModel.logProfileTabClickedEvent() binding.viewPager.setCurrentItem(3, false) } } diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index 3b36cc2be..662be715f 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -18,6 +18,7 @@ import org.openedx.dashboard.notifier.DashboardNotifier class MainViewModel( private val config: Config, private val notifier: DashboardNotifier, + private val analytics: AppAnalytics, ) : BaseViewModel() { private val _isBottomBarEnabled = MutableLiveData(true) @@ -44,4 +45,27 @@ class MainViewModel( fun enableBottomBar(enable: Boolean) { _isBottomBarEnabled.value = enable } + + fun logDiscoveryTabClickedEvent() { + logEvent(AppAnalyticEvent.DISCOVER) + } + + fun logMyCoursesTabClickedEvent() { + logEvent(AppAnalyticEvent.MY_COURSES) + } + + fun logMyProgramsTabClickedEvent() { + logEvent(AppAnalyticEvent.MY_PROGRAMS) + } + + fun logProfileTabClickedEvent() { + logEvent(AppAnalyticEvent.PROFILE) + } + + private fun logEvent(event: AppAnalyticEvent) { + analytics.logEvent(event.event, + buildMap { + put(AppAnalyticKey.NAME.key, AppAnalyticValues.SCREEN_NAVIGATION.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 f798d8559..5b31d8d4b 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -33,6 +33,7 @@ import org.openedx.core.interfaces.EnrollInCourseInteractor import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.TranscriptManager import org.openedx.core.module.download.FileDownloader +import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics import org.openedx.core.presentation.dialog.appreview.AppReviewManager import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.global.WhatsNewGlobalManager @@ -64,6 +65,7 @@ import org.openedx.profile.system.notifier.ProfileNotifier import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.data.storage.WhatsNewPreferences +import org.openedx.whatsnew.presentation.WhatsNewAnalytics val appModule = module { @@ -168,6 +170,8 @@ val appModule = module { single { get() } single { get() } single { get() } + single { get() } + single { get() } factory { AgreementProvider(get(), get()) } factory { FacebookAuthHelper() } 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 c74d007a5..73746c06e 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -24,6 +24,7 @@ import org.openedx.course.presentation.outline.CourseOutlineViewModel import org.openedx.course.presentation.section.CourseSectionViewModel import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel import org.openedx.course.presentation.unit.html.HtmlUnitViewModel +import org.openedx.course.presentation.unit.video.BaseVideoViewModel import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoViewModel @@ -60,7 +61,7 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel val screenModule = module { viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get()) } - viewModel { MainViewModel(get(), get()) } + viewModel { MainViewModel(get(), get(), get()) } factory { AuthRepository(get(), get(), get()) } factory { AuthInteractor(get()) } @@ -84,7 +85,19 @@ val screenModule = module { } viewModel { (courseId: String?, infoType: String?) -> - SignUpViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), courseId, infoType) + SignUpViewModel( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + courseId, + infoType + ) } viewModel { RestorePasswordViewModel(get(), get(), get(), get()) } @@ -123,7 +136,7 @@ val screenModule = module { ) } viewModel { (account: Account) -> EditProfileViewModel(get(), get(), get(), get(), account) } - viewModel { VideoSettingsViewModel(get(), get()) } + viewModel { VideoSettingsViewModel(get(), get(), get(), get()) } viewModel { (qualityType: String) -> VideoQualityViewModel(get(), get(), qualityType) } viewModel { DeleteProfileViewModel(get(), get(), get(), get()) } viewModel { (username: String) -> AnothersProfileViewModel(get(), get(), username) } @@ -155,10 +168,11 @@ val screenModule = module { get() ) } - viewModel { (courseId: String, courseTitle: String) -> + viewModel { (courseId: String, courseTitle: String, enrollmentMode: String) -> CourseContainerViewModel( courseId, courseTitle, + enrollmentMode, get(), get(), get(), @@ -197,13 +211,14 @@ val screenModule = module { courseId ) } - viewModel { (courseId: String) -> + viewModel { (courseId: String, unitId: String) -> CourseUnitContainerViewModel( get(), get(), get(), get(), - courseId + courseId, + unitId ) } viewModel { (courseId: String) -> @@ -221,8 +236,9 @@ val screenModule = module { get() ) } - viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get()) } - viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get()) } + viewModel { (courseId: String) -> BaseVideoViewModel(courseId, get()) } + viewModel { (courseId: String) -> VideoViewModel(courseId, get(), get(), get(), get()) } + viewModel { (courseId: String) -> VideoUnitViewModel(courseId, get(), get(), get(), get(), get()) } viewModel { (courseId: String, blockId: String) -> EncodedVideoUnitViewModel( courseId, @@ -232,14 +248,17 @@ val screenModule = module { get(), get(), get(), - get() + get(), + get(), ) } - viewModel { (courseId: String, courseName: String, isSelfPaced: Boolean) -> + viewModel { (courseId: String, courseName: String, isSelfPaced: Boolean, enrollmentMode: String) -> CourseDatesViewModel( courseId, courseName, isSelfPaced, + enrollmentMode, + get(), get(), get(), get(), @@ -253,7 +272,8 @@ val screenModule = module { courseId, get(), handoutsType, - get() + get(), + get(), ) } viewModel { CourseSearchViewModel(get(), get(), get(), get(), get()) } @@ -302,7 +322,8 @@ val screenModule = module { WhatsNewViewModel( courseId, infoType, - get() + get(), + get(), ) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt index 669c749b4..4f1a1acf9 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/AuthAnalytics.kt @@ -1,11 +1,42 @@ package org.openedx.auth.presentation interface AuthAnalytics { - fun userLoginEvent(method: String) - fun signUpClickedEvent() - fun createAccountClickedEvent(provider: String) - fun registrationSuccessEvent(provider: String) - fun forgotPasswordClickedEvent() - fun resetPasswordClickedEvent(success: Boolean) fun setUserIdForSession(userId: Long) -} \ No newline at end of file + fun logEvent(event: String, params: Map) +} + +enum class LogistrationAnalyticEvent(val event: String) { + DISCOVERY_COURSES_SEARCH("Logistration:Discovery Courses Search"), + EXPLORE_ALL_COURSES("Logistration:Explore All Courses"), + REGISTER_CLICKED("Logistration:Register Clicked"), + REGISTER_VIEWED("Logistration:Register Viewed"), + CREATE_ACCOUNT_CLICKED("Logistration:Create Account Clicked"), + REGISTER_SUCCESSFULLY("Logistration:Register Successfully"), + SIGN_IN_CLICKED("Logistration:Sign In Clicked"), + SIGN_IN_VIEWED("Logistration:Sign In Viewed"), + FORGOT_PASSWORD_CLICKED("Logistration:Forgot Password Clicked"), + RESET_PASSWORD_CLICKED("Logistration:Reset Password Clicked"), + RESET_PASSWORD("Logistration:Reset Password"), + SIGN_IN_SUCCESSFULLY("Logistration:Sign In Successfully"), +} + +enum class LogistrationAnalyticValues(val biValue: String) { + DISCOVERY_COURSES_SEARCH("edx.bi.app.discovery.courses_search"), + EXPLORE_ALL_COURSES("edx.bi.app.discovery.explore.all.courses"), + REGISTER_CLICKED("edx.bi.app.user.register.clicked"), + SCREEN_NAVIGATION("edx.bi.app.navigation.screen"), + CREATE_ACCOUNT_CLICKED("edx.bi.app.user.create_account.clicked"), + REGISTER_SUCCESSFULLY("edx.bi.app.user.register.success"), + SIGN_IN_CLICKED("edx.bi.app.user.signin.clicked"), + RESET_PASSWORD_CLICKED("edx.bi.app.user.reset_password.clicked"), + RESET_PASSWORD("edx.bi.app.user.reset_password"), + SIGN_IN_SUCCESSFULLY("edx.bi.app.user.signin"), +} + +enum class LogistrationAnalyticKey(val key: String) { + NAME("name"), + LABEL("label"), + SUCCESS("success"), + METHOD("method"), + PROVIDER("provider"), +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt index eb63bbc19..ba88c3c71 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt @@ -38,10 +38,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf import org.openedx.auth.R -import org.openedx.auth.presentation.AuthRouter -import org.openedx.core.config.Config import org.openedx.core.ui.AuthButtonsPanel import org.openedx.core.ui.SearchBar import org.openedx.core.ui.displayCutoutForLandscape @@ -53,8 +52,9 @@ import org.openedx.core.ui.theme.compose.LogistrationLogoView class LogistrationFragment : Fragment() { - private val router: AuthRouter by inject() - private val config by inject() + private val viewModel: LogistrationViewModel by viewModel { + parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) + } override fun onCreateView( inflater: LayoutInflater, @@ -64,27 +64,15 @@ class LogistrationFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { OpenEdXTheme { - val courseId = arguments?.getString(ARG_COURSE_ID, "") - val isDiscoveryTypeWebView = config.getDiscoveryConfig().isViewTypeWebView() LogistrationScreen( onSignInClick = { - router.navigateToSignIn(parentFragmentManager, courseId, null) + viewModel.navigateToSignIn(parentFragmentManager) }, onRegisterClick = { - router.navigateToSignUp(parentFragmentManager, courseId, null) + viewModel.navigateToSignUp(parentFragmentManager) }, onSearchClick = { querySearch -> - if (isDiscoveryTypeWebView) { - router.navigateToWebDiscoverCourses( - parentFragmentManager, - querySearch - ) - } else { - router.navigateToNativeDiscoverCourses( - parentFragmentManager, - querySearch - ) - } + viewModel.navigateToDiscovery(parentFragmentManager, querySearch) } ) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt new file mode 100644 index 000000000..f5cb042eb --- /dev/null +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt @@ -0,0 +1,71 @@ +package org.openedx.auth.presentation.logistration + +import androidx.fragment.app.FragmentManager +import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.AuthRouter +import org.openedx.auth.presentation.LogistrationAnalyticEvent +import org.openedx.auth.presentation.LogistrationAnalyticKey +import org.openedx.auth.presentation.LogistrationAnalyticValues +import org.openedx.core.BaseViewModel +import org.openedx.core.config.Config +import org.openedx.core.extension.takeIfNotEmpty + +class LogistrationViewModel( + private val courseId: String, + private val router: AuthRouter, + private val config: Config, + private val analytics: AuthAnalytics, +) : BaseViewModel() { + + private val discoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() + + fun navigateToSignIn(parentFragmentManager: FragmentManager) { + router.navigateToSignIn(parentFragmentManager, courseId, null) + logEvent( + LogistrationAnalyticEvent.SIGN_IN_CLICKED, + LogistrationAnalyticValues.SIGN_IN_CLICKED + ) + } + + fun navigateToSignUp(parentFragmentManager: FragmentManager) { + router.navigateToSignUp(parentFragmentManager, courseId, null) + logEvent( + LogistrationAnalyticEvent.REGISTER_CLICKED, + LogistrationAnalyticValues.REGISTER_CLICKED + ) + } + + fun navigateToDiscovery(parentFragmentManager: FragmentManager, querySearch: String) { + if (discoveryTypeWebView) { + router.navigateToWebDiscoverCourses( + parentFragmentManager, + querySearch + ) + } else { + router.navigateToNativeDiscoverCourses( + parentFragmentManager, + querySearch + ) + } + querySearch.takeIfNotEmpty()?.let { + logEvent( + LogistrationAnalyticEvent.DISCOVERY_COURSES_SEARCH, + LogistrationAnalyticValues.DISCOVERY_COURSES_SEARCH, + buildMap { put(LogistrationAnalyticKey.LABEL.key, querySearch) }) + } ?: logEvent( + LogistrationAnalyticEvent.EXPLORE_ALL_COURSES, + LogistrationAnalyticValues.EXPLORE_ALL_COURSES + ) + } + + private fun logEvent( + eventName: LogistrationAnalyticEvent, + biValue: LogistrationAnalyticValues, + params: Map = emptyMap() + ) { + analytics.logEvent(eventName.event, buildMap { + put(LogistrationAnalyticKey.NAME.key, biValue.biValue) + putAll(params) + }) + } +} diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt index 427f2f263..3f9fb8afb 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt @@ -6,6 +6,9 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics +import org.openedx.auth.presentation.LogistrationAnalyticEvent +import org.openedx.auth.presentation.LogistrationAnalyticKey +import org.openedx.auth.presentation.LogistrationAnalyticValues import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.SingleEventLiveData @@ -41,28 +44,29 @@ class RestorePasswordViewModel( } fun passwordReset(email: String) { + logResetPasswordClickedEvent() _uiState.value = RestorePasswordUIState.Loading viewModelScope.launch { try { if (email.isNotEmpty() && email.isEmailValid()) { if (interactor.passwordReset(email)) { _uiState.value = RestorePasswordUIState.Success(email) - analytics.resetPasswordClickedEvent(true) + logResetPasswordEvent(true) } else { _uiState.value = RestorePasswordUIState.Initial _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) - analytics.resetPasswordClickedEvent(false) + logResetPasswordEvent(false) } } else { _uiState.value = RestorePasswordUIState.Initial _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email)) - analytics.resetPasswordClickedEvent(false) + logResetPasswordEvent(false) } } catch (e: Exception) { _uiState.value = RestorePasswordUIState.Initial - analytics.resetPasswordClickedEvent(false) + logResetPasswordEvent(false) if (e is EdxError.ValidationException) { _uiMessage.value = UIMessage.SnackBarMessage(e.error) } else if (e.isInternetError()) { @@ -84,4 +88,22 @@ class RestorePasswordViewModel( } } + private fun logResetPasswordClickedEvent() { + analytics.logEvent( + LogistrationAnalyticEvent.RESET_PASSWORD_CLICKED.event, + buildMap { + put(LogistrationAnalyticKey.NAME.key, LogistrationAnalyticValues.RESET_PASSWORD_CLICKED.biValue) + } + ) + } + + private fun logResetPasswordEvent(success: Boolean) { + analytics.logEvent( + LogistrationAnalyticEvent.RESET_PASSWORD.event, + buildMap { + put(LogistrationAnalyticKey.NAME.key, LogistrationAnalyticValues.RESET_PASSWORD.biValue) + put(LogistrationAnalyticKey.SUCCESS.key, success) + } + ) + } } \ No newline at end of file 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 cf1c6afa5..a4d065d3a 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 @@ -18,6 +18,9 @@ import org.openedx.auth.domain.model.SocialAuthResponse import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter +import org.openedx.auth.presentation.LogistrationAnalyticEvent +import org.openedx.auth.presentation.LogistrationAnalyticKey +import org.openedx.auth.presentation.LogistrationAnalyticValues import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.BaseViewModel import org.openedx.core.SingleEventLiveData @@ -75,6 +78,10 @@ class SignInViewModel( init { collectAppUpgradeEvent() + logEvent( + LogistrationAnalyticEvent.SIGN_IN_VIEWED, + LogistrationAnalyticValues.SCREEN_NAVIGATION + ) } fun login(username: String, password: String) { @@ -95,7 +102,13 @@ class SignInViewModel( interactor.login(username, password) _uiState.update { it.copy(loginSuccess = true) } setUserId() - analytics.userLoginEvent(AuthType.PASSWORD.methodName) + logEvent( + LogistrationAnalyticEvent.SIGN_IN_SUCCESSFULLY, + LogistrationAnalyticValues.SIGN_IN_SUCCESSFULLY, + buildMap { + put(LogistrationAnalyticKey.METHOD.key, AuthType.PASSWORD.methodName) + } + ) } catch (e: Exception) { if (e is EdxError.InvalidGrantException) { _uiMessage.value = @@ -135,12 +148,18 @@ class SignInViewModel( fun navigateToSignUp(parentFragmentManager: FragmentManager) { router.navigateToSignUp(parentFragmentManager, null, null) - analytics.signUpClickedEvent() + logEvent( + LogistrationAnalyticEvent.REGISTER_CLICKED, + LogistrationAnalyticValues.REGISTER_CLICKED + ) } fun navigateToForgotPassword(parentFragmentManager: FragmentManager) { router.navigateToRestorePassword(parentFragmentManager) - analytics.forgotPasswordClickedEvent() + logEvent( + LogistrationAnalyticEvent.FORGOT_PASSWORD_CLICKED, + LogistrationAnalyticValues.SCREEN_NAVIGATION + ) } override fun onCleared() { @@ -158,7 +177,7 @@ class SignInViewModel( logger.d { "Social login (${authType.methodName}) success" } _uiState.update { it.copy(loginSuccess = true) } setUserId() - analytics.userLoginEvent(authType.methodName) +// analytics.userLoginEvent(authType.methodName) _uiState.update { it.copy(showProgress = false) } } } @@ -217,4 +236,15 @@ class SignInViewModel( } } } + + private fun logEvent( + eventName: LogistrationAnalyticEvent, + biValue: LogistrationAnalyticValues, + params: Map = emptyMap() + ) { + analytics.logEvent(eventName.event, buildMap { + put(LogistrationAnalyticKey.NAME.key, biValue.biValue) + putAll(params) + }) + } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index 2b7cdc09f..1aecb2d25 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -18,6 +18,9 @@ import org.openedx.auth.domain.model.SocialAuthResponse import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter +import org.openedx.auth.presentation.LogistrationAnalyticEvent +import org.openedx.auth.presentation.LogistrationAnalyticKey +import org.openedx.auth.presentation.LogistrationAnalyticValues import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.ApiConstants import org.openedx.core.BaseViewModel @@ -69,6 +72,10 @@ class SignUpViewModel( init { collectAppUpgradeEvent() + logEvent( + LogistrationAnalyticEvent.REGISTER_VIEWED, + LogistrationAnalyticValues.SCREEN_NAVIGATION + ) } fun getRegistrationFields() { @@ -105,8 +112,10 @@ class SignUpViewModel( val agreementFields = mutableListOf() val agreementText = agreementProvider.getAgreement(isSignIn = false) if (agreementText != null) { - val honourCode = allFields.find { it.name == ApiConstants.RegistrationFields.HONOR_CODE } - val marketingEmails = allFields.find { it.name == ApiConstants.RegistrationFields.MARKETING_EMAILS } + val honourCode = + allFields.find { it.name == ApiConstants.RegistrationFields.HONOR_CODE } + val marketingEmails = + allFields.find { it.name == ApiConstants.RegistrationFields.MARKETING_EMAILS } mutableAllFields.remove(honourCode) requiredFields.addAll(mutableAllFields.filter { it.required }) optionalFields.addAll(mutableAllFields.filter { !it.required }) @@ -129,9 +138,12 @@ class SignUpViewModel( } fun register() { - analytics.createAccountClickedEvent("") + logEvent( + LogistrationAnalyticEvent.CREATE_ACCOUNT_CLICKED, + LogistrationAnalyticValues.CREATE_ACCOUNT_CLICKED + ) val mapFields = uiState.value.allFields.associate { it.name to it.placeholder } + - mapOf(ApiConstants.RegistrationFields.HONOR_CODE to true.toString()) + mapOf(ApiConstants.RegistrationFields.HONOR_CODE to true.toString()) val resultMap = mapFields.toMutableMap() uiState.value.allFields.filter { !it.required }.forEach { (k, _) -> if (mapFields[k].isNullOrEmpty()) { @@ -154,7 +166,13 @@ class SignUpViewModel( resultMap[ApiConstants.CLIENT_ID] = config.getOAuthClientId() } interactor.register(resultMap.toMap()) - analytics.registrationSuccessEvent(socialAuth?.authType?.postfix.orEmpty()) + logEvent( + LogistrationAnalyticEvent.REGISTER_SUCCESSFULLY, + LogistrationAnalyticValues.REGISTER_SUCCESSFULLY, + buildMap { + put(LogistrationAnalyticKey.PROVIDER.key, socialAuth?.authType?.postfix.orEmpty()) + } + ) if (socialAuth == null) { interactor.login( resultMap.getValue(ApiConstants.EMAIL), @@ -226,7 +244,13 @@ class SignUpViewModel( updateFields(fields) }.onSuccess { setUserId() - analytics.userLoginEvent(socialAuth.authType.methodName) + logEvent( + LogistrationAnalyticEvent.SIGN_IN_SUCCESSFULLY, + LogistrationAnalyticValues.SIGN_IN_SUCCESSFULLY, + buildMap { + put(LogistrationAnalyticKey.METHOD.key, socialAuth.authType.methodName) + } + ) _uiState.update { it.copy(successLogin = true) } logger.d { "Social login (${socialAuth.authType.methodName}) success" } } @@ -279,4 +303,15 @@ class SignUpViewModel( } } } + + private fun logEvent( + eventName: LogistrationAnalyticEvent, + biValue: LogistrationAnalyticValues, + params: Map = emptyMap() + ) { + analytics.logEvent(eventName.event, buildMap { + put(LogistrationAnalyticKey.NAME.key, biValue.biValue) + putAll(params) + }) + } } diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewAnalytics.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewAnalytics.kt new file mode 100644 index 000000000..3527e6d1c --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/AppReviewAnalytics.kt @@ -0,0 +1,36 @@ +package org.openedx.core.presentation.dialog.appreview + +interface AppReviewAnalytics { + fun logEvent(event: String, params: Map) +} + +enum class AppReviewEvent(val event: String) { + RATING_DIALOG("AppReviews:Rating Dialog Viewed"), + RATING_DIALOG_ACTION("AppReviews:Rating Dialog Action"), + SHARE_FEEDBACK_DIALOG("AppReviews:Submit Feedback Dialog Viewed"), + SHARE_FEEDBACK_DIALOG_ACTION("AppReviews:Submit Feedback Dialog Action"), + THANKYOU_DIALOG("AppReviews:Thankyou Dialog Viewed"), + THANKYOU_DIALOG_ACTION("AppReviews:Thankyou Dialog Action"), +} + +enum class AppReviewValue(val biValue: String) { + RATING_DIALOG("edx.bi.app.app_reviews.rating_alert.viewed"), + RATING_DIALOG_ACTION("edx.bi.app.app_reviews.rating_alert.action"), + SHARE_FEEDBACK_DIALOG("edx.bi.app.app_reviews.share_feedback.viewed"), + SHARE_FEEDBACK_DIALOG_ACTION("edx.bi.app.app_reviews.share_feedback.action"), + THANKYOU_DIALOG("edx.bi.app.app_reviews.thankyou_dialog.viewed"), + THANKYOU_DIALOG_ACTION("edx.bi.app.app_reviews.thankyou_dialog.action"), +} + +enum class AppReviewKey(val key: String) { + NAME("name"), + CATEGORY("category"), + RATING("rating"), + APP_VERSION("app_version"), + APP_REVIEW("app_review"), + ACTION("action"), + DISMISSED("dismissed"), + NOT_NOW("not_now"), + SUBMIT_FEEDBACK("submit_feedback"), + SHARE_FEEDBACK("share_feedback"), +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/BaseAppReviewDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/BaseAppReviewDialogFragment.kt index 427c71959..79f19e939 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/BaseAppReviewDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/BaseAppReviewDialogFragment.kt @@ -9,6 +9,7 @@ open class BaseAppReviewDialogFragment : DialogFragment() { private val reviewPreferences: InAppReviewPreferences by inject() protected val appData: AppData by inject() + protected val analytics: AppReviewAnalytics by inject() fun saveVersionName() { val versionName = appData.versionName @@ -19,8 +20,112 @@ open class BaseAppReviewDialogFragment : DialogFragment() { reviewPreferences.wasPositiveRated = true } - fun notNowClick() { + fun onRatingDialogShowed() { + analytics.logEvent( + event = AppReviewEvent.RATING_DIALOG.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.RATING_DIALOG.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + } + ) + } + + fun onFeedbackDialogShowed() { + analytics.logEvent( + event = AppReviewEvent.SHARE_FEEDBACK_DIALOG.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.SHARE_FEEDBACK_DIALOG.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + } + ) + } + + fun onThankYouDialogShowed() { + analytics.logEvent( + event = AppReviewEvent.THANKYOU_DIALOG.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.THANKYOU_DIALOG.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + } + ) + } + + fun notNowClick(dialogType: AppReviewDialogType, rating: Int = 0) { saveVersionName() + analytics.logEvent( + event = dialogType.event.event, + params = buildMap { + put(AppReviewKey.NAME.key, dialogType.biValue.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + put(AppReviewKey.ACTION.key, AppReviewKey.NOT_NOW.key) + put(AppReviewKey.RATING.key, rating) + } + ) dismiss() } -} \ No newline at end of file + + fun onSubmitRatingClick(rating: Int) { + analytics.logEvent( + event = AppReviewEvent.RATING_DIALOG_ACTION.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.RATING_DIALOG_ACTION.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + put(AppReviewKey.ACTION.key, AppReviewKey.SUBMIT_FEEDBACK.key) + put(AppReviewKey.RATING.key, rating) + } + ) + dismiss() + } + + fun onShareFeedbackClick() { + analytics.logEvent( + event = AppReviewEvent.SHARE_FEEDBACK_DIALOG_ACTION.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.SHARE_FEEDBACK_DIALOG_ACTION.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + put(AppReviewKey.ACTION.key, AppReviewKey.SHARE_FEEDBACK.key) + } + ) + dismiss() + } + + fun onSendFeedbackClick() { + analytics.logEvent( + event = AppReviewEvent.SHARE_FEEDBACK_DIALOG.event, + params = buildMap { + put(AppReviewKey.NAME.key, AppReviewValue.SHARE_FEEDBACK_DIALOG.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + } + ) + dismiss() + } + + fun onDismiss(dialogType: AppReviewDialogType) { + analytics.logEvent( + event = dialogType.event.event, + params = buildMap { + put(AppReviewKey.NAME.key, dialogType.biValue.biValue) + put(AppReviewKey.CATEGORY.key, AppReviewKey.APP_REVIEW.key) + put(AppReviewKey.APP_VERSION.key, appData.versionName) + put(AppReviewKey.ACTION.key, AppReviewKey.DISMISSED.key) + } + ) + dismiss() + } +} + +enum class AppReviewDialogType(val event: AppReviewEvent, val biValue: AppReviewValue) { + RATE(AppReviewEvent.RATING_DIALOG_ACTION, AppReviewValue.RATING_DIALOG_ACTION), + FEEDBACK( + AppReviewEvent.SHARE_FEEDBACK_DIALOG_ACTION, + AppReviewValue.SHARE_FEEDBACK_DIALOG_ACTION + ), + THANK_YOU(AppReviewEvent.THANKYOU_DIALOG_ACTION, AppReviewValue.THANKYOU_DIALOG_ACTION), +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt index f882cbe8a..5091c0e87 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/FeedbackDialogFragment.kt @@ -33,13 +33,14 @@ class FeedbackDialogFragment : BaseAppReviewDialogFragment() { val feedback = rememberSaveable { mutableStateOf("") } FeedbackDialog( feedback = feedback, - onNotNowClick = this@FeedbackDialogFragment::notNowClick, + onNotNowClick = { this@FeedbackDialogFragment.notNowClick(AppReviewDialogType.FEEDBACK) }, onShareClick = { onShareClick(feedback.value) } ) } } + onFeedbackDialogShowed() } override fun onResume() { @@ -51,6 +52,7 @@ class FeedbackDialogFragment : BaseAppReviewDialogFragment() { } private fun onShareClick(feedback: String) { + onShareFeedbackClick() saveVersionName() wasShareClicked = true sendEmail(feedback) @@ -75,6 +77,11 @@ class FeedbackDialogFragment : BaseAppReviewDialogFragment() { ) } + + override fun dismiss() { + onDismiss(AppReviewDialogType.FEEDBACK) + } + companion object { fun newInstance(): FeedbackDialogFragment { return FeedbackDialogFragment() diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt index 1cfa034b9..d01f70f58 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/RateDialogFragment.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import org.openedx.core.ui.theme.OpenEdXTheme -class RateDialogFragment: BaseAppReviewDialogFragment() { +class RateDialogFragment : BaseAppReviewDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -26,17 +26,20 @@ class RateDialogFragment: BaseAppReviewDialogFragment() { val rating = rememberSaveable { mutableIntStateOf(0) } RateDialog( rating = rating, - onNotNowClick = this@RateDialogFragment::notNowClick, + onNotNowClick = { + notNowClick(AppReviewDialogType.RATE, rating.intValue) + }, onSubmitClick = { onSubmitClick(rating.intValue) } ) } } + onRatingDialogShowed() } private fun onSubmitClick(rating: Int) { - dismiss() + onSubmitRatingClick(rating) if (rating > 3) { openThankYouDialog() } else { @@ -45,6 +48,7 @@ class RateDialogFragment: BaseAppReviewDialogFragment() { } private fun openFeedbackDialog() { + onSendFeedbackClick() val dialog = FeedbackDialogFragment.newInstance() dialog.show( requireActivity().supportFragmentManager, @@ -62,6 +66,10 @@ class RateDialogFragment: BaseAppReviewDialogFragment() { ) } + override fun dismiss() { + onDismiss(AppReviewDialogType.RATE) + } + companion object { fun newInstance(): RateDialogFragment { return RateDialogFragment() diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt index 89fe98c1c..4c813caa5 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appreview/ThankYouDialogFragment.kt @@ -47,13 +47,16 @@ class ThankYouDialogFragment : BaseAppReviewDialogFragment() { ThankYouDialog( description = description, showButtons = isFeedbackPositive.value, - onNotNowClick = this@ThankYouDialogFragment::notNowClick, + onNotNowClick = { + this@ThankYouDialogFragment.notNowClick(AppReviewDialogType.THANK_YOU) + }, onRateUsClick = this@ThankYouDialogFragment::openInAppReview ) closeDialogDelay(isFeedbackPositive.value) } } + onThankYouDialogShowed() } private fun closeDialogDelay(isFeedbackPositive: Boolean) { @@ -83,6 +86,10 @@ class ThankYouDialogFragment : BaseAppReviewDialogFragment() { } } + override fun dismiss() { + onDismiss(AppReviewDialogType.THANK_YOU) + } + companion object { private const val ARG_IS_FEEDBACK_POSITIVE = "is_feedback_positive" 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 afa8d7b12..3214a6869 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -165,7 +165,7 @@ fun Toolbar( modifier = Modifier .testTag("txt_toolbar_title") .align(Alignment.CenterVertically) - .padding(end = 16.dp), + .padding(start = if (canShowBackBtn.not()) 16.dp else 0.dp, end = 16.dp), text = label, color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleMedium, @@ -449,7 +449,8 @@ fun HyperlinkText( annotatedString .getStringAnnotations("URL", it, it) .firstOrNull()?.let { stringAnnotation -> - action?.invoke(stringAnnotation.item) ?: uriHandler.openUri(stringAnnotation.item) + action?.invoke(stringAnnotation.item) + ?: uriHandler.openUri(stringAnnotation.item) } } ) diff --git a/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt b/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt index cdae67678..874f003ac 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseAnalytics.kt @@ -1,20 +1,163 @@ package org.openedx.course.presentation interface CourseAnalytics { - fun courseEnrollClickedEvent(courseId: String, courseName: String) - fun courseEnrollSuccessEvent(courseId: String, courseName: String) - fun viewCourseClickedEvent(courseId: String, courseName: String) - fun resumeCourseTappedEvent(courseId: String, courseName: String, blockId: String) - fun sequentialClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) - fun verticalClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) - fun nextBlockClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) - fun prevBlockClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) - fun finishVerticalClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) - fun finishVerticalNextClickedEvent(courseId: String, courseName: String, blockId: String, blockName: String) + fun sequentialClickedEvent( + courseId: String, + courseName: String, + blockId: String, + blockName: String, + ) + + fun nextBlockClickedEvent( + courseId: String, + courseName: String, + blockId: String, + blockName: String, + ) + + fun prevBlockClickedEvent( + courseId: String, + courseName: String, + blockId: String, + blockName: String, + ) + + fun finishVerticalClickedEvent( + courseId: String, + courseName: String, + blockId: String, + blockName: String, + ) + + fun finishVerticalNextClickedEvent( + courseId: String, + courseName: String, + blockId: String, + blockName: String, + ) + fun finishVerticalBackClickedEvent(courseId: String, courseName: String) - fun courseTabClickedEvent(courseId: String, courseName: String) - fun videoTabClickedEvent(courseId: String, courseName: String) - fun discussionTabClickedEvent(courseId: String, courseName: String) - fun datesTabClickedEvent(courseId: String, courseName: String) - fun handoutsTabClickedEvent(courseId: String, courseName: String) + fun logEvent(event: String, params: Map) +} + +enum class CourseAnalyticEvent(val event: String) { + + COURSE_ENROLL_CLICKED("Discovery:Course Enroll Clicked"), + COURSE_ENROLL_SUCCESS("Discovery:Course Enroll Success"), + COURSE_INFO("Discovery:Course Info"), + + DASHBOARD("Course:Dashboard"), + HOME_TAB("Course:Home Tab"), + VIDEOS_TAB("Course:Videos Tab"), + DISCUSSION_TAB("Course:Discussion Tab"), + DATES_TAB("Course:Dates Tab"), + HANDOUTS_TAB("Course:Handouts Tab"), + ANNOUNCEMENTS("Course:Announcements"), + HANDOUTS("Course:Handouts"), + UNIT_DETAIL("Course:Unit Detail"), + VIEW_CERTIFICATE("Course:View Certificate"), + RESUME_COURSE_CLICKED("Course:Resume Course Clicked"), + + VIDEO_LOADED("Video:Loaded"), + VIDEO_CHANGE_SPEED("Video:Change Speed"), + VIDEO_PLAYED("Video:Played"), + VIDEO_PAUSED("Video:Paused"), + VIDEO_SEEKED("Video:Seeked"), + VIDEO_COMPLETED("Video:Completed"), + + + CAST_CONNECTED("Cast:Connected"), + CAST_DISCONNECTED("Cast:Disconnected"), + + DATES_COURSE_COMPONENT_TAPPED("Dates:Course Component Tapped"), + DATES_UNSUPPORTED_COMPONENT_TAPPED("Dates:Unsupported Component Tapped"), + PLS_BANNER_VIEWED("PLS:Banner Viewed"), + PLS_SHIFT_BUTTON_TAPPED("PLS:Shift Button Tapped"), + PLS_SHIFT_DATES("PLS:Shift Dates"), + + DATES_CALENDAR_TOGGLE_ON("Dates:Calendar Toggle On"), + DATES_CALENDAR_TOGGLE_OFF("Dates:Calendar Toggle Off"), + DATES_CALENDAR_ACCESS_ALLOWED("Dates:Calendar Access Allowed"), + DATES_CALENDAR_ACCESS_DONT_ALLOW("Dates:Calendar Access Don't Allow"), + DATES_CALENDAR_ADD_DATES("Dates:Calendar Add Dates"), + DATES_CALENDAR_ADD_CANCELLED("Dates:Calendar Add Cancelled"), + DATES_CALENDAR_REMOVE_DATES("Dates:Calendar Remove Dates"), + DATES_CALENDAR_REMOVE_CANCELLED("Dates:Calendar Remove Cancelled"), + DATES_CALENDAR_ADD_CONFIRMATION("Dates:Calendar Add Confirmation"), + DATES_CALENDAR_VIEW_EVENTS("Dates:Calendar View Events"), + DATES_CALENDAR_SYNC_UPDATE_DATES("Dates:Calendar Sync Update Dates"), + DATES_CALENDAR_SYNC_REMOVE_CALENDAR("Dates:Calendar Sync Remove Calendar"), + DATES_CALENDAR_ADD_DATES_SUCCESS("Dates:Calendar Add Dates Success"), + DATES_CALENDAR_REMOVE_DATES_SUCCESS("Dates:Calendar Remove Dates Success"), + DATES_CALENDAR_UPDATE_DATES_SUCCESS("Dates:Calendar Update Dates Success"), +} + +enum class CourseAnalyticValue(val biValue: String) { + + SCREEN_NAVIGATION("edx.bi.app.navigation.screen"), + + COURSE_ENROLL_CLICKED("edx.bi.app.course.enroll.clicked"), + COURSE_ENROLL_SUCCESS("edx.bi.app.course.enroll.success"), + + VIEW_CERTIFICATE("edx.bi.app.course.view_certificate.clicked"), + RESUME_COURSE_CLICKED("edx.bi.app.course.resume_course.clicked"), + + VIDEO_LOADED("edx.bi.app.videos.loaded"), + VIDEO_CHANGE_SPEED("edx.bi.app.videos.speed.changed"), + VIDEO_PLAYED("edx.bi.app.videos.played"), + VIDEO_PAUSED("edx.bi.app.videos.paused"), + VIDEO_SEEKED("edx.bi.app.videos.position.changed"), + VIDEO_COMPLETED("edx.bi.app.videos.completed"), + + GOOGLE_CAST("google_cast"), + CAST_CONNECTED("edx.bi.app.cast.connected"), + CAST_DISCONNECTED("edx.bi.app.cast.disconnected"), + + COURSE_DATES("course_dates"), + DATES_COURSE_COMPONENT_TAPPED("edx.bi.app.coursedates.component.tapped"), + DATES_UNSUPPORTED_COMPONENT_TAPPED("edx.bi.app.coursedates.unsupported.component.tapped"), + PLS_BANNER_VIEWED("edx.bi.app.coursedates.pls_banner.viewed"), + PLS_SHIFT_BUTTON_TAPPED("edx.bi.app.coursedates.pls_banner.shift_button.tapped"), + PLS_SHIFT_DATES("edx.bi.app.coursedates.pls_banner.shift_dates"), + + DATES_CALENDAR_TOGGLE_ON("edx.bi.app.calendar.toggle_on"), + DATES_CALENDAR_TOGGLE_OFF("edx.bi.app.calendar.toggle_off"), + DATES_CALENDAR_ACCESS_ALLOWED("edx.bi.app.calendar.access_ok"), + DATES_CALENDAR_ACCESS_DONT_ALLOW("edx.bi.app.calendar.access_dont_allow"), + DATES_CALENDAR_ADD_DATES("edx.bi.app.calendar.add_ok"), + DATES_CALENDAR_ADD_CANCELLED("edx.bi.app.calendar.add_cancel"), + DATES_CALENDAR_REMOVE_DATES("edx.bi.app.calendar.remove_ok"), + DATES_CALENDAR_REMOVE_CANCELLED("edx.bi.app.calendar.remove_cancel"), + DATES_CALENDAR_ADD_CONFIRMATION("edx.bi.app.calendar.confirmation_done"), + DATES_CALENDAR_VIEW_EVENTS("edx.bi.app.calendar.confirmation_view_events"), + DATES_CALENDAR_SYNC_UPDATE_DATES("edx.bi.app.calendar.sync_update"), + DATES_CALENDAR_SYNC_REMOVE_CALENDAR("edx.bi.app.calendar.sync_remove"), + DATES_CALENDAR_ADD_DATES_SUCCESS("edx.bi.app.calendar.add_success"), + DATES_CALENDAR_REMOVE_DATES_SUCCESS("edx.bi.app.calendar.remove_success"), + DATES_CALENDAR_UPDATE_DATES_SUCCESS("edx.bi.app.calendar.update_success"), +} + +enum class CourseAnalyticKey(val key: String) { + NAME("name"), + COURSE_ID("course_id"), + OPEN_IN_BROWSER("open_in_browser_url"), + COMPONENT("component"), + VIDEO_PLAYER("videoplayer"), + ENROLLMENT_MODE("enrollment_mode"), + PACING("pacing"), + SCREEN_NAME("screen_name"), + BANNER_TYPE("banner_type"), + CATEGORY("category"), + SUCCESS("success"), + LINK("link"), + BLOCK_ID("block_id"), + BLOCK_TYPE("block_type"), + PLAY_MEDIUM("play_medium"), + NATIVE("native"), + YOUTUBE("youtube"), + GOOGLE_CAST("google_cast"), + CURRENT_TIME("current_time"), + SKIP_INTERVAL("requested_skip_interval"), + SPEED("speed"), + NAVIGATION("navigation"), } 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 fae658fde..a9bb8dfcf 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -8,7 +8,7 @@ import org.openedx.course.presentation.handouts.HandoutsType interface CourseRouter { fun navigateToCourseOutline( - fm: FragmentManager, courseId: String, courseTitle: String + fm: FragmentManager, courseId: String, courseTitle: String, enrollmentMode: String ) fun navigateToNoAccess( diff --git a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt b/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt index 59be5999b..4938f5c2b 100644 --- a/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt +++ b/course/src/main/java/org/openedx/course/presentation/calendarsync/CalendarSyncDialog.kt @@ -38,7 +38,7 @@ fun CalendarSyncDialog( syncDialogType: CalendarSyncDialogType, calendarTitle: String, syncDialogAction: (CalendarSyncDialogType) -> Unit, - dismissSyncDialog: () -> Unit, + dismissSyncDialog: (CalendarSyncDialogType) -> Unit, ) { when (syncDialogType) { CalendarSyncDialogType.SYNC_DIALOG, @@ -51,7 +51,7 @@ fun CalendarSyncDialog( negativeButton = stringResource(syncDialogType.negativeButtonResId), positiveAction = { syncDialogAction(syncDialogType) } ), - onDismiss = dismissSyncDialog, + onDismiss = { dismissSyncDialog(syncDialogType) }, ) } @@ -71,7 +71,7 @@ fun CalendarSyncDialog( negativeButton = stringResource(syncDialogType.negativeButtonResId), positiveAction = { syncDialogAction(syncDialogType) } ), - onDismiss = dismissSyncDialog + onDismiss = { dismissSyncDialog(syncDialogType) } ) } @@ -84,7 +84,7 @@ fun CalendarSyncDialog( negativeButton = stringResource(syncDialogType.negativeButtonResId), positiveAction = { syncDialogAction(syncDialogType) }, ), - onDismiss = dismissSyncDialog + onDismiss = { dismissSyncDialog(syncDialogType) } ) } @@ -98,7 +98,7 @@ fun CalendarSyncDialog( positiveAction = { syncDialogAction(syncDialogType) }, negativeAction = { syncDialogAction(CalendarSyncDialogType.UN_SYNC_DIALOG) } ), - onDismiss = dismissSyncDialog + onDismiss = { dismissSyncDialog(syncDialogType) } ) } 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 a5f22084b..e851d2bc9 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 @@ -39,7 +39,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { private val viewModel by viewModel { parametersOf( requireArguments().getString(ARG_COURSE_ID, ""), - requireArguments().getString(ARG_TITLE, "") + requireArguments().getString(ARG_TITLE, ""), + requireArguments().getString(ARG_ENROLLMENT_MODE, "") ) } private val router by inject() @@ -49,6 +50,7 @@ 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) } @@ -132,7 +134,8 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { CourseDatesFragment.newInstance( viewModel.courseId, viewModel.courseName, - viewModel.isSelfPaced + viewModel.isSelfPaced, + viewModel.enrollmentMode, ) ) addFragment( @@ -191,12 +194,14 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { syncDialogAction = { dialog -> when (dialog) { CalendarSyncDialogType.SYNC_DIALOG -> { + viewModel.logCalendarAddDates() viewModel.addOrUpdateEventsInCalendar( updatedEvent = false, ) } CalendarSyncDialogType.UN_SYNC_DIALOG -> { + viewModel.logCalendarRemoveDates() viewModel.deleteCourseCalendar() } @@ -205,12 +210,14 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } CalendarSyncDialogType.OUT_OF_SYNC_DIALOG -> { + viewModel.logCalendarSyncUpdateDates() viewModel.addOrUpdateEventsInCalendar( updatedEvent = true, ) } CalendarSyncDialogType.EVENTS_DIALOG -> { + viewModel.logCalendarViewEvents() viewModel.openCalendarApp() } @@ -219,7 +226,24 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } } }, - dismissSyncDialog = { + dismissSyncDialog = { dialog -> + when (dialog) { + CalendarSyncDialogType.SYNC_DIALOG -> + viewModel.logCalendarAddCancelled() + + CalendarSyncDialogType.UN_SYNC_DIALOG -> + viewModel.logCalendarRemoveCancelled() + + CalendarSyncDialogType.OUT_OF_SYNC_DIALOG -> + viewModel.logCalendarSyncRemoveCalendar() + + CalendarSyncDialogType.EVENTS_DIALOG-> + viewModel.logCalendarAddedConfirmation() + CalendarSyncDialogType.LOADING_DIALOG, + CalendarSyncDialogType.PERMISSION_DIALOG, + CalendarSyncDialogType.NONE -> {} + } + viewModel.setCalendarSyncDialogType(CalendarSyncDialogType.NONE) } ) @@ -247,14 +271,17 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "title" + private const val ARG_ENROLLMENT_MODE = "enrollmentMode" fun newInstance( courseId: String, - courseTitle: String + courseTitle: String, + enrollmentMode: String ): CourseContainerFragment { val fragment = CourseContainerFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_TITLE to courseTitle + ARG_TITLE to courseTitle, + ARG_ENROLLMENT_MODE to enrollmentMode ) return fragment } 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 886c63319..b3b585b42 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 @@ -28,6 +28,9 @@ 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.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.calendarsync.CalendarManager import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType @@ -39,15 +42,16 @@ import org.openedx.core.R as CoreR class CourseContainerViewModel( val courseId: String, var courseName: String, + val enrollmentMode: String, private val config: Config, private val interactor: CourseInteractor, private val calendarManager: CalendarManager, private val resourceManager: ResourceManager, private val notifier: CourseNotifier, private val networkConnection: NetworkConnection, - private val analytics: CourseAnalytics, private val corePreferences: CorePreferences, private val coursePreferences: CoursePreferences, + private val courseAnalytics: CourseAnalytics, ) : BaseViewModel() { val isCourseTopTabBarEnabled get() = config.isCourseTopTabBarEnabled() @@ -106,6 +110,7 @@ class CourseContainerViewModel( } fun preloadCourseStructure() { + courseDashboardViewed() if (_dataReady.value != null) { return } @@ -212,10 +217,13 @@ class CourseContainerViewModel( updateCalendarSyncState() if (updatedEvent) { + logCalendarUpdateDatesSuccess() setUiMessage(R.string.course_snackbar_course_calendar_updated) } else if (coursePreferences.isCalendarSyncEventsDialogShown(courseName)) { + logCalendarAddDatesSuccess() setUiMessage(R.string.course_snackbar_course_calendar_added) } else { + logCalendarAddDatesSuccess() coursePreferences.setCalendarSyncEventsDialogShown(courseName) setCalendarSyncDialogType(CalendarSyncDialogType.EVENTS_DIALOG) } @@ -256,6 +264,7 @@ class CourseContainerViewModel( } updateCalendarSyncState() } + logCalendarRemoveDatesSuccess() setUiMessage(R.string.course_snackbar_course_calendar_removed) } } @@ -282,23 +291,145 @@ class CourseContainerViewModel( (calendarSync.isInstructorPacedEnabled && !isSelfPaced)) } + private fun courseDashboardViewed(){ + logCourseContainerEvent(CourseAnalyticEvent.DASHBOARD) + } + private fun courseTabClickedEvent() { - analytics.courseTabClickedEvent(courseId, courseName) + logCourseContainerEvent(CourseAnalyticEvent.HOME_TAB) } private fun videoTabClickedEvent() { - analytics.videoTabClickedEvent(courseId, courseName) + logCourseContainerEvent(CourseAnalyticEvent.VIDEOS_TAB) } private fun discussionTabClickedEvent() { - analytics.discussionTabClickedEvent(courseId, courseName) + logCourseContainerEvent(CourseAnalyticEvent.DISCUSSION_TAB) } private fun datesTabClickedEvent() { - analytics.datesTabClickedEvent(courseId, courseName) + logCourseContainerEvent(CourseAnalyticEvent.DATES_TAB) } private fun handoutsTabClickedEvent() { - analytics.handoutsTabClickedEvent(courseId, courseName) + logCourseContainerEvent(CourseAnalyticEvent.HANDOUTS_TAB) + } + + private fun logCourseContainerEvent(event: CourseAnalyticEvent) { + courseAnalytics.logEvent( + event = event.event, + params = buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + } + ) + } + + fun logCalendarPermissionAccess(isAllowed: Boolean) { + if (isAllowed) { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ACCESS_ALLOWED, + CourseAnalyticValue.DATES_CALENDAR_ACCESS_ALLOWED + ) + } else { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ACCESS_DONT_ALLOW, + CourseAnalyticValue.DATES_CALENDAR_ACCESS_DONT_ALLOW + ) + } + } + + fun logCalendarAddDates() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ADD_DATES, + CourseAnalyticValue.DATES_CALENDAR_ADD_DATES + ) + } + + fun logCalendarAddCancelled() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ADD_CANCELLED, + CourseAnalyticValue.DATES_CALENDAR_ADD_CANCELLED + ) + } + + fun logCalendarRemoveDates() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_REMOVE_DATES, + CourseAnalyticValue.DATES_CALENDAR_REMOVE_DATES + ) + } + + fun logCalendarRemoveCancelled() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_REMOVE_CANCELLED, + CourseAnalyticValue.DATES_CALENDAR_REMOVE_CANCELLED + ) + } + + fun logCalendarAddedConfirmation() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ADD_CONFIRMATION, + CourseAnalyticValue.DATES_CALENDAR_ADD_CONFIRMATION + ) + } + + fun logCalendarViewEvents() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_VIEW_EVENTS, + CourseAnalyticValue.DATES_CALENDAR_VIEW_EVENTS + ) + } + + fun logCalendarSyncUpdateDates() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_SYNC_UPDATE_DATES, + CourseAnalyticValue.DATES_CALENDAR_SYNC_UPDATE_DATES + ) + } + + fun logCalendarSyncRemoveCalendar() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_SYNC_REMOVE_CALENDAR, + CourseAnalyticValue.DATES_CALENDAR_SYNC_REMOVE_CALENDAR + ) + } + + private fun logCalendarAddDatesSuccess() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_ADD_DATES_SUCCESS, + CourseAnalyticValue.DATES_CALENDAR_ADD_DATES_SUCCESS + ) + } + + private fun logCalendarRemoveDatesSuccess() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_REMOVE_DATES_SUCCESS, + CourseAnalyticValue.DATES_CALENDAR_REMOVE_DATES_SUCCESS + ) + } + + private fun logCalendarUpdateDatesSuccess() { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_UPDATE_DATES_SUCCESS, + CourseAnalyticValue.DATES_CALENDAR_UPDATE_DATES_SUCCESS + ) + } + + private fun logCalendarSyncEvent( + event: CourseAnalyticEvent, + value: CourseAnalyticValue, + param: Map = emptyMap(), + ) { + courseAnalytics.logEvent( + event = event.event, + params = buildMap { + put(CourseAnalyticKey.NAME.key, value.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.ENROLLMENT_MODE.key, enrollmentMode) + put(CourseAnalyticKey.PACING.key, isSelfPaced) + putAll(param) + } + ) } } 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 39a342634..b8fb95ce4 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 @@ -86,6 +86,7 @@ import org.openedx.core.domain.model.CourseDatesResult import org.openedx.core.domain.model.DatesSection import org.openedx.core.extension.isNotEmptyThenLet import org.openedx.core.presentation.course.CourseViewMode +import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.WindowSize @@ -110,11 +111,12 @@ import org.openedx.core.R as coreR class CourseDatesFragment : Fragment() { - private val viewModel by viewModel { + val viewModel by viewModel { parametersOf( requireArguments().getString(ARG_COURSE_ID, ""), requireArguments().getString(ARG_COURSE_NAME, ""), requireArguments().getBoolean(ARG_IS_SELF_PACED, true), + requireArguments().getString(ARG_ENROLLMENT_MODE, "") ) } private val router by inject() @@ -153,11 +155,12 @@ class CourseDatesFragment : Fragment() { onSwipeRefresh = { viewModel.getCourseDates(swipeToRefresh = true) }, - onItemClick = { blockId -> - if (blockId.isNotEmpty()) { - viewModel.getVerticalBlock(blockId)?.let { verticalBlock -> + onItemClick = { block -> + if (block.blockId.isNotEmpty()) { + viewModel.getVerticalBlock(block.blockId)?.let { verticalBlock -> viewModel.getSequentialBlock(verticalBlock.id) ?.let { sequentialBlock -> + viewModel.logCourseComponentTapped(true, block) router.navigateToCourseSubsections( fm = requireActivity().supportFragmentManager, subSectionId = sequentialBlock.id, @@ -165,12 +168,30 @@ class CourseDatesFragment : Fragment() { unitId = verticalBlock.id, mode = CourseViewMode.FULL ) - } + } ?: { + viewModel.logCourseComponentTapped(false, block) + ActionDialogFragment.newInstance( + title = getString(coreR.string.core_leaving_the_app), + message = getString( + coreR.string.core_leaving_the_app_message, + getString(coreR.string.platform_name) + ), + url = block.link, + ).show( + requireActivity().supportFragmentManager, + ActionDialogFragment::class.simpleName + ) + } } } }, + onPLSBannerViewed = { + viewModel.logPlsBannerViewed() + }, onSyncDates = { + viewModel.logPlsShiftButtonClicked() viewModel.resetCourseDatesBanner { + viewModel.logPlsShiftDates(it) if (it) { (parentFragment as CourseContainerFragment) .updateCourseStructure(false) @@ -178,6 +199,7 @@ class CourseDatesFragment : Fragment() { } }, onCalendarSyncSwitch = { isChecked -> + viewModel.logCalendarSyncToggle(isChecked) viewModel.handleCalendarSyncState(isChecked) }, ) @@ -193,11 +215,13 @@ class CourseDatesFragment : Fragment() { private const val ARG_COURSE_ID = "courseId" private const val ARG_COURSE_NAME = "courseName" private const val ARG_IS_SELF_PACED = "selfPaced" + private const val ARG_ENROLLMENT_MODE = "enrollmentMode" fun newInstance( courseId: String, courseName: String, isSelfPaced: Boolean, + enrollmentMode: String, ): CourseDatesFragment { val fragment = CourseDatesFragment() fragment.arguments = @@ -205,6 +229,7 @@ class CourseDatesFragment : Fragment() { ARG_COURSE_ID to courseId, ARG_COURSE_NAME to courseName, ARG_IS_SELF_PACED to isSelfPaced, + ARG_ENROLLMENT_MODE to enrollmentMode, ) return fragment } @@ -223,7 +248,8 @@ internal fun CourseDatesScreen( calendarSyncUIState: CalendarSyncUIState, onReloadClick: () -> Unit, onSwipeRefresh: () -> Unit, - onItemClick: (String) -> Unit, + onItemClick: (CourseDateBlock) -> Unit, + onPLSBannerViewed: () -> Unit, onSyncDates: () -> Unit, onCalendarSyncSwitch: (Boolean) -> Unit = {}, ) { @@ -307,6 +333,7 @@ internal fun CourseDatesScreen( if (courseBanner.isBannerAvailableForUserType(isSelfPaced)) { item { + onPLSBannerViewed() if (windowSize.isTablet) { CourseDatesBannerTablet( modifier = Modifier.padding(top = 16.dp), @@ -455,7 +482,7 @@ fun CalendarSyncCard( fun ExpandableView( sectionKey: DatesSection = DatesSection.NONE, sectionDates: List, - onItemClick: (String) -> Unit, + onItemClick: (CourseDateBlock) -> Unit, ) { var expanded by remember { mutableStateOf(false) } // expandable view Animation @@ -546,7 +573,7 @@ fun ExpandableView( private fun CourseDateBlockSection( sectionKey: DatesSection = DatesSection.NONE, sectionDates: List, - onItemClick: (String) -> Unit, + onItemClick: (CourseDateBlock) -> Unit, ) { Column(modifier = Modifier.padding(start = 8.dp)) { if (sectionKey != DatesSection.COMPLETED) { @@ -599,7 +626,7 @@ private fun DateBullet( @Composable private fun DateBlock( dateBlocks: List, - onItemClick: (String) -> Unit, + onItemClick: (CourseDateBlock) -> Unit, ) { Column( modifier = Modifier @@ -624,7 +651,7 @@ private fun CourseDateItem( dateBlock: CourseDateBlock, canShowDate: Boolean, isMiddleChild: Boolean, - onItemClick: (String) -> Unit + onItemClick: (CourseDateBlock) -> Unit ) { Column( modifier = Modifier @@ -652,7 +679,7 @@ private fun CourseDateItem( .fillMaxWidth() .padding(end = 4.dp) .clickable(enabled = dateBlock.blockId.isNotEmpty() && dateBlock.learnerHasAccess, - onClick = { onItemClick(dateBlock.blockId) }) + onClick = { onItemClick(dateBlock) }) ) { dateBlock.dateType.drawableResId?.let { icon -> Icon( @@ -720,6 +747,7 @@ private fun CourseDatesScreenPreview() { onReloadClick = {}, onSwipeRefresh = {}, onItemClick = {}, + onPLSBannerViewed = {}, onSyncDates = {}, onCalendarSyncSwitch = {}, ) @@ -742,6 +770,7 @@ private fun CourseDatesScreenTabletPreview() { onReloadClick = {}, onSwipeRefresh = {}, 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 2380dbab4..e1148b2e2 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 @@ -13,6 +13,8 @@ import org.openedx.core.SingleEventLiveData import org.openedx.core.UIMessage 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 import org.openedx.core.extension.getSequentialBlocks import org.openedx.core.extension.getVerticalBlocks import org.openedx.core.extension.isInternetError @@ -22,6 +24,10 @@ import org.openedx.core.system.notifier.CalendarSyncEvent.CheckCalendarSyncEvent 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.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue +import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.calendarsync.CalendarManager import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.presentation.calendarsync.CalendarSyncUIState @@ -29,14 +35,16 @@ import org.openedx.core.R as CoreR class CourseDatesViewModel( val courseId: String, - var courseName: String, + val courseName: String, val isSelfPaced: Boolean, + private val enrollmentMode: String, private val notifier: CourseNotifier, private val interactor: CourseInteractor, private val calendarManager: CalendarManager, private val networkConnection: NetworkConnection, private val resourceManager: ResourceManager, private val corePreferences: CorePreferences, + private val courseAnalytics: CourseAnalytics, ) : BaseViewModel() { private val _uiState = MutableLiveData(DatesUIState.Loading) @@ -64,6 +72,8 @@ class CourseDatesViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + private var courseBannerType: CourseBannerType = CourseBannerType.BLANK + init { getCourseDates() viewModelScope.launch { @@ -91,6 +101,7 @@ class CourseDatesViewModel( _uiState.value = DatesUIState.Empty } else { _uiState.value = DatesUIState.Dates(datesResponse) + courseBannerType = datesResponse.courseBanner.bannerType checkIfCalendarOutOfDate() } } catch (e: Exception) { @@ -197,4 +208,98 @@ class CourseDatesViewModel( return calendarSync.isEnabled && ((calendarSync.isSelfPacedEnabled && isSelfPaced) || (calendarSync.isInstructorPacedEnabled && !isSelfPaced)) } + + fun logPlsBannerViewed() { + logPLSBannerEvent( + CourseAnalyticEvent.PLS_BANNER_VIEWED, + CourseAnalyticValue.PLS_BANNER_VIEWED + ) + } + + fun logPlsShiftButtonClicked() { + logPLSBannerEvent( + CourseAnalyticEvent.PLS_SHIFT_BUTTON_TAPPED, + CourseAnalyticValue.PLS_SHIFT_BUTTON_TAPPED + ) + } + + fun logPlsShiftDates(isSuccess: Boolean) { + logPLSBannerEvent( + CourseAnalyticEvent.PLS_SHIFT_DATES, + CourseAnalyticValue.PLS_SHIFT_DATES, + isSuccess + ) + } + + fun logCourseComponentTapped(isSupported: Boolean, block: CourseDateBlock) { + val params = buildMap { + put(CourseAnalyticKey.BLOCK_ID.key, block.blockId) + put(CourseAnalyticKey.BLOCK_TYPE.key, block.dateType) + put(CourseAnalyticKey.LINK.key, block.link) + } + + if (isSupported) { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_COURSE_COMPONENT_TAPPED, + CourseAnalyticValue.DATES_COURSE_COMPONENT_TAPPED, + params + ) + } else { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_UNSUPPORTED_COMPONENT_TAPPED, + CourseAnalyticValue.DATES_UNSUPPORTED_COMPONENT_TAPPED, + params + ) + } + } + + fun logCalendarSyncToggle(isChecked: Boolean) { + if (isChecked) { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_TOGGLE_ON, + CourseAnalyticValue.DATES_CALENDAR_TOGGLE_ON + ) + } else { + logCalendarSyncEvent( + CourseAnalyticEvent.DATES_CALENDAR_TOGGLE_OFF, + CourseAnalyticValue.DATES_CALENDAR_TOGGLE_OFF + ) + } + } + + private fun logCalendarSyncEvent( + event: CourseAnalyticEvent, + value: CourseAnalyticValue, + param: Map = emptyMap(), + ) { + courseAnalytics.logEvent( + event = event.event, + params = buildMap { + put(CourseAnalyticKey.NAME.key, value.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.ENROLLMENT_MODE.key, enrollmentMode) + put(CourseAnalyticKey.PACING.key, isSelfPaced) + putAll(param) + } + ) + } + + private fun logPLSBannerEvent( + event: CourseAnalyticEvent, + value: CourseAnalyticValue, + isSuccess: Boolean? = null, + ) { + courseAnalytics.logEvent( + event = event.event, + params = buildMap { + put(CourseAnalyticKey.NAME.key, value.biValue) + put(CourseAnalyticKey.CATEGORY.key, CourseAnalyticValue.COURSE_DATES) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.ENROLLMENT_MODE.key, enrollmentMode) + put(CourseAnalyticKey.BANNER_TYPE.key, courseBannerType.name) + put(CourseAnalyticKey.SCREEN_NAME.key, CourseAnalyticValue.COURSE_DATES) + isSuccess?.let { put(CourseAnalyticKey.SUCCESS.key, it) } + } + ) + } } diff --git a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt index 8b031fc4f..b2c3d682f 100644 --- a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsFragment.kt @@ -111,12 +111,12 @@ class CourseDetailsFragment : Fragment() { currentState.course.isEnrolled -> { viewModel.viewCourseClickedEvent( currentState.course.courseId, - currentState.course.name ) router.navigateToCourseOutline( requireActivity().supportFragmentManager, currentState.course.courseId, - currentState.course.name + currentState.course.name, + "", ) } diff --git a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsViewModel.kt index 8a217de14..fbf85e67c 100644 --- a/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/detail/CourseDetailsViewModel.kt @@ -17,6 +17,9 @@ import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue import org.openedx.course.presentation.CourseAnalytics class CourseDetailsViewModel( @@ -27,7 +30,7 @@ class CourseDetailsViewModel( private val interactor: CourseInteractor, private val resourceManager: ResourceManager, private val notifier: CourseNotifier, - private val analytics: CourseAnalytics + private val analytics: CourseAnalytics, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() val isUserLoggedIn get() = corePreferences.user != null @@ -83,13 +86,13 @@ class CourseDetailsViewModel( try { val courseData = _uiState.value if (courseData is CourseDetailsUIState.CourseData) { - courseEnrollClickedEvent(id, courseData.course.name) + courseEnrollClickedEvent(id) } interactor.enrollInACourse(id) val course = interactor.getCourseDetails(id) if (courseData is CourseDetailsUIState.CourseData) { _uiState.value = courseData.copy(course = course) - courseEnrollSuccessEvent(id, course.name) + courseEnrollSuccessEvent(id) notifier.send(CourseDashboardUpdate()) } } catch (e: Exception) { @@ -127,15 +130,33 @@ class CourseDetailsViewModel( return java.lang.Long.toHexString(color.toLong()).substring(2, 8) } - private fun courseEnrollClickedEvent(courseId: String, courseName: String) { - analytics.courseEnrollClickedEvent(courseId, courseName) + private fun courseEnrollClickedEvent(courseId: String) { + analytics.logEvent( + CourseAnalyticEvent.COURSE_ENROLL_CLICKED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.COURSE_ENROLL_CLICKED.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + } + ) } - private fun courseEnrollSuccessEvent(courseId: String, courseName: String) { - analytics.courseEnrollSuccessEvent(courseId, courseName) + private fun courseEnrollSuccessEvent(courseId: String) { + analytics.logEvent( + CourseAnalyticEvent.COURSE_ENROLL_SUCCESS.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.COURSE_ENROLL_SUCCESS.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + } + ) } - fun viewCourseClickedEvent(courseId: String, courseName: String) { - analytics.viewCourseClickedEvent(courseId, courseName) + fun viewCourseClickedEvent(courseId: String) { + analytics.logEvent( + CourseAnalyticEvent.COURSE_INFO.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + } + ) } } diff --git a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt index 117e8130a..39fdd7c07 100644 --- a/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/handouts/HandoutsViewModel.kt @@ -9,12 +9,17 @@ import org.openedx.core.config.Config import org.openedx.core.domain.model.AnnouncementModel import org.openedx.core.domain.model.HandoutsModel import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue +import org.openedx.course.presentation.CourseAnalytics class HandoutsViewModel( private val courseId: String, private val config: Config, private val handoutsType: String, - private val interactor: CourseInteractor + private val interactor: CourseInteractor, + private val courseAnalytics: CourseAnalytics, ) : BaseViewModel() { val apiHostUrl get() = config.getApiHostURL() @@ -25,6 +30,11 @@ class HandoutsViewModel( init { getEnrolledCourse() + if (HandoutsType.valueOf(handoutsType) == HandoutsType.Handouts) { + logEvent(CourseAnalyticEvent.HANDOUTS) + } else { + logEvent(CourseAnalyticEvent.ANNOUNCEMENTS) + } } private fun getEnrolledCourse() { @@ -93,5 +103,13 @@ class HandoutsViewModel( return java.lang.Long.toHexString(color.toLong()).substring(2, 8) } - -} \ No newline at end of file + private fun logEvent(event: CourseAnalyticEvent) { + courseAnalytics.logEvent( + event = event.event, + params = buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + } + ) + } +} diff --git a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt index f2da38168..3fc15c105 100644 --- a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt @@ -108,7 +108,8 @@ class CourseInfoViewModel( router.navigateToCourseOutline( fm = fragmentManager, courseId = courseId, - courseTitle = "" + courseTitle = "", + enrollmentMode = "" ) } } diff --git a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineFragment.kt b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineFragment.kt index f00055fa9..9b487050b 100644 --- a/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/outline/CourseOutlineFragment.kt @@ -43,6 +43,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AndroidUriHandler import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource @@ -66,6 +67,7 @@ import org.openedx.core.domain.model.BlockCounts import org.openedx.core.domain.model.CourseDatesBannerInfo import org.openedx.core.domain.model.CourseStructure import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog @@ -155,7 +157,7 @@ class CourseOutlineFragment : Fragment() { }, onSubSectionClick = { subSectionBlock -> viewModel.courseSubSectionUnit[subSectionBlock.id]?.let { unit -> - viewModel.verticalClickedEvent(unit.blockId, unit.displayName) + viewModel.logUnitDetailViewedEvent(unit.blockId) router.navigateToCourseContainer( requireActivity().supportFragmentManager, courseId = viewModel.courseId, @@ -202,6 +204,10 @@ class CourseOutlineFragment : Fragment() { (parentFragment as CourseContainerFragment).updateCourseDates() showDatesUpdateSnackbar(it) }) + }, + onCertificateClick = { + viewModel.viewCertificateTappedEvent() + it.takeIfNotEmpty()?.let { AndroidUriHandler(requireContext()).openUri(it) } } ) } @@ -237,7 +243,7 @@ class CourseOutlineFragment : Fragment() { private const val ARG_TITLE = "title" fun newInstance( courseId: String, - title: String + title: String, ): CourseOutlineFragment { val fragment = CourseOutlineFragment() fragment.arguments = bundleOf( @@ -278,6 +284,7 @@ internal fun CourseOutlineScreen( onResumeClick: (String) -> Unit, onDownloadClick: (Block) -> Unit, onResetDatesClick: () -> Unit, + onCertificateClick: (String) -> Unit, ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = @@ -360,6 +367,7 @@ internal fun CourseOutlineScreen( courseImage = uiState.courseStructure.media?.image?.large ?: "", courseCertificate = uiState.courseStructure.certificate, + onCertificateClick = onCertificateClick, courseName = uiState.courseStructure.name ) } @@ -651,6 +659,7 @@ private fun CourseOutlineScreenPreview() { onReloadClick = {}, onDownloadClick = {}, onResetDatesClick = {}, + onCertificateClick = {}, ) } } @@ -691,6 +700,7 @@ private fun CourseOutlineScreenTabletPreview() { onReloadClick = {}, onDownloadClick = {}, onResetDatesClick = {}, + onCertificateClick = {}, ) } } 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 cf83fd041..8aef50d46 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 @@ -28,6 +28,9 @@ import org.openedx.core.system.notifier.CalendarSyncEvent.CreateCalendarSyncEven import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.calendarsync.CalendarSyncDialogType import org.openedx.course.R as courseR @@ -42,7 +45,7 @@ class CourseOutlineViewModel( private val preferencesManager: CorePreferences, private val analytics: CourseAnalytics, downloadDao: DownloadDao, - workerController: DownloadWorkerController + workerController: DownloadWorkerController, ) : BaseDownloadViewModel(downloadDao, preferencesManager, workerController) { val apiHostUrl get() = config.getApiHostURL() @@ -253,7 +256,7 @@ class CourseOutlineViewModel( private fun getResumeBlock( blocks: List, - continueBlockId: String + continueBlockId: String, ): Block? { val resumeBlock = blocks.firstOrNull { it.id == continueBlockId } resumeVerticalBlock = @@ -282,10 +285,26 @@ class CourseOutlineViewModel( } } + fun viewCertificateTappedEvent() { + analytics.logEvent( + CourseAnalyticEvent.VIEW_CERTIFICATE.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.VIEW_CERTIFICATE.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + }) + } + fun resumeCourseTappedEvent(blockId: String) { val currentState = uiState.value if (currentState is CourseOutlineUIState.CourseData) { - analytics.resumeCourseTappedEvent(courseId, currentState.courseStructure.name, blockId) + analytics.logEvent( + CourseAnalyticEvent.RESUME_COURSE_CLICKED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.RESUME_COURSE_CLICKED.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.BLOCK_ID.key, blockId) + put(CourseAnalyticKey.CATEGORY.key, CourseAnalyticKey.NAVIGATION.key) + }) } } @@ -301,10 +320,17 @@ class CourseOutlineViewModel( } } - fun verticalClickedEvent(blockId: String, blockName: String) { + fun logUnitDetailViewedEvent(blockId: String) { val currentState = uiState.value if (currentState is CourseOutlineUIState.CourseData) { - analytics.verticalClickedEvent(courseId, courseTitle, blockId, blockName) + analytics.logEvent( + CourseAnalyticEvent.UNIT_DETAIL.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.BLOCK_ID.key, blockId) + put(CourseAnalyticKey.CATEGORY.key, CourseAnalyticKey.NAVIGATION.key) + }) } } diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt index 882bafc9b..297545117 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionFragment.kt @@ -86,7 +86,7 @@ class CourseSectionFragment : Fragment() { }, onItemClick = { block -> if (block.descendants.isNotEmpty()) { - viewModel.verticalClickedEvent(block.blockId, block.displayName) + viewModel.verticalClickedEvent(block.blockId) router.navigateToCourseContainer( fm = requireActivity().supportFragmentManager, courseId = viewModel.courseId, diff --git a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt index aaa6e99d0..5100260ac 100644 --- a/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/section/CourseSectionViewModel.kt @@ -22,6 +22,9 @@ import org.openedx.core.system.notifier.CourseSectionChanged import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import kotlinx.coroutines.launch +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue class CourseSectionViewModel( private val interactor: CourseInteractor, @@ -141,10 +144,17 @@ class CourseSectionViewModel( } } - fun verticalClickedEvent(blockId: String, blockName: String) { + fun verticalClickedEvent(blockId: String) { val currentState = uiState.value if (currentState is CourseSectionUIState.Blocks) { - analytics.verticalClickedEvent(courseId, currentState.courseName, blockId, blockName) + analytics.logEvent( + CourseAnalyticEvent.UNIT_DETAIL.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.BLOCK_ID.key, blockId) + put(CourseAnalyticKey.CATEGORY.key, CourseAnalyticKey.NAVIGATION.key) + }) } } } \ No newline at end of file diff --git a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt index 051466d25..404d474d2 100644 --- a/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt +++ b/course/src/main/java/org/openedx/course/presentation/ui/CourseUI.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -118,7 +117,8 @@ fun CourseImageHeader( apiHostUrl: String, courseImage: String?, courseCertificate: Certificate?, - courseName: String + onCertificateClick: (String) -> Unit = {}, + courseName: String, ) { val configuration = LocalConfiguration.current val windowSize = rememberWindowSize() @@ -128,7 +128,6 @@ fun CourseImageHeader( } else { ContentScale.Crop } - val uriHandler = LocalUriHandler.current val imageUrl = if (courseImage?.isLinkValid() == true) { courseImage } else { @@ -186,8 +185,8 @@ fun CourseImageHeader( textColor = MaterialTheme.appColors.buttonText, text = stringResource(id = R.string.course_view_certificate), onClick = { - courseCertificate.certificateURL?.let { - uriHandler.openUri(it) + courseCertificate?.certificateURL?.let { + onCertificateClick(it) } }) } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt index a4d6de5bb..9f868651e 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerFragment.kt @@ -57,12 +57,14 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta private var _binding: FragmentCourseUnitContainerBinding? = null private val viewModel by viewModel { - parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) + parametersOf( + requireArguments().getString(ARG_COURSE_ID, ""), + requireArguments().getString(UNIT_ID, "") + ) } private val router by inject() - private var unitId: String = "" private var componentId: String = "" private lateinit var adapter: CourseUnitContainerAdapter @@ -132,10 +134,10 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) - unitId = requireArguments().getString(UNIT_ID, "") componentId = requireArguments().getString(ARG_COMPONENT_ID, "") viewModel.loadBlocks(requireArguments().serializable(ARG_MODE)!!) - viewModel.setupCurrentIndex(unitId, componentId) + viewModel.setupCurrentIndex(componentId) + viewModel.courseUnitContainerShowedEvent() } override fun onCreateView( @@ -244,7 +246,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta if (viewModel.isCourseExpandableSectionsEnabled) { binding.subSectionUnitsTitle.setContent { val unitBlocks by viewModel.subSectionUnitBlocks.collectAsState() - val currentUnit = unitBlocks.firstOrNull { it.id == unitId } + val currentUnit = unitBlocks.firstOrNull { it.id == viewModel.unitId } val unitName = currentUnit?.displayName ?: "" val unitsListShowed by viewModel.unitsListShowed.observeAsState(false) @@ -262,7 +264,7 @@ class CourseUnitContainerFragment : Fragment(R.layout.fragment_course_unit_conta binding.subSectionUnitsList.setContent { val unitBlocks by viewModel.subSectionUnitBlocks.collectAsState() - val selectedUnitIndex = unitBlocks.indexOfFirst { it.id == unitId } + val selectedUnitIndex = unitBlocks.indexOfFirst { it.id == viewModel.unitId } OpenEdXTheme { SubSectionUnitsList( unitBlocks = unitBlocks, diff --git a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt index 0b6df71d4..3cab097e9 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/container/CourseUnitContainerViewModel.kt @@ -22,6 +22,9 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseSectionChanged import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue import org.openedx.course.presentation.CourseAnalytics class CourseUnitContainerViewModel( @@ -29,7 +32,8 @@ class CourseUnitContainerViewModel( private val interactor: CourseInteractor, private val notifier: CourseNotifier, private val analytics: CourseAnalytics, - val courseId: String + val courseId: String, + val unitId: String ) : BaseViewModel() { private val blocks = ArrayList() @@ -110,7 +114,7 @@ class CourseUnitContainerViewModel( } } - fun setupCurrentIndex(unitId: String, componentId: String = "") { + fun setupCurrentIndex(componentId: String = "") { if (currentSectionIndex != -1) { return } @@ -248,6 +252,17 @@ class CourseUnitContainerViewModel( return blocks.first { it.descendants.contains(unitId) } } + fun courseUnitContainerShowedEvent() { + analytics.logEvent( + CourseAnalyticEvent.UNIT_DETAIL.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.SCREEN_NAVIGATION.biValue) + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.BLOCK_ID.key, unitId) + put(CourseAnalyticKey.CATEGORY.key, CourseAnalyticKey.NAVIGATION.key) + }) + } + fun nextBlockClickedEvent(blockId: String, blockName: String) { analytics.nextBlockClickedEvent(courseId, courseName, blockId, blockName) } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt new file mode 100644 index 000000000..bf3ef97d9 --- /dev/null +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/BaseVideoViewModel.kt @@ -0,0 +1,114 @@ +package org.openedx.course.presentation.unit.video + +import org.openedx.core.BaseViewModel +import org.openedx.course.presentation.CourseAnalyticEvent +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalyticValue +import org.openedx.course.presentation.CourseAnalytics + +open class BaseVideoViewModel( + private val courseId: String, + private val courseAnalytics: CourseAnalytics, +) : BaseViewModel() { + + fun logVideoSpeedEvent(videoUrl: String, speed: Float, currentVideoTime: Long, medium: String) { + logVideoEvent( + CourseAnalyticEvent.VIDEO_CHANGE_SPEED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.VIDEO_CHANGE_SPEED.biValue) + put(CourseAnalyticKey.OPEN_IN_BROWSER.key, videoUrl) + put(CourseAnalyticKey.SPEED.key, speed) + put(CourseAnalyticKey.CURRENT_TIME.key, currentVideoTime) + put(CourseAnalyticKey.PLAY_MEDIUM.key, medium) + } + ) + } + + fun logVideoSeekEvent( + videoUrl: String, + duration: Long, + currentVideoTime: Long, + medium: String, + ) { + logVideoEvent( + CourseAnalyticEvent.VIDEO_SEEKED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.VIDEO_SEEKED.biValue) + put(CourseAnalyticKey.OPEN_IN_BROWSER.key, videoUrl) + put(CourseAnalyticKey.SKIP_INTERVAL.key, duration) + put(CourseAnalyticKey.CURRENT_TIME.key, currentVideoTime) + put(CourseAnalyticKey.PLAY_MEDIUM.key, medium) + } + ) + } + + fun logLoadedCompletedEvent( + videoUrl: String, + isLoaded: Boolean, + currentVideoTime: Long, + medium: String, + ) { + logVideoEvent( + if (isLoaded) CourseAnalyticEvent.VIDEO_LOADED.event else CourseAnalyticEvent.VIDEO_COMPLETED.event, + buildMap { + put( + CourseAnalyticKey.NAME.key, + if (isLoaded) CourseAnalyticValue.VIDEO_LOADED.biValue + else CourseAnalyticValue.VIDEO_COMPLETED.biValue + ) + put(CourseAnalyticKey.OPEN_IN_BROWSER.key, videoUrl) + put(CourseAnalyticKey.CURRENT_TIME.key, currentVideoTime) + put(CourseAnalyticKey.PLAY_MEDIUM.key, medium) + } + ) + } + + fun logPlayPauseEvent( + videoUrl: String, + isPlaying: Boolean, + currentVideoTime: Long, + medium: String, + ) { + logVideoEvent( + if (isPlaying) CourseAnalyticEvent.VIDEO_PLAYED.event else CourseAnalyticEvent.VIDEO_PAUSED.event, + buildMap { + put( + CourseAnalyticKey.NAME.key, + if (isPlaying) CourseAnalyticValue.VIDEO_PLAYED.biValue + else CourseAnalyticValue.VIDEO_PAUSED.biValue + ) + put(CourseAnalyticKey.OPEN_IN_BROWSER.key, videoUrl) + put(CourseAnalyticKey.CURRENT_TIME.key, currentVideoTime) + put(CourseAnalyticKey.PLAY_MEDIUM.key, medium) + } + ) + } + + private fun logVideoEvent(event: String, params: Map) { + courseAnalytics.logEvent(event, buildMap { + put(CourseAnalyticKey.COURSE_ID.key, courseId) + put(CourseAnalyticKey.COMPONENT.key, CourseAnalyticKey.VIDEO_PLAYER.key) + putAll(params) + }) + } + + fun logCastConnection(isConnected: Boolean) { + if (isConnected) { + courseAnalytics.logEvent( + event = CourseAnalyticEvent.CAST_CONNECTED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.CAST_CONNECTED.biValue) + put(CourseAnalyticKey.PLAY_MEDIUM.key, CourseAnalyticValue.GOOGLE_CAST.biValue) + } + ) + } else { + courseAnalytics.logEvent( + event = CourseAnalyticEvent.CAST_DISCONNECTED.event, + buildMap { + put(CourseAnalyticKey.NAME.key, CourseAnalyticValue.CAST_DISCONNECTED.biValue) + put(CourseAnalyticKey.PLAY_MEDIUM.key, CourseAnalyticValue.GOOGLE_CAST.biValue) + } + ) + } + } +} diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt index 1ff0df22e..b03977d2b 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/EncodedVideoUnitViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.media3.cast.CastPlayer +import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.Clock import androidx.media3.exoplayer.DefaultLoadControl @@ -23,23 +24,27 @@ import org.openedx.core.module.TranscriptManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseNotifier import org.openedx.course.data.repository.CourseRepository +import org.openedx.course.presentation.CourseAnalyticKey +import org.openedx.course.presentation.CourseAnalytics import java.util.concurrent.Executors class EncodedVideoUnitViewModel( courseId: String, val blockId: String, - courseRepository: CourseRepository, - notifier: CourseNotifier, - networkConnection: NetworkConnection, - transcriptManager: TranscriptManager, - val preferencesManager: CorePreferences, private val context: Context, + private val courseRepository: CourseRepository, + private val notifier: CourseNotifier, + private val networkConnection: NetworkConnection, + private val transcriptManager: TranscriptManager, + private val preferencesManager: CorePreferences, + private val courseAnalytics: CourseAnalytics, ) : VideoUnitViewModel( courseId, courseRepository, notifier, networkConnection, - transcriptManager + transcriptManager, + courseAnalytics ) { private val _isVideoEnded = MutableLiveData(false) @@ -66,8 +71,24 @@ class EncodedVideoUnitViewModel( super.onPlaybackStateChanged(playbackState) if (playbackState == Player.STATE_ENDED) { _isVideoEnded.value = true - markBlockCompleted(blockId) + markBlockCompleted(blockId, CourseAnalyticKey.NATIVE.key) } + + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + logPlayPauseEvent(videoUrl, isPlaying, getCurrentVideoTime(), getPlayingMedium()) + } + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + super.onPlaybackParametersChanged(playbackParameters) + logVideoSpeedEvent( + videoUrl, + playbackParameters.speed, + getCurrentVideoTime(), + getPlayingMedium() + ) } } @@ -139,7 +160,23 @@ class EncodedVideoUnitViewModel( DefaultBandwidthMeter.getSingletonInstance(context), DefaultAnalyticsCollector(Clock.DEFAULT) ).build() + logLoadedCompletedEvent(videoUrl, true, getCurrentVideoTime(), getPlayingMedium()) } private fun getVideoQuality() = preferencesManager.videoSettings.videoStreamingQuality + + override fun markBlockCompleted(blockId: String, medium: String) { + super.markBlockCompleted( + blockId, + getPlayingMedium() + ) + } + + private fun getPlayingMedium(): String { + return if (getActivePlayer() == castPlayer) { + CourseAnalyticKey.GOOGLE_CAST.key + } else { + CourseAnalyticKey.NATIVE.key + } + } } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt index 78133286e..14ee8246f 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoFullScreenFragment.kt @@ -9,13 +9,13 @@ import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.media3.common.C import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.util.Clock import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer -import org.koin.android.ext.android.inject import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -23,6 +23,7 @@ import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter import androidx.media3.extractor.DefaultExtractorsFactory +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.domain.model.VideoQuality @@ -31,6 +32,7 @@ import org.openedx.core.presentation.dialog.appreview.AppReviewManager import org.openedx.core.presentation.global.viewBinding import org.openedx.course.R import org.openedx.course.databinding.FragmentVideoFullScreenBinding +import org.openedx.course.presentation.CourseAnalyticKey class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { @@ -54,7 +56,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { if (!appReviewManager.isDialogShowed) { appReviewManager.tryToOpenRateDialog() } - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(blockId, CourseAnalyticKey.NATIVE.key) } } } @@ -130,12 +132,32 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { } exoPlayer?.addListener(object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + viewModel.logPlayPauseEvent( + viewModel.videoUrl, + isPlaying, + viewModel.currentVideoTime, + CourseAnalyticKey.NATIVE.key + ) + } + override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) if (playbackState == Player.STATE_ENDED) { - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(blockId, CourseAnalyticKey.NATIVE.key) } } + + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + super.onPlaybackParametersChanged(playbackParameters) + viewModel.logVideoSpeedEvent( + viewModel.videoUrl, + playbackParameters.speed, + viewModel.currentVideoTime, + CourseAnalyticKey.NATIVE.key + ) + } }) } } @@ -144,7 +166,8 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { private fun setPlayerMedia(mediaItem: MediaItem) { if (viewModel.videoUrl.endsWith(".m3u8")) { val factory = DefaultDataSource.Factory(requireContext()) - val mediaSource: HlsMediaSource = HlsMediaSource.Factory(factory).createMediaSource(mediaItem) + val mediaSource: HlsMediaSource = + HlsMediaSource.Factory(factory).createMediaSource(mediaItem) exoPlayer?.setMediaSource(mediaSource, viewModel.currentVideoTime) } else { exoPlayer?.setMediaItem( @@ -196,7 +219,7 @@ class VideoFullScreenFragment : Fragment(R.layout.fragment_video_full_screen) { videoTime: Long, blockId: String, courseId: String, - isPlaying: Boolean + isPlaying: Boolean, ): VideoFullScreenFragment { val fragment = VideoFullScreenFragment() fragment.arguments = bundleOf( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt index 4d231471c..566500140 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt @@ -41,6 +41,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.utils.LocaleUtils import org.openedx.course.R import org.openedx.course.databinding.FragmentVideoUnitBinding +import org.openedx.course.presentation.CourseAnalyticKey import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.VideoSubtitles import org.openedx.course.presentation.ui.VideoTitle @@ -69,7 +70,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } val completePercentage = it.currentPosition.toDouble() / it.duration.toDouble() if (completePercentage >= 0.8f) { - viewModel.markBlockCompleted(viewModel.blockId) + viewModel.markBlockCompleted(viewModel.blockId, CourseAnalyticKey.NATIVE.key) } } handler.postDelayed(this, 200) @@ -89,6 +90,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { viewModel.isDownloaded = getBoolean(ARG_DOWNLOADED) } viewModel.downloadSubtitles() + handler.removeCallbacks(videoTimeRunnable) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -174,8 +176,11 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } viewModel.isVideoEnded.observe(viewLifecycleOwner) { isVideoEnded -> - if (isVideoEnded && !appReviewManager.isDialogShowed) { - appReviewManager.tryToOpenRateDialog() + if (isVideoEnded) { + handler.removeCallbacks(videoTimeRunnable) + if (appReviewManager.isDialogShowed) { + appReviewManager.tryToOpenRateDialog() + } } } } @@ -206,6 +211,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { viewModel.castPlayer?.setSessionAvailabilityListener( object : SessionAvailabilityListener { override fun onCastSessionAvailable() { + viewModel.logCastConnection(true) viewModel.isCastActive = true viewModel.exoPlayer?.pause() playerView.player = viewModel.castPlayer @@ -218,6 +224,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { } override fun onCastSessionUnavailable() { + viewModel.logCastConnection(false) viewModel.isCastActive = false playerView.player = viewModel.exoPlayer viewModel.exoPlayer?.seekTo(viewModel.castPlayer?.currentPosition ?: 0L) @@ -270,7 +277,8 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { private fun setPlayerMedia(mediaItem: MediaItem) { if (viewModel.videoUrl.endsWith(".m3u8")) { val factory = DefaultDataSource.Factory(requireContext()) - val mediaSource: HlsMediaSource = HlsMediaSource.Factory(factory).createMediaSource(mediaItem) + val mediaSource: HlsMediaSource = + HlsMediaSource.Factory(factory).createMediaSource(mediaItem) viewModel.exoPlayer?.setMediaSource(mediaSource, viewModel.getCurrentVideoTime()) } else { viewModel.getActivePlayer()?.setMediaItem( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt index 1005521a0..cfd8431db 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitViewModel.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.openedx.core.AppDataConstants -import org.openedx.core.BaseViewModel import org.openedx.core.module.TranscriptManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseCompletionSet @@ -17,6 +16,7 @@ import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseSubtitleLanguageChanged import org.openedx.core.system.notifier.CourseVideoPositionChanged import org.openedx.course.data.repository.CourseRepository +import org.openedx.course.presentation.CourseAnalytics import subtitleFile.TimedTextObject open class VideoUnitViewModel( @@ -25,7 +25,8 @@ open class VideoUnitViewModel( private val notifier: CourseNotifier, private val networkConnection: NetworkConnection, private val transcriptManager: TranscriptManager, -) : BaseViewModel() { + private val courseAnalytics: CourseAnalytics, +) : BaseVideoViewModel(courseId, courseAnalytics) { var videoUrl = "" var transcripts = emptyMap() @@ -98,7 +99,8 @@ open class VideoUnitViewModel( } - fun markBlockCompleted(blockId: String) { + open fun markBlockCompleted(blockId: String, medium: String) { + logLoadedCompletedEvent(videoUrl, false, getCurrentVideoTime(), medium) if (!isBlockAlreadyCompleted) { viewModelScope.launch { try { @@ -128,5 +130,4 @@ open class VideoUnitViewModel( } fun getCurrentVideoTime() = currentVideoTime.value ?: 0 - } \ No newline at end of file diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt index c5d1430a7..3226ff159 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoViewModel.kt @@ -2,20 +2,21 @@ package org.openedx.course.presentation.unit.video import androidx.lifecycle.viewModelScope import androidx.media3.common.C -import org.openedx.core.BaseViewModel -import org.openedx.course.data.repository.CourseRepository -import org.openedx.core.system.notifier.CourseNotifier -import org.openedx.core.system.notifier.CourseVideoPositionChanged import kotlinx.coroutines.launch import org.openedx.core.data.storage.CorePreferences import org.openedx.core.system.notifier.CourseCompletionSet +import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.core.system.notifier.CourseVideoPositionChanged +import org.openedx.course.data.repository.CourseRepository +import org.openedx.course.presentation.CourseAnalytics class VideoViewModel( private val courseId: String, private val courseRepository: CourseRepository, private val notifier: CourseNotifier, - private val preferencesManager: CorePreferences -) : BaseViewModel() { + private val preferencesManager: CorePreferences, + private val courseAnalytics: CourseAnalytics, +) : BaseVideoViewModel(courseId, courseAnalytics) { var videoUrl = "" var currentVideoTime = 0L @@ -27,12 +28,19 @@ class VideoViewModel( fun sendTime() { if (currentVideoTime != C.TIME_UNSET) { viewModelScope.launch { - notifier.send(CourseVideoPositionChanged(videoUrl, currentVideoTime, isPlaying ?: false)) + notifier.send( + CourseVideoPositionChanged( + videoUrl, + currentVideoTime, + isPlaying ?: false + ) + ) } } } - fun markBlockCompleted(blockId: String) { + fun markBlockCompleted(blockId: String, medium: String) { + logLoadedCompletedEvent(videoUrl, false, currentVideoTime, medium) if (!isBlockAlreadyCompleted) { viewModelScope.launch { try { diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt index 0d6ce6d84..d7dc208b4 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoFullScreenFragment.kt @@ -21,6 +21,7 @@ import org.openedx.core.presentation.dialog.appreview.AppReviewManager import org.openedx.core.presentation.global.viewBinding import org.openedx.course.R import org.openedx.course.databinding.FragmentYoutubeVideoFullScreenBinding +import org.openedx.course.presentation.CourseAnalyticKey class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_full_screen) { @@ -74,13 +75,13 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ override fun onStateChange( youTubePlayer: YouTubePlayer, - state: PlayerConstants.PlayerState + state: PlayerConstants.PlayerState, ) { super.onStateChange(youTubePlayer, state) if (state == PlayerConstants.PlayerState.ENDED) { - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(blockId, CourseAnalyticKey.YOUTUBE.key) } - viewModel.isPlaying = when(state) { + viewModel.isPlaying = when (state) { PlayerConstants.PlayerState.PLAYING -> true PlayerConstants.PlayerState.PAUSED -> false else -> return @@ -92,7 +93,7 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ viewModel.currentVideoTime = (second * 1000f).toLong() val completePercentage = second / youtubeTrackerListener.videoDuration if (completePercentage >= 0.8f && !isMarkBlockCompletedCalled) { - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(blockId, CourseAnalyticKey.YOUTUBE.key) isMarkBlockCompletedCalled = true } if (completePercentage >= 0.99f && !appReviewManager.isDialogShowed) { @@ -105,7 +106,8 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ override fun onReady(youTubePlayer: YouTubePlayer) { super.onReady(youTubePlayer) binding.youtubePlayerView.isVisible = true - val defPlayerUiController = DefaultPlayerUiController(binding.youtubePlayerView, youTubePlayer) + val defPlayerUiController = + DefaultPlayerUiController(binding.youtubePlayerView, youTubePlayer) defPlayerUiController.setFullScreenButtonClickListener { parentFragmentManager.popBackStack() } @@ -142,7 +144,7 @@ class YoutubeVideoFullScreenFragment : Fragment(R.layout.fragment_youtube_video_ videoTime: Long, blockId: String, courseId: String, - isPlaying: Boolean + isPlaying: Boolean, ): YoutubeVideoFullScreenFragment { val fragment = YoutubeVideoFullScreenFragment() fragment.arguments = bundleOf( diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index bdf47e990..78d41cc20 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -36,6 +36,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.utils.LocaleUtils import org.openedx.course.R import org.openedx.course.databinding.FragmentYoutubeVideoUnitBinding +import org.openedx.course.presentation.CourseAnalyticKey import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.VideoSubtitles import org.openedx.course.presentation.ui.VideoTitle @@ -77,7 +78,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { _binding = FragmentYoutubeVideoUnitBinding.inflate(inflater, container, false) return binding.root @@ -153,7 +154,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) viewModel.setCurrentVideoTime((second * 1000f).toLong()) val completePercentage = second / youtubeTrackerListener.videoDuration if (completePercentage >= 0.8f && !isMarkBlockCompletedCalled) { - viewModel.markBlockCompleted(blockId) + viewModel.markBlockCompleted(blockId, CourseAnalyticKey.YOUTUBE.key) isMarkBlockCompletedCalled = true } if (completePercentage >= 0.99f && !appReviewManager.isDialogShowed) { @@ -161,13 +162,22 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } } - override fun onStateChange(youTubePlayer: YouTubePlayer, state: PlayerConstants.PlayerState) { + override fun onStateChange( + youTubePlayer: YouTubePlayer, + state: PlayerConstants.PlayerState, + ) { super.onStateChange(youTubePlayer, state) viewModel.isPlaying = when (state) { PlayerConstants.PlayerState.PLAYING -> true PlayerConstants.PlayerState.PAUSED -> false else -> return } + viewModel.logPlayPauseEvent( + viewModel.videoUrl, + viewModel.isPlaying, + viewModel.getCurrentVideoTime(), + CourseAnalyticKey.YOUTUBE.key + ) } override fun onReady(youTubePlayer: YouTubePlayer) { @@ -193,11 +203,23 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) val videoId = viewModel.videoUrl.split("watch?v=")[1] if (viewModel.isPlaying) { - youTubePlayer.loadVideo(videoId, viewModel.getCurrentVideoTime().toFloat() / 1000) + youTubePlayer.loadVideo( + videoId, + viewModel.getCurrentVideoTime().toFloat() / 1000 + ) } else { - youTubePlayer.cueVideo(videoId, viewModel.getCurrentVideoTime().toFloat() / 1000) + youTubePlayer.cueVideo( + videoId, + viewModel.getCurrentVideoTime().toFloat() / 1000 + ) } youTubePlayer.addListener(youtubeTrackerListener) + viewModel.logLoadedCompletedEvent( + viewModel.videoUrl, + true, + viewModel.getCurrentVideoTime(), + CourseAnalyticKey.YOUTUBE.key + ) } } @@ -232,7 +254,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) courseId: String, videoUrl: String, transcriptsUrl: Map, - blockTitle: String + blockTitle: String, ): YoutubeVideoUnitFragment { val fragment = YoutubeVideoUnitFragment() fragment.arguments = bundleOf( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index 83ba221e2..cca5edecd 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -7,7 +7,8 @@ interface DashboardRouter { fun navigateToCourseOutline( fm: FragmentManager, courseId: String, - courseTitle: String + courseTitle: String, + enrollmentMode: String, ) fun navigateToProgramInfo( diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt index 0582f663e..bdc282e1d 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt @@ -139,7 +139,8 @@ class DashboardFragment : Fragment() { router.navigateToCourseOutline( requireParentFragment().parentFragmentManager, it.course.id, - it.course.name + it.course.name, + it.mode ) }, onSwipeRefresh = { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt index da5453fdf..28de4a3d9 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt @@ -90,7 +90,8 @@ class ProgramViewModel( router.navigateToCourseOutline( fm = fragmentManager, courseId = courseId, - courseTitle = "" + courseTitle = "", + enrollmentMode = "" ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt index 93a5086b2..b4ffc35ec 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryAnalytics.kt @@ -4,4 +4,5 @@ interface DiscoveryAnalytics { fun discoverySearchBarClickedEvent() fun discoveryCourseSearchEvent(label: String, coursesCount: Int) fun discoveryCourseClickedEvent(courseId: String, courseName: String) + fun logEvent(event: String, params: Map) } \ No newline at end of file diff --git a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionAnalytics.kt b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionAnalytics.kt index 79b2130bd..65ecbcb89 100644 --- a/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionAnalytics.kt +++ b/discussion/src/main/java/org/openedx/discussion/presentation/DiscussionAnalytics.kt @@ -9,4 +9,34 @@ interface DiscussionAnalytics { topicId: String, topicName: String ) -} \ No newline at end of file + + fun logEvent(event: String, params: Map) +} + +enum class DiscussionAnalyticEvent(val eventName: String) { + FORUM_ADD_RESPONSE_COMMENT("Forum:Add Response Comment"), + FORUM_ADD_THREAD_RESPONSE("Forum:Add Thread Response"), + FORUM_CREATE_TOPIC_THREAD("Forum:Create Topic Thread"), + FORUM_SEARCH_THREADS("Forum:Search Threads"), + FORUM_VIEW_RESPONSE_COMMENTS("Forum:View Response Comments"), + FORUM_VIEW_THREAD("Forum:View Thread"), + FORUM_VIEW_TOPIC_THREADS("Forum:View Topic Threads"), + FORUM_VIEW_TOPICS("Forum:View Topics") +} + +enum class DiscussionAnalyticValue(val value: String) { + SCREEN_NAVIGATION("edx.bi.app.navigation.screen"), + ALL_POSTS("all_posts"), + POSTS_FOLLOWING("posts_following"), + DISCUSSION_TOPIC("discussion_topic"), +} + +enum class DiscussionAnalyticKey(val key: String) { + COURSE_ID("course_id"), + ACTION("action"), + THREAD_ID("thread_id"), + TOPIC_ID("topic_id"), + RESPONSE_ID("response_id"), + AUTHOR("author"), + SEARCH_QUERY("search_query"), +} diff --git a/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt b/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt index 66d43b607..ff3211d2e 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/ProfileAnalytics.kt @@ -1,15 +1,57 @@ package org.openedx.profile.presentation interface ProfileAnalytics { - fun profileEditClickedEvent() - fun profileEditDoneClickedEvent() - fun profileDeleteAccountClickedEvent() - fun profileVideoSettingsClickedEvent() - fun privacyPolicyClickedEvent() - fun termsOfUseClickedEvent() - fun cookiePolicyClickedEvent() - fun dataSellClickedEvent() - fun faqClickedEvent() - fun emailSupportClickedEvent() fun logoutEvent(force: Boolean) + fun logEvent(event: String, params: Map) +} + +enum class ProfileAnalyticEvent(val event: String) { + SETUP_PICTURE("Profile:Setup Picture"), + EDIT_CLICKED("Profile:Edit Clicked"), + SWITCH_PROFILE("Profile:Switch Profile"), + EDIT_DONE_CLICKED("Profile:Edit Done Clicked"), + VIDEO_SETTING_CLICKED("Profile:Video Setting Clicked"), + CONTACT_SUPPORT_CLICKED("Profile:Contact Support Clicked"), + FAQ_CLICKED("Profile:FAQ Clicked"), + TERMS_OF_USE_CLICKED("Profile:Terms of Use Clicked"), + PRIVACY_POLICY_CLICKED("Profile:Privacy Policy Clicked"), + COOKIE_POLICY_CLICKED("Profile:Cookie Policy Clicked"), + DATA_SELL_CLICKED("Profile:Data Sell Clicked"), + DELETE_ACCOUNT_CLICKED("Profile:Delete Account Clicked"), + WIFI_TOGGLE("Profile:Wifi Toggle"), + VIDEO_STREAMING_QUALITY_CLICKED("Profile:Video Streaming Quality Clicked"), + VIDEO_DOWNLOAD_QUALITY_CLICKED("Profile:Video Download Quality Clicked"), + LOGOUT_CLICKED("Profile:Logout Clicked"), + LOGGED_OUT("Profile:Logged Out"), +} + +enum class ProfileAnalyticValue(val biValue: String) { + SETUP_PICTURE("edx.bi.app.profile.setphoto"), + EDIT_CLICKED("edx.bi.app.profile.edit.clicked"), + SWITCH_PROFILE("edx.bi.app.profile.switch_profile.clicked"), + EDIT_DONE_CLICKED("edx.bi.app.profile.edit_done.clicked"), + VIDEO_SETTING_CLICKED("edx.bi.app.profile.video_setting.clicked"), + CONTACT_SUPPORT_CLICKED("edx.bi.app.profile.email_support.clicked"), + FAQ_CLICKED("edx.bi.app.profile.faq.clicked"), + TERMS_OF_USE_CLICKED("edx.bi.app.profile.terms_of_use.clicked"), + PRIVACY_POLICY_CLICKED("edx.bi.app.profile.privacy_policy.clicked"), + COOKIE_POLICY_CLICKED("edx.bi.app.profile.cookie_policy.clicked"), + DATA_SELL_CLICKED("edx.bi.app.profile.do_not_sell_data.clicked"), + DELETE_ACCOUNT_CLICKED("edx.bi.app.profile.delete_account.clicked"), + WIFI_TOGGLE("edx.bi.app.profile.wifi_toggle.action"), + VIDEO_STREAMING_QUALITY_CLICKED("edx.bi.app.profile.video_streaming_quality.clicked"), + VIDEO_DOWNLOAD_QUALITY_CLICKED("edx.bi.app.profile.video_download_quality.clicked"), + LOGOUT_CLICKED("edx.bi.app.profile.logout.clicked"), + LOGGED_OUT("edx.bi.app.user.logout"), +} + +enum class ProfileAnalyticKey(val key: String) { + NAME("name"), + CATEGORY("Category"), + PROFILE("Profile"), + ACTION("action"), + FULL_PROFILE("full_profile"), + LIMITED_PROFILE("limited_profile"), + ON("On"), + OFF("Off"), } 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 b9f4c0991..758bd49cb 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 @@ -4,17 +4,20 @@ import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.UIMessage -import org.openedx.profile.domain.model.Account import org.openedx.core.extension.isInternetError 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.ProfileAnalyticEvent +import org.openedx.profile.presentation.ProfileAnalyticKey +import org.openedx.profile.presentation.ProfileAnalyticValue import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.system.notifier.AccountUpdated import org.openedx.profile.system.notifier.ProfileNotifier -import kotlinx.coroutines.launch import java.io.File class EditProfileViewModel( @@ -22,7 +25,7 @@ class EditProfileViewModel( private val resourceManager: ResourceManager, private val notifier: ProfileNotifier, private val analytics: ProfileAnalytics, - account: Account + account: Account, ) : BaseViewModel() { private val _uiState = MutableLiveData() @@ -49,6 +52,13 @@ class EditProfileViewModel( set(value) { field = value _uiState.value = EditProfileUIState(account, isLimited = value) + logProfileEvent(ProfileAnalyticEvent.SWITCH_PROFILE.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.SWITCH_PROFILE.biValue) + put( + ProfileAnalyticKey.ACTION.key, + if (isLimitedProfile) ProfileAnalyticKey.LIMITED_PROFILE else ProfileAnalyticKey.FULL_PROFILE + ) + }) } private val _showLeaveDialog = MutableLiveData() @@ -117,6 +127,9 @@ class EditProfileViewModel( fun setImageUri(uri: Uri) { _selectedImageUri.value = uri _deleteImage.value = false + logProfileEvent(ProfileAnalyticEvent.SETUP_PICTURE.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.SETUP_PICTURE.biValue) + }) } fun setShowLeaveDialog(value: Boolean) { @@ -128,11 +141,30 @@ class EditProfileViewModel( } fun profileEditDoneClickedEvent() { - analytics.profileEditDoneClickedEvent() + logProfileEvent( + ProfileAnalyticEvent.EDIT_DONE_CLICKED.event, + buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.EDIT_DONE_CLICKED.biValue) + } + ) } fun profileDeleteAccountClickedEvent() { - analytics.profileDeleteAccountClickedEvent() + logProfileEvent( + ProfileAnalyticEvent.DELETE_ACCOUNT_CLICKED.event, + buildMap { + put( + ProfileAnalyticKey.NAME.key, + ProfileAnalyticValue.DELETE_ACCOUNT_CLICKED.biValue + ) + } + ) } + private fun logProfileEvent(event: String, params: Map) { + analytics.logEvent(event, buildMap { + put(ProfileAnalyticKey.CATEGORY.key, ProfileAnalyticKey.PROFILE.key) + putAll(params) + }) + } } 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 2ed5818ad..49ea8639f 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 @@ -28,6 +28,9 @@ import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.utils.EmailUtil import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Configuration +import org.openedx.profile.presentation.ProfileAnalyticEvent +import org.openedx.profile.presentation.ProfileAnalyticKey +import org.openedx.profile.presentation.ProfileAnalyticValue import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.system.notifier.AccountDeactivated @@ -134,12 +137,18 @@ class ProfileViewModel( } fun logout() { + logProfileEvent(ProfileAnalyticEvent.LOGOUT_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.LOGOUT_CLICKED.biValue) + }) viewModelScope.launch { try { workerController.removeModels() withContext(dispatcher) { interactor.logout() } + logProfileEvent(ProfileAnalyticEvent.LOGGED_OUT.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.LOGGED_OUT.biValue) + }) } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -171,12 +180,16 @@ class ProfileViewModel( data.account ) } - analytics.profileEditClickedEvent() + logProfileEvent(ProfileAnalyticEvent.EDIT_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.EDIT_CLICKED.biValue) + }) } fun profileVideoSettingsClicked(fragmentManager: FragmentManager) { router.navigateToVideoSettings(fragmentManager) - analytics.profileVideoSettingsClickedEvent() + logProfileEvent(ProfileAnalyticEvent.VIDEO_SETTING_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.VIDEO_SETTING_CLICKED.biValue) + }) } fun privacyPolicyClicked(fragmentManager: FragmentManager) { @@ -185,7 +198,9 @@ class ProfileViewModel( title = resourceManager.getString(R.string.core_privacy_policy), url = configuration.agreementUrls.privacyPolicyUrl, ) - analytics.privacyPolicyClickedEvent() + logProfileEvent(ProfileAnalyticEvent.PRIVACY_POLICY_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.PRIVACY_POLICY_CLICKED.biValue) + }) } fun cookiePolicyClicked(fragmentManager: FragmentManager) { @@ -194,7 +209,9 @@ class ProfileViewModel( title = resourceManager.getString(R.string.core_cookie_policy), url = configuration.agreementUrls.cookiePolicyUrl, ) - analytics.cookiePolicyClickedEvent() + logProfileEvent(ProfileAnalyticEvent.COOKIE_POLICY_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.COOKIE_POLICY_CLICKED.biValue) + }) } fun dataSellClicked(fragmentManager: FragmentManager) { @@ -203,11 +220,15 @@ class ProfileViewModel( title = resourceManager.getString(R.string.core_data_sell), url = configuration.agreementUrls.dataSellConsentUrl, ) - analytics.dataSellClickedEvent() + logProfileEvent(ProfileAnalyticEvent.DATA_SELL_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.DATA_SELL_CLICKED.biValue) + }) } fun faqClicked() { - analytics.faqClickedEvent() + logProfileEvent(ProfileAnalyticEvent.FAQ_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.FAQ_CLICKED.biValue) + }) } fun termsOfUseClicked(fragmentManager: FragmentManager) { @@ -216,7 +237,9 @@ class ProfileViewModel( title = resourceManager.getString(R.string.core_terms_of_use), url = configuration.agreementUrls.tosUrl, ) - analytics.termsOfUseClickedEvent() + logProfileEvent(ProfileAnalyticEvent.TERMS_OF_USE_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.TERMS_OF_USE_CLICKED.biValue) + }) } fun emailSupportClicked(context: Context) { @@ -225,7 +248,9 @@ class ProfileViewModel( feedbackEmailAddress = config.getFeedbackEmailAddress(), appVersion = appData.versionName ) - analytics.emailSupportClickedEvent() + logProfileEvent(ProfileAnalyticEvent.CONTACT_SUPPORT_CLICKED.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.CONTACT_SUPPORT_CLICKED.biValue) + }) } fun appVersionClickedEvent(context: Context) { @@ -239,4 +264,10 @@ class ProfileViewModel( ) } + private fun logProfileEvent(event: String, params: Map) { + analytics.logEvent(event, buildMap { + put(ProfileAnalyticKey.CATEGORY.key, ProfileAnalyticKey.PROFILE.key) + putAll(params) + }) + } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt index 1fba67564..19a19245f 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsFragment.kt @@ -48,8 +48,6 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.openedx.core.presentation.settings.VideoQualityType -import org.openedx.core.ui.BackBtn import org.openedx.core.domain.model.VideoSettings import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize @@ -78,7 +76,7 @@ class VideoSettingsFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ) = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { @@ -97,14 +95,10 @@ class VideoSettingsFragment : Fragment() { viewModel.setWifiDownloadOnly(it) }, videoStreamingQualityClick = { - router.navigateToVideoQuality( - requireActivity().supportFragmentManager, VideoQualityType.Streaming - ) + viewModel.navigateToVideoStreamingQuality(requireActivity().supportFragmentManager) }, videoDownloadQualityClick = { - router.navigateToVideoQuality( - requireActivity().supportFragmentManager, VideoQualityType.Download - ) + viewModel.navigateToVideoDownloadQuality(requireActivity().supportFragmentManager) } ) } diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt index f5ca673c6..1524a0e28 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/video/VideoSettingsViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.profile.presentation.settings.video +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -9,12 +10,20 @@ 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.system.notifier.VideoNotifier import org.openedx.core.system.notifier.VideoQualityChanged +import org.openedx.profile.presentation.ProfileAnalyticEvent +import org.openedx.profile.presentation.ProfileAnalyticKey +import org.openedx.profile.presentation.ProfileAnalyticValue +import org.openedx.profile.presentation.ProfileAnalytics +import org.openedx.profile.presentation.ProfileRouter class VideoSettingsViewModel( private val preferencesManager: CorePreferences, - private val notifier: VideoNotifier + private val notifier: VideoNotifier, + private val analytics: ProfileAnalytics, + private val router: ProfileRouter, ) : BaseViewModel() { private val _videoSettings = MutableLiveData() @@ -43,6 +52,43 @@ class VideoSettingsViewModel( val currentSettings = preferencesManager.videoSettings preferencesManager.videoSettings = currentSettings.copy(wifiDownloadOnly = value) _videoSettings.value = preferencesManager.videoSettings + logProfileEvent(ProfileAnalyticEvent.WIFI_TOGGLE.event, buildMap { + put(ProfileAnalyticKey.NAME.key, ProfileAnalyticValue.WIFI_TOGGLE.biValue) + put( + ProfileAnalyticKey.ACTION.key, + if (value) ProfileAnalyticKey.ON.key else ProfileAnalyticKey.OFF.key + ) + }) } + fun navigateToVideoStreamingQuality(fragmentManager: FragmentManager) { + router.navigateToVideoQuality( + fragmentManager, VideoQualityType.Streaming + ) + logProfileEvent(ProfileAnalyticEvent.VIDEO_STREAMING_QUALITY_CLICKED.event, buildMap { + put( + ProfileAnalyticKey.NAME.key, + ProfileAnalyticValue.VIDEO_STREAMING_QUALITY_CLICKED.biValue + ) + }) + } + + fun navigateToVideoDownloadQuality(fragmentManager: FragmentManager) { + router.navigateToVideoQuality( + fragmentManager, VideoQualityType.Download + ) + logProfileEvent(ProfileAnalyticEvent.VIDEO_DOWNLOAD_QUALITY_CLICKED.event, buildMap { + put( + ProfileAnalyticKey.NAME.key, + ProfileAnalyticValue.VIDEO_DOWNLOAD_QUALITY_CLICKED.biValue + ) + }) + } + + private fun logProfileEvent(event: String, params: Map) { + analytics.logEvent(event, buildMap { + put(ProfileAnalyticKey.CATEGORY.key, ProfileAnalyticKey.PROFILE.key) + putAll(params) + }) + } } diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/WhatsNewAnalytics.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/WhatsNewAnalytics.kt new file mode 100644 index 000000000..8bfa4af19 --- /dev/null +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/WhatsNewAnalytics.kt @@ -0,0 +1,22 @@ +package org.openedx.whatsnew.presentation + +interface WhatsNewAnalytics { + fun logEvent(event: String, params: Map) +} + +enum class WhatsNewAnalyticEvent(val eventName: String) { + WHATS_NEW_VIEW("WhatsNew:View"), + WHATS_NEW_COMPLETED("WhatsNew:Completed"), +} + +enum class WhatsNewAnalyticValue(val value: String) { + SCREEN_NAVIGATION("edx.bi.app.navigation.screen"), + WHATS_NEW("whats_new"), +} + +enum class WhatsNewAnalyticKey(val key: String) { + NAME("name"), + APP_VERSION("app_version"), + CATEGORY("category"), + TOTAL_SCREENS("total_screens"), +} diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt index 6ba02e558..17538f802 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewFragment.kt @@ -106,10 +106,12 @@ class WhatsNewFragment : Fragment() { viewModel.courseId, viewModel.infoType ) + viewModel.logWhatsNewCompleted() } ) } } + viewModel.logWhatsNewViewed() } companion object { diff --git a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt index ba4ee3ee5..6b8ddb468 100644 --- a/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt +++ b/whatsnew/src/main/java/org/openedx/whatsnew/presentation/whatsnew/WhatsNewViewModel.kt @@ -5,11 +5,16 @@ import androidx.compose.runtime.mutableStateOf import org.openedx.core.BaseViewModel import org.openedx.whatsnew.WhatsNewManager import org.openedx.whatsnew.domain.model.WhatsNewItem +import org.openedx.whatsnew.presentation.WhatsNewAnalyticEvent +import org.openedx.whatsnew.presentation.WhatsNewAnalyticKey +import org.openedx.whatsnew.presentation.WhatsNewAnalyticValue +import org.openedx.whatsnew.presentation.WhatsNewAnalytics class WhatsNewViewModel( val courseId: String?, val infoType: String?, - private val whatsNewManager: WhatsNewManager + private val whatsNewManager: WhatsNewManager, + private val analytics: WhatsNewAnalytics, ) : BaseViewModel() { private val _whatsNewItem = mutableStateOf(null) @@ -23,4 +28,26 @@ class WhatsNewViewModel( private fun getNewestData() { _whatsNewItem.value = whatsNewManager.getNewestData() } + + fun logWhatsNewViewed() { + logEvent(WhatsNewAnalyticEvent.WHATS_NEW_VIEW, emptyMap()) + } + + fun logWhatsNewCompleted() { + logEvent(WhatsNewAnalyticEvent.WHATS_NEW_COMPLETED, buildMap { + put(WhatsNewAnalyticKey.TOTAL_SCREENS.key, whatsNewItem.value?.messages?.size) + }) + } + + + private fun logEvent(event: WhatsNewAnalyticEvent, params: Map) { + analytics.logEvent( + event.eventName, + buildMap { + put(WhatsNewAnalyticKey.NAME.key, WhatsNewAnalyticValue.SCREEN_NAVIGATION.value) + put(WhatsNewAnalyticKey.CATEGORY.key, WhatsNewAnalyticValue.WHATS_NEW.value) + put(WhatsNewAnalyticKey.APP_VERSION.key, whatsNewItem.value?.version) + putAll(params) + }) + } }