티스토리 뷰

1. 안드로이드 Firebase 활용 Push 알림 수신기 어플 만들기

실제 어플들을 사용해보면 앱이 화면에 실행중이지 않고있는데도 알림이 수신되는 것을 알 수 있습니다. 이러한 푸쉬 알림 기능은 간단하게 Firebase를 활용해서 구현할 수 있는데 해당 프로젝트에서 실습해 보도록 하겠습니다.

주요 기능

  • 앱이 켜져있지 않은 상태(백그라운드) 일 때 푸시 알림을 수신 할 수 있음
  • 기본형, 확장형, 사용자형(커스터마이징) 알림에 대한 각각 처리 및  수신

사용 기술

  • Firebase 활용
    • 클라우드 메세징 (Firebase Cloud Messaging, FCM)
    • Firebase 토큰
  • Notification
    • 일반, 확장형, 커스텀 알림

결과 스크린샷

메인 화면에서는 Firebase 토큰을 확인할 수 있고 해당 토큰을 사용해서 Firebase에서 푸시 알림을 보낸다. 그러면 화면이 꺼진 상태에서 푸쉬알림을 수신할 수 있는데 이때 우리가 설정한 3가지 알림을 각각 확인해본 모습이 위 스크린 샷이다.

2. 기본 레이아웃 구성

기본 레이아웃은 텍스트 뷰 4개를 사용해서 결과와 토큰을 나타내주도록 합니다. 결과의 경우에는 어플을 앱 런처로 켰는 지 아니면 푸시알람을 터치 클릭 눌러서 열었는지를 확인하기 위해 추가했습니다.

텍스트 박스의 경우에는 android:textIsSelectable="true" 속성 추가해서 길게 눌러서 텍스트를 선택할 수 있게 설정해줍니다. 해당 토큰을 쉽게 복사하여 사용하기 위해서 이 속성을 추가해주었습니다.

여태까지 마진을 줄 때 layout_marginStart 또는 Left를 사용했었는 데 이 둘은 어찌 보면 동일하게 동작하는 것 처럼 보입니다. (동일한 속성을 넣어 놓을리가 없는데..) 사실 이 부분은 Support layout mirroring 이라고 해서 아랍어 등 과 같이 오른쪽 에서 부터 시작하는 언어 들 까지 커버하기 위해서 있습니다.

한국어 같은 경우 영어와 마찬가지로 왼쪽에서 부터 써내려가 오른쪽에서 마무리 되는 형식이기 때문에 여태 특별한 차이가 없어 보였지만, 그 외 언어로 가면 해당 언어에 맞추어 가독성이 좋아지도록 컴포넌트가 오른쪽으로 정렬 되는 등 기능을 수행할 겁니다.

2.1. Firebase 프로젝트 생성

구글 로그인을 하고 파이어베이스 개발자 콘솔로 이동하면 손쉽게 Firebase 프로젝트를 생성할 수 있습니다. 여기서 요구하는 대로 패키지 이름 등을 넣어주시고 (applicationId 는 앱수준 gradle 를 열어보시면 찾을 수 있습니다.) 

이후에 google-services.json 파일을 받아 프로젝트 수준 탐색기에서 app 의 하위 디렉터리에 해당 json 파일을 추가해주시면됩니다. (해당 파일을 깃이나 외부에 공유하는 것은 추천드리지 않고 따로 private 하게 갖고 계시면 되겠습니다.)

2.2. gradle, SDK 설정

SDK 를 정상적으로 사용하기 위해서 플러그인 등을 추가해줄건데 앱(모듈) 수준 gradle 을 아래와 같이 수정해주었습니다. plugins에 오타가 있는데 plugin: 이 부분을 제외하시고 id 'com.google.gms.google-services' 이렇게 추가 해주셔야합니다. 이후 sync.

2.3. firebase token 가져오기

다음은 파이어베이스 토큰을 가져오는 부분입니다.

    // firebase 토큰을 가져오는 부분
    private fun initFirebase() {
        FirebaseMessaging.getInstance().token
            .addOnCompleteListener { task ->
                if (task.isSuccessful) { // 성공 시
                    firebaseTokenTextView.text = task.result // 토큰 가져오기
                }

            }
    }

토근의 경우에는 위 코트를 통해 가져올 수 있으며 json 파일과 sdk 설정이 올바르게 되어있어야합니다. 토큰을 가져오는데 성공했다면 기본 레이아웃에 있는 텍스트뷰를 통해 보여주도록 하고 이후 해당 토큰을 사용해보도록 하겠습니다.

먼저 매니페스트 파일에 아래와 같이 추가 해주세요.

            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>

이후 클래스를 따로 하나 파서 FirebaseMessagingService 를 상속 받도록 하고 이후에 여기 이벤트 리스너를 정의해서 사용하도록 하겠습니다.

package lilcode.aop.p3.c01.push_alarm_receiver

import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

/*
다음과 같은 경우에 등록 토큰이 변경될 수 있습니다.
- 새 기기에서 앱 복원
- 사용자가 앱 삭제/재설치
- 사용자가 앱 데이터 소거
 */

class MyFirebaseMessagingService: FirebaseMessagingService() {

    override fun onNewToken(p0: String) {
        // 토큰이 갱신될 때마다 처리 해주는 작업 여기에 필요 (실무에서)
        super.onNewToken(p0)
    }

    override fun onMessageReceived(message: RemoteMessage) {
        // FCM 수신 마다 실행
        super.onMessageReceived(message)
    }
}

기본적으로 필요한 onNewToken 을 재정의 해주는데 해당 기능은 토큰이 변경되는 경우 호출되게 되는데 대게는 새 기기에서 앱을 복원하거나, 앱 삭제 후 재설 치, 앱 데이터 삭제될 때 등이 될 수 있습니다.

그 밑에 재정의 해준 onMessageReceived 함수의 경우에는 파이어베이스 메세지를 받을 때마다 호출되게 됩니다. 실제로 해당 부분에 브레이크 포인트를 걸고 파이어베이스에서 메세지를 보내면 잘 타는 것을 확인할 수 있습니다.

3. FCM 이벤트 리스너 정의

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // FCM 수신 마다 실행
        super.onMessageReceived(remoteMessage)

        // 채널 생성
        createNotificationchannel()

        val title = remoteMessage.data["title"]
        val message = remoteMessage.data["message"]

        // 실제 알림 컨텐츠 만들기
        val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)// 아이콘 보여주기
            .setContentTitle(title) // 메세지 에서 받은 타이틀 활용
            .setContentText(message) // 메세지 에서 받은 메세지 활용
            .setPriority(NotificationCompat.PRIORITY_DEFAULT) // 오레오 이하 버전 에서는 지정 필요

        // 실제로 노티파이를 하기 위해서
        // 메세지가 왔을 때 타이틀과 메세지에 맞게 각각 보여줌
        NotificationManagerCompat.from(this)// 매니저 가져옴
            .notify(1, notificationBuilder.build())
    }

파이어베이스 클라우드 메시지가 발생하게되면 위 부분을 타고 전송 받은 메세지를 처리하게 됩니다. 여기서는 간단하게 title 과 message 를 받아서 알림 컨텐츠를 만들어서 띄워주게 됩니다. 이때 채널 생성을 해주는 부분이 있는데 먼저 이부분을 보도록 하겠습니다.

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                NotificationManager.IMPORTANCE_DEFAULT
            )
            channel.description = CHANNEL_DESCRIPTION

            (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
                .createNotificationChannel(channel)
        }

채널의 경우에는 버전코드 O 부터 하도록 되어있으며 어플의 알림 설정에 보면 여러개의 채널 중에 수신할 알림을 설정해줄 수 있는 기능을 제공하기 위해서 추가된 것으로 보여집니다. 채널 아이디와 이름은 상수를 통해서 할당되도록 하였고 중요도는 Default로 설정 해주었습니다.

이후 해당 채널로 시스템 서비스로 가져와 알림 매니저로 사용하여 채널을 생성해주면 됩니다.

이후 알림을 보내면 해당 채널을 통해 알림이 오게되며 설정해준 아이콘 , 타이틀, 내용이 전달되어 알맞게 알림창에 뜨는 것을 확인할 수 있습니다. (파이어베이스 개발자 도구를 통해서 테스트 할 수 있습니다.)

4. 다양한 알림 구현하기

일반형 알림 말고도 사용자화된 알림 및 확장형 알림을 사용할 수 있습니다. 이를 위해 onMessageReceived 를 아래와 같이 수정하도록 하겠습니다.

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // FCM 수신 마다 실행
        super.onMessageReceived(remoteMessage)

        // 채널 생성
        createNotificationchannel()

        val type = remoteMessage.data["type"] //enum 이랑 동일한 값
            ?.let { NotificationType.valueOf(it) }
        val title = remoteMessage.data["title"]
        val message = remoteMessage.data["message"]


        // type 이 null 이면 알림 생성 x
        type ?: return


        // 실제로 노티파이를 하기 위해서
        // 메세지가 왔을 때 타이틀과 메세지에 맞게 각각 보여줌
        NotificationManagerCompat.from(this)// 매니저 가져옴
            .notify(type.id, createNotification(type, title, message))
    }

타이틀과 메세지 외에 type 이라는 값을 추가해서 추가적으로 받아오도록 합니다. 이때 사용된 NotificationType 의 경우는 enum 타입 클래스로 아래와 같습니다.

enum class NotificationType(val title: String, val id: Int) {
    NORMAL("일반 알림", 0),
    EXPANDABLE("확장형 알림", 1),
    CUSTOM("커스텀 알림", 3)
}

즉 받아온 타입이 NULL이 아닌 경우에 해당 값으로 타입을 결정합니다.

이후 알림을 생성할 때는 createNotification 을 거쳐 타입별로 알림을 생성하도록 하며 이를 알림 매니저를 통해 전달해주도록 설정합니다.

    private fun createNotification(
        type: NotificationType,
        title: String?,
        message: String?
    ): Notification {

        // 실제 알림 컨텐츠 만들기
        val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)// 아이콘 보여주기
            .setContentTitle(title) // 메세지 에서 받은 타이틀 활용
            .setContentText(message) // 메세지 에서 받은 메세지 활용
            .setPriority(NotificationCompat.PRIORITY_DEFAULT) // 오레오 이하 버전 에서는 지정 필요
            .setContentIntent(pendingIntent)
            .setAutoCancel(true) // 알림 클릭시 자동 제거

        when (type) {
            NotificationType.NORMAL -> Unit
            // 확장 가능한 알림
            NotificationType.EXPANDABLE -> {
                notificationBuilder.setStyle(
                    NotificationCompat.BigTextStyle()
                        .bigText(
                            "...다수의 이모지..."
                        )
                )
            }
            // 커스텀 알림
            // https://developer.android.com/training/notify-user/custom-notification
            NotificationType.CUSTOM -> {
                notificationBuilder
                    .setStyle(NotificationCompat.DecoratedCustomViewStyle())
                    .setCustomContentView(
                        RemoteViews(
                            packageName,
                            R.layout.view_custom_notification
                        ).apply {
                            setTextViewText(R.id.title, title) // 타이틀 영역에 받아온 타이틀 넣기
                            setTextViewText(R.id.message, message) // 메세지 넣기
                        }
                    )
            }
        }

        return notificationBuilder.build()
    }

커스텀 알림의 경우에는 따로 커스텀 레이아웃을 아래와 같이 추가해주었습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        style="@style/TextAppearance.Compat.Notification.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        tools:text="Title" />

    <TextView
        android:id="@+id/message"
        style="@style/TextAppearance.Compat.Notification"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        tools:text="Message" />

</LinearLayout>

이후 setCustomContentView 를 통해서 해당 뷰를 설정하고 받아온 title 과 message 를 바인딩 해주기 위해서 setTextViewText 로 텍스트를 갱신해주도록 했습니다.

4.1. 알림 터치 및 누를 때 동작 구현 (새 인텐트 열기)

다음으로 알림을 누르거나 터치했을 때 실제 앱이 실행되도록 하는 부분을 구현 해보도록 합시다. 메인 엑티비티 코드에 아래와 같이 추가해줍니다.

그냥 우리가 어플 아이콘을 눌러서 최초 앱을 실행한 경우에는 앱 런처로 갱신했다는 메세지를 띄워줄 것이며 그것이 아니라 새로운 인텐트로 실행된 경우는 각각 타입에 맞는 알림으로 실행했다는 텍스트를 띄워줄 겁니다.

그러기 위해 onNewIntent() 를 오버라이드 하여 새로 들어온 인텐트로 교체할 수 있도록 합니다.

다음은 파이어베이스 서비스 클래스 부분을 수정합니다. (createNotification 부분을 수정)

알림이 들어 올 때마다 팬딩 인텐트를 생성하여 알림 컨텐츠를 만들 때 setContentIntent 에다가 박아주도록 합니다. 이후 해당 알림을 클릭하면 팬딩된 인텐트가 메인엑티비티를 통해 실행되게 되는 것이죠. 이때 setAutoCancel 을 true로 설정하여 알림 클릭시 자동 제거되도록 해줍니다.

이때 FLAG_ACTIVITY_SINGLE_TOP 를 플래그로 주어서 같은 알림 타입의 인텐트의 경우에는 오로지 한개만 생성되도록 하여 보여질 수 있게 설정합니다. (원래는 스택과 비슷하게 쌓이면서 가장 위 인텐트를 보여주게 되어있음.)

해당 프로젝트 전체 코드 및 보다 더 자세한 내용은 저의 깃허브 저장소를 통해 확인하실 수 있습니다.

2021.05.31 - [Android/App] - [Android] 심플 웹 브라우저 만들어보기 with Kotlin

2021.05.26 - [Android/App] - [Android] 음성 녹음기 어플 만들어보기 with Kotlin

2021.05.24 - [Android/App] - [Android] 뽀모도로 타이머 앱 만들기 with 코틀린

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
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
글 보관함