diff --git a/app/src/main/java/org/openedx/app/AppAnalytics.kt b/app/src/main/java/org/openedx/app/AppAnalytics.kt index a122e79c1..0fe3ed4be 100644 --- a/app/src/main/java/org/openedx/app/AppAnalytics.kt +++ b/app/src/main/java/org/openedx/app/AppAnalytics.kt @@ -12,6 +12,10 @@ enum class AppAnalyticsEvent(val eventName: String, val biValue: String) { "Launch", "edx.bi.app.launch" ), + LEARN( + "MainDashboard:Learn", + "edx.bi.app.main_dashboard.learn" + ), DISCOVER( "MainDashboard:Discover", "edx.bi.app.main_dashboard.discover" diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index d6a28f926..3ab735d27 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -46,6 +46,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.bottomNavView.setOnItemSelectedListener { when (it.itemId) { R.id.fragmentLearn -> { + viewModel.logLearnTabClickedEvent() binding.viewPager.setCurrentItem(0, false) } @@ -89,7 +90,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { putString(ARG_INFO_TYPE, "") } - when (requireArguments().getString(ARG_OPEN_TAB, HomeTab.LEARN.name)) { + when (requireArguments().getString(ARG_OPEN_TAB, "")) { HomeTab.LEARN.name, HomeTab.PROGRAMS.name -> { binding.bottomNavView.selectedItemId = R.id.fragmentLearn diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index ff24f4ff8..69c809b5c 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -49,6 +49,10 @@ class MainViewModel( _isBottomBarEnabled.value = enable } + fun logLearnTabClickedEvent() { + logScreenEvent(AppAnalyticsEvent.LEARN) + } + fun logDiscoveryTabClickedEvent() { logScreenEvent(AppAnalyticsEvent.DISCOVER) } 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 cd2f57c0b..2c1dffad1 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -157,7 +157,9 @@ val screenModule = module { ) } viewModel { AllEnrolledCoursesViewModel(get(), get(), get(), get(), get(), get(), get()) } - viewModel { LearnViewModel(get(), get(), get()) } + viewModel { (openTab: String) -> + LearnViewModel(openTab, get(), get(), get()) + } factory { DiscoveryRepository(get(), get(), get()) } factory { DiscoveryInteractor(get()) } 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 572a3aabf..5cc3a3088 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 @@ -322,6 +322,7 @@ class CourseContainerViewModel( private fun courseDashboardViewed() { logCourseContainerEvent(CourseAnalyticsEvent.DASHBOARD) + courseTabClickedEvent() } private fun courseTabClickedEvent() { diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 73be2ca83..a2a2a1d71 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -241,6 +241,12 @@ class CourseContainerViewModelTest { any() ) } returns Unit + every { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } returns Unit viewModel.fetchCourseDetails() advanceUntilIdle() @@ -251,6 +257,12 @@ class CourseContainerViewModelTest { any() ) } + verify(exactly = 1) { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } assert(!viewModel.refreshing.value) assert(viewModel.courseAccessStatus.value == CourseAccessError.UNKNOWN) } @@ -281,6 +293,12 @@ class CourseContainerViewModelTest { any() ) } returns Unit + every { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } returns Unit viewModel.fetchCourseDetails() advanceUntilIdle() @@ -291,6 +309,12 @@ class CourseContainerViewModelTest { any() ) } + verify(exactly = 1) { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } assert(viewModel.errorMessage.value == null) assert(!viewModel.refreshing.value) assert(viewModel.courseAccessStatus.value != null) @@ -321,6 +345,12 @@ class CourseContainerViewModelTest { any() ) } returns Unit + every { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } returns Unit viewModel.fetchCourseDetails() advanceUntilIdle() coVerify(exactly = 0) { courseApi.getEnrollmentDetails(any()) } @@ -330,6 +360,12 @@ class CourseContainerViewModelTest { any() ) } + verify(exactly = 1) { + analytics.logScreenEvent( + CourseAnalyticsEvent.HOME_TAB.eventName, + any() + ) + } assert(viewModel.errorMessage.value == null) assert(!viewModel.refreshing.value) diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt index cf7097a64..066b8ff73 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt @@ -7,12 +7,12 @@ interface DashboardAnalytics { enum class DashboardAnalyticsEvent(val eventName: String, val biValue: String) { MY_COURSES( - "MainDashboard:My Courses", - "edx.bi.app.main_dashboard.my_course" + "Learn:My Courses", + "edx.bi.app.main_dashboard.learn.my_course" ), MY_PROGRAMS( - "MainDashboard:My Programs", - "edx.bi.app.main_dashboard.my_program" + "Learn:My Programs", + "edx.bi.app.main_dashboard.learn.my_programs" ), } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt index 94e649535..c6843a5f8 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnFragment.kt @@ -22,7 +22,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,7 +31,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -42,6 +41,7 @@ import androidx.fragment.app.FragmentManager import androidx.viewpager2.widget.ViewPager2 import org.koin.androidx.compose.koinViewModel import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf import org.openedx.core.adapter.NavigationFragmentAdapter import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.crop @@ -60,24 +60,30 @@ import org.openedx.core.R as CoreR class LearnFragment : Fragment(R.layout.fragment_learn) { private val binding by viewBinding(FragmentLearnBinding::bind) - private val viewModel by viewModel() + private val viewModel by viewModel { + parametersOf(requireArguments().getString(ARG_OPEN_TAB, LearnTab.COURSES.name)) + } private lateinit var adapter: NavigationFragmentAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViewPager() - val openTab = requireArguments().getString(ARG_OPEN_TAB, LearnTab.COURSES.name) - val defaultLearnType = if (openTab == LearnTab.PROGRAMS.name) { - LearnType.PROGRAMS - } else { - LearnType.COURSES - } binding.header.setContent { OpenEdXTheme { + val uiState by viewModel.uiState.collectAsState() + binding.viewPager.setCurrentItem( + when (uiState.learnType) { + LearnType.COURSES -> 0 + LearnType.PROGRAMS -> 1 + }, + false + ) Header( fragmentManager = requireParentFragment().parentFragmentManager, - defaultLearnType = defaultLearnType, - viewPager = binding.viewPager + selectedLearnType = uiState.learnType, + onUpdateLearnType = { learnType -> + viewModel.updateLearnType(learnType) + }, ) } } @@ -93,23 +99,12 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { } binding.viewPager.adapter = adapter binding.viewPager.setUserInputEnabled(false) - - binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - if (LearnType.COURSES.ordinal == position) { - viewModel.logMyCoursesTabClickedEvent() - } else { - viewModel.logMyProgramsTabClickedEvent() - } - } - }) } companion object { private const val ARG_OPEN_TAB = "open_tab" fun newInstance( - openTab: String = LearnTab.COURSES.name + openTab: String = LearnTab.COURSES.name, ): LearnFragment { val fragment = LearnFragment() fragment.arguments = bundleOf( @@ -123,8 +118,8 @@ class LearnFragment : Fragment(R.layout.fragment_learn) { @Composable private fun Header( fragmentManager: FragmentManager, - defaultLearnType: LearnType, - viewPager: ViewPager2, + selectedLearnType: LearnType, + onUpdateLearnType: (LearnType) -> Unit ) { val viewModel: LearnViewModel = koinViewModel() val windowSize = rememberWindowSize() @@ -156,8 +151,8 @@ private fun Header( modifier = Modifier .align(Alignment.Start) .padding(horizontal = 16.dp), - defaultLearnType = defaultLearnType, - viewPager = viewPager + selectedLearnType = selectedLearnType, + onUpdateLearnType = onUpdateLearnType ) } } @@ -200,26 +195,15 @@ private fun Title( @Composable private fun LearnDropdownMenu( modifier: Modifier = Modifier, - defaultLearnType: LearnType, - viewPager: ViewPager2, + selectedLearnType: LearnType, + onUpdateLearnType: (LearnType) -> Unit ) { var expanded by remember { mutableStateOf(false) } - var currentValue by remember { mutableStateOf(defaultLearnType) } val iconRotation by animateFloatAsState( targetValue = if (expanded) 180f else 0f, label = "" ) - LaunchedEffect(currentValue) { - viewPager.setCurrentItem( - when (currentValue) { - LearnType.COURSES -> 0 - LearnType.PROGRAMS -> 1 - }, - false - ) - } - Column( modifier = modifier ) { @@ -231,7 +215,7 @@ private fun LearnDropdownMenu( verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(id = currentValue.title), + text = stringResource(id = selectedLearnType.title), color = MaterialTheme.appColors.textDark, style = MaterialTheme.appTypography.titleSmall ) @@ -262,7 +246,7 @@ private fun LearnDropdownMenu( for (learnType in LearnType.entries) { val background: Color val textColor: Color - if (currentValue == learnType) { + if (selectedLearnType == learnType) { background = MaterialTheme.appColors.primary textColor = MaterialTheme.appColors.primaryButtonText } else { @@ -273,7 +257,7 @@ private fun LearnDropdownMenu( modifier = Modifier .background(background), onClick = { - currentValue = learnType + onUpdateLearnType(learnType) expanded = false } ) { @@ -304,10 +288,9 @@ private fun HeaderPreview() { @Composable private fun LearnDropdownMenuPreview() { OpenEdXTheme { - val context = LocalContext.current LearnDropdownMenu( - defaultLearnType = LearnType.COURSES, - viewPager = ViewPager2(context) + selectedLearnType = LearnType.COURSES, + onUpdateLearnType = {} ) } } diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt new file mode 100644 index 000000000..934caa374 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnUIState.kt @@ -0,0 +1,5 @@ +package org.openedx.learn.presentation + +import org.openedx.learn.LearnType + +data class LearnUIState(val learnType: LearnType) diff --git a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt index ee38caf75..21e746374 100644 --- a/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt +++ b/dashboard/src/main/java/org/openedx/learn/presentation/LearnViewModel.kt @@ -1,6 +1,12 @@ package org.openedx.learn.presentation import androidx.fragment.app.FragmentManager +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.openedx.DashboardNavigator import org.openedx.core.config.Config import org.openedx.dashboard.presentation.DashboardAnalytics @@ -8,12 +14,26 @@ import org.openedx.dashboard.presentation.DashboardAnalyticsEvent import org.openedx.dashboard.presentation.DashboardAnalyticsKey import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.foundation.presentation.BaseViewModel +import org.openedx.learn.LearnType class LearnViewModel( + openTab: String, private val config: Config, private val dashboardRouter: DashboardRouter, private val analytics: DashboardAnalytics, ) : BaseViewModel() { + private val _uiState = MutableStateFlow( + LearnUIState( + if (openTab == LearnTab.PROGRAMS.name) { + LearnType.PROGRAMS + } else { + LearnType.COURSES + } + ) + ) + + val uiState: StateFlow + get() = _uiState.asStateFlow() private val dashboardType get() = config.getDashboardConfig().getType() val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() @@ -26,6 +46,24 @@ class LearnViewModel( val getProgramFragment get() = dashboardRouter.getProgramFragment() + init { + viewModelScope.launch { + _uiState.collect { uiState -> + if (uiState.learnType == LearnType.COURSES) { + logMyCoursesTabClickedEvent() + } else { + logMyProgramsTabClickedEvent() + } + } + } + } + + fun updateLearnType(learnType: LearnType) { + viewModelScope.launch { + _uiState.update { it.copy(learnType = learnType) } + } + } + fun logMyCoursesTabClickedEvent() { logScreenEvent(DashboardAnalyticsEvent.MY_COURSES) } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt index bf230a9e9..c82df34d8 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/LearnViewModelTest.kt @@ -6,30 +6,51 @@ import io.mockk.mockk import io.mockk.verify import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before import org.junit.Test import org.openedx.DashboardNavigator import org.openedx.core.config.Config import org.openedx.core.config.DashboardConfig +import org.openedx.learn.presentation.LearnTab import org.openedx.learn.presentation.LearnViewModel +@OptIn(ExperimentalCoroutinesApi::class) class LearnViewModelTest { + private val dispatcher = StandardTestDispatcher() + private val config = mockk() private val dashboardRouter = mockk(relaxed = true) private val analytics = mockk(relaxed = true) private val fragmentManager = mockk() - private val viewModel = LearnViewModel(config, dashboardRouter, analytics) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } @Test fun `onSettingsClick calls navigateToSettings`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) viewModel.onSettingsClick(fragmentManager) verify { dashboardRouter.navigateToSettings(fragmentManager) } } @Test fun `getDashboardFragment returns correct fragment based on dashboardType`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) DashboardConfig.DashboardType.entries.forEach { type -> every { config.getDashboardConfig().getType() } returns type val dashboardFragment = viewModel.getDashboardFragment @@ -39,18 +60,21 @@ class LearnViewModelTest { @Test fun `getProgramFragment returns correct program fragment`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) viewModel.getProgramFragment verify { dashboardRouter.getProgramFragment() } } @Test fun `isProgramTypeWebView returns correct view type`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) every { config.getProgramConfig().isViewTypeWebView() } returns true assertTrue(viewModel.isProgramTypeWebView) } @Test fun `logMyCoursesTabClickedEvent logs correct analytics event`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) viewModel.logMyCoursesTabClickedEvent() verify { @@ -65,6 +89,7 @@ class LearnViewModelTest { @Test fun `logMyProgramsTabClickedEvent logs correct analytics event`() = runTest { + val viewModel = LearnViewModel(LearnTab.COURSES.name, config, dashboardRouter, analytics) viewModel.logMyProgramsTabClickedEvent() verify {