Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0047] FCM #344

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
54 changes: 42 additions & 12 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,40 @@ class AppViewModel(

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
setUserId()

HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
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()
if (event is SignInEvent && config.getFirebaseConfig().isCloudMessagingEnabled) {
SyncFirebaseTokenWorker.schedule(context)
} else if (event is LogoutEvent && 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()
}
analytics.logoutEvent(true)
_logoutUser.value = Unit
}
}
}
Expand All @@ -79,8 +109,8 @@ class AppViewModel(
deepLinkRouter.makeRoute(fm, deepLink)
}

private fun setUserId() {
preferencesManager.user?.let {
private fun setUserId(user: User?) {
user?.let {
analytics.setUserIdForSession(it.id)
}
}
Expand Down
16 changes: 16 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,16 @@
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
)

HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
}
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
39 changes: 38 additions & 1 deletion app/src/main/java/org/openedx/app/deeplink/DeepLink.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,57 @@ package org.openedx.app.deeplink
class DeepLink(params: Map<String, String>) {

val screenName = params[Keys.SCREEN_NAME.value]
val notificationType = params[Keys.NOTIFICATION_TYPE.value]
HamzaIsrar12 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading