Skip to content

Commit d659f71

Browse files
feat: [FC-0078] Downloads page (#432)
* feat: added downloads tab to main navigation * feat: course item UI * feat: download course list request * feat: downloads fragment empty state * feat: downloading logic * refactor: dynamic main menu * feat: show loading course structure state * feat: downloads analytic * feat: junit test * feat: navigate to course outline, swipe refresh on empty state * fix: changes according PR review * feat: show course item on dialog * refactor: improved code for better readability, optimized downloading logic * fix: dialog icon * fix: remove course size * feat: landscape and tablet ui * fix: remove course during downloading * fix: update download page after getting course structure on outline page * fix: update downloading state if new blocks was added * feat: added height limit and scroll to download dialog * feat: rename config flag * feat: fix available course size in download dialog. Improved logic of getting downloads models from room * feat: added school icon to DownloadDialogItem * fix: using real unarchived size instead bloc size * fix: junit tests * feat: navigation icons update * fix: changes according PR review * feat: put config to experimental value * fix: junit test fix
1 parent 4dbff8b commit d659f71

File tree

100 files changed

+2730
-561
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+2730
-561
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ dependencies {
125125
implementation project(path: ':profile')
126126
implementation project(path: ':discussion')
127127
implementation project(path: ':whatsnew')
128+
implementation project(path: ':downloads')
128129

129130
ksp "androidx.room:room-compiler:$room_version"
130131

app/src/main/java/org/openedx/app/AnalyticsManager.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.openedx.app
22

33
import org.openedx.auth.presentation.AuthAnalytics
44
import org.openedx.core.presentation.CoreAnalytics
5+
import org.openedx.core.presentation.DownloadsAnalytics
56
import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
67
import org.openedx.course.presentation.CourseAnalytics
78
import org.openedx.dashboard.presentation.DashboardAnalytics
@@ -21,7 +22,8 @@ class AnalyticsManager :
2122
DiscoveryAnalytics,
2223
DiscussionAnalytics,
2324
ProfileAnalytics,
24-
WhatsNewAnalytics {
25+
WhatsNewAnalytics,
26+
DownloadsAnalytics {
2527

2628
private val analytics: MutableList<Analytics> = mutableListOf()
2729

app/src/main/java/org/openedx/app/AppActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import org.openedx.auth.presentation.logistration.LogistrationFragment
2727
import org.openedx.auth.presentation.signin.SignInFragment
2828
import org.openedx.core.ApiConstants
2929
import org.openedx.core.data.storage.CorePreferences
30+
import org.openedx.core.presentation.dialog.downloaddialog.DownloadDialogManager
3031
import org.openedx.core.presentation.global.InsetHolder
3132
import org.openedx.core.presentation.global.WindowSizeHolder
3233
import org.openedx.core.utils.Logger
3334
import org.openedx.core.worker.CalendarSyncScheduler
34-
import org.openedx.course.presentation.download.DownloadDialogManager
3535
import org.openedx.foundation.extension.requestApplyInsetsWhenAttached
3636
import org.openedx.foundation.presentation.WindowSize
3737
import org.openedx.foundation.presentation.WindowType

app/src/main/java/org/openedx/app/AppAnalytics.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ enum class AppAnalyticsEvent(val eventName: String, val biValue: String) {
2020
"MainDashboard:Discover",
2121
"edx.bi.app.main_dashboard.discover"
2222
),
23+
DOWNLOADS(
24+
"MainDashboard:Downloads",
25+
"edx.bi.app.main_dashboard.downloads"
26+
),
2327
PROFILE(
2428
"MainDashboard:Profile",
2529
"edx.bi.app.main_dashboard.profile"

app/src/main/java/org/openedx/app/AppRouter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.openedx.discussion.presentation.responses.DiscussionResponsesFragment
4444
import org.openedx.discussion.presentation.search.DiscussionSearchThreadFragment
4545
import org.openedx.discussion.presentation.threads.DiscussionAddThreadFragment
4646
import org.openedx.discussion.presentation.threads.DiscussionThreadsFragment
47+
import org.openedx.downloads.presentation.DownloadsRouter
4748
import org.openedx.profile.domain.model.Account
4849
import org.openedx.profile.presentation.ProfileRouter
4950
import org.openedx.profile.presentation.anothersaccount.AnothersProfileFragment
@@ -67,7 +68,8 @@ class AppRouter :
6768
ProfileRouter,
6869
AppUpgradeRouter,
6970
WhatsNewRouter,
70-
CalendarRouter {
71+
CalendarRouter,
72+
DownloadsRouter {
7173

7274
// region AuthRouter
7375
override fun navigateToMain(

app/src/main/java/org/openedx/app/MainFragment.kt

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.openedx.app
22

33
import android.os.Bundle
4+
import android.view.Menu
45
import android.view.View
56
import androidx.core.os.bundleOf
67
import androidx.core.view.forEach
@@ -17,6 +18,7 @@ import org.openedx.core.adapter.NavigationFragmentAdapter
1718
import org.openedx.core.presentation.global.appupgrade.UpgradeRequiredFragment
1819
import org.openedx.core.presentation.global.viewBinding
1920
import org.openedx.discovery.presentation.DiscoveryRouter
21+
import org.openedx.downloads.presentation.download.DownloadsFragment
2022
import org.openedx.learn.presentation.LearnFragment
2123
import org.openedx.learn.presentation.LearnTab
2224
import org.openedx.profile.presentation.profile.ProfileFragment
@@ -40,29 +42,104 @@ class MainFragment : Fragment(R.layout.fragment_main) {
4042

4143
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
4244
super.onViewCreated(view, savedInstanceState)
45+
handleArguments()
46+
setupBottomNavigation()
47+
setupViewPager()
48+
observeViewModel()
49+
}
4350

44-
initViewPager()
45-
46-
binding.bottomNavView.setOnItemSelectedListener {
47-
when (it.itemId) {
48-
R.id.fragmentLearn -> {
49-
viewModel.logLearnTabClickedEvent()
50-
binding.viewPager.setCurrentItem(0, false)
51+
private fun handleArguments() {
52+
requireArguments().apply {
53+
getString(ARG_COURSE_ID).takeIf { it.isNullOrBlank().not() }?.let { courseId ->
54+
val infoType = getString(ARG_INFO_TYPE)
55+
if (viewModel.isDiscoveryTypeWebView && infoType != null) {
56+
router.navigateToCourseInfo(parentFragmentManager, courseId, infoType)
57+
} else {
58+
router.navigateToCourseDetail(parentFragmentManager, courseId)
5159
}
60+
putString(ARG_COURSE_ID, "")
61+
putString(ARG_INFO_TYPE, "")
62+
}
63+
}
64+
}
5265

53-
R.id.fragmentDiscover -> {
54-
viewModel.logDiscoveryTabClickedEvent()
55-
binding.viewPager.setCurrentItem(1, false)
56-
}
66+
private fun setupBottomNavigation() {
67+
val openTabArg = requireArguments().getString(ARG_OPEN_TAB, HomeTab.LEARN.name)
68+
val initialMenuId = getInitialMenuId(openTabArg)
69+
binding.bottomNavView.selectedItemId = initialMenuId
5770

58-
R.id.fragmentProfile -> {
59-
viewModel.logProfileTabClickedEvent()
60-
binding.viewPager.setCurrentItem(2, false)
61-
}
71+
val menu = binding.bottomNavView.menu
72+
menu.clear()
73+
74+
val tabList = createTabList(openTabArg)
75+
addMenuItems(menu, tabList)
76+
setupBottomNavListener(tabList)
77+
78+
requireArguments().remove(ARG_OPEN_TAB)
79+
}
80+
81+
private fun createTabList(openTabArg: String): List<Pair<Int, Fragment>> {
82+
val learnFragment = LearnFragment.newInstance(
83+
openTab = if (openTabArg == HomeTab.PROGRAMS.name) {
84+
LearnTab.PROGRAMS.name
85+
} else {
86+
LearnTab.COURSES.name
87+
}
88+
)
89+
90+
return mutableListOf<Pair<Int, Fragment>>().apply {
91+
add(R.id.fragmentLearn to learnFragment)
92+
add(R.id.fragmentDiscover to viewModel.getDiscoveryFragment)
93+
if (viewModel.isDownloadsFragmentEnabled) {
94+
add(R.id.fragmentDownloads to DownloadsFragment())
95+
}
96+
add(R.id.fragmentProfile to ProfileFragment())
97+
}
98+
}
99+
100+
private fun addMenuItems(menu: Menu, tabList: List<Pair<Int, Fragment>>) {
101+
val tabTitles = mapOf(
102+
R.id.fragmentLearn to resources.getString(R.string.app_navigation_learn),
103+
R.id.fragmentDiscover to resources.getString(R.string.app_navigation_discovery),
104+
R.id.fragmentDownloads to resources.getString(R.string.app_navigation_downloads),
105+
R.id.fragmentProfile to resources.getString(R.string.app_navigation_profile),
106+
)
107+
val tabIconSelectors = mapOf(
108+
R.id.fragmentLearn to R.drawable.app_ic_learn_selector,
109+
R.id.fragmentDiscover to R.drawable.app_ic_discover_selector,
110+
R.id.fragmentDownloads to R.drawable.app_ic_downloads_selector,
111+
R.id.fragmentProfile to R.drawable.app_ic_profile_selector
112+
)
113+
114+
for ((id, _) in tabList) {
115+
val menuItem = menu.add(Menu.NONE, id, Menu.NONE, tabTitles[id] ?: "")
116+
tabIconSelectors[id]?.let { menuItem.setIcon(it) }
117+
}
118+
}
119+
120+
private fun setupBottomNavListener(tabList: List<Pair<Int, Fragment>>) {
121+
val menuIdToIndex = tabList.mapIndexed { index, pair -> pair.first to index }.toMap()
122+
123+
binding.bottomNavView.setOnItemSelectedListener { menuItem ->
124+
when (menuItem.itemId) {
125+
R.id.fragmentLearn -> viewModel.logLearnTabClickedEvent()
126+
R.id.fragmentDiscover -> viewModel.logDiscoveryTabClickedEvent()
127+
R.id.fragmentDownloads -> viewModel.logDownloadsTabClickedEvent()
128+
R.id.fragmentProfile -> viewModel.logProfileTabClickedEvent()
129+
}
130+
menuIdToIndex[menuItem.itemId]?.let { index ->
131+
binding.viewPager.setCurrentItem(index, false)
62132
}
63133
true
64134
}
135+
}
65136

137+
private fun setupViewPager() {
138+
val tabList = createTabList(requireArguments().getString(ARG_OPEN_TAB, HomeTab.LEARN.name))
139+
initViewPager(tabList)
140+
}
141+
142+
private fun observeViewModel() {
66143
viewModel.isBottomBarEnabled.observe(viewLifecycleOwner) { isBottomBarEnabled ->
67144
enableBottomBar(isBottomBarEnabled)
68145
}
@@ -74,55 +151,30 @@ class MainFragment : Fragment(R.layout.fragment_main) {
74151
}
75152
}
76153
}
154+
}
77155

78-
requireArguments().apply {
79-
getString(ARG_COURSE_ID).takeIf { it.isNullOrBlank().not() }?.let { courseId ->
80-
val infoType = getString(ARG_INFO_TYPE)
81-
82-
if (viewModel.isDiscoveryTypeWebView && infoType != null) {
83-
router.navigateToCourseInfo(parentFragmentManager, courseId, infoType)
84-
} else {
85-
router.navigateToCourseDetail(parentFragmentManager, courseId)
86-
}
87-
88-
// Clear arguments after navigation
89-
putString(ARG_COURSE_ID, "")
90-
putString(ARG_INFO_TYPE, "")
91-
}
92-
93-
when (requireArguments().getString(ARG_OPEN_TAB, "")) {
94-
HomeTab.LEARN.name,
95-
HomeTab.PROGRAMS.name -> {
96-
binding.bottomNavView.selectedItemId = R.id.fragmentLearn
97-
}
98-
99-
HomeTab.DISCOVER.name -> {
100-
binding.bottomNavView.selectedItemId = R.id.fragmentDiscover
101-
}
102-
103-
HomeTab.PROFILE.name -> {
104-
binding.bottomNavView.selectedItemId = R.id.fragmentProfile
105-
}
156+
private fun getInitialMenuId(openTabArg: String): Int {
157+
return when (openTabArg) {
158+
HomeTab.LEARN.name, HomeTab.PROGRAMS.name -> R.id.fragmentLearn
159+
HomeTab.DISCOVER.name -> R.id.fragmentDiscover
160+
HomeTab.DOWNLOADS.name -> if (viewModel.isDownloadsFragmentEnabled) {
161+
R.id.fragmentDownloads
162+
} else {
163+
R.id.fragmentLearn
106164
}
107-
requireArguments().remove(ARG_OPEN_TAB)
165+
HomeTab.PROFILE.name -> R.id.fragmentProfile
166+
else -> R.id.fragmentLearn
108167
}
109168
}
110169

111-
@Suppress("MagicNumber")
112-
private fun initViewPager() {
170+
private fun initViewPager(tabList: List<Pair<Int, Fragment>>) {
113171
binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
114-
binding.viewPager.offscreenPageLimit = 4
172+
binding.viewPager.offscreenPageLimit = tabList.size
115173

116-
val openTab = requireArguments().getString(ARG_OPEN_TAB, HomeTab.LEARN.name)
117-
val learnTab = if (openTab == HomeTab.PROGRAMS.name) {
118-
LearnTab.PROGRAMS
119-
} else {
120-
LearnTab.COURSES
121-
}
122174
adapter = NavigationFragmentAdapter(this).apply {
123-
addFragment(LearnFragment.newInstance(openTab = learnTab.name))
124-
addFragment(viewModel.getDiscoveryFragment)
125-
addFragment(ProfileFragment())
175+
tabList.forEach { (_, fragment) ->
176+
addFragment(fragment)
177+
}
126178
}
127179
binding.viewPager.adapter = adapter
128180
binding.viewPager.isUserInputEnabled = false

app/src/main/java/org/openedx/app/MainViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class MainViewModel(
3333
val isDiscoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView()
3434
val getDiscoveryFragment get() = DiscoveryNavigator(isDiscoveryTypeWebView).getDiscoveryFragment()
3535

36+
val isDownloadsFragmentEnabled get() = config.getDownloadsConfig().isEnabled
37+
3638
override fun onCreate(owner: LifecycleOwner) {
3739
super.onCreate(owner)
3840
notifier.notifier
@@ -57,6 +59,10 @@ class MainViewModel(
5759
logScreenEvent(AppAnalyticsEvent.DISCOVER)
5860
}
5961

62+
fun logDownloadsTabClickedEvent() {
63+
logScreenEvent(AppAnalyticsEvent.DOWNLOADS)
64+
}
65+
6066
fun logProfileTabClickedEvent() {
6167
logScreenEvent(AppAnalyticsEvent.PROFILE)
6268
}

app/src/main/java/org/openedx/app/deeplink/HomeTab.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ enum class HomeTab {
44
LEARN,
55
PROGRAMS,
66
DISCOVER,
7+
DOWNLOADS,
78
PROFILE
89
}

app/src/main/java/org/openedx/app/di/AppModule.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import org.openedx.core.module.TranscriptManager
4040
import org.openedx.core.module.download.DownloadHelper
4141
import org.openedx.core.module.download.FileDownloader
4242
import org.openedx.core.presentation.CoreAnalytics
43+
import org.openedx.core.presentation.DownloadsAnalytics
4344
import org.openedx.core.presentation.dialog.appreview.AppReviewAnalytics
4445
import org.openedx.core.presentation.dialog.appreview.AppReviewManager
46+
import org.openedx.core.presentation.dialog.downloaddialog.DownloadDialogManager
4547
import org.openedx.core.presentation.global.AppData
4648
import org.openedx.core.presentation.global.WhatsNewGlobalManager
4749
import org.openedx.core.presentation.global.appupgrade.AppUpgradeRouter
@@ -58,7 +60,6 @@ import org.openedx.core.worker.CalendarSyncScheduler
5860
import org.openedx.course.data.storage.CoursePreferences
5961
import org.openedx.course.presentation.CourseAnalytics
6062
import org.openedx.course.presentation.CourseRouter
61-
import org.openedx.course.presentation.download.DownloadDialogManager
6263
import org.openedx.course.utils.ImageProcessor
6364
import org.openedx.course.worker.OfflineProgressSyncScheduler
6465
import org.openedx.dashboard.presentation.DashboardAnalytics
@@ -68,6 +69,7 @@ import org.openedx.discovery.presentation.DiscoveryRouter
6869
import org.openedx.discussion.presentation.DiscussionAnalytics
6970
import org.openedx.discussion.presentation.DiscussionRouter
7071
import org.openedx.discussion.system.notifier.DiscussionNotifier
72+
import org.openedx.downloads.presentation.DownloadsRouter
7173
import org.openedx.foundation.system.ResourceManager
7274
import org.openedx.foundation.utils.FileUtil
7375
import org.openedx.profile.data.storage.ProfilePreferences
@@ -127,6 +129,7 @@ val appModule = module {
127129
single<AppUpgradeRouter> { get<AppRouter>() }
128130
single { DeepLinkRouter(get(), get(), get(), get(), get(), get()) }
129131
single<CalendarRouter> { get<AppRouter>() }
132+
single<DownloadsRouter> { get<AppRouter>() }
130133

131134
single { NetworkConnection(get()) }
132135

@@ -205,6 +208,7 @@ val appModule = module {
205208
single<DiscussionAnalytics> { get<AnalyticsManager>() }
206209
single<ProfileAnalytics> { get<AnalyticsManager>() }
207210
single<WhatsNewAnalytics> { get<AnalyticsManager>() }
211+
single<DownloadsAnalytics> { get<AnalyticsManager>() }
208212

209213
factory { AgreementProvider(get(), get()) }
210214
factory { FacebookAuthHelper() }

0 commit comments

Comments
 (0)