안드로이드 앱개발

Firebase를 이용해 FCM 구현하기(웹서버 이용)

팀(Tim) 2021. 1. 8. 01:21

 

구현하려는 것:

내 게시물에 누군가가 댓글을 남기면 나에게 알림이 오게한다.

또, 그 알림을 누르면 해당 게시물로 이동한다.

 

 

프로젝트 설정에 대한 자세한 설명은 공식문서를 참고하기 바란다.

 

firebase.google.com/docs/cloud-messaging/android/client?authuser=0

 

Android에서 Firebase 클라우드 메시징 클라이언트 앱 설정

Firebase 클라우드 메시징 Android 클라이언트 앱을 만들려면 FirebaseMessaging API와 Gradle이 있는 Android 스튜디오 1.4 이상을 사용하세요. 이 페이지의 안내에서는 Android 프로젝트에 Firebase를 추가하는 단

firebase.google.com

 

만약 A가 B의 게시물에 댓글을 달았고, B에게 FCM을 보내려면 B에게 발급된 토큰이 어떤 것인지 알아야한다.

여기서 토큰은 파이어베이스 메시징을 위해 사용되는 앱 인스턴스용 등록 토큰을 말한다. 

이 토큰을 통해 파이어베이스는 메시징을 보낼 앱 인스턴스를 구분한다.

 

먼저, 토큰을 얻는다.

 

 

 //파이어베이스 클라우드 메시징을 위한 토큰 요청
        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.w(Constants.LOG_TAG, "Fetching FCM registration token failed", task.exception)
                return@OnCompleteListener
            }

            // Get new FCM registration token
            val token = task.result
            //웹서버에 토큰 등록
            mUserViewModel.registerFirebaseToken(user.mEmail, token!!);
        })

 

웹서버에 토큰을 등록하는 이유는 어떤 앱 인스턴스 혹은 계정이 어떤 토큰을 받았는지 확인할 수 있어야 하기 때문이다.

registerFirebaseToken은 웹서버로 토큰과 유저 이메일을 담은 post 요청을 날려서 , 토큰을 데이터베이스의 유저정보에 추가한다. 

 

이제 FCM을 앱에서 받으면 된다.

제일먼저 FirebaseMessagingService를 상속받는 클래스를 만들자.

 

class MyFirebaseMessagingService : FirebaseMessagingService() {

}

 

onCreate에서 노티피케이션 채널을 만들고 노티피케이션매니저에 등록하자.

 

    override fun onCreate() {
        super.onCreate()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            createNotificationChannel(mNotificationManager)

    }
    
        @RequiresApi(Build.VERSION_CODES.O)
    private fun createNotificationChannel(notificationManager: NotificationManager) {
        val notificationChannel = NotificationChannel(
            Constants.NOTIFICATION_CHANNEL_ID,
            Constants.NOTIFICATION_CHANNEL_NAME,
            IMPORTANCE_LOW //LOW로 해줘야 진동안울림
        )

        notificationManager.createNotificationChannel(notificationChannel);
    }

 

이제 onMessageReceived()를 구현하자.

 

 /**
     *  웹서버에서 FCM을 요청하고, 파이어베이스에서 이 앱 인스턴스에 FCM을 보내면 호출된다.
     *  @param p0 웹서버에서 FCM 요청과 함께보낸 데이터.
     */
    override fun onMessageReceived(p0: RemoteMessage) {
        super.onMessageReceived(p0)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            /*
            * p0에는 notification 객체와 메시지에 포함된 데이터(웹서버측에서 보낸 데이터다)가 있다.
            * 이것들을 가지고 노티피케이션을 수행하자.
            */
            notifyNotification(
                p0.notification?.title, /*노티피케이션의 타이틀*/
                p0.notification?.body, /*노티피케이션의 내용*/
                p0.data["topic_id"] ?: "0", /*메시지의 데이터*/
                p0.data["picture_id"] ?: "0"
            );
        }
    }

 

p0로 들어오는 메시지는 node.js 서버에서 아래처럼 보내준다.

firebase_admin.messaging().send(message) 를 통해 firebase에 FCM을 요청할 수 있다. 그러면 token을 발급받았던 앱 인스턴스에 FCM을 보내준다.

 

 let message = {
                notification: {
                    title: msg_title,
                    body: msg_body
                },
                data: {
                    topic_id: topic_id,
                    picture_id: picture_id
                },
                token: /*토큰*/
            };

            firebase_admin.messaging().send(message)

 

이제 받은 FCM의 데이터를 참조해서 실제로 notification을 notify 해보자.

 

/**
 * 실제 노티피케이션을 실행한다.
 */
    @RequiresApi(Build.VERSION_CODES.O)
    private fun notifyNotification(title: String?, content: String?, notifiedTopicId: String, notifiedPictureId: String) {


        //알림을 클릭했을때 알림이 대상이되는 토픽이나 사진으로 이동하게한는 인텐트.
        //인텐트 안에는 알림의 대상의 id를 담는다.
        val notificationIntent = Intent(this, MainActivity::class.java).apply {
            if (notifiedTopicId != "0")
                putExtra(Constants.EXTRA_NOTIFIED_TOPIC_ID, notifiedTopicId)
            else if (notifiedPictureId != "0")
                putExtra(Constants.EXTRA_NOTIFIED_PICTURE_ID, notifiedPictureId)
        }
        
        val pendingIntent = PendingIntent.getActivity(
            this, 0, notificationIntent, FLAG_UPDATE_CURRENT
        )


        val notification = Notification.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
            .setAutoCancel(true) //사용자가 스와이프했을때 노티피케이션을 지울것인가
            .setSmallIcon(R.drawable.icon_hestia) //노티피케이션 아이콘
            .setContentTitle(title) //노티피케이션 제목
            .setContentText(content) //노티피케이션 본문
            .setContentIntent(pendingIntent) //노티피케이션을 클릭했을때 실행할 팬딩인텐트
            .setGroup(NOTIFICATION_GROUP_COMMENT) //그룹
            .build()

        //각각의 노티피케이션은 id가 달라야 별개의 노티피케이션으로 인식된다.
        mNotificationManager.notify(Constants.NOTIFICATION_ID + mNotificationCnt++, notification)


        //노티피케이션을 그룹짓는 노티피케이션
        val notificationSummary = Notification.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
            .setAutoCancel(true)
            .setSmallIcon(R.drawable.icon_hestia)
            .setContentIntent(pendingIntent)
            .setGroup(NOTIFICATION_GROUP_COMMENT)
            .setGroupSummary(true) //이 노티피케이션은 그룹의 부모이다.
            .build()

        //현존하는 노티피케이션 중 NOTIFICATION_GROUP_COMMENT에 해당하는 것들을 그룹으로 묶는다.
        mNotificationManager.notify(Constants.NOTIFICATION_ID_SUMMERY, notificationSummary)
    }

 

위 코드에 notification이 두개다. 하나는 제목과 본문이 있는 실질적인 notification이고, 그런 notification이 여러개있을때 이것들을 한번에 묶는 역할을 하는 notification이 notificationSummary다. 

 

위가 notification summary이고 아래가 notification들을 펼친상태다.

 

여기까지 했으면 구현은 끝났다.

추가적으로 나는 앱을 열었을때, 저 노티피케이션 목록의 제목들과 본문들을 가져오고 싶었는데 잘 안됬었다.

내가 notifynotification()에서 만든 notification과 앱을 열었을때 getSystemService()를 통해 받아온 notification은 다른 객체였기 때문이다. getSystemService()을 통해 notification을 가져오면 몇개의 notification이 있는지는 알 수 있지만 그 내용까지는 알 수 없었다.

때문에 나는 SharedPreference를 이용했다.

밑의 코드는 그냥 FCM으로 전해온 데이터를 SharedPreference에 저장하는 코드다.

 

private fun notifyNotification(title: String?, content: String?, notifiedTopicId: String, notifiedPictureId: String) {
        if (title == null || content == null) return;

        //알림을 받은 topicId나 pictureID를 로컬에 저장한다.
        SaveNotification(notifiedTopicId, notifiedPictureId)
        
        //생략
        
}
  
/**
* 알림을 받은 topicId나 pictureID를 sharedPreference에 저장한다.
* 앱을 켰을때, 그동안 온 알림에 대한 정보를 알아야하기 떄문이다.
* 파라미터 예시) 사진에 댓글이 달렸을경우 notifiedTPictureId는 "0" 이 아니고,
* notifiedTopicId 는 "0" 이다.
* @param notifiedPictureId 알림의 대상이되는 사진의 id
* @param notifiedTopicId 알림의 대상이되는 토픽의 id
*/
private fun SaveNotification(notifiedTopicId: String, notifiedPictureId: String) {
   if (!notifiedTopicId.equals("0") || !notifiedPictureId.equals("0")) {
       val sharedPref = applicationContext.getSharedPreferences(Constants.PREF_FILE_NOTIFICATION, Context.MODE_PRIVATE)
       val prefEdit = sharedPref.edit();
       var notifiedPictureIDs: MutableSet<String>?
       var notifiedTopicIDs: MutableSet<String>?

       if (!notifiedTopicId.equals("0")) {
             notifiedTopicIDs = sharedPref.getStringSet(Constants.PREF_NOTIFIED_TOPIC_ID, HashSet());
             notifiedTopicIDs?.add(notifiedTopicId);
             prefEdit.putStringSet(Constants.PREF_NOTIFIED_TOPIC_ID, notifiedTopicIDs);
          }
       if (!notifiedPictureId.equals("0")) {
              notifiedPictureIDs = sharedPref.getStringSet(Constants.PREF_NOTIFIED_PICTURE_ID, HashSet());
              notifiedPictureIDs?.add(notifiedPictureId);
              prefEdit.putStringSet(Constants.PREF_NOTIFIED_PICTURE_ID, notifiedPictureIDs)
            }
               prefEdit.apply();
            }

        }

 

이런식으로 저장한 후 앱을 열었을때 꺼내썼다.