본문 바로가기
🖥 Programming/📱 Android (Kotlin)

[Android][kotlin] FCM(Firebase Cloud Message) 구현하기! - 2편

by MinChan-Youn 2022. 2. 25.

FCM(Firebase Cloud Message)에 대해서 알아보겠습니다.

Firebase 클라우드 메시징(FCM)은 무료로 메세지를 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션

FCM을 통하여 새 이메일, 기타 데이터를 동기화 할 수 있음, 또 알림을 통하여 사용자를 유지하고 재참여 유도가능

한번의 메세지로 최대 4,000바이트의 페이로드를 클라이언트 앱에 전송가능

 

1편에서 Firebase셋팅하는 방법을 

2편에서는 FCM Android App을 구현 및 Push(푸시)알림을 발송하여 어떻게 앱이 실행되는지 알아보겠습니다.

 

참고자료:

https://firebase.google.com/docs/cloud-messaging?hl=ko 

 

⊙ FCM(Firebase Cloud Message) Android Application 구현법

필요한 부분은 주석으로 설명글을 적어두었으니 필요한부분은 잘 확인하여 알맞게 커스텀하면 되겠습니다.

 

 

필요 라이브러리 추가

● build.gradle(:Module)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.gms.google-services' //FCM
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.fcm_kotlin"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures{
        viewBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'


    //FCM
    implementation platform('com.google.firebase:firebase-bom:29.1.0')
    implementation 'com.google.firebase:firebase-analytics-ktx'
    implementation 'com.google.firebase:firebase-messaging-ktx' //FCM
//    implementation 'com.google.firebase:firebase-messaging'  //??


//    implementation 'androidx.work:work-runtime:2.7.1'
}

 

● build.gradle(:Project)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
//FCM
buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.10'
    }
}

plugins {
    id 'com.android.application' version '7.1.1' apply false
    id 'com.android.library' version '7.1.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

 

● Androidmanifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.fcm_kotlin">

    <uses-permission android:name="android.permission.INTERNET"/>

    <!-- Android 13 PUSH 대응 -->
    <uses-permission
        android:name="android.permission.POST_NOTIFICATIONS"
        android:minSdkVersion="33" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FCMkotlin">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <service
            android:name=".MyFirebaseMessagingService"
            android:enabled="true"
            android:exported="true"
            android:stopWithTask="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        <!--
            TODO: enabled: 시스템에서 활동을 인스턴스화할 수 있는지 여부
            true: 인스턴스화 가능(기본값)
            flase: 인스턴스화 불가능

            TODO: exported: 다른 애플리케이션의 구성요소에서 활동을 시작할 수 있는지를 설정
            'true': 모든 앱에서 활동에 액세스할 수 있으며 정확한 클래스 이름으로 활동을 시작할 수 있습니다.
            'false': 활동은 같은 애플리케이션의 구성요소나 사용자 ID가 같은 애플리케이션, 권한이 있는 시스템 구성요소에서만 시작될 수 있음. 이는 인텐트 필터가 없는 경우의 기본값

            TODO: stopWithTask: 휴대폰의 태스크에서 모두닫기 및 스와이프했을때 현상
            true: 휴대폰의 태스크에서 모두닫기 및 스와이프 했을 때 액티비는 종료되고 서비스도 종료
            false: 휴대폰의 태스크에서 모두닫기 및 스와이프 했을 때 액티비티는 종료되고 서비스는 onTackRemoved() 호출이 되며 곧바로 서비스가 재시작되어 onCreate()를 타게 된다.
        -->

    </application>

</manifest>

 

 

 

 

FCM연동 코드

23.05.22 ChatGPT에서 전달해준 내용 - 다음을 사용해도 되고 안해도 될지는 판단 알아서..;
FirebaseMessaging.getInstance().isAutoInitEnabled = true는 Firebase Cloud Messaging(Firebase 메시징)에서 자동 초기화 기능을 활성화하는 코드입니다.

Firebase Cloud Messaging은 기기에 대한 푸시 알림을 처리하기 위해 사용되며, 기기가 FCM 서비스에 등록되어야 알림을 수신할 수 있습니다. isAutoInitEnabled 속성을 true로 설정하면 앱이 시작될 때 자동으로 FCM을 초기화하여 기기를 등록하고 알림을 수신할 준비를 합니다.
즉, 위의 코드는 앱이 시작될 때 Firebase Messaging을 자동으로 초기화하고 기기를 FCM 서비스에 등록하는 역할을 합니다. 이를 통해 앱이 시작될 때마다 별도의 초기화 코드 없이 FCM을 사용할 수 있습니다.

Firebase Cloud Messaging을 사용하기 위해서는 google-services.json 파일을 프로젝트에 추가하고 Firebase 프로젝트와 연결되어 있어야 합니다. 또한 implementation 'com.google.firebase:firebase-messaging:22.0.0'와 같이 Firebase Messaging 의존성을 프로젝트에 추가해야 합니다.
이 코드를 사용하여 Firebase Messaging을 자동 초기화하고 FCM 서비스에 기기를 등록할 수 있습니다.

FirebaseMessaging.getInstance().isAutoInitEnabled = true

 

● MainActivity.kt

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        /** FCM설정, Token값 가져오기 */
        MyFirebaseMessagingService().getFirebaseToken()

        /** PostNotification 대응 */
        checkAppPushNotification()
        
        //사용안하면 삭제하기
        /** DynamicLink 수신확인 */
        initDynamicLink()
    }

    /** Android 13 PostNotification */
    private fun checkAppPushNotification() {
        //Android 13 이상 && 푸시권한 없음
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 
            && PackageManager.PERMISSION_DENIED == ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)) {
            // 푸쉬 권한 없음
            permissionPostNotification.launch(Manifest.permission.POST_NOTIFICATIONS)
            return
        }

        //권한이 있을때
        TODO....
    }

    /** 권한 요청 */
    private val permissionPostNotification = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            //권한 허용
        } else {
            //권한 비허용
        }
    }
    
    
    
    
    //사용안하면 삭제하기
    /** DynamicLink */
    private fun initDynamicLink() {
        val dynamicLinkData = intent.extras
        if (dynamicLinkData != null) {
            var dataStr = "DynamicLink 수신받은 값\n"
            for (key in dynamicLinkData.keySet()) {
                dataStr += "key: $key / value: ${dynamicLinkData.getString(key)}\n"
            }

            binding.tvToken.text = dataStr
        }
    }
}

 

● MyFirebaseMessagingService.kt 

class MyFirebaseMessagingService : FirebaseMessagingService() {
    /** 푸시 알림으로 보낼 수 있는 메세지는 2가지
     * 1. Notification: 앱이 실행중(포그라운드)일 떄만 푸시 알림이 옴
     * 2. Data: 실행중이거나 백그라운드(앱이 실행중이지 않을때) 알림이 옴 -> TODO: 대부분 사용하는 방식 */

    private val TAG = "FirebaseService"

    /** Token 생성 메서드(FirebaseInstanceIdService 사라짐) */
    override fun onNewToken(token: String) {
        Log.d(TAG, "new Token: $token")

        // 토큰 값을 따로 저장
        val pref = this.getSharedPreferences("token", Context.MODE_PRIVATE)
        val editor = pref.edit()
        editor.putString("token", token).apply()
        editor.commit()
        Log.i(TAG, "성공적으로 토큰을 저장함")
    }

    /** 메시지 수신 메서드(포그라운드) */
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        Log.d(TAG, "From: " + remoteMessage!!.from)

        // Notification 메시지를 수신할 경우
        // remoteMessage.notification?.body!! 여기에 내용이 저장되있음
        // Log.d(TAG, "Notification Message Body: " + remoteMessage.notification?.body!!)

        //받은 remoteMessage의 값 출력해보기. 데이터메세지 / 알림메세지
        Log.d(TAG, "Message data : ${remoteMessage.data}")
        Log.d(TAG, "Message noti : ${remoteMessage.notification}")

        if(remoteMessage.data.isNotEmpty()){
            //알림생성
            sendNotification(remoteMessage)
//            Log.d(TAG, remoteMessage.data["title"].toString())
//            Log.d(TAG, remoteMessage.data["body"].toString())
        }else {
            Log.e(TAG, "data가 비어있습니다. 메시지를 수신하지 못했습니다.")
        }
    }

    /** 알림 생성 메서드 */
    private fun sendNotification(remoteMessage: RemoteMessage) {
        //channel 설정
        val channelId = "channelId -- 앱 마다 설정" // 알림 채널 이름
        val channelName = "channelName -- 앱 마다 설정"
        val channelDescription = "channelDescription -- 앱 마다 설정"
        val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) // 알림 소리
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 오레오 버전 이후에는 채널이 필요
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val importance = NotificationManager.IMPORTANCE_HIGH // 중요도 (HIGH: 상단바 표시 가능)
            val channel = NotificationChannel(channelId, channelName, importance).apply {
                description = channelDescription
            }
            notificationManager.createNotificationChannel(channel)
        }
        
        
       
        // RequestCode, Id를 고유값으로 지정하여 알림이 개별 표시
        val uniId: Int = (System.currentTimeMillis() / 7).toInt()

        // 일회용 PendingIntent : Intent 의 실행 권한을 외부의 어플리케이션에게 위임
        val intent = Intent(this, MainActivity::class.java)
        //각 key, value 추가
        for(key in remoteMessage.data.keys){
            intent.putExtra(key, remoteMessage.data.getValue(key))
        }
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) // Activity Stack 을 경로만 남김(A-B-C-D-B => A-B)

        //23.05.22 Android 최신버전 대응 (FLAG_MUTABLE, FLAG_IMMUTABLE)
        //PendingIntent.FLAG_MUTABLE은 PendingIntent의 내용을 변경할 수 있도록 허용, PendingIntent.FLAG_IMMUTABLE은 PendingIntent의 내용을 변경할 수 없음
        //val pendingIntent = PendingIntent.getActivity(this, uniId, intent, PendingIntent.FLAG_ONE_SHOT)
        val pendingIntent = PendingIntent.getActivity(this, uniId, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE)

        // 알림 채널 이름
        val channelId = "my_channel"
        // 알림 소리
        val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)

        // 알림에 대한 UI 정보, 작업
        val notificationBuilder = NotificationCompat.Builder(this, channelId)
	        .setPriority(NotificationCompat.PRIORITY_HIGH) // 중요도 (HIGH: 상단바 표시 가능)
            .setSmallIcon(R.mipmap.ic_launcher) // 아이콘 설정
            .setContentTitle(remoteMessage.data["title"].toString()) // 제목
            .setContentText(remoteMessage.data["body"].toString()) // 메시지 내용
            .setAutoCancel(true) // 알람클릭시 삭제여부
            .setSound(soundUri)  // 알림 소리
            .setContentIntent(pendingIntent) // 알림 실행 시 Intent

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 오레오 버전 이후에는 채널이 필요
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, "Notice", NotificationManager.IMPORTANCE_DEFAULT)
            notificationManager.createNotificationChannel(channel)
        }

        // 알림 생성
        notificationManager.notify(uniId, notificationBuilder.build())
    }

    /** Token 가져오기 */
    fun getFirebaseToken() {
    	//비동기 방식
        FirebaseMessaging.getInstance().token.addOnSuccessListener {
            Log.d(TAG, "token=${it}")
        }

//		  //동기방식
//        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
//                if (!task.isSuccessful) {
//                    Log.d(TAG, "Fetching FCM registration token failed ${task.exception}")
//                    return@OnCompleteListener
//                }
//                var deviceToken = task.result
//                Log.e(TAG, "token=${deviceToken}")
//            })
    }
}

/**
    "notificationBuilder" 알림 생성시 여러가지 옵션을 이용해 커스텀 가능.
    setSmallIcon : 작은 아이콘 (필수)
    setContentTitle : 제목 (필수)
    setContentText : 내용 (필수)
    setColor : 알림내 앱 이름 색
    setWhen : 받은 시간 커스텀 ( 기본 시스템에서 제공합니다 )
    setShowWhen : 알림 수신 시간 ( default 값은 true, false시 숨길 수 있습니다 )
    setOnlyAlertOnce : 알림 1회 수신 ( 동일 아이디의 알림을 처음 받았을때만 알린다, 상태바에 알림이 잔존하면 무음 )
    setContentTitle : 제목
    setContentText : 내용
    setFullScreenIntent : 긴급 알림 ( 자세한 설명은 아래에서 설명합니다 )
    setTimeoutAfter : 알림 자동 사라지기 ( 지정한 시간 후 수신된 알림이 사라집니다 )
    setContentIntent : 알림 클릭시 이벤트 ( 지정하지 않으면 클릭했을때 아무 반응이 없고 setAutoCancel 또한 작동하지 않는다 )
    setLargeIcon : 큰 아이콘 ( mipmap 에 있는 아이콘이 아닌 drawable 폴더에 있는 아이콘을 사용해야 합니다. )
    setAutoCancel : 알림 클릭시 삭제 여부 ( true = 클릭시 삭제 , false = 클릭시 미삭제 )
    setPriority : 알림의 중요도를 설정 ( 중요도에 따라 head up 알림으로 설정할 수 있는데 자세한 내용은 밑에서 설명하겠습니다. )
    setVisibility : 잠금 화면내 알림 노출 여부
    Notification.VISIBILITY_PRIVATE : 알림의 기본 정보만 노출 (제목, 타이틀 등등)
    Notification.VISIBILITY_PUBLIC : 알림의 모든 정보 노출
    Notification.VISIBILITY_SECRET : 알림의 모든 정보 비노출
 */

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_token"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello DynamicLink!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

Push(푸시, 푸쉬) 알림 보내기

Push알림은 POSTMAN사이트에서 API호출 서비스를 이용하여 테스트를 진행

Firebase에서 지원하는 테스트 알림은 포그라운드에서만 작동이되고, 백그라운드에서는 작동이 되지않습니다.

이부분은 좀더 깊게 들어가면 Firebae에서 제공하는 테스트는 notification값이 들어가기 때문에 백그라운데에서는 안되고, 실제 데이터를 직접 입력하는 POSTMAN을 사용하여 테스트를 진행해보겠습니다.

 

왜 POSTMAN을 사용하고 Firebase에서 테스트를 하지 않는지는 다음글을 통해 확인부탁드립니다.

https://bacassf.tistory.com/119

 

 

● POSTMAN-Headers

Content-Type은 "application/json", Authorization은 "key={FCM 서버키}"를 입력

URL: "https://fcm.googleapis.com/fcm/send" 입력

 

 

● POSTMAN-Body

to에는 Application의 Token값을 입력

{
	"to" : "Application의 Token값 입력",
	"priority" : "high",
	"data" : {
		"title" : "FCM_Background_title",
		"body" : "FCM_Background_body"
	}
}

 

 

● SEND / 결과값

 

구현화면

● 푸시알림 받기 전 / 후

 

여기까지 따라오셨다면 FCM을 구현을 할 수 있습니다~

 

글 정리 & 소스코드

[정리]

FCM구현 1편: https://minchanyoun.tistory.com/99

 

[Android][kotlin] FCM(Firebase Cloud Message) 구현하기! - 1편

FCM(Firebase Cloud Message)에 대해서 알아보겠습니다. Firebase 클라우드 메시징(FCM)은 무료로 메세지를 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션 FCM을 통하여 새 이메일, 기타 데이터를 동

minchanyoun.tistory.com

FCM구현 2편: https://minchanyoun.tistory.com/101

 

[Android][kotlin] FCM(Firebase Cloud Message) 구현하기! - 2편

FCM(Firebase Cloud Message)에 대해서 알아보겠습니다. Firebase 클라우드 메시징(FCM)은 무료로 메세지를 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션 FCM을 통하여 새 이메일, 기타 데이터를 동

minchanyoun.tistory.com

 

[소스코드]
FCM: https://github.com/younminchan/kotlin-study/tree/main/FCMkotlin

 

GitHub - younminchan/kotlin-study: kotlin-example

kotlin-example. Contribute to younminchan/kotlin-study development by creating an account on GitHub.

github.com

 

 

질문 또는 궁굼한 부분은 댓글을 남겨주세요! 친절하게 답변드리겠습니다!

응원의 댓글은 저에게 큰 힘이 된답니다! :)

즐거운 하루되세요!

 

깃허브 보러 놀러오세요 👇 (맞팔환영)

https://github.com/younminchan

 

younminchan - Overview

Android Developer. younminchan has 6 repositories available. Follow their code on GitHub.

github.com