Skip to content

Commit

Permalink
feat: [FC-0047] FCM (openedx#344)
Browse files Browse the repository at this point in the history
* feat: fcm

* fix: address feedback
  • Loading branch information
volodymyr-chekyrta authored Jun 25, 2024
1 parent 7ff0ff4 commit 948277a
Show file tree
Hide file tree
Showing 50 changed files with 839 additions and 405 deletions.
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@
android:foregroundServiceType="dataSync"
tools:node="merge" />

<!-- Braze init -->
<!-- FirebaseMessaging init -->
<service
android:name="com.braze.push.BrazeFirebaseMessagingService"
android:name=".system.push.OpenEdXFirebaseMessagingService"
android:enabled="${fcmEnabled}"
android:exported="false">
<intent-filter>
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/org/openedx/app/AppActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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"
Expand Down
62 changes: 49 additions & 13 deletions app/src/main/java/org/openedx/app/AppViewModel.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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<Unit>()
Expand All @@ -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)
}
}
}
Expand All @@ -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()
}
}
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/openedx/app/data/api/NotificationsApi.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ 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
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
Expand Down Expand Up @@ -119,7 +119,7 @@ class OauthRefreshTokenAuthenticator(
}

runBlocking {
appNotifier.send(LogoutEvent())
appNotifier.send(LogoutEvent(true))
}
}

Expand All @@ -128,7 +128,7 @@ class OauthRefreshTokenAuthenticator(
JWT_USER_EMAIL_MISMATCH,
-> {
runBlocking {
appNotifier.send(LogoutEvent())
appNotifier.send(LogoutEvent(true))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences
remove(ACCESS_TOKEN)
remove(REFRESH_TOKEN)
remove(USER)
remove(ACCOUNT)
remove(EXPIRES_IN)
}.apply()
}
Expand All @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
41 changes: 39 additions & 2 deletions app/src/main/java/org/openedx/app/deeplink/DeepLink.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,58 @@ package org.openedx.app.deeplink

class DeepLink(params: Map<String, String>) {

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
}
}
}
Loading

0 comments on commit 948277a

Please sign in to comment.