Skip to content

Commit

Permalink
Feature/app update (#79)
Browse files Browse the repository at this point in the history
* App update header parsing logic

* Added dialog with a recommendation to update the application

* Added AppUpgradeRecommendedBox and AppUpgradeRequiredScreen

* Minor code style changes

* Added app version item in profile

* Fixed logout logic

* Added upgrade required screen to sign in, sign up and restore password fragment

* jUnit test fixes

* Minor code style changes

* Fixed dialogs bugs, minor code refactoring

* Minor review changes
  • Loading branch information
PavloNetrebchuk authored Nov 2, 2023
1 parent 3170de0 commit 166a1ee
Show file tree
Hide file tree
Showing 50 changed files with 1,530 additions and 249 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ android {
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
versionName "1.0.0"

resourceConfigurations += ["en", "uk"]

Expand Down
17 changes: 16 additions & 1 deletion app/src/main/java/org/openedx/app/AppRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import org.openedx.auth.presentation.signup.SignUpFragment
import org.openedx.core.FragmentViewType
import org.openedx.core.domain.model.CoursewareAccess
import org.openedx.core.presentation.course.CourseViewMode
import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter
import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment
import org.openedx.course.presentation.CourseRouter
import org.openedx.course.presentation.container.CourseContainerFragment
import org.openedx.course.presentation.container.NoAccessCourseContainerFragment
Expand All @@ -36,14 +38,15 @@ import org.openedx.profile.presentation.ProfileRouter
import org.openedx.profile.presentation.anothers_account.AnothersProfileFragment
import org.openedx.profile.presentation.delete.DeleteProfileFragment
import org.openedx.profile.presentation.edit.EditProfileFragment
import org.openedx.profile.presentation.profile.ProfileFragment
import org.openedx.profile.presentation.settings.video.VideoQualityFragment
import org.openedx.profile.presentation.settings.video.VideoSettingsFragment
import org.openedx.whatsnew.WhatsNewRouter
import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment
import java.util.Date

class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter,
ProfileRouter, WhatsNewRouter {
ProfileRouter, AppUpgradeRouter, WhatsNewRouter {

//region AuthRouter
override fun navigateToMain(fm: FragmentManager) {
Expand Down Expand Up @@ -77,6 +80,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
override fun navigateToCourseSearch(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, CourseSearchFragment())
}

override fun navigateToUpgradeRequired(fm: FragmentManager) {
replaceFragmentWithBackStack(fm, UpgradeRequiredFragment())
}
//endregion

//region DashboardRouter
Expand Down Expand Up @@ -289,4 +296,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di
.replace(R.id.container, fragment, fragment.javaClass.simpleName)
.commit()
}

//App upgrade
override fun navigateToUserProfile(fm: FragmentManager) {
fm.popBackStack()
fm.beginTransaction()
.replace(R.id.container, ProfileFragment())
.commit()
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/org/openedx/app/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ class AppViewModel(
private val analytics: AppAnalytics
) : BaseViewModel() {

private val _logoutUser = SingleEventLiveData<Unit>()
val logoutUser: LiveData<Unit>
get() = _logoutUser
private val _logoutUser = SingleEventLiveData<Unit>()

private var logoutHandledAt: Long = 0

Expand Down
33 changes: 29 additions & 4 deletions app/src/main/java/org/openedx/app/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@ package org.openedx.app

import android.os.Bundle
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.viewpager2.widget.ViewPager2
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.openedx.app.adapter.MainNavigationFragmentAdapter
import org.openedx.app.databinding.FragmentMainBinding
import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment
import org.openedx.core.presentation.global.viewBinding
import org.openedx.dashboard.presentation.DashboardFragment
import org.openedx.discovery.presentation.DiscoveryFragment
import org.openedx.app.adapter.MainNavigationFragmentAdapter
import org.openedx.profile.presentation.profile.ProfileFragment
import org.koin.android.ext.android.inject
import org.openedx.app.databinding.FragmentMainBinding

class MainFragment : Fragment(R.layout.fragment_main) {

private val binding by viewBinding(FragmentMainBinding::bind)
private val analytics by inject<AppAnalytics>()
private val viewModel by viewModel<MainViewModel>()

private lateinit var adapter: MainNavigationFragmentAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener(UpgradeRequiredFragment.REQUEST_KEY) { _, _ ->
binding.bottomNavView.selectedItemId = R.id.fragmentProfile
viewModel.enableBottomBar(false)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand All @@ -30,21 +42,28 @@ class MainFragment : Fragment(R.layout.fragment_main) {
analytics.discoveryTabClickedEvent()
binding.viewPager.setCurrentItem(0, false)
}

R.id.fragmentDashboard -> {
analytics.dashboardTabClickedEvent()
binding.viewPager.setCurrentItem(1, false)
}

R.id.fragmentPrograms -> {
analytics.programsTabClickedEvent()
binding.viewPager.setCurrentItem(2, false)
}

R.id.fragmentProfile -> {
analytics.profileTabClickedEvent()
binding.viewPager.setCurrentItem(3, false)
}
}
true
}

viewModel.isBottomBarEnabled.observe(viewLifecycleOwner) { isBottomBarEnabled ->
enableBottomBar(isBottomBarEnabled)
}
}

private fun initViewPager() {
Expand All @@ -59,4 +78,10 @@ class MainFragment : Fragment(R.layout.fragment_main) {
binding.viewPager.adapter = adapter
binding.viewPager.isUserInputEnabled = false
}

private fun enableBottomBar(enable: Boolean) {
for (i in 0 until binding.bottomNavView.menu.size()) {
binding.bottomNavView.menu.getItem(i).isEnabled = enable
}
}
}
15 changes: 15 additions & 0 deletions app/src/main/java/org/openedx/app/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.openedx.app

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.openedx.core.BaseViewModel

class MainViewModel: BaseViewModel() {
private val _isBottomBarEnabled = MutableLiveData(true)
val isBottomBarEnabled: LiveData<Boolean>
get() = _isBottomBarEnabled

fun enableBottomBar(enable: Boolean) {
_isBottomBarEnabled.value = enable
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.openedx.app.data.networking

import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import org.openedx.app.BuildConfig
import org.openedx.core.system.notifier.AppUpgradeEvent
import org.openedx.core.system.notifier.AppUpgradeNotifier
import org.openedx.core.utils.TimeUtils
import java.util.Date

class AppUpgradeInterceptor(
private val appUpgradeNotifier: AppUpgradeNotifier
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val responseCode = response.code
val latestAppVersion = response.header(HEADER_APP_LATEST_VERSION) ?: ""
val lastSupportedDateString = response.header(HEADER_APP_VERSION_LAST_SUPPORTED_DATE) ?: ""
val lastSupportedDateTime = TimeUtils.iso8601WithTimeZoneToDate(lastSupportedDateString)?.time ?: 0L
runBlocking {
when {
responseCode == 426 -> {
appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent)
}

BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime > Date().time -> {
appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRecommendedEvent(latestAppVersion))
}

latestAppVersion.isNotEmpty() && BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime < Date().time -> {
appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent)
}
}
}
return response
}

companion object {
const val HEADER_APP_LATEST_VERSION = "EDX-APP-LATEST-VERSION"
const val HEADER_APP_VERSION_LAST_SUPPORTED_DATE = "EDX-APP-VERSION-LAST-SUPPORTED-DATE"
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class HandleErrorInterceptor(
return response
}
}
} else if (errorResponse.errorDescription != null) {
} else if (errorResponse?.errorDescription != null) {
throw EdxError.ValidationException(errorResponse.errorDescription ?: "")
}
} catch (e: JsonSyntaxException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.openedx.app.data.networking

import android.content.Context
import okhttp3.Interceptor
import okhttp3.Response
import org.openedx.core.data.storage.CorePreferences
import org.openedx.app.BuildConfig
import org.openedx.core.BuildConfig.ACCESS_TOKEN_TYPE
import org.openedx.core.data.storage.CorePreferences

class HeadersInterceptor(private val preferencesManager: CorePreferences) : Interceptor {
class HeadersInterceptor(private val context: Context, private val preferencesManager: CorePreferences) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response = chain.run {
proceed(
Expand All @@ -18,6 +20,12 @@ class HeadersInterceptor(private val preferencesManager: CorePreferences) : Inte
}

addHeader("Accept", "application/json")
addHeader(
"User-Agent", System.getProperty("http.agent") + " " +
context.getString(org.openedx.core.R.string.app_name) + "/" +
BuildConfig.APPLICATION_ID + "/" +
BuildConfig.VERSION_NAME
)
}.build()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
putString(key, value)
}.apply()
}

private fun getString(key: String): String = sharedPreferences.getString(key, "") ?: ""

override fun clear() {
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/org/openedx/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import org.koin.android.ext.koin.androidApplication
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter
import org.openedx.core.system.notifier.AppUpgradeNotifier
import org.openedx.profile.data.storage.ProfilePreferences
import org.openedx.whatsnew.WhatsNewFileManager
import org.openedx.whatsnew.WhatsNewRouter
Expand All @@ -59,6 +61,7 @@ val appModule = module {
single { CourseNotifier() }
single { DiscussionNotifier() }
single { ProfileNotifier() }
single { AppUpgradeNotifier() }

single { AppRouter() }
single<AuthRouter> { get<AppRouter>() }
Expand All @@ -68,6 +71,7 @@ val appModule = module {
single<DiscussionRouter> { get<AppRouter>() }
single<ProfileRouter> { get<AppRouter>() }
single<WhatsNewRouter> { get<AppRouter>() }
single<AppUpgradeRouter> { get<AppRouter>() }


single { NetworkConnection(get()) }
Expand Down
20 changes: 11 additions & 9 deletions app/src/main/java/org/openedx/app/di/NetworkingModule.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package org.openedx.app.di

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import org.openedx.app.data.networking.AppUpgradeInterceptor
import org.openedx.app.data.networking.HandleErrorInterceptor
import org.openedx.app.data.networking.HeadersInterceptor
import org.openedx.app.data.networking.OauthRefreshTokenAuthenticator
import org.openedx.auth.data.api.AuthApi
import org.openedx.core.BuildConfig
import org.openedx.core.data.api.CookiesApi
import org.openedx.core.data.api.CourseApi
import org.openedx.discussion.data.api.DiscussionApi
import org.openedx.app.data.networking.HandleErrorInterceptor
import org.openedx.app.data.networking.HeadersInterceptor
import org.openedx.app.data.networking.OauthRefreshTokenAuthenticator
import org.openedx.profile.data.api.ProfileApi
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import org.openedx.core.BuildConfig
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
Expand All @@ -24,18 +25,19 @@ val networkingModule = module {
OkHttpClient.Builder().apply {
writeTimeout(60, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
addInterceptor(HeadersInterceptor(get()))
addInterceptor(HeadersInterceptor(get(), get()))
if (BuildConfig.DEBUG) {
addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
}
addInterceptor(HandleErrorInterceptor(get()))
addInterceptor(AppUpgradeInterceptor(get()))
authenticator(get<OauthRefreshTokenAuthenticator>())
}.build()
}

single<Retrofit> {
Retrofit.Builder()
.baseUrl(org.openedx.core.BuildConfig.BASE_URL)
.baseUrl(BuildConfig.BASE_URL)
.client(get())
.addConverterFactory(GsonConverterFactory.create(get()))
.build()
Expand Down
Loading

0 comments on commit 166a1ee

Please sign in to comment.