diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bc3b5fa..b273990 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1bf2fe8..e7263e2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/cornellappdev/hustle/data/remote/HustleFirebaseMessagingService.kt b/app/src/main/java/com/cornellappdev/hustle/data/remote/HustleFirebaseMessagingService.kt
new file mode 100644
index 0000000..b6d2f75
--- /dev/null
+++ b/app/src/main/java/com/cornellappdev/hustle/data/remote/HustleFirebaseMessagingService.kt
@@ -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) {
+ 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
+ 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
+ )
+
+ // 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"
+ }
+}
diff --git a/app/src/main/java/com/cornellappdev/hustle/data/repository/FcmTokenRepository.kt b/app/src/main/java/com/cornellappdev/hustle/data/repository/FcmTokenRepository.kt
new file mode 100644
index 0000000..9934600
--- /dev/null
+++ b/app/src/main/java/com/cornellappdev/hustle/data/repository/FcmTokenRepository.kt
@@ -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
+ suspend fun updateFcmToken(token: String): Result
+}
+
+@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 = runCatching {
+ firebaseMessaging.token.await()
+ }
+
+ override suspend fun updateFcmToken(token: String): Result = runCatching {
+ // TODO: Send token to backend
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt b/app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt
index 13124a2..ce082d4 100644
--- a/app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt
+++ b/app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt
@@ -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
@@ -30,6 +36,12 @@ abstract class AppModule {
authRepositoryImpl: AuthRepositoryImpl
): AuthRepository
+ @Binds
+ abstract fun bindFcmTokenRepository(
+ notificationRepositoryImpl: FcmTokenRepositoryImpl
+ ): FcmTokenRepository
+
+
companion object {
@Provides
@Singleton
@@ -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)
+ }
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 906ede8..8c84f19 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }