관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] FCM 서버 통신과 노티피케이션 (feat.FCM, Notification, Retrofit2, 이전글 업데이트 버전 2020) 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] FCM 서버 통신과 노티피케이션 (feat.FCM, Notification, Retrofit2, 이전글 업데이트 버전 2020)

막무가내막내 2020. 10. 10. 16:30
728x90

 

 

[2021-05-10 업데이트]

 

youngest-programming.tistory.com/103

 

[안드로이드] 노티피케이션 FCM 정리 ( 누르면 해당 액티비티와 내용 불러올 수 있도록)

단순 알림을 주고 알림을 누르면 런처액티비티로 이동하는 것은 예전에 해봤으나 알림을 누르면 채팅방이나 게시물로 이동하고 해당 내용들을 보여주게 하는 것은 이번에 처음 해봤다. 그에 대

youngest-programming.tistory.com

youngest-programming.tistory.com/76

 

파이어베이스 노티(notification) FCM 하는 방법 정리 2019

예를들어 카톡알림처럼 내가 누군가에게 채팅을 했을때 상대방에게 알림을 주고싶을 때 즉 , 디바이스에서 디바이스로 알림을 주고 싶은데 하는방법에 대해 포스팅해볼려고합니다. 먼저 OkHttp3,

youngest-programming.tistory.com

 

예전에 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에는 내가 보내줄 데이터! 라고 생각하시면 편합니다.

 

추가 참고용 남깁니다. 읽어보시면 도움이 크게 될겁니당

https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref?hl=ko#notification-payload-support
https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref?hl=ko#notification-payload-support

firebase.google.com/docs/cloud-messaging/android/topic-messaging

 

Android에서 주제 메시징  |  Firebase

FCM 주제 메시징은 게시/구독 모델을 기반으로 특정 주제를 구독하는 여러 기기에 메시지를 보내 줍니다. 필요에 따라 주제 메시지를 작성하면 FCM에서 라우팅을 처리하여 올바른 기기에 정확히 �

firebase.google.com

firebase.google.com/docs/cloud-messaging/concept-options

 

FCM 메시지 정보  |  Firebase

Firebase 클라우드 메시징(FCM)은 다양한 메시징 옵션과 기능을 제공합니다. 이 페이지의 정보는 다양한 유형의 FCM 메시지에 관한 이해를 돕고 FCM으로 구현할 수 있는 기능을 소개하기 위한 내용입�

firebase.google.com

youngest-programming.tistory.com/103

 

[안드로이드] 노티피케이션 FCM 정리 ( 누르면 해당 액티비티와 내용 불러올 수 있도록)

단순 알림을 주고 알림을 누르면 런처액티비티로 이동하는 것은 예전에 해봤으나 알림을 누르면 채팅방이나 게시물로 이동하고 해당 내용들을 보여주게 하는 것은 이번에 처음 해봤다. 그에 대

youngest-programming.tistory.com

 

 

 

 

 

 

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()
        })
    }

 

 

 

궁금하신 부분이 있다면 댓글 부탁드립니다.

 

댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!

 

 

728x90
Comments