From 948277af5a7621ffe795b6171684d8eaa078cc46 Mon Sep 17 00:00:00 2001 From: Volodymyr Chekyrta <127732735+volodymyr-chekyrta@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:32:38 +0200 Subject: [PATCH] feat: [FC-0047] FCM (#344) * feat: fcm * fix: address feedback --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 4 +- .../main/java/org/openedx/app/AppActivity.kt | 16 + .../main/java/org/openedx/app/AppViewModel.kt | 62 ++- .../openedx/app/data/api/NotificationsApi.kt | 14 + .../data/networking/AppUpgradeInterceptor.kt | 12 +- .../OauthRefreshTokenAuthenticator.kt | 8 +- .../app/data/storage/PreferencesManager.kt | 8 + .../java/org/openedx/app/deeplink/DeepLink.kt | 41 +- .../openedx/app/deeplink/DeepLinkRouter.kt | 406 +++++++++++------- .../java/org/openedx/app/deeplink/Screen.kt | 20 - .../main/java/org/openedx/app/di/AppModule.kt | 6 +- .../org/openedx/app/di/NetworkingModule.kt | 2 + .../java/org/openedx/app/di/ScreenModule.kt | 1 + .../openedx/app/system/notifier/AppEvent.kt | 3 - .../app/system/notifier/LogoutEvent.kt | 3 - .../push/OpenEdXFirebaseMessagingService.kt | 95 ++++ .../system/push/RefreshFirebaseTokenWorker.kt | 46 ++ .../system/push/SyncFirebaseTokenWorker.kt | 47 ++ .../test/java/org/openedx/AppViewModelTest.kt | 87 ++-- .../restore/RestorePasswordViewModel.kt | 12 +- .../presentation/signin/SignInViewModel.kt | 15 +- .../auth/presentation/signup/SignUpUIState.kt | 2 +- .../presentation/signup/SignUpViewModel.kt | 14 +- .../restore/RestorePasswordViewModelTest.kt | 34 +- .../signin/SignInViewModelTest.kt | 33 +- .../signup/SignUpViewModelTest.kt | 34 +- build.gradle | 6 +- .../java/org/openedx/core/AppUpdateState.kt | 2 +- .../core/data/storage/CorePreferences.kt | 1 + .../system/notifier/AppUpgradeNotifier.kt | 15 - .../core/system/notifier/app/AppEvent.kt | 3 + .../core/system/notifier/app}/AppNotifier.kt | 8 +- .../notifier/{ => app}/AppUpgradeEvent.kt | 4 +- .../core/system/notifier/app/LogoutEvent.kt | 3 + .../core/system/notifier/app/SignInEvent.kt | 3 + .../container/CourseContainerFragment.kt | 14 + .../presentation/DashboardGalleryViewModel.kt | 7 + .../presentation/DashboardListFragment.kt | 2 +- .../presentation/DashboardListViewModel.kt | 15 +- .../presentation/DashboardViewModelTest.kt | 46 +- .../presentation/NativeDiscoveryFragment.kt | 2 +- .../presentation/NativeDiscoveryViewModel.kt | 9 +- .../NativeDiscoveryViewModelTest.kt | 26 +- .../discussion/data/api/DiscussionApi.kt | 11 +- .../data/model/response/CommentsResponse.kt | 6 +- .../data/repository/DiscussionRepository.kt | 9 +- .../domain/interactor/DiscussionInteractor.kt | 5 +- .../presentation/settings/SettingsScreenUI.kt | 2 +- .../settings/SettingsViewModel.kt | 14 +- 50 files changed, 839 insertions(+), 405 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/data/api/NotificationsApi.kt delete mode 100644 app/src/main/java/org/openedx/app/deeplink/Screen.kt delete mode 100644 app/src/main/java/org/openedx/app/system/notifier/AppEvent.kt delete mode 100644 app/src/main/java/org/openedx/app/system/notifier/LogoutEvent.kt create mode 100644 app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt create mode 100644 app/src/main/java/org/openedx/app/system/push/RefreshFirebaseTokenWorker.kt create mode 100644 app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt delete mode 100644 core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/app/AppEvent.kt rename {app/src/main/java/org/openedx/app/system/notifier => core/src/main/java/org/openedx/core/system/notifier/app}/AppNotifier.kt (67%) rename core/src/main/java/org/openedx/core/system/notifier/{ => app}/AppUpgradeEvent.kt (61%) create mode 100644 core/src/main/java/org/openedx/core/system/notifier/app/LogoutEvent.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/app/SignInEvent.kt diff --git a/app/build.gradle b/app/build.gradle index f949e477f..e0992b266 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,6 +130,9 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' + api platform("com.google.firebase:firebase-bom:$firebase_version") + api "com.google.firebase:firebase-messaging" + // Segment Library implementation "com.segment.analytics.kotlin:android:1.14.2" // Segment's Firebase integration @@ -138,9 +141,6 @@ dependencies { implementation "com.braze:braze-segment-kotlin:1.4.2" implementation "com.braze:android-sdk-ui:30.2.0" - // Firebase Cloud Messaging Integration for Braze - implementation 'com.google.firebase:firebase-messaging-ktx:23.4.1' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efc65add4..a3921ac64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -105,9 +105,9 @@ android:foregroundServiceType="dataSync" tools:node="merge" /> - + diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 9781b0ca7..c8d4c9259 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import androidx.window.layout.WindowMetricsCalculator +import com.braze.support.toStringMap import io.branch.referral.Branch import io.branch.referral.Branch.BranchUniversalReferralInitListener import org.koin.android.ext.android.inject @@ -135,6 +136,11 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { addFragment(MainFragment.newInstance()) } } + + val extras = intent.extras + if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) { + handlePushNotification(extras) + } } viewModel.logoutUser.observe(this) { @@ -170,6 +176,11 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { super.onNewIntent(intent) this.intent = intent + val extras = intent?.extras + if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) { + handlePushNotification(extras) + } + if (viewModel.isBranchEnabled) { if (intent?.getBooleanExtra(BRANCH_FORCE_NEW_SESSION, false) == true) { Branch.sessionBuilder(this).withCallback { referringParams, error -> @@ -218,6 +229,11 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { } } + private fun handlePushNotification(data: Bundle) { + val deepLink = DeepLink(data.toStringMap()) + viewModel.makeExternalRoute(supportFragmentManager, deepLink) + } + companion object { const val TOP_INSET = "topInset" const val BOTTOM_INSET = "bottomInset" diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index 3fc49859f..20b3b0c97 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -1,5 +1,8 @@ package org.openedx.app +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.content.Context import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData @@ -10,14 +13,20 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.openedx.app.deeplink.DeepLink import org.openedx.app.deeplink.DeepLinkRouter -import org.openedx.app.system.notifier.AppNotifier -import org.openedx.app.system.notifier.LogoutEvent +import org.openedx.app.system.push.RefreshFirebaseTokenWorker +import org.openedx.app.system.push.SyncFirebaseTokenWorker import org.openedx.core.BaseViewModel import org.openedx.core.SingleEventLiveData import org.openedx.core.config.Config +import org.openedx.core.data.model.User import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.LogoutEvent +import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.FileUtil + +@SuppressLint("StaticFieldLeak") class AppViewModel( private val config: Config, private val notifier: AppNotifier, @@ -27,6 +36,7 @@ class AppViewModel( private val analytics: AppAnalytics, private val deepLinkRouter: DeepLinkRouter, private val fileUtil: FileUtil, + private val context: Context ) : BaseViewModel() { private val _logoutUser = SingleEventLiveData() @@ -42,20 +52,25 @@ class AppViewModel( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) - setUserId() + + val user = preferencesManager.user + + setUserId(user) + + if (user != null && preferencesManager.pushToken.isNotEmpty()) { + SyncFirebaseTokenWorker.schedule(context) + } + if (canResetAppDirectory) { resetAppDirectory() } + viewModelScope.launch { notifier.notifier.collect { event -> - if (event is LogoutEvent && System.currentTimeMillis() - logoutHandledAt > 5000) { - logoutHandledAt = System.currentTimeMillis() - preferencesManager.clear() - withContext(dispatcher) { - room.clearAllTables() - } - analytics.logoutEvent(true) - _logoutUser.value = Unit + if (event is SignInEvent && config.getFirebaseConfig().isCloudMessagingEnabled) { + SyncFirebaseTokenWorker.schedule(context) + } else if (event is LogoutEvent) { + handleLogoutEvent(event) } } } @@ -79,9 +94,30 @@ class AppViewModel( deepLinkRouter.makeRoute(fm, deepLink) } - private fun setUserId() { - preferencesManager.user?.let { + private fun setUserId(user: User?) { + user?.let { analytics.setUserIdForSession(it.id) } } + + private suspend fun handleLogoutEvent(event: LogoutEvent) { + if (System.currentTimeMillis() - logoutHandledAt > 5000) { + if (event.isForced) { + logoutHandledAt = System.currentTimeMillis() + preferencesManager.clear() + withContext(dispatcher) { + room.clearAllTables() + } + analytics.logoutEvent(true) + _logoutUser.value = Unit + } + + if (config.getFirebaseConfig().isCloudMessagingEnabled) { + RefreshFirebaseTokenWorker.schedule(context) + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancelAll() + } + } + } } diff --git a/app/src/main/java/org/openedx/app/data/api/NotificationsApi.kt b/app/src/main/java/org/openedx/app/data/api/NotificationsApi.kt new file mode 100644 index 000000000..9106944c3 --- /dev/null +++ b/app/src/main/java/org/openedx/app/data/api/NotificationsApi.kt @@ -0,0 +1,14 @@ +package org.openedx.app.data.api + +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +interface NotificationsApi { + @POST("/api/mobile/v4/notifications/create-token/") + @FormUrlEncoded + suspend fun syncFirebaseToken( + @Field("registration_id") token: String, + @Field("active") active: Boolean = true + ) +} diff --git a/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt index 4e88eec42..abf90d7a2 100644 --- a/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt @@ -4,13 +4,13 @@ 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.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.utils.TimeUtils import java.util.Date class AppUpgradeInterceptor( - private val appUpgradeNotifier: AppUpgradeNotifier + private val appNotifier: AppNotifier ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) @@ -21,15 +21,15 @@ class AppUpgradeInterceptor( runBlocking { when { responseCode == 426 -> { - appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) + appNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) } BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime > Date().time -> { - appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRecommendedEvent(latestAppVersion)) + appNotifier.send(AppUpgradeEvent.UpgradeRecommendedEvent(latestAppVersion)) } latestAppVersion.isNotEmpty() && BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime < Date().time -> { - appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) + appNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) } } } diff --git a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt index 3cc6b82ae..38305c007 100644 --- a/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt +++ b/app/src/main/java/org/openedx/app/data/networking/OauthRefreshTokenAuthenticator.kt @@ -9,8 +9,7 @@ import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONException import org.json.JSONObject -import org.openedx.app.system.notifier.AppNotifier -import org.openedx.app.system.notifier.LogoutEvent +import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.auth.data.api.AuthApi import org.openedx.auth.domain.model.AuthResponse import org.openedx.core.ApiConstants @@ -18,6 +17,7 @@ import org.openedx.core.ApiConstants.TOKEN_TYPE_JWT import org.openedx.core.BuildConfig import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.utils.TimeUtils import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -119,7 +119,7 @@ class OauthRefreshTokenAuthenticator( } runBlocking { - appNotifier.send(LogoutEvent()) + appNotifier.send(LogoutEvent(true)) } } @@ -128,7 +128,7 @@ class OauthRefreshTokenAuthenticator( JWT_USER_EMAIL_MISMATCH, -> { runBlocking { - appNotifier.send(LogoutEvent()) + appNotifier.send(LogoutEvent(true)) } } } diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt index e0b65af14..473340beb 100644 --- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt +++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt @@ -54,6 +54,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences remove(ACCESS_TOKEN) remove(REFRESH_TOKEN) remove(USER) + remove(ACCOUNT) remove(EXPIRES_IN) }.apply() } @@ -70,6 +71,12 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences } get() = getString(REFRESH_TOKEN) + override var pushToken: String + set(value) { + saveString(PUSH_TOKEN, value) + } + get() = getString(PUSH_TOKEN) + override var accessTokenExpiresAt: Long set(value) { saveLong(EXPIRES_IN, value) @@ -168,6 +175,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences companion object { private const val ACCESS_TOKEN = "access_token" private const val REFRESH_TOKEN = "refresh_token" + private const val PUSH_TOKEN = "push_token" private const val EXPIRES_IN = "expires_in" private const val USER = "user" private const val ACCOUNT = "account" diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt index 2b65a92b1..ac494df06 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt @@ -2,21 +2,58 @@ package org.openedx.app.deeplink class DeepLink(params: Map) { - val screenName = params[Keys.SCREEN_NAME.value] + private val screenName = params[Keys.SCREEN_NAME.value] + private val notificationType = params[Keys.NOTIFICATION_TYPE.value] val courseId = params[Keys.COURSE_ID.value] val pathId = params[Keys.PATH_ID.value] val componentId = params[Keys.COMPONENT_ID.value] val topicId = params[Keys.TOPIC_ID.value] val threadId = params[Keys.THREAD_ID.value] val commentId = params[Keys.COMMENT_ID.value] + val parentId = params[Keys.PARENT_ID.value] + val type = DeepLinkType.typeOf(screenName ?: notificationType ?: "") enum class Keys(val value: String) { SCREEN_NAME("screen_name"), + NOTIFICATION_TYPE("notification_type"), COURSE_ID("course_id"), PATH_ID("path_id"), COMPONENT_ID("component_id"), TOPIC_ID("topic_id"), THREAD_ID("thread_id"), - COMMENT_ID("comment_id") + COMMENT_ID("comment_id"), + PARENT_ID("parent_id"), + } +} + +enum class DeepLinkType(val type: String) { + DISCOVERY("discovery"), + DISCOVERY_COURSE_DETAIL("discovery_course_detail"), + DISCOVERY_PROGRAM_DETAIL("discovery_program_detail"), + COURSE_DASHBOARD("course_dashboard"), + COURSE_VIDEOS("course_videos"), + COURSE_DISCUSSION("course_discussion"), + COURSE_DATES("course_dates"), + COURSE_HANDOUT("course_handout"), + COURSE_ANNOUNCEMENT("course_announcement"), + COURSE_COMPONENT("course_component"), + PROGRAM("program"), + DISCUSSION_TOPIC("discussion_topic"), + DISCUSSION_POST("discussion_post"), + DISCUSSION_COMMENT("discussion_comment"), + PROFILE("profile"), + USER_PROFILE("user_profile"), + ENROLL("enroll"), + UNENROLL("unenroll"), + ADD_BETA_TESTER("add_beta_tester"), + REMOVE_BETA_TESTER("remove_beta_tester"), + FORUM_RESPONSE("forum_response"), + FORUM_COMMENT("forum_comment"), + NONE(""); + + companion object { + fun typeOf(type: String): DeepLinkType { + return entries.firstOrNull { it.type == type } ?: NONE + } } } diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt index 02bc5cd0e..31564edf7 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt @@ -14,6 +14,8 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.presentation.course.CourseViewMode import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.handouts.HandoutsType +import org.openedx.discovery.domain.interactor.DiscoveryInteractor +import org.openedx.discovery.domain.model.Course import org.openedx.discovery.presentation.catalog.WebViewLink import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel @@ -23,6 +25,7 @@ class DeepLinkRouter( private val config: Config, private val appRouter: AppRouter, private val corePreferences: CorePreferences, + private val discoveryInteractor: DiscoveryInteractor, private val courseInteractor: CourseInteractor, private val discussionInteractor: DiscussionInteractor ) : CoroutineScope { @@ -34,15 +37,14 @@ class DeepLinkRouter( get() = corePreferences.user != null fun makeRoute(fm: FragmentManager, deepLink: DeepLink) { - val screenName = deepLink.screenName ?: return - when (screenName) { + when (deepLink.type) { // Discovery - Screen.DISCOVERY.screenName -> { + DeepLinkType.DISCOVERY -> { navigateToDiscoveryScreen(fm = fm) return } - Screen.DISCOVERY_COURSE_DETAIL.screenName -> { + DeepLinkType.DISCOVERY_COURSE_DETAIL -> { navigateToCourseDetail( fm = fm, deepLink = deepLink @@ -50,13 +52,17 @@ class DeepLinkRouter( return } - Screen.DISCOVERY_PROGRAM_DETAIL.screenName -> { + DeepLinkType.DISCOVERY_PROGRAM_DETAIL -> { navigateToProgramDetail( fm = fm, deepLink = deepLink ) return } + + else -> { + //ignore + } } if (!isUserLoggedIn) { @@ -64,125 +70,162 @@ class DeepLinkRouter( return } - when (screenName) { - // Course - Screen.COURSE_DASHBOARD.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDashboard( + when (deepLink.type) { + // Program + DeepLinkType.PROGRAM -> { + navigateToProgram( fm = fm, deepLink = deepLink ) + return } - - Screen.COURSE_VIDEOS.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseVideos( - fm = fm, - deepLink = deepLink - ) + // Profile + DeepLinkType.PROFILE, + DeepLinkType.USER_PROFILE -> { + navigateToProfile(fm = fm) + return } - - Screen.COURSE_DATES.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDates( - fm = fm, - deepLink = deepLink - ) + else -> { + //ignore } + } - Screen.COURSE_DISCUSSION.screenName -> { + launch(Dispatchers.Main) { + val courseId = deepLink.courseId ?: return@launch navigateToDashboard(fm = fm) + val course = getCourseDetails(courseId) ?: return@launch navigateToDashboard(fm = fm) + if (!course.isEnrolled) { navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) + return@launch } - Screen.COURSE_HANDOUT.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseMore( - fm = fm, - deepLink = deepLink - ) - navigateToCourseHandout( - fm = fm, - deepLink = deepLink - ) - } + when (deepLink.type) { + // Course + DeepLinkType.COURSE_DASHBOARD, DeepLinkType.ENROLL, DeepLinkType.ADD_BETA_TESTER -> { + navigateToDashboard(fm = fm) + navigateToCourseDashboard( + fm = fm, + deepLink = deepLink, + courseTitle = course.name + ) + } - Screen.COURSE_ANNOUNCEMENT.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseMore( - fm = fm, - deepLink = deepLink - ) - navigateToCourseAnnouncement( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.UNENROLL, DeepLinkType.REMOVE_BETA_TESTER -> { + navigateToDashboard(fm = fm) + } - Screen.COURSE_COMPONENT.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDashboard( - fm = fm, - deepLink = deepLink - ) - navigateToCourseComponent( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.COURSE_VIDEOS -> { + navigateToDashboard(fm = fm) + navigateToCourseVideos( + fm = fm, + deepLink = deepLink + ) + } - // Program - Screen.PROGRAM.screenName -> { - navigateToProgram( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.COURSE_DATES -> { + navigateToDashboard(fm = fm) + navigateToCourseDates( + fm = fm, + deepLink = deepLink + ) + } - // Discussions - Screen.DISCUSSION_TOPIC.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionTopic( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.COURSE_DISCUSSION -> { + navigateToDashboard(fm = fm) + navigateToCourseDiscussion( + fm = fm, + deepLink = deepLink + ) + } - Screen.DISCUSSION_POST.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionPost( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.COURSE_HANDOUT -> { + navigateToDashboard(fm = fm) + navigateToCourseMore( + fm = fm, + deepLink = deepLink + ) + navigateToCourseHandout( + fm = fm, + deepLink = deepLink + ) + } - Screen.DISCUSSION_COMMENT.screenName -> { - navigateToDashboard(fm = fm) - navigateToCourseDiscussion( - fm = fm, - deepLink = deepLink - ) - navigateToDiscussionComment( - fm = fm, - deepLink = deepLink - ) - } + DeepLinkType.COURSE_ANNOUNCEMENT -> { + navigateToDashboard(fm = fm) + navigateToCourseMore( + fm = fm, + deepLink = deepLink + ) + navigateToCourseAnnouncement( + fm = fm, + deepLink = deepLink + ) + } - // Profile - Screen.PROFILE.screenName, - Screen.USER_PROFILE.screenName -> { - navigateToProfile(fm = fm) + DeepLinkType.COURSE_COMPONENT -> { + navigateToDashboard(fm = fm) + navigateToCourseDashboard( + fm = fm, + deepLink = deepLink, + courseTitle = course.name + ) + navigateToCourseComponent( + fm = fm, + deepLink = deepLink + ) + } + + // Discussions + DeepLinkType.DISCUSSION_TOPIC -> { + navigateToDashboard(fm = fm) + navigateToCourseDiscussion( + fm = fm, + deepLink = deepLink + ) + navigateToDiscussionTopic( + fm = fm, + deepLink = deepLink + ) + } + + DeepLinkType.DISCUSSION_POST -> { + navigateToDashboard(fm = fm) + navigateToCourseDiscussion( + fm = fm, + deepLink = deepLink + ) + navigateToDiscussionPost( + fm = fm, + deepLink = deepLink + ) + } + + DeepLinkType.DISCUSSION_COMMENT, DeepLinkType.FORUM_RESPONSE -> { + navigateToDashboard(fm = fm) + navigateToCourseDiscussion( + fm = fm, + deepLink = deepLink + ) + navigateToDiscussionResponse( + fm = fm, + deepLink = deepLink + ) + } + + DeepLinkType.FORUM_COMMENT -> { + navigateToDashboard(fm = fm) + navigateToCourseDiscussion( + fm = fm, + deepLink = deepLink + ) + navigateToDiscussionComment( + fm = fm, + deepLink = deepLink + ) + } + + else -> { + //ignore + } } } } @@ -247,12 +290,16 @@ class DeepLinkRouter( } } - private fun navigateToCourseDashboard(fm: FragmentManager, deepLink: DeepLink) { + private fun navigateToCourseDashboard( + fm: FragmentManager, + deepLink: DeepLink, + courseTitle: String + ) { deepLink.courseId?.let { courseId -> appRouter.navigateToCourseOutline( fm = fm, courseId = courseId, - courseTitle = "", + courseTitle = courseTitle, enrollmentMode = "" ) } @@ -427,53 +474,99 @@ class DeepLinkRouter( } } + private fun navigateToDiscussionResponse(fm: FragmentManager, deepLink: DeepLink) { + val courseId = deepLink.courseId + val topicId = deepLink.topicId + val threadId = deepLink.threadId + val commentId = deepLink.commentId + if (courseId == null || topicId == null || threadId == null || commentId == null) { + return + } + launch { + try { + discussionInteractor.getCourseTopics(courseId) + .find { it.id == topicId }?.let { topic -> + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionThread( + fm = fm, + action = DiscussionTopicsViewModel.TOPIC, + courseId = courseId, + topicId = topicId, + title = topic.name, + viewType = FragmentViewType.FULL_CONTENT + ) + } + } + val thread = discussionInteractor.getThread( + threadId, + courseId, + topicId + ) + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionComments( + fm = fm, + thread = thread + ) + } + val response = discussionInteractor.getResponse(commentId) + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionResponses( + fm = fm, + comment = response, + isClosed = false + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + private fun navigateToDiscussionComment(fm: FragmentManager, deepLink: DeepLink) { - deepLink.courseId?.let { courseId -> - deepLink.topicId?.let { topicId -> - deepLink.threadId?.let { threadId -> - deepLink.commentId?.let { commentId -> - launch { - try { - discussionInteractor.getCourseTopics(courseId) - .find { it.id == topicId }?.let { topic -> - launch(Dispatchers.Main) { - appRouter.navigateToDiscussionThread( - fm = fm, - action = DiscussionTopicsViewModel.TOPIC, - courseId = courseId, - topicId = topicId, - title = topic.name, - viewType = FragmentViewType.FULL_CONTENT - ) - } - } - val thread = discussionInteractor.getThread( - threadId, - courseId, - topicId - ) - launch(Dispatchers.Main) { - appRouter.navigateToDiscussionComments( - fm = fm, - thread = thread - ) - } - val commentsData = discussionInteractor.getThreadComment(commentId) - commentsData.results.firstOrNull()?.let { comment -> - launch(Dispatchers.Main) { - appRouter.navigateToDiscussionResponses( - fm = fm, - comment = comment, - isClosed = false - ) - } - } - } catch (e: Exception) { - e.printStackTrace() - } + val courseId = deepLink.courseId + val topicId = deepLink.topicId + val threadId = deepLink.threadId + val commentId = deepLink.commentId + val parentId = deepLink.parentId + if (courseId == null || topicId == null || threadId == null || commentId == null || parentId == null) { + return + } + launch { + try { + discussionInteractor.getCourseTopics(courseId) + .find { it.id == topicId }?.let { topic -> + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionThread( + fm = fm, + action = DiscussionTopicsViewModel.TOPIC, + courseId = courseId, + topicId = topicId, + title = topic.name, + viewType = FragmentViewType.FULL_CONTENT + ) } } + val thread = discussionInteractor.getThread( + threadId, + courseId, + topicId + ) + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionComments( + fm = fm, + thread = thread + ) } + val comment = discussionInteractor.getResponse(parentId) + launch(Dispatchers.Main) { + appRouter.navigateToDiscussionResponses( + fm = fm, + comment = comment, + isClosed = false + ) + } + } catch (e: Exception) { + e.printStackTrace() } } } @@ -504,4 +597,13 @@ class DeepLinkRouter( openTab = "PROFILE" ) } + + private suspend fun getCourseDetails(courseId: String): Course? { + return try { + discoveryInteractor.getCourseDetails(courseId) + } catch (e: Exception) { + e.printStackTrace() + null + } + } } diff --git a/app/src/main/java/org/openedx/app/deeplink/Screen.kt b/app/src/main/java/org/openedx/app/deeplink/Screen.kt deleted file mode 100644 index e877649e8..000000000 --- a/app/src/main/java/org/openedx/app/deeplink/Screen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.openedx.app.deeplink - -enum class Screen(val screenName: String) { - DISCOVERY("discovery"), - DISCOVERY_COURSE_DETAIL("discovery_course_detail"), - DISCOVERY_PROGRAM_DETAIL("discovery_program_detail"), - COURSE_DASHBOARD("course_dashboard"), - COURSE_VIDEOS("course_videos"), - COURSE_DISCUSSION("course_discussion"), - COURSE_DATES("course_dates"), - COURSE_HANDOUT("course_handout"), - COURSE_ANNOUNCEMENT("course_announcement"), - COURSE_COMPONENT("course_component"), - PROGRAM("program"), - DISCUSSION_TOPIC("discussion_topic"), - DISCUSSION_POST("discussion_post"), - DISCUSSION_COMMENT("discussion_comment"), - PROFILE("profile"), - USER_PROFILE("user_profile"), -} 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 1d1dc7e0c..9e3a1709d 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -18,7 +18,6 @@ import org.openedx.app.BuildConfig import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME -import org.openedx.app.system.notifier.AppNotifier import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter @@ -44,11 +43,11 @@ import org.openedx.core.system.AppCookieManager import org.openedx.core.system.CalendarManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.DiscoveryNotifier import org.openedx.core.system.notifier.DownloadNotifier import org.openedx.core.system.notifier.VideoNotifier +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.utils.FileUtil import org.openedx.course.data.storage.CoursePreferences import org.openedx.course.presentation.CourseAnalytics @@ -96,7 +95,6 @@ val appModule = module { single { CourseNotifier() } single { DiscussionNotifier() } single { ProfileNotifier() } - single { AppUpgradeNotifier() } single { DownloadNotifier() } single { VideoNotifier() } single { DiscoveryNotifier() } @@ -110,7 +108,7 @@ val appModule = module { single { get() } single { get() } single { get() } - single { DeepLinkRouter(get(), get(), get(), get(), get()) } + single { DeepLinkRouter(get(), get(), get(), get(), get(), get()) } single { NetworkConnection(get()) } diff --git a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt index c281d0465..aae32b433 100644 --- a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt +++ b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt @@ -3,6 +3,7 @@ package org.openedx.app.di import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.dsl.module +import org.openedx.app.data.api.NotificationsApi import org.openedx.app.data.networking.AppUpgradeInterceptor import org.openedx.app.data.networking.HandleErrorInterceptor import org.openedx.app.data.networking.HeadersInterceptor @@ -53,6 +54,7 @@ val networkingModule = module { single { provideApi(get()) } single { provideApi(get()) } single { provideApi(get()) } + single { provideApi(get()) } } diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index 11e70d4f8..3fb4667df 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -77,6 +77,7 @@ val screenModule = module { get(named("IODispatcher")), get(), get(), + get(), get() ) } diff --git a/app/src/main/java/org/openedx/app/system/notifier/AppEvent.kt b/app/src/main/java/org/openedx/app/system/notifier/AppEvent.kt deleted file mode 100644 index 1a6f750f4..000000000 --- a/app/src/main/java/org/openedx/app/system/notifier/AppEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.app.system.notifier - -interface AppEvent \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/system/notifier/LogoutEvent.kt b/app/src/main/java/org/openedx/app/system/notifier/LogoutEvent.kt deleted file mode 100644 index 209ac8815..000000000 --- a/app/src/main/java/org/openedx/app/system/notifier/LogoutEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.openedx.app.system.notifier - -class LogoutEvent : AppEvent diff --git a/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt b/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt new file mode 100644 index 000000000..60917940e --- /dev/null +++ b/app/src/main/java/org/openedx/app/system/push/OpenEdXFirebaseMessagingService.kt @@ -0,0 +1,95 @@ +package org.openedx.app.system.push + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.os.Build +import android.os.SystemClock +import androidx.core.app.NotificationCompat +import com.braze.push.BrazeFirebaseMessagingService +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import org.koin.android.ext.android.inject +import org.openedx.app.AppActivity +import org.openedx.app.R +import org.openedx.core.config.Config +import org.openedx.core.data.storage.CorePreferences + +class OpenEdXFirebaseMessagingService : FirebaseMessagingService() { + + private val preferences: CorePreferences by inject() + private val config: Config by inject() + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + if (BrazeFirebaseMessagingService.handleBrazeRemoteMessage(this, message)) { + // This Remote Message originated from Braze and a push notification was displayed. + // No further action is needed. + return + } else { + // This Remote Message did not originate from Braze. + handlePushNotification(message) + } + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + preferences.pushToken = token + if (preferences.user != null) { + SyncFirebaseTokenWorker.schedule(this) + } + } + + private fun handlePushNotification(message: RemoteMessage) { + val notification = message.notification ?: return + val data = message.data + + val intent = Intent(this, AppActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + data.forEach { (k, v) -> + intent.putExtra(k, v) + } + + val code = createId() + val pendingIntent = PendingIntent.getActivity( + this, + code, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val channelId = "${config.getPlatformName()}_channel" + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val notificationBuilder = NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(notification.title) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(notification.body)) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setContentIntent(pendingIntent) + + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Since android Oreo notification channel is needed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + config.getPlatformName(), + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(channel) + } + + notificationManager.notify(code, notificationBuilder.build()) + } + + private fun createId(): Int { + return SystemClock.uptimeMillis().toInt() + } +} diff --git a/app/src/main/java/org/openedx/app/system/push/RefreshFirebaseTokenWorker.kt b/app/src/main/java/org/openedx/app/system/push/RefreshFirebaseTokenWorker.kt new file mode 100644 index 000000000..0f37f36e3 --- /dev/null +++ b/app/src/main/java/org/openedx/app/system/push/RefreshFirebaseTokenWorker.kt @@ -0,0 +1,46 @@ +package org.openedx.app.system.push + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.openedx.core.data.storage.CorePreferences + +class RefreshFirebaseTokenWorker(context: Context, params: WorkerParameters) : + CoroutineWorker(context, params), + KoinComponent { + + private val preferences: CorePreferences by inject() + + override suspend fun doWork(): Result { + FirebaseMessaging.getInstance().deleteToken().await() + + val newPushToken = FirebaseMessaging.getInstance().getToken().await() + + preferences.pushToken = newPushToken + + return Result.success() + } + + companion object { + private const val WORKER_TAG = "RefreshFirebaseTokenWorker" + + fun schedule(context: Context) { + val work = OneTimeWorkRequest + .Builder(RefreshFirebaseTokenWorker::class.java) + .addTag(WORKER_TAG) + .build() + WorkManager.getInstance(context).beginUniqueWork( + WORKER_TAG, + ExistingWorkPolicy.REPLACE, + work + ).enqueue() + } + } +} diff --git a/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt b/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt new file mode 100644 index 000000000..ed4d841eb --- /dev/null +++ b/app/src/main/java/org/openedx/app/system/push/SyncFirebaseTokenWorker.kt @@ -0,0 +1,47 @@ +package org.openedx.app.system.push + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.openedx.app.data.api.NotificationsApi +import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.module.DownloadWorker + +class SyncFirebaseTokenWorker(context: Context, params: WorkerParameters) : + CoroutineWorker(context, params), + KoinComponent { + + private val preferences: CorePreferences by inject() + private val api: NotificationsApi by inject() + + override suspend fun doWork(): Result { + if (preferences.user != null && preferences.pushToken.isNotEmpty()) { + + api.syncFirebaseToken(preferences.pushToken) + + return Result.success() + } + return Result.failure() + } + + companion object { + private const val WORKER_TAG = "SyncFirebaseTokenWorker" + + fun schedule(context: Context) { + val work = OneTimeWorkRequest + .Builder(SyncFirebaseTokenWorker::class.java) + .addTag(WORKER_TAG) + .build() + WorkManager.getInstance(context).beginUniqueWork( + WORKER_TAG, + ExistingWorkPolicy.REPLACE, + work + ).enqueue() + } + } +} diff --git a/app/src/test/java/org/openedx/AppViewModelTest.kt b/app/src/test/java/org/openedx/AppViewModelTest.kt index 35a2d3d96..87a34e790 100644 --- a/app/src/test/java/org/openedx/AppViewModelTest.kt +++ b/app/src/test/java/org/openedx/AppViewModelTest.kt @@ -1,5 +1,6 @@ package org.openedx +import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -25,10 +26,11 @@ import org.openedx.app.deeplink.DeepLinkRouter import org.openedx.app.AppViewModel import org.openedx.app.data.storage.PreferencesManager import org.openedx.app.room.AppDatabase -import org.openedx.app.system.notifier.AppNotifier -import org.openedx.app.system.notifier.LogoutEvent +import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.core.config.Config +import org.openedx.core.config.FirebaseConfig import org.openedx.core.data.model.User +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.core.utils.FileUtil @ExperimentalCoroutinesApi @@ -46,6 +48,7 @@ class AppViewModelTest { private val analytics = mockk() private val fileUtil = mockk() private val deepLinkRouter = mockk() + private val context = mockk() private val user = User(0, "", "", "") @@ -65,17 +68,19 @@ class AppViewModelTest { every { preferencesManager.user } returns user every { notifier.notifier } returns flow { } every { preferencesManager.canResetAppDirectory } returns false - val viewModel = - AppViewModel( - config, - notifier, - room, - preferencesManager, - dispatcher, - analytics, - deepLinkRouter, - fileUtil - ) + every { preferencesManager.pushToken } returns "" + + val viewModel = AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + deepLinkRouter, + fileUtil, + context + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -89,7 +94,7 @@ class AppViewModelTest { @Test fun forceLogout() = runTest { every { notifier.notifier } returns flow { - emit(LogoutEvent()) + emit(LogoutEvent(true)) } every { preferencesManager.clear() } returns Unit every { analytics.setUserIdForSession(any()) } returns Unit @@ -97,17 +102,20 @@ class AppViewModelTest { every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit every { preferencesManager.canResetAppDirectory } returns false - val viewModel = - AppViewModel( - config, - notifier, - room, - preferencesManager, - dispatcher, - analytics, - deepLinkRouter, - fileUtil - ) + every { preferencesManager.pushToken } returns "" + every { config.getFirebaseConfig() } returns FirebaseConfig() + + val viewModel = AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + deepLinkRouter, + fileUtil, + context + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -122,8 +130,8 @@ class AppViewModelTest { @Test fun forceLogoutTwice() = runTest { every { notifier.notifier } returns flow { - emit(LogoutEvent()) - emit(LogoutEvent()) + emit(LogoutEvent(true)) + emit(LogoutEvent(true)) } every { preferencesManager.clear() } returns Unit every { analytics.setUserIdForSession(any()) } returns Unit @@ -131,17 +139,20 @@ class AppViewModelTest { every { room.clearAllTables() } returns Unit every { analytics.logoutEvent(true) } returns Unit every { preferencesManager.canResetAppDirectory } returns false - val viewModel = - AppViewModel( - config, - notifier, - room, - preferencesManager, - dispatcher, - analytics, - deepLinkRouter, - fileUtil - ) + every { preferencesManager.pushToken } returns "" + every { config.getFirebaseConfig() } returns FirebaseConfig() + + val viewModel = AppViewModel( + config, + notifier, + room, + preferencesManager, + dispatcher, + analytics, + deepLinkRouter, + fileUtil, + context + ) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) 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 b21c694da..6827d8e78 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 @@ -16,14 +16,14 @@ import org.openedx.core.extension.isEmailValid import org.openedx.core.extension.isInternetError import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent class RestorePasswordViewModel( private val interactor: AuthInteractor, private val resourceManager: ResourceManager, private val analytics: AuthAnalytics, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier ) : BaseViewModel() { private val _uiState = MutableLiveData() @@ -81,8 +81,10 @@ class RestorePasswordViewModel( private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _appUpgradeEvent.value = event + appNotifier.notifier.collect { event -> + if (event is AppUpgradeEvent) { + _appUpgradeEvent.value = event + } } } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index 7ebc5a569..9fbd8c2fe 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 @@ -32,8 +32,9 @@ import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent +import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.Logger import org.openedx.core.R as CoreRes @@ -42,7 +43,7 @@ class SignInViewModel( private val resourceManager: ResourceManager, private val preferencesManager: CorePreferences, private val validator: Validator, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier, private val analytics: AuthAnalytics, private val oAuthHelper: OAuthHelper, private val router: AuthRouter, @@ -107,6 +108,7 @@ class SignInViewModel( ) } ) + appNotifier.send(SignInEvent()) } catch (e: Exception) { if (e is EdxError.InvalidGrantException) { _uiMessage.value = @@ -125,8 +127,10 @@ class SignInViewModel( private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _appUpgradeEvent.value = event + appNotifier.notifier.collect { event -> + if (event is AppUpgradeEvent) { + _appUpgradeEvent.value = event + } } } } @@ -170,6 +174,7 @@ class SignInViewModel( _uiState.update { it.copy(loginSuccess = true) } setUserId() _uiState.update { it.copy(showProgress = false) } + appNotifier.send(SignInEvent()) } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt index 0f7873b78..7e60beb1d 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpUIState.kt @@ -2,7 +2,7 @@ package org.openedx.auth.presentation.signup import org.openedx.auth.domain.model.SocialAuthResponse import org.openedx.core.domain.model.RegistrationField -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.app.AppUpgradeEvent data class SignUpUIState( val allFields: List = emptyList(), 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 08bbce466..42b6bf2d1 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 @@ -31,7 +31,9 @@ import org.openedx.core.domain.model.RegistrationFieldType import org.openedx.core.domain.model.createHonorCodeField import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent +import org.openedx.core.system.notifier.app.SignInEvent import org.openedx.core.utils.Logger import org.openedx.core.R as coreR @@ -40,7 +42,7 @@ class SignUpViewModel( private val resourceManager: ResourceManager, private val analytics: AuthAnalytics, private val preferencesManager: CorePreferences, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier, private val agreementProvider: AgreementProvider, private val oAuthHelper: OAuthHelper, private val config: Config, @@ -175,6 +177,7 @@ class SignUpViewModel( ) setUserId() _uiState.update { it.copy(successLogin = true, isButtonLoading = false) } + appNotifier.send(SignInEvent()) } else { exchangeToken(socialAuth) } @@ -255,6 +258,7 @@ class SignUpViewModel( ) _uiState.update { it.copy(successLogin = true) } logger.d { "Social login (${socialAuth.authType.methodName}) success" } + appNotifier.send(SignInEvent()) } } @@ -274,8 +278,10 @@ class SignUpViewModel( private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _uiState.update { it.copy(appUpgradeEvent = event) } + appNotifier.notifier.collect { event -> + if (event is AppUpgradeEvent) { + _uiState.update { it.copy(appUpgradeEvent = event) } + } } } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt index 4c92b317f..0f040e908 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt @@ -26,7 +26,7 @@ import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -39,7 +39,7 @@ class RestorePasswordViewModelTest { private val resourceManager = mockk() private val interactor = mockk() private val analytics = mockk() - private val appUpgradeNotifier = mockk() + private val appNotifier = mockk() //region parameters @@ -60,7 +60,7 @@ class RestorePasswordViewModelTest { every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email) } returns invalidEmail every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_password) } returns invalidPassword - every { appUpgradeNotifier.notifier } returns emptyFlow() + every { appNotifier.notifier } returns emptyFlow() } @After @@ -71,14 +71,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset empty email validation error`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(emptyEmail) } returns true every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(emptyEmail) advanceUntilIdle() coVerify(exactly = 0) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -89,14 +89,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset invalid email validation error`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(invalidEmail) } returns true every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(invalidEmail) advanceUntilIdle() coVerify(exactly = 0) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -107,14 +107,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset validation error`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(correctEmail) } throws EdxError.ValidationException("error") every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -125,14 +125,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset no internet error`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(correctEmail) } throws UnknownHostException() every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -143,14 +143,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset unknown error`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(correctEmail) } throws Exception() every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -161,14 +161,14 @@ class RestorePasswordViewModelTest { @Test fun `unSuccess restore password`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(correctEmail) } returns false every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -180,14 +180,14 @@ class RestorePasswordViewModelTest { @Test fun `success restore password`() = runTest { val viewModel = - RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) + RestorePasswordViewModel(interactor, resourceManager, analytics, appNotifier) coEvery { interactor.passwordReset(correctEmail) } returns true every { analytics.logEvent(any(), any()) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val state = viewModel.uiState.value as? RestorePasswordUIState.Success val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index b36aabb10..d35e34040 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -38,7 +38,9 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.presentation.global.WhatsNewGlobalManager import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppEvent +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.SignInEvent import java.net.UnknownHostException import org.openedx.core.R as CoreRes @@ -56,7 +58,7 @@ class SignInViewModelTest { private val preferencesManager = mockk() private val interactor = mockk() private val analytics = mockk() - private val appUpgradeNotifier = mockk() + private val appNotifier = mockk() private val agreementProvider = mockk() private val oAuthHelper = mockk() private val router = mockk() @@ -78,7 +80,7 @@ class SignInViewModelTest { every { resourceManager.getString(CoreRes.string.core_error_unknown_error) } returns somethingWrong every { resourceManager.getString(R.string.auth_invalid_email_username) } returns invalidEmailOrUsername every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword - every { appUpgradeNotifier.notifier } returns emptyFlow() + every { appNotifier.notifier } returns emptyFlow() every { agreementProvider.getAgreement(true) } returns null every { config.isPreLoginExperienceEnabled() } returns false every { config.isSocialAuthEnabled() } returns false @@ -104,7 +106,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -137,7 +139,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -171,7 +173,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -204,7 +206,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -233,13 +235,14 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit every { analytics.logEvent(any(), any()) } returns Unit + coEvery { appNotifier.send(any()) } returns Unit val viewModel = SignInViewModel( interactor = interactor, resourceManager = resourceManager, preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -255,7 +258,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 1) { analytics.setUserIdForSession(any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val uiState = viewModel.uiState.value assertFalse(uiState.showProgress) assert(uiState.loginSuccess) @@ -275,7 +278,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -291,7 +294,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } verify(exactly = 1) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage val uiState = viewModel.uiState.value @@ -313,7 +316,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -328,7 +331,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } verify(exactly = 1) { analytics.logEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage @@ -351,7 +354,7 @@ class SignInViewModelTest { preferencesManager = preferencesManager, validator = validator, analytics = analytics, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -366,7 +369,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } verify(exactly = 1) { analytics.logEvent(any(), any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index f304f7363..5be80557c 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -44,7 +44,7 @@ import org.openedx.core.domain.model.AgreementUrls import org.openedx.core.domain.model.RegistrationField import org.openedx.core.domain.model.RegistrationFieldType import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier import java.net.UnknownHostException @ExperimentalCoroutinesApi @@ -59,7 +59,7 @@ class SignUpViewModelTest { private val preferencesManager = mockk() private val interactor = mockk() private val analytics = mockk() - private val appUpgradeNotifier = mockk() + private val appNotifier = mockk() private val agreementProvider = mockk() private val oAuthHelper = mockk() private val router = mockk() @@ -111,7 +111,7 @@ class SignUpViewModelTest { every { resourceManager.getString(R.string.core_error_invalid_grant) } returns "Invalid credentials" every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { appUpgradeNotifier.notifier } returns emptyFlow() + every { appNotifier.notifier } returns emptyFlow() every { agreementProvider.getAgreement(false) } returns null every { config.isSocialAuthEnabled() } returns false every { config.getAgreement(Locale.current.language) } returns AgreementUrls() @@ -133,7 +133,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -162,7 +162,7 @@ class SignUpViewModelTest { coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertEquals(true, viewModel.uiState.value.validationError) assertFalse(viewModel.uiState.value.successLogin) @@ -176,7 +176,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -210,7 +210,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.validationError) assertFalse(viewModel.uiState.value.successLogin) @@ -225,7 +225,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -248,7 +248,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.validationError) assertFalse(viewModel.uiState.value.successLogin) @@ -263,7 +263,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -298,7 +298,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.register(any()) } coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 2) { analytics.logEvent(any(), any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.validationError) assertFalse(viewModel.uiState.value.isButtonLoading) @@ -312,7 +312,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -326,7 +326,7 @@ class SignUpViewModelTest { viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.isLoading) assertEquals(noInternet, (deferred.await() as? UIMessage.SnackBarMessage)?.message) @@ -339,7 +339,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -353,7 +353,7 @@ class SignUpViewModelTest { viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assertFalse(viewModel.uiState.value.isLoading) assertEquals(somethingWrong, (deferred.await() as? UIMessage.SnackBarMessage)?.message) @@ -366,7 +366,7 @@ class SignUpViewModelTest { resourceManager = resourceManager, analytics = analytics, preferencesManager = preferencesManager, - appUpgradeNotifier = appUpgradeNotifier, + appNotifier = appNotifier, oAuthHelper = oAuthHelper, agreementProvider = agreementProvider, config = config, @@ -378,7 +378,7 @@ class SignUpViewModelTest { viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } //val fields = viewModel.uiState.value as? SignUpUIState.Fields diff --git a/build.gradle b/build.gradle index 250f56863..c163d3982 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ plugins { id 'com.android.application' version '8.4.0' apply false id 'com.android.library' version '8.4.0' apply false id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false - id 'com.google.gms.google-services' version '4.3.15' apply false - id "com.google.firebase.crashlytics" version "2.9.6" apply false + id 'com.google.gms.google-services' version '4.4.1' apply false + id "com.google.firebase.crashlytics" version "3.0.1" apply false } tasks.register('clean', Delete) { @@ -35,7 +35,7 @@ ext { media3_version = "1.1.1" youtubeplayer_version = "11.1.0" - firebase_version = "32.1.0" + firebase_version = "33.0.0" retrofit_version = '2.9.0' logginginterceptor_version = '4.9.1' diff --git a/core/src/main/java/org/openedx/core/AppUpdateState.kt b/core/src/main/java/org/openedx/core/AppUpdateState.kt index bf347cd29..6d6a8e357 100644 --- a/core/src/main/java/org/openedx/core/AppUpdateState.kt +++ b/core/src/main/java/org/openedx/core/AppUpdateState.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.compose.runtime.mutableStateOf -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.app.AppUpgradeEvent object AppUpdateState { var wasUpdateDialogDisplayed = false diff --git a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt index f9cacbd04..29495bae8 100644 --- a/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt +++ b/core/src/main/java/org/openedx/core/data/storage/CorePreferences.kt @@ -7,6 +7,7 @@ import org.openedx.core.domain.model.VideoSettings interface CorePreferences { var accessToken: String var refreshToken: String + var pushToken: String var accessTokenExpiresAt: Long var user: User? var videoSettings: VideoSettings diff --git a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt deleted file mode 100644 index 0f5a274d5..000000000 --- a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.openedx.core.system.notifier - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -class AppUpgradeNotifier { - - private val channel = MutableSharedFlow(replay = 0, extraBufferCapacity = 0) - - val notifier: Flow = channel.asSharedFlow() - - suspend fun send(event: AppUpgradeEvent) = channel.emit(event) - -} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/AppEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppEvent.kt new file mode 100644 index 000000000..7dd8f0407 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.app + +interface AppEvent diff --git a/app/src/main/java/org/openedx/app/system/notifier/AppNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt similarity index 67% rename from app/src/main/java/org/openedx/app/system/notifier/AppNotifier.kt rename to core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt index d0c579d8f..804d84a65 100644 --- a/app/src/main/java/org/openedx/app/system/notifier/AppNotifier.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppNotifier.kt @@ -1,4 +1,4 @@ -package org.openedx.app.system.notifier +package org.openedx.core.system.notifier.app import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -10,6 +10,10 @@ class AppNotifier { val notifier: Flow = channel.asSharedFlow() + suspend fun send(event: SignInEvent) = channel.emit(event) + suspend fun send(event: LogoutEvent) = channel.emit(event) -} \ No newline at end of file + suspend fun send(event: AppUpgradeEvent) = channel.emit(event) + +} diff --git a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt similarity index 61% rename from core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt rename to core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt index f99086a11..81dba6177 100644 --- a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt +++ b/core/src/main/java/org/openedx/core/system/notifier/app/AppUpgradeEvent.kt @@ -1,6 +1,6 @@ -package org.openedx.core.system.notifier +package org.openedx.core.system.notifier.app -sealed class AppUpgradeEvent { +sealed class AppUpgradeEvent: AppEvent { object UpgradeRequiredEvent : AppUpgradeEvent() class UpgradeRecommendedEvent(val newVersionName: String) : AppUpgradeEvent() } diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/LogoutEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/LogoutEvent.kt new file mode 100644 index 000000000..12154f3f1 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/app/LogoutEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.app + +class LogoutEvent(val isForced: Boolean) : AppEvent diff --git a/core/src/main/java/org/openedx/core/system/notifier/app/SignInEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/app/SignInEvent.kt new file mode 100644 index 000000000..340d04476 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/app/SignInEvent.kt @@ -0,0 +1,3 @@ +package org.openedx.core.system.notifier.app + +class SignInEvent : AppEvent 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 49d6b8cae..d83cd0c18 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 @@ -1,6 +1,8 @@ package org.openedx.course.presentation.container +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi @@ -97,6 +99,12 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } } + private val pushNotificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + Log.d(CourseContainerFragment::class.java.simpleName, "Permission granted: $granted") + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.preloadCourseStructure() @@ -130,6 +138,12 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { requireActivity().supportFragmentManager, viewModel.courseName ) + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pushNotificationPermissionLauncher.launch( + android.Manifest.permission.POST_NOTIFICATIONS + ) + } } } viewModel.errorMessage.observe(viewLifecycleOwner) { diff --git a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt index 136b914e8..7f1036e1d 100644 --- a/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt +++ b/dashboard/src/main/java/org/openedx/courses/presentation/DashboardGalleryViewModel.kt @@ -55,6 +55,8 @@ class DashboardGalleryViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + private var isLoading = false + init { collectDiscoveryNotifier() getCourses() @@ -64,6 +66,7 @@ class DashboardGalleryViewModel( viewModelScope.launch { try { if (networkConnection.isOnline()) { + isLoading = true val pageSize = if (windowSize.isTablet) { PAGE_SIZE_TABLET } else { @@ -92,11 +95,15 @@ class DashboardGalleryViewModel( } } finally { _updating.value = false + isLoading = false } } } fun updateCourses() { + if (isLoading) { + return + } _updating.value = true getCourses() } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt index e3d6abdf6..2d8e81d6b 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListFragment.kt @@ -82,7 +82,7 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.domain.model.EnrolledCourseData import org.openedx.core.domain.model.Progress import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.OfflineModeDialog import org.openedx.core.ui.WindowSize diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt index 82814561a..bfafc81c4 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardListViewModel.kt @@ -14,10 +14,10 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor class DashboardListViewModel( @@ -27,7 +27,7 @@ class DashboardListViewModel( private val resourceManager: ResourceManager, private val discoveryNotifier: DiscoveryNotifier, private val analytics: DashboardAnalytics, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -82,6 +82,9 @@ class DashboardListViewModel( } fun updateCourses() { + if (isLoading) { + return + } viewModelScope.launch { try { _updating.value = true @@ -167,8 +170,10 @@ class DashboardListViewModel( private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _appUpgradeEvent.value = event + appNotifier.notifier.collect { event -> + if (event is AppUpgradeEvent) { + _appUpgradeEvent.value = event + } } } } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index 6ca20a255..2a1131392 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -31,9 +31,9 @@ import org.openedx.core.domain.model.DashboardCourseList import org.openedx.core.domain.model.Pagination import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.DiscoveryNotifier +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor import java.net.UnknownHostException @@ -51,7 +51,7 @@ class DashboardViewModelTest { private val networkConnection = mockk() private val discoveryNotifier = mockk() private val analytics = mockk() - private val appUpgradeNotifier = mockk() + private val appNotifier = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -66,7 +66,7 @@ class DashboardViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { appUpgradeNotifier.notifier } returns emptyFlow() + every { appNotifier.notifier } returns emptyFlow() every { config.getApiHostURL() } returns "http://localhost:8000" } @@ -84,7 +84,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() @@ -92,7 +92,7 @@ class DashboardViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -108,7 +108,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws Exception() @@ -116,7 +116,7 @@ class DashboardViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -132,7 +132,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList @@ -141,7 +141,7 @@ class DashboardViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -156,7 +156,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( @@ -173,7 +173,7 @@ class DashboardViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -190,14 +190,14 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) advanceUntilIdle() coVerify(exactly = 0) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 1) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -214,7 +214,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() @@ -223,7 +223,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -242,7 +242,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) coEvery { interactor.getEnrolledCourses(any()) } throws Exception() @@ -251,7 +251,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -270,7 +270,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) viewModel.updateCourses() @@ -278,7 +278,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.updating.value == false) @@ -303,7 +303,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) viewModel.updateCourses() @@ -311,7 +311,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.updating.value == false) @@ -328,7 +328,7 @@ class DashboardViewModelTest { resourceManager, discoveryNotifier, analytics, - appUpgradeNotifier + appNotifier ) val mockLifeCycleOwner: LifecycleOwner = mockk() @@ -339,7 +339,7 @@ class DashboardViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt index ee99a5bb3..3a50ab707 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt @@ -63,7 +63,7 @@ import org.openedx.core.UIMessage import org.openedx.core.domain.model.Media import org.openedx.core.presentation.dialog.appupgrade.AppUpgradeDialogFragment import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.AuthButtonsPanel import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt index 271e05535..3f1098433 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt @@ -15,8 +15,8 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.Course @@ -26,7 +26,7 @@ class NativeDiscoveryViewModel( private val interactor: DiscoveryInteractor, private val resourceManager: ResourceManager, private val analytics: DiscoveryAnalytics, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier, private val corePreferences: CorePreferences, ) : BaseViewModel() { @@ -160,14 +160,13 @@ class NativeDiscoveryViewModel( @OptIn(FlowPreview::class) private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier + appNotifier.notifier .debounce(100) .collect { event -> when (event) { is AppUpgradeEvent.UpgradeRecommendedEvent -> { _appUpgradeEvent.value = event } - is AppUpgradeEvent.UpgradeRequiredEvent -> { _appUpgradeEvent.value = AppUpgradeEvent.UpgradeRequiredEvent } diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt index 898a227c3..7360ef131 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/NativeDiscoveryViewModelTest.kt @@ -27,7 +27,7 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.Pagination import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.domain.model.CourseList import java.net.UnknownHostException @@ -46,7 +46,7 @@ class NativeDiscoveryViewModelTest { private val interactor = mockk() private val networkConnection = mockk() private val analytics = mockk() - private val appUpgradeNotifier = mockk() + private val appNotifier = mockk() private val corePreferences = mockk() private val noInternet = "Slow or no internet connection" @@ -57,7 +57,7 @@ class NativeDiscoveryViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - every { appUpgradeNotifier.notifier } returns emptyFlow() + every { appNotifier.notifier } returns emptyFlow() every { corePreferences.user } returns null every { config.getApiHostURL() } returns "http://localhost:8000" every { config.isPreLoginExperienceEnabled() } returns false @@ -76,7 +76,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -85,7 +85,7 @@ class NativeDiscoveryViewModelTest { coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } - verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { appNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -101,7 +101,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -125,7 +125,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns false @@ -148,7 +148,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -178,7 +178,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -209,7 +209,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -234,7 +234,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -259,7 +259,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true @@ -290,7 +290,7 @@ class NativeDiscoveryViewModelTest { interactor, resourceManager, analytics, - appUpgradeNotifier, + appNotifier, corePreferences ) every { networkConnection.isOnline() } returns true diff --git a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt index 75a780d72..4d0343d69 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/api/DiscussionApi.kt @@ -1,5 +1,6 @@ package org.openedx.discussion.data.api +import org.json.JSONObject import org.openedx.core.data.model.BlocksCompletionBody import org.openedx.discussion.data.model.request.* import org.openedx.discussion.data.model.response.CommentResult @@ -50,11 +51,11 @@ interface DiscussionApi { @Query("requested_fields") requestedFields: List = listOf("profile_image") ): CommentsResponse - @GET("/api/discussion/v1/comments/{comment_id}") - suspend fun getThreadComment( - @Path("comment_id") commentId: String, - @Query("requested_fields") requestedFields: List = listOf("profile_image") - ): CommentsResponse + @Headers("Content-type: application/merge-patch+json") + @PATCH("/api/discussion/v1/comments/{response_id}/") + suspend fun getResponse( + @Path("response_id") responseId: String + ): CommentResult @GET("/api/discussion/v1/comments/") suspend fun getThreadQuestionComments( diff --git a/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt b/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt index a2248b036..711bab32c 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/model/response/CommentsResponse.kt @@ -76,9 +76,9 @@ data class CommentResult( authorLabel ?: "", createdAt, updatedAt, - rawBody, - renderedBody, - TextConverter.textToLinkedImageText(renderedBody), + rawBody ?: "", + renderedBody ?: "", + TextConverter.textToLinkedImageText(renderedBody ?: ""), abuseFlagged, voted, voteCount, diff --git a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt index 3ee4f74a5..95c603cf5 100644 --- a/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt +++ b/discussion/src/main/java/org/openedx/discussion/data/repository/DiscussionRepository.kt @@ -12,6 +12,7 @@ import org.openedx.discussion.data.model.request.ReportBody import org.openedx.discussion.data.model.request.ThreadBody import org.openedx.discussion.data.model.request.VoteBody import org.openedx.discussion.domain.model.CommentsData +import org.openedx.discussion.domain.model.DiscussionComment import org.openedx.discussion.domain.model.ThreadsData import org.openedx.discussion.domain.model.Topic @@ -81,10 +82,10 @@ class DiscussionRepository( return api.getThreadComments(threadId, page).mapToDomain() } - suspend fun getThreadComment( - commentId: String - ): CommentsData { - return api.getThreadComment(commentId).mapToDomain() + suspend fun getResponse( + responseId: String + ): DiscussionComment { + return api.getResponse(responseId).mapToDomain() } suspend fun getThreadQuestionComments( diff --git a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt index 561a75006..90960011c 100644 --- a/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt +++ b/discussion/src/main/java/org/openedx/discussion/domain/interactor/DiscussionInteractor.kt @@ -2,6 +2,7 @@ package org.openedx.discussion.domain.interactor import org.openedx.discussion.data.repository.DiscussionRepository import org.openedx.discussion.domain.model.CommentsData +import org.openedx.discussion.domain.model.DiscussionComment class DiscussionInteractor( private val repository: DiscussionRepository @@ -41,8 +42,8 @@ class DiscussionInteractor( suspend fun getThreadComments(threadId: String, page: Int) = repository.getThreadComments(threadId, page) - suspend fun getThreadComment(commentId: String): CommentsData = - repository.getThreadComment(commentId) + suspend fun getResponse(responseId: String): DiscussionComment = + repository.getResponse(responseId) suspend fun getThreadQuestionComments(threadId: String, endorsed: Boolean, page: Int) = repository.getThreadQuestionComments(threadId, endorsed, page) diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt index f94064d30..5e044ca46 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsScreenUI.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.window.Dialog import org.openedx.core.R import org.openedx.core.domain.model.AgreementUrls import org.openedx.core.presentation.global.AppData -import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.app.AppUpgradeEvent import org.openedx.core.ui.OpenEdXButton import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize diff --git a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt index 9715eb774..6e622e2cc 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/settings/SettingsViewModel.kt @@ -23,8 +23,9 @@ import org.openedx.core.module.DownloadWorkerController import org.openedx.core.presentation.global.AppData import org.openedx.core.system.AppCookieManager import org.openedx.core.system.ResourceManager -import org.openedx.core.system.notifier.AppUpgradeEvent -import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.system.notifier.app.AppNotifier +import org.openedx.core.system.notifier.app.AppUpgradeEvent +import org.openedx.core.system.notifier.app.LogoutEvent import org.openedx.core.utils.EmailUtil import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.domain.model.Configuration @@ -44,7 +45,7 @@ class SettingsViewModel( private val workerController: DownloadWorkerController, private val analytics: ProfileAnalytics, private val router: ProfileRouter, - private val appUpgradeNotifier: AppUpgradeNotifier, + private val appNotifier: AppNotifier, private val profileNotifier: ProfileNotifier, ) : BaseViewModel() { @@ -100,6 +101,7 @@ class SettingsViewModel( } } finally { cookieManager.clearWebViewCookie() + appNotifier.send(LogoutEvent(false)) _successLogout.emit(true) } } @@ -107,8 +109,10 @@ class SettingsViewModel( private fun collectAppUpgradeEvent() { viewModelScope.launch { - appUpgradeNotifier.notifier.collect { event -> - _appUpgradeEvent.value = event + appNotifier.notifier.collect { event -> + if (event is AppUpgradeEvent) { + _appUpgradeEvent.value = event + } } } }