Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
22d5c73
Chore: oauth 관련 파일 gitignore에 추가
YuGyeong98 Mar 6, 2024
470f1fe
Chore: fcm 관련 라이브러리 추가
YuGyeong98 Mar 6, 2024
3ffaade
Feat: fcm 유저 토큰 datastore에 저장 및 불러오기 구현
YuGyeong98 Mar 6, 2024
d8b00cf
Feat: 새로운 토큰이 생성될 때마다 datastore에 저장
YuGyeong98 Mar 6, 2024
dac1376
Chore: manifest에 FirebaseMessagingService 추가
YuGyeong98 Mar 6, 2024
8b34791
Feat: StudyGroup 데이터 클래스 구현
YuGyeong98 Mar 6, 2024
1c620ec
Feat: FcmMessage 데이터 클래스 구현
YuGyeong98 Mar 6, 2024
41bf8cc
Feat: FcmService 인터페이스 구현
YuGyeong98 Mar 6, 2024
89e40cd
Feat: Fcm 레포지토리 구현
YuGyeong98 Mar 6, 2024
887731e
Feat: fcm 레포지토리 application 클래스에 추가
YuGyeong98 Mar 6, 2024
151c119
Feat: 스터디 별로 notification key 파이어베이스에 저장 및 불러오는 함수 구현
YuGyeong98 Mar 6, 2024
83fa94f
Feat: 알림 그룹에 토큰 저장하는 함수 구현
YuGyeong98 Mar 7, 2024
47bdac1
Feat: 스터디 만들기 화면 뷰모델 notification 관련 함수 구현
YuGyeong98 Mar 7, 2024
4684189
Feat: 스터디 만들기 화면 알림 권한 요청 구현
YuGyeong98 Mar 7, 2024
71b82a2
Feat: 스터디 참여 다이얼로그 뷰모델 notification 관련 함수 구현
YuGyeong98 Mar 7, 2024
c8b8db4
Feat: 스터디 참여 다이얼로그 화면 알림 권한 요청 구현
YuGyeong98 Mar 7, 2024
77e7625
Feat: 메시지 전송 시 알림 전송하는 함수 구현
YuGyeong98 Mar 7, 2024
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
3 changes: 2 additions & 1 deletion app/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/build
google-services.json
google-services.json
/src/main/assets/service-account.json
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -46,6 +47,9 @@ android {
buildConfig = true
dataBinding = true
}
packaging {
resources.excludes.add("META-INF/*")
}
}

fun getProperty(key: String): String {
Expand All @@ -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")
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyFirebaseMessagingService"
android:directBootAware="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_launcher_foreground" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,6 +26,7 @@ class StudyApplication : Application() {
bookmarkRepository = BookmarkRepository()
myStudyDao = db.myStudyDao()
myStudyRepository = MyStudyRepository()
fcmRepository = FcmRepository(this)
}

override fun onTerminate() {
Expand All @@ -39,5 +41,6 @@ class StudyApplication : Application() {
lateinit var bookmarkRepository: BookmarkRepository
lateinit var myStudyDao: MyStudyDao
lateinit var myStudyRepository: MyStudyRepository
lateinit var fcmRepository: FcmRepository
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> = mapOf(),
)
Original file line number Diff line number Diff line change
@@ -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<String> = listOf(),
@SerialName("notification_key") val notificationKey: String = "",
)
Original file line number Diff line number Diff line change
@@ -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<Preferences> 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<String> {
return context.fcmTokenDataStore.data
.map { preferences ->
preferences[FCM_TOKEN_KEY] ?: ""
}
}

companion object {
private val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token")
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> {
return fcmService.updateStudyGroup(studyGroup)
}

suspend fun sendNotification(message: FcmMessage) {
fcmService.sendNotification(message)
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String>

@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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<String, Message>> = flow {
while (true) {
kotlin.runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ interface StudyService {
@Path("sid") sid: String
)

@PUT("studies/{sid}/notificationKey.json")
suspend fun addNotificationKey(
@Path("sid") sid: String,
@Body notificationKey: String
)

@GET("studies/{sid}/notificationKey.json")
suspend fun getNotificationKey(
@Path("sid") sid: String,
): String?

@PATCH("studies/{sid}/registrationIds.json")
suspend fun addRegistrationId(
@Path("sid") sid: String,
@Body registrationId: Map<String, Boolean>
)

companion object {
private const val BASE_URL = BuildConfig.FIREBASE_BASE_URL
private val contentType = "application/json".toMediaType()
Expand Down
Loading