diff --git a/app/.gitignore b/app/.gitignore
index 65d12b95..98858169 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1,2 +1,3 @@
/build
-google-services.json
\ No newline at end of file
+google-services.json
+/src/main/assets/service-account.json
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1b7aa0e6..7a66deff 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,6 +21,7 @@ android {
versionCode = 1
versionName = "1.0"
buildConfigField("String", "FIREBASE_BASE_URL", getProperty("FIREBASE_BASE_URL"))
+ buildConfigField("String", "FIREBASE_SENDER_ID", getProperty("FIREBASE_SENDER_ID"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -46,6 +47,9 @@ android {
buildConfig = true
dataBinding = true
}
+ packaging {
+ resources.excludes.add("META-INF/*")
+ }
}
fun getProperty(key: String): String {
@@ -62,6 +66,9 @@ dependencies {
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-storage")
+ implementation("com.google.firebase:firebase-messaging")
+ implementation("com.google.firebase:firebase-messaging-directboot")
+ implementation("com.google.auth:google-auth-library-oauth2-http:1.23.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8f48849b..4a3e847a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,5 +26,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt b/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt
new file mode 100644
index 00000000..3db43455
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/MyFirebaseMessagingService.kt
@@ -0,0 +1,38 @@
+package com.sesac.developer_study_platform
+
+import android.util.Log
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.sesac.developer_study_platform.data.source.local.FcmTokenRepository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MyFirebaseMessagingService : FirebaseMessagingService() {
+
+ private val fcmTokenRepository = FcmTokenRepository(this)
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+
+ CoroutineScope(Dispatchers.IO).launch {
+ fcmTokenRepository.setToken(token)
+ }
+ }
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ // TODO(developer): Handle FCM messages here.
+ // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
+ Log.d("fcm", "From: ${remoteMessage.from}")
+
+ // Check if message contains a data payload.
+ if (remoteMessage.data.isNotEmpty()) {
+ Log.d("fcm", "Message data payload: ${remoteMessage.data}")
+ }
+
+ // Check if message contains a notification payload.
+ remoteMessage.notification?.let {
+ Log.d("fcm", "Message Notification Body: ${it.body}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/StudyApplication.kt b/app/src/main/java/com/sesac/developer_study_platform/StudyApplication.kt
index dd1423e1..e530db6c 100644
--- a/app/src/main/java/com/sesac/developer_study_platform/StudyApplication.kt
+++ b/app/src/main/java/com/sesac/developer_study_platform/StudyApplication.kt
@@ -7,6 +7,7 @@ import com.sesac.developer_study_platform.data.source.local.BookmarkDao
import com.sesac.developer_study_platform.data.source.local.BookmarkRepository
import com.sesac.developer_study_platform.data.source.local.MyStudyDao
import com.sesac.developer_study_platform.data.source.local.MyStudyRepository
+import com.sesac.developer_study_platform.data.source.remote.FcmRepository
import com.sesac.developer_study_platform.data.source.remote.GithubRepository
import com.sesac.developer_study_platform.data.source.remote.StudyRepository
@@ -25,6 +26,7 @@ class StudyApplication : Application() {
bookmarkRepository = BookmarkRepository()
myStudyDao = db.myStudyDao()
myStudyRepository = MyStudyRepository()
+ fcmRepository = FcmRepository(this)
}
override fun onTerminate() {
@@ -39,5 +41,6 @@ class StudyApplication : Application() {
lateinit var bookmarkRepository: BookmarkRepository
lateinit var myStudyDao: MyStudyDao
lateinit var myStudyRepository: MyStudyRepository
+ lateinit var fcmRepository: FcmRepository
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt b/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt
new file mode 100644
index 00000000..896cb275
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/FcmMessage.kt
@@ -0,0 +1,14 @@
+package com.sesac.developer_study_platform.data
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class FcmMessage(
+ val message: FcmMessageData,
+)
+
+@Serializable
+data class FcmMessageData(
+ val token: String = "",
+ val data: Map = mapOf(),
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/StudyGroup.kt b/app/src/main/java/com/sesac/developer_study_platform/data/StudyGroup.kt
new file mode 100644
index 00000000..9612b8bd
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/StudyGroup.kt
@@ -0,0 +1,12 @@
+package com.sesac.developer_study_platform.data
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class StudyGroup(
+ val operation: String = "",
+ @SerialName("notification_key_name") val notificationKeyName: String = "",
+ @SerialName("registration_ids") val registrationIdList: List = listOf(),
+ @SerialName("notification_key") val notificationKey: String = "",
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/local/FcmTokenRepository.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/local/FcmTokenRepository.kt
new file mode 100644
index 00000000..d500fd73
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/local/FcmTokenRepository.kt
@@ -0,0 +1,34 @@
+package com.sesac.developer_study_platform.data.source.local
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+private const val FCM_TOKEN_DATASTORE = "fcm_token_datastore"
+
+val Context.fcmTokenDataStore: DataStore by preferencesDataStore(FCM_TOKEN_DATASTORE)
+
+class FcmTokenRepository(private val context: Context) {
+
+ suspend fun setToken(token: String) {
+ context.fcmTokenDataStore.edit { preferences ->
+ preferences[FCM_TOKEN_KEY] = token
+ }
+ }
+
+ fun getToken(): Flow {
+ return context.fcmTokenDataStore.data
+ .map { preferences ->
+ preferences[FCM_TOKEN_KEY] ?: ""
+ }
+ }
+
+ companion object {
+ private val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmRepository.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmRepository.kt
new file mode 100644
index 00000000..2f3df458
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmRepository.kt
@@ -0,0 +1,18 @@
+package com.sesac.developer_study_platform.data.source.remote
+
+import android.content.Context
+import com.sesac.developer_study_platform.data.FcmMessage
+import com.sesac.developer_study_platform.data.StudyGroup
+
+class FcmRepository(context: Context) {
+
+ private val fcmService = FcmService.create(context)
+
+ suspend fun updateStudyGroup(studyGroup: StudyGroup): Map {
+ return fcmService.updateStudyGroup(studyGroup)
+ }
+
+ suspend fun sendNotification(message: FcmMessage) {
+ fcmService.sendNotification(message)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmService.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmService.kt
new file mode 100644
index 00000000..86943033
--- /dev/null
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/FcmService.kt
@@ -0,0 +1,63 @@
+package com.sesac.developer_study_platform.data.source.remote
+
+import android.content.Context
+import com.google.auth.oauth2.GoogleCredentials
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import com.sesac.developer_study_platform.BuildConfig
+import com.sesac.developer_study_platform.data.FcmMessage
+import com.sesac.developer_study_platform.data.StudyGroup
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface FcmService {
+
+ @POST("fcm/notification")
+ suspend fun updateStudyGroup(
+ @Body studyGroup: StudyGroup
+ ): Map
+
+ @POST("v1/projects/developer-study-platform/messages:send")
+ suspend fun sendNotification(
+ @Body message: FcmMessage
+ )
+
+ companion object {
+ private const val BASE_URL = "https://fcm.googleapis.com"
+ private const val SCOPES = "https://www.googleapis.com/auth/firebase.messaging"
+ private val contentType = "application/json".toMediaType()
+ private val jsonConfig = Json { ignoreUnknownKeys = true }
+
+ private fun getClient(context: Context): OkHttpClient {
+ return OkHttpClient.Builder().addInterceptor { chain ->
+ val builder = chain.request().newBuilder().apply {
+ addHeader("access_token_auth", "true")
+ addHeader("Authorization", "Bearer ${getAccessToken(context)}")
+ addHeader("project_id", BuildConfig.FIREBASE_SENDER_ID)
+ }
+ chain.proceed(builder.build())
+ }.build()
+ }
+
+ private fun getAccessToken(context: Context): String {
+ val inputStream = context.resources.assets.open("service-account.json")
+ val googleCredential = GoogleCredentials
+ .fromStream(inputStream)
+ .createScoped(listOf(SCOPES))
+ googleCredential.refresh()
+ return googleCredential.accessToken.tokenValue
+ }
+
+ fun create(context: Context): FcmService {
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(getClient(context))
+ .addConverterFactory(jsonConfig.asConverterFactory(contentType))
+ .build()
+ .create(FcmService::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt
index 59eb89b1..55cd3e09 100644
--- a/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt
+++ b/app/src/main/java/com/sesac/developer_study_platform/data/source/remote/StudyRepository.kt
@@ -90,6 +90,18 @@ class StudyRepository {
studyService.deleteUserStudy(uid, sid)
}
+ suspend fun addNotificationKey(sid: String, notificationKey: String) {
+ studyService.addNotificationKey(sid, notificationKey)
+ }
+
+ suspend fun getNotificationKey(sid: String): String? {
+ return studyService.getNotificationKey(sid)
+ }
+
+ suspend fun addRegistrationId(sid: String, registrationId: String) {
+ studyService.addRegistrationId(sid, mapOf(registrationId to true))
+ }
+
fun getMessageList(sid: String): Flow