일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 막내의막무가내
- 막내의막무가내 알고리즘
- 막내의막무가내 안드로이드 에러 해결
- 부스트코스에이스
- 부스트코스
- 막내의막무가내 목표 및 회고
- flutter network call
- 주엽역 생활맥주
- 안드로이드 Sunflower 스터디
- 막내의막무가내 플러터
- 프로그래머스 알고리즘
- 막내의막무가내 코틀린 안드로이드
- 프래그먼트
- 주택가 잠실새내
- 막내의막무가내 일상
- 막내의막무가내 프로그래밍
- 막내의막무가내 안드로이드
- 막내의막무가내 플러터 flutter
- 안드로이드
- 안드로이드 sunflower
- 막내의 막무가내 알고리즘
- Fragment
- 막내의막무가내 코틀린
- 막내의막무가내 안드로이드 코틀린
- 막내의막무가내 rxjava
- 막내의 막무가내
- 막내의막무가내 SQL
- 막내의막무가내 코볼 COBOL
- 2022년 6월 일상
- 막무가내
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] FCM 서버 통신과 노티피케이션 (feat.FCM, Notification, Retrofit2, 이전글 업데이트 버전 2020) 본문
[안드로이드] FCM 서버 통신과 노티피케이션 (feat.FCM, Notification, Retrofit2, 이전글 업데이트 버전 2020)
막무가내막내 2020. 10. 10. 16:30
[2021-05-10 업데이트]
youngest-programming.tistory.com/103
youngest-programming.tistory.com/76
예전에 FCM 관련글을 작성한적이 있는데 해당 글에서 질문댓글이 많이 달렸었습니다. AsyncTask를 사용 등 비효울적이거나 오래되어 이상한 부분도 많아 업데이트를 조금 해놔야겠다는 생각이 들어 포스팅합니다. ㅎㅎ
FCM을 쏴주는 방법을 중심으로 포스팅해보겠습니다.
먼저 상대방의 FCM 토큰을 알고있다고 가정하고(데이터베이스 같은데 저장해놔야겠지요) 파이어베이스와 연결이 되어있다고 가정합니다. 파이어베이스 연결하는 내용은 찾아보시면 많이 나올겁니당
들어가기 앞서 이번 글의 요약입니다.
1. 상대방의 FCM 토큰을 알고있다.
2. FCM Server 통신규약에 맞게 Retrofit2 통신을 한다.
3. 내가 정한 상대방에게 내가 보낸 FCM 메시지가 수신된다.
4. 수신된 데이터를 바탕으로 노티피케이션(알림)을 띄우든 로직을 처리한다.
먼저 필요 라이브러리입니다. (빠진게 있다면 댓글 부탁드립니다.)
이 부분은 다른 블로그나 안드로이드 스튜디오에서 tools 탭에 들어가 쉽게 추가하는 방법이 있으니 찾아보시길 바랍니다. ㅠ (manifest 도 수정하고 몇개 더있을겁니다.)
implementation 'com.google.firebase:firebase-messaging:20.2.4'
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
implementation "com.squareup.retrofit2:adapter-rxjava2:2.8.1"
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.3'
}
<uses-permission android:name="android.permission.INTERNET" />
<service
android:name=".service.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
먼저 자신의 FCM토큰은 다음과 같이 받을 수 있습니다. (참고 :
- 앱에서 인스턴스 ID 삭제
- 새 기기에서 앱 복원
- 사용자가 앱 삭제/재설치
- 사용자가 앱 데이터 소거
위와 같은 이유로 매번 바뀌니 업데이트 해주어야합니다.)
FirebaseInstanceId.getInstance().instanceId
.addOnCompleteListener(object : OnCompleteListener<InstanceIdResult?> {
override fun onComplete(task: Task<InstanceIdResult?>) {
if (!task.isSuccessful) {
return
}
// Get new Instance ID token
task.result?.let {
fcm = it.token
}
}
})
이제 보낼 FCM 아이디를 안다는 가정하에 이어서 작성해보겠습니다.
우선 파이어베이스 FCM 서버에 Retrofit2으로 통신을 하여 FCM을 상대방에게 쏴주어야합니다.
제 방식대로는 다음과 같이 구현했습니다.
1. 먼저 BaseUrl 을 담는 클래스를 만듭니다. (FCM_URL)
object ApiClient {
const val TOUR_BASE_URL = "http://api.visitkorea.or.kr/openapi/service/"
const val FCM_URL = "https://fcm.googleapis.com/"
}
2. FCM 인터페이스를 만듭니다 url은 fcm/send 입니다. RxKotlin 을 사용할 것이기 때문에 Return 타입을 Single로 해놨습니다. 만약 사용을 안하시면 ResponseBody 같은거로 하시면 될겁니다
package com.mtjin.nomoneytrip.api
import com.mtjin.nomoneytrip.service.NotificationBody
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.POST
interface FcmInterface {
@POST("fcm/send")
fun sendNotification(
@Body notification: NotificationBody
): Single<ResponseBody>
}
data class NotificationBody(val to: String, val data: NotificationData)
data class NotificationData(
val title: String,
val message: String,
val productId: String,
val uuid: String,
val alarmTimestamp: Long,
val alarmCase: Int,
var isScheduled: String = "false",
var reservationId: String = ""
)
여기서 저는 notification은 사용하지 않고 필요한 실질적인 데이터들을 data 에 담고 to에는 상대방의 FCM 토큰을 담아 줄 것입니다. (data, notification 을 페이로드라고 부릅니다. 밑에 참고 내용에 링크와 내용 추가로 적어놨습니다)
//json 예시
{
"message":{
"token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
"notification":{
"title":"Portugal vs. Denmark",
"body":"great match!"
},
"data" : {
"Nick" : "Mario",
"Room" : "PortugalVSDenmark"
}
}
}
추가로 주의해야할 점은 to, data 는 FCM 서버와 통신하는 json 규칙이기 때문에 변경하시면 안됩니다. 만약 이름을 바꾸시고 싶으시면 @SerializedName 로 to , data라고 설정해주셔야합니다. 예약어는 밑에 표를 참고하시면 됩니다.
참고로 to, data 외에도 'from', 'notification', 'message_type', 등 필드도(예약어) 있는데 이에대한 자세한 내용도 밑을 참고해주세요. 일단은 to 에는 토큰! data에는 내가 보내줄 데이터! 라고 생각하시면 편합니다.
추가 참고용 남깁니다. 읽어보시면 도움이 크게 될겁니당
firebase.google.com/docs/cloud-messaging/android/topic-messaging
firebase.google.com/docs/cloud-messaging/concept-options
youngest-programming.tistory.com/103
3. 저는 Koin 라이브러리를 사용해서 다음과 같이 레트로핏을 생성하는 부분을 DI 해놨는데 이 부분은 원하시는 방법으로 하시면 됩니다. (fcm 작성된 부분만 보시면 됩니다.)
여기서 FCM KEY는 전 const val 로 전역으로 저장해놨는데
본인의 FCM KEY는 파이어베이스 콘솔 들어가서 프로젝트 설정에서 클라우드메시징을 보시면 볼 수 있을겁니다. 그것을 사용하면 됩니당
package com.mtjin.nomoneytrip.module
import com.mtjin.nomoneytrip.api.ApiClient
import com.mtjin.nomoneytrip.api.ApiInterface
import com.mtjin.nomoneytrip.api.FcmInterface
import com.mtjin.nomoneytrip.utils.FCM_KEY
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
val apiModule: Module = module {
single<ApiInterface>(named("tour")) { get<Retrofit>(named("tour")).create(ApiInterface::class.java) }
single<FcmInterface>(named("fcm")) { get<Retrofit>(named("fcm")).create(FcmInterface::class.java) }
single<Retrofit>(named("fcm")) {
Retrofit.Builder()
.baseUrl(ApiClient.FCM_URL)
.client(get(named("fcm")))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(get<GsonConverterFactory>())
.build()
}
single<Retrofit>(named("tour")) {
Retrofit.Builder()
.baseUrl(ApiClient.TOUR_BASE_URL)
.client(get(named("tour")))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(get<GsonConverterFactory>())
.build()
}
single<GsonConverterFactory> { GsonConverterFactory.create() }
single<OkHttpClient>(named("tour")) {
OkHttpClient.Builder()
.run {
addInterceptor(get<Interceptor>(named("tour")))
build()
}
}
single<OkHttpClient>(named("fcm")) {
OkHttpClient.Builder()
.run {
addInterceptor(get<Interceptor>(named("fcm")))
build()
}
}
single<Interceptor>(named("fcm")) {
Interceptor { chain ->
with(chain) {
val newRequest = request().newBuilder()
.addHeader("Authorization", "key=$FCM_KEY")
.addHeader("Content-Type", "application/json")
.build()
proceed(newRequest)
}
}
}
single<Interceptor>(named("tour")) {
Interceptor { chain ->
with(chain) {
val newRequest = request().newBuilder()
.build()
proceed(newRequest)
}
}
}
}
4. 다음은 FCM을 Rx와 Retrofit2를 사용하여 쏴주는 예제 코드입니다. Rx를 사용안하시면 enqueue() 로 하시면 될겁니다.
fcmInterface.sendNotification(
NotificationBody(
masterProduct.user.fcm,
NotificationData(
title = product.title,
message = message,
productId = product.id,
uuid = masterProduct.user.id,
alarmTimestamp = getTimestamp(),
alarmCase = case,
isScheduled = "false",
reservationId = masterProduct.reservation.id
)
)
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = { Log.d(TAG, "FCM SendNoti Success") },
onError = { Log.d(TAG, "FCM SendNoti Fail") }
)
5. 그럼 자신이 쏴준 FCM이 상대방의 Service를 통해 수신이 될 거고 처리해줘서 노티피케이션을 띄워주든지 하면 됩니다. 밑은 예제코드입니다. Gson을 사용하면 json<->Data class 변환이 쉬울겁니다.
다음과 FirebaseMessagingService() 객체에서 onMesseageReceived() 로 FCM 데이터가 수신이 되고 RemoteMessage의 data Field 에서 보낸 데이터들의 필드 값들을 꺼내보시면 됩니다.
그리고 보낸 데이터나 상황에 맞게 노티피케이션(알림)을 띄워주셔도 되고요.
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val to = remoteMessage.to.toString()
if (remoteMessage.data["isScheduled"] == "false") { // 즉시 전송
sendNotification(remoteMessage)
} else { // 예약전송
}
}
datam notification 페이로드 유무에 따른 처리 예제입니다.
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(TAG, "From: ${remoteMessage.from}")
// Check if message contains a data payload.
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
if (/* Check if data needs to be processed by long running job */ true) {
// For long-running tasks (10 seconds or more) use WorkManager.
scheduleJob()
} else {
// Handle message within 10 seconds
handleNow()
}
}
// Check if message contains a notification payload.
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Body: ${it.body}")
}
// Also if you intend on generating your own notifications as a result of a received FCM
// message, here is where that should be initiated. See sendNotification method below.
}
밑은 전체코드인데 참고용으로 첨부했습니다. WorkManager를 추가로 사용했습니다.
package com.mtjin.nomoneytrip.service
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.mtjin.nomoneytrip.utils.*
import java.util.concurrent.TimeUnit
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val to = remoteMessage.to.toString()
if (remoteMessage.data["isScheduled"] == "false") { // 즉시 전송
sendNotification(remoteMessage)
} else { // 예약전송
}
}
private fun sendNotification(remoteMessage: RemoteMessage) {
val notificationData = Data.Builder()
.putString(EXTRA_NOTIFICATION_TITLE, remoteMessage.data["title"].toString())
.putString(EXTRA_NOTIFICATION_MESSAGE, remoteMessage.data["message"].toString())
.putString(EXTRA_ALARM_PRODUCT_ID, remoteMessage.data["productId"].toString())
.putString(EXTRA_ALARM_USER_ID, remoteMessage.data["uuid"].toString())
.putLong(EXTRA_ALARM_TIMESTAMP, remoteMessage.data["alarmTimestamp"]?.toLong()!!)
.putInt(EXTRA_ALARM_CASE, remoteMessage.data["alarmCase"]!!.toInt())
.putString(EXTRA_RESERVATION_ID, remoteMessage.data["reservationId"].toString())
.build()
val workRequest =
OneTimeWorkRequestBuilder<ScheduledWorker>()
.setInputData(notificationData)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30000, TimeUnit.MILLISECONDS)
.build()
val workManager = WorkManager.getInstance(applicationContext)
workManager.enqueue(workRequest)
}
private fun scheduleAlarm(
scheduledTime: String,
title: String,
message: String
) {
val alarmMgr = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val alarmIntent =
Intent(applicationContext, NotificationBroadcastReceiver::class.java).let { intent ->
intent.putExtra(EXTRA_NOTIFICATION_TITLE, title)
intent.putExtra(EXTRA_NOTIFICATION_MESSAGE, message)
PendingIntent.getBroadcast(applicationContext, 0, intent, 0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmMgr.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
scheduledTime.toLong() - TimeUnit.HOURS.toMillis(15),// 15분전알림
alarmIntent
)
} else {
alarmMgr.setExact(
AlarmManager.RTC_WAKEUP,
scheduledTime.toLong() - TimeUnit.HOURS.toMillis(15),// 15분전알림
alarmIntent
)
}
}
override fun onNewToken(token: String) {
Log.d("FCM new token -> ", token)
}
}
시간관계상 더 자세히는 못다루고 전체적인 구조를 파악할 수 있게 간단하게 포스팅해봤는데요. 초보가 한거라 완벽하지 못한점 양해부탁드립니다. ㅠㅠㅎㅎ
요약하면 다음과 같습니다.
1. 상대방의 FCM 토큰을 알고있다.
2. FCM Server 통신규약에 맞게 Retrofit2 통신을 한다.
3. 내가 정한 상대방에게 내가 보낸 FCM 메시지가 수신된다.
4. 수신된 데이터를 바탕으로 노티피케이션(알림)을 띄우든 로직을 처리한다.
[2020-12-27 ScheduledWorker 관련 댓글답변]
워크매니저를 상속받은 ScheduledWorker 는 받은 파라미터에 따른 동작을 분기처리 합니다.
ackage com.mtjin.nomoneytrip.service.work_manager
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.mtjin.nomoneytrip.data.alarm.Alarm
import com.mtjin.nomoneytrip.data.master_login.MasterUser
import com.mtjin.nomoneytrip.data.reservation.Reservation
import com.mtjin.nomoneytrip.utils.*
class ScheduledWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
// 알람 On/Off
val prefManager = PreferenceManager(applicationContext)
if (prefManager.alarmSetting) {
// Get Notification Data
val title = inputData.getString(EXTRA_NOTIFICATION_TITLE).toString()
val message = inputData.getString(EXTRA_NOTIFICATION_MESSAGE).toString()
val productId = inputData.getString(EXTRA_ALARM_PRODUCT_ID).toString()
val userId = inputData.getString(EXTRA_ALARM_USER_ID).toString()
val timestamp = inputData.getLong(EXTRA_ALARM_TIMESTAMP, 0)
val case = inputData.getInt(EXTRA_ALARM_CASE, 0)
val reservationId = inputData.getString(EXTRA_RESERVATION_ID).toString()
val dbKey = Firebase.database.reference.push().key.toString()
if (case == ALARM_START_CASE3 || case == ALARM_REVIEW_CASE4) { // (시작전날 및 종료날리뷰 알림의 경우) 해당예약시간까지 이장님이 수락한 경우 FCM 전송
Firebase.database.reference.child(RESERVATION).child(reservationId)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {
Log.d(
TAG,
"ScheduledWorker Firebase DB error -> " + error.toException()
)
}
override fun onDataChange(snapshot: DataSnapshot) {
snapshot.getValue(Reservation::class.java)?.let { reservation ->
if (reservation.state == 2 || reservation.state == 0) {
Firebase.database.reference.child(ALARM).child(userId)
.child(dbKey)
.setValue(
Alarm(
id = dbKey,
productId = productId,
userId = userId,
case = case,
readState = false,
content = message,
timestamp = timestamp,
reservationId = reservationId
)
)
NotificationUtil(applicationContext).showNotification(
title,
message
)
}
}
}
})
} else if (case == ALARM_RESERVATION_REQUEST_CASE5) {
Firebase.database.reference.child(MASTER_USER).orderByChild(PRODUCT_ID).equalTo(
productId
).addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {
Log.d(TAG, error.toException().toString())
}
override fun onDataChange(snapshot: DataSnapshot) {
for (snapshot2 in snapshot.children) {
snapshot2.getValue(MasterUser::class.java)?.let { masterUser ->
Firebase.database.reference.child(ALARM).child(masterUser.id)
.child(dbKey)
.setValue(
Alarm(
id = dbKey,
productId = productId,
userId = masterUser.id,
case = case,
readState = false,
content = message,
timestamp = timestamp,
reservationId = reservationId
)
)
NotificationUtil(applicationContext).showNotification(
title,
message
)
}
}
}
})
} else { //이장님 수락,거절, 예약완료
// Firebase.database.reference.child(ALARM).child(userId).child(dbKey).setValue(
// Alarm(
// id = dbKey,
// productId = productId,
// userId = userId,
// case = case,
// readState = false,
// content = message,
// timestamp = timestamp
// )
// )
NotificationUtil(applicationContext).showNotification(
title,
message
)
}
}
return Result.success()
}
}
[참고 및 유의사항 - 사용자의 FCM 토큰은 바뀔수도 있으므로 항상 최신 FCM 토큰으로 업데이트 해주어야합니다.]
다음과 같은 코드로 사용자 자신의 최신 FCM 을 발급받을 수 있습니다. 이를 DB같은데에 잘 업데이트 해주도록 합시다.
private fun initFcmToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
// Get new FCM registration token
UserInfo.fcm = task.result.toString()
})
}
궁금하신 부분이 있다면 댓글 부탁드립니다.
댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!
'안드로이드 > 코틀린 & 아키텍처 & Recent' 카테고리의 다른 글
[안드로이드] 파이어베이스 전화번호 인증 구현 방법 (38) | 2020.11.26 |
---|---|
[안드로이드] Release key (출시키) 잃어버렸을때 복구 방법 2020 (0) | 2020.11.02 |
[안드로이드] Scoped Storage(범위지정 저장소) 정리 (Legacy Storage와 차이점 정리) (33) | 2020.09.29 |
[안드로이드] Spinner 사용시 해당 프래그먼트에서 다른 프래그먼트갔다가 돌아왔을 시 나는 에러 처리 (Fragment spinner error) (0) | 2020.09.28 |
[안드로이드] 하나의 바인딩어댑터에서 여러개의 리사이클러뷰 작업을 할 때 에러 처리 (0) | 2020.09.28 |