Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ dependencies {
implementation(libs.google.id)
// Firebase Analytics
implementation(libs.firebase.analytics)
// Firebase Messaging
implementation(libs.firebase.messaging)
// Coil Image Loading
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".data.remote.HustleFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.cornellappdev.hustle.data.remote

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.cornellappdev.hustle.MainActivity
import com.cornellappdev.hustle.R
import com.cornellappdev.hustle.data.repository.FcmTokenRepository
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class HustleFirebaseMessagingService : FirebaseMessagingService() {

@Inject
lateinit var fcmTokenRepository: FcmTokenRepository

@Inject
lateinit var applicationScope: CoroutineScope

private val notificationManager by lazy {
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}

override fun onCreate() {
super.onCreate()
createNotificationChannel()
}

override fun onNewToken(token: String) {
super.onNewToken(token)
applicationScope.launch {
fcmTokenRepository.updateFcmToken(token)
}
}

override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
//TODO: Handle data messages for different notification types (eg. chat)
val title = message.notification?.title ?: message.data["title"] ?: "Hustle"
val body = message.notification?.body ?: message.data["body"] ?: ""

showNotification(title, body, message.data)
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Hustle Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
enableVibration(true)
}
notificationManager.createNotificationChannel(channel)
}
}

private fun showNotification(title: String, body: String, data: Map<String, String>) {
val intent = Intent(this, MainActivity::class.java).apply {
// new task for launching activity from service and clear top to avoid multiple activity instances
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you decide on these flags, perhaps leave a comment about this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admittedly, I didn't think too much about these flags and their functionality and mainly followed example code, but I believe the new task is to prevent crashes if the app is launched from a service in the background, and the clear top flag is to prevent duplicate instances of activities if say we minimized the app and then clicked on the notification to open it. I'll probably add a comment before merging

data.forEach { (key, value) -> putExtra(key, value) }
}

// update current to ensure each notification has correct data and immutable for security best practices
val pendingIntent = PendingIntent.getActivity(
this,
data.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with these flags?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar situation as above. Update current flag is to make sure the data associated with the notification is the correct and current one (eg. potential hash collision with pending intent request codes for 2 separate messages) and immutable flag is required by Android for security reasons I believe to make sure malicious apps can't hijack your notifications or something like that. Will also add comment before merging

)

// TODO: Replace with actual app icon
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(R.drawable.ic_google)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.build()

notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}

//TODO: Add more channels for different notification types
companion object {
private const val CHANNEL_ID = "hustle_notifications_general"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.cornellappdev.hustle.data.repository

import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton

interface FcmTokenRepository {
suspend fun getFcmToken(): Result<String>
suspend fun updateFcmToken(token: String): Result<Unit>
}

@Singleton
class FcmTokenRepositoryImpl @Inject constructor(
private val firebaseMessaging: FirebaseMessaging,
// TODO: Add your API service to send token to backend
) : FcmTokenRepository {

override suspend fun getFcmToken(): Result<String> = runCatching {
firebaseMessaging.token.await()
}

override suspend fun updateFcmToken(token: String): Result<Unit> = runCatching {
// TODO: Send token to backend
}
}
24 changes: 24 additions & 0 deletions app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import com.cornellappdev.hustle.data.repository.AuthRepository
import com.cornellappdev.hustle.data.repository.AuthRepositoryImpl
import com.cornellappdev.hustle.data.repository.ExampleRepository
import com.cornellappdev.hustle.data.repository.ExampleRepositoryImpl
import com.cornellappdev.hustle.data.repository.FcmTokenRepository
import com.cornellappdev.hustle.data.repository.FcmTokenRepositoryImpl
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.messaging.FirebaseMessaging
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton

@Module
Expand All @@ -30,6 +36,12 @@ abstract class AppModule {
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository

@Binds
abstract fun bindFcmTokenRepository(
notificationRepositoryImpl: FcmTokenRepositoryImpl
): FcmTokenRepository


companion object {
@Provides
@Singleton
Expand All @@ -40,5 +52,17 @@ abstract class AppModule {
fun provideCredentialManager(@ApplicationContext context: Context): CredentialManager {
return CredentialManager.create(context)
}

@Provides
@Singleton
fun provideFirebaseMessaging(): FirebaseMessaging {
return FirebaseMessaging.getInstance()
}

@Provides
@Singleton
fun provideApplicationScope(): CoroutineScope {
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-auth = { group = "com.google.firebase", name = "firebase-auth" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
credential-manager = { group = "androidx.credentials", name = "credentials", version.ref = "credentialManager" }
credential-manager-play-services = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialManager" }
google-id = { module = "com.google.android.libraries.identity.googleid:googleid", version.ref = "googleId" }
Expand Down