읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
  • 전체 프로젝트 코드는 깃헙 링크 - MyFirebaseNotification에 올려두겠습니다. 참고해주세요.

이전 포스팅에서 네트워크 의존 없이 WorkManager로 푸시알림 로직을 구현했는데 비정기 Notification 요구사항이 있어 Firebase Notification을 채택하였다. 이번 기회에 Android에서 Firebase 푸시알림을 받는 과정을 정리하려 한다.

프로젝트 경로

프로젝트 링크 - Android_Firebase_Notification

Firebase에 프로젝트 등록하기

앱에 Firebase Notification을 수신하기 위해 Firebase Console에서 프로젝트를 등록해야 한다. 대부분 블로그들이 정리를 잘해주고 있으나 UI가 자주 바뀌므로 공식 Reference를 애용하자.

Firebase 공식문서 링크

이전 Android | Google SNS 로그인에서도 프로젝트 등록 과정을 다룬 적이 있다.

안드로이드 프로젝트에 Firebase SDK 추가하기

앞서 FCM에 프로젝트를 등록한 뒤 google-service.json을 다운받아 프로젝트 폴더에 넣고 build.gradle에 플러그인을 모두 적용했으면 Firebase Cloud Messaging 모듈을 추가해야 한다.

Android_Firebase_Notification_001

Firebase background Service 정의

백그라운드 에서 Firebase로부터 신호를 수신받을 Service class를 생성한다. 신호 수신 시 호출되는 메소드이므로 호출될 때 Notification을 생성하는 메소드를 실행하게 한다.

특정 기기에 전송 시 registration key가 필요하지만 일반적인 경우 유형에 따라 알림을 관리하므로 "update"와 "notify" 주제를 구독한 기기에 대해 전송해보자.

  • 적당히 작은 크기의 이미지 파일을 drawable 폴더에 넣고 small Icon으로 쓰게 했다.
public class MessagingService extends FirebaseMessagingService {
    private static final String[] topics = {"/topics/custom", "/topics/notify"};

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        makeNotification(remoteMessage);
    }

    @Override
    public void onNewToken(String s) {
        Log.e("token?", s);
        super.onNewToken(s);
    }

    private void makeNotification(RemoteMessage remoteMessage) {
        try {
            int notificationId = -1;
            Context mContext = getApplicationContext();

            Intent intent = new Intent(this, MainActivity.class);
            intent.setAction(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_LAUNCHER);

            String title = remoteMessage.getData().get("title");
            String message = remoteMessage.getData().get("body");
            String topic = remoteMessage.getFrom();

            NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "10001");
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
                builder.setVibrate(new long[] {200, 100, 200});
            }
            builder.setSmallIcon(R.drawable.smile)
                    .setAutoCancel(true)
                    .setDefaults(Notification.DEFAULT_SOUND)
                    .setContentTitle(title)
                    .setContentText(message);

            if (topic.equals(topics[0])) {
                notificationId = 0;
            } else if (topic.equals(topics[1])) {
                notificationId = 1;
            }

            if (notificationId >= 0) {
                PendingIntent pendingIntent = PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
                builder.setContentIntent(pendingIntent);
                notificationManager.notify(notificationId, builder.build());
            }

        } catch (NullPointerException nullException) {
            Toast.makeText(getApplicationContext(), "알림에 오류가 발생했습니다.", Toast.LENGTH_SHORT).show();
            Log.e("error Notify", nullException.toString());
        }
    }

백그라운드 관련 이슈

여기서 의도한 대로 동작하지 않아 이틀이나 허비하였다...

개발 의도는 앱이 Foreground 상태에서 푸시알림 클릭 시 별다른 이벤트 없이 자동으로 삭제, Home 버튼을 눌러 내렸을 경우 내렸던 앱 인스턴스를 다시 Foreground 상태로 전환, 앱을 종료하거나 Recent Tray에서 제거했을 경우 앱 재시작하게끔 동작을 목적으로 했었다. 문제는 Home 버튼을 눌러서 내린 케이스였다.

Notification 태그가 붙은 채로 수신되면 Firebase Messaging Service가 OnMessageReceived() 메소드를 호출하지 않는다. 따라서 단순히 home 버튼을 눌러 앱을 내린 경우임에도 푸시 알림 수신 후 클릭하면 앱을 reopen하여 인스턴스가 2개로 겹치는 현상이 발생했다.

stackoverflow와 몇몇 블로그 포스팅을 돌아본 결과 Firebase Console이나 구 버젼 pyfcm 등의 모듈을 통해 전송했을 경우 기본적으로 Notification 태그를 붙여서 송신한다. 이 경우 안드로이드에서 수신했을 때 Foreground에서 수신했을 시에만 OnMessageReceived()을 호출하게 된다. 즉, Notification 태그를 싹 빼고 Data Message로 푸시 알림 구성내용을 담아 전송해야 한다는 의미이다. 따라서 위 코드에서 알 수 있다시피 remoteMessage.getData().get("title") 처럼 태그 기반으로 접근하여 Notification을 구성하고 있음을 확인할 수 있다.

채널 생성 및 주제 구독 - InitApplication.class

Oreo 버전 이후로는 푸시알림을 사용하려면 채널을 생성해야 한다. 적당히 생성한 뒤 원하는 topic 키워드를 구독하게끔 코드를 작성한다.

public class InitApplication extends Application {
    private static volatile InitApplication mInstance = null;

    public static InitApplication getGlobalApplicationContext() {
        if (mInstance == null) {
            throw new IllegalStateException("This Application does not GlobalAuthHelper");
        }
        return mInstance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mInstance = this;
        createNotificationChannel(mInstance.getApplicationContext());
    }


    private void createNotificationChannel(Context context) {
        try{
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                NotificationChannel notificationChannel = new NotificationChannel("10001", getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
                NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
                // Configure the notification channel
                notificationChannel.setDescription("Firebase 푸시알림");
                notificationChannel.enableLights(true);
                notificationChannel.setVibrationPattern(new long[]{200, 100, 200});
                notificationChannel.enableVibration(true);
                notificationManager.createNotificationChannel(notificationChannel);
            }
            FirebaseMessaging.getInstance().subscribeToTopic("custom").addOnSuccessListener(new OnSuccessListener<Void>() {
                @Override
                public void onSuccess(Void aVoid) {
                    Toast.makeText(getApplicationContext(),"custom topic 구독",Toast.LENGTH_SHORT).show();
                }
            });
            FirebaseMessaging.getInstance().subscribeToTopic("notify").addOnSuccessListener(new OnSuccessListener<Void>() {
                @Override
                public void onSuccess(Void aVoid) {
                    Toast.makeText(getApplicationContext(),"notify topic 구독",Toast.LENGTH_SHORT).show();
                }
            });
        } catch (NullPointerException nullException) {
            Toast.makeText(context, "푸시 알림 채널 생성에 실패했습니다. 앱을 재실행하거나 재설치해주세요.", Toast.LENGTH_SHORT).show();
            nullException.printStackTrace();
        }
    }
}

앱 시작 시 topic 구독을 갱신해도 되는지 찾아본 결과 FCM 개발팀 소속이었던 구글 개발자가 괜찮다고 코멘트를 달아둔 걸 보니 별 문제는 없지 않을까 싶다.

이후 AndroidMenifest.xml에 InitApplication과 앞서 생성했던 FirebaseMessagingService를 추가해주자.

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

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

    <application
        android:name=".InitApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:replace="android:allowBackup">
        <service
            android:name=".MessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

Firebase Notification 송신

이번 프로젝트에서는 AWS Lambda에서 송신함을 요구받아 다음 포스팅에서 관련 내용을 정리하려 한다.

포스팅 링크 - AWS Lambda에서 Firebase Notification 전송하기

Firebase Console에서 Test Message를 보낼 수 있지만 기본적으로 Notification 태그를 붙여 송신하기 때문에 수신하는 Android 측에서 DataMessage 수신이 아니라 getNotifcation()메소드를 사용해서 구성해야 한다.

+ Recent posts