Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

android Firebase Cloud Message 보내고 받아보기 ...

Billcorea 2021. 8. 13. 14:03
반응형

https://medium.com/nybles/sending-push-notifications-by-using-firebase-cloud-messaging-249aa34f4f4c

 

Sending Push Notifications by Using Firebase Cloud Messaging

Ever wondered how does your smartphone receive and handle notifications whether be in foreground, background or even killed?

medium.com

앱을 만들꺼다... 저번에 하던 geofences 관련된 앱도 만들고 있는 중이고, 진척이 더디다... 그래도 만들꺼다. 이번에는 FCM 을 이용해서   message  push 을 해 보고자 한다. 

다수의 사용자에게 또는 특정 사용자에게 메시지 전송을 하고 싶다.   FCM 의 문서를 읽어보면 서버 구성을 할 수 있으면 좋을 것 같다. 그러나 일반적인 사용자에게는 서버가 있을턱은 없고. 그럼 그냥 앱에서 그것들을 구현해 보는 것이다. 

그럼 어떻게 할 것 인가 ?

https://stackoverflow.com/questions/37576705/firebase-java-server-to-send-push-notification-to-all-devices 

 

Firebase Java Server to send push notification to all devices

I am trying to send a Push notification to my android device with the new Firebase service. I registered and setup an app, also I put all the code needed to receive notification in the android app....

stackoverflow.com

어떤 주제를 가지고 공통의견을 가지는 사람들에게 알림을 보내는 방법.  일단 그 방법을 적용해 보기로 했다.

클라이언트 앱에서는 topic 을 2개 수신 등록을 해 두고 서버앱에서는 topic 을 이용해서 클라이언트 앱으로 메시지를 push 해 보는 것이다.

다수의 시간을 소비해 가면서 찾은 것들을 정리해 둠... 이런 저런 검색들 끝에 찾은건... 문서을 잘 읽어 보자. 그리고 서버가 없는 나는 어떻게 할 것 인가 ? 그것에 대한 고민을 해 보자. ㅠㅠ

아래 소스는 오늘 저녁 4시간 가량을 소비해 가면서 구글링 한 결과 얻은 소스을 일부 수정했다. android apps 에서 사용할 수 있도록 

저 class 을 따로 하나 만들어서 내가 사용하고 싶은 곳에서 호출해서 사용하면 된다.

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.Scanner;

/**
 * Firebase Cloud Messaging (FCM) can be used to send messages to clients on iOS, Android and Web.
 *
 * This sample uses FCM to send two types of messages to clients that are subscribed to the `news`
 * topic. One type of message is a simple notification message (display message). The other is
 * a notification message (display notification) with platform specific customizations, for example,
 * a badge is added to messages that are sent to iOS devices.
 */
public class Messaging {

    private static final String PROJECT_ID = "my-appl0fb";
    private static final String BASE_URL = "https://fcm.googleapis.com";
    private static final String FCM_SEND_ENDPOINT = "/v1/projects/" + PROJECT_ID + "/messages:send";

    private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
    private static final String[] SCOPES = { MESSAGING_SCOPE };

    private static final String TITLE = "FCM Notification";
    private static final String BODY = "Notification from FCM";
    public static final String MESSAGE_KEY = "message";

    public Messaging() {
        
    }

    /**
     * Retrieve a valid access token that can be use to authorize requests to the FCM REST
     * API.
     *
     * @return Access token.
     * @throws IOException
     */
    // [START retrieve_access_token]
    public String getAccessToken(Context context) throws IOException {

        AssetManager assetManager = context.getAssets();
        AssetFileDescriptor fileDescriptor = assetManager.openFd("my-ap0fb.json");
        FileInputStream fileInputStream = fileDescriptor.createInputStream();

        GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(fileInputStream)
                .createScoped(Arrays.asList(SCOPES));
        // googleCredentials.refreshAccessToken().getTokenValue();
        return googleCredentials.refreshAccessToken().getTokenValue();
    }
    // [END retrieve_access_token]

    /**
     * Create HttpURLConnection that can be used for both retrieving and publishing.
     *
     * @return Base HttpURLConnection.
     * @throws IOException
     */
    public HttpURLConnection getConnection(Context context) throws IOException {
        // [START use_access_token]
        URL url = new URL(BASE_URL + FCM_SEND_ENDPOINT);
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
        httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken(context));
        httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8");
        return httpURLConnection;
        // [END use_access_token]
    }

    /**
     * Send request to FCM message using HTTP.
     * Encoded with UTF-8 and support special characters.
     *
     * @param fcmMessage Body of the HTTP request.
     * @throws IOException
     */
    public void sendMessage(JsonObject fcmMessage, Context context) throws IOException {
        HttpURLConnection connection = getConnection(context);
        connection.setDoOutput(true);
        OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
        writer.write(fcmMessage.toString());
        writer.flush();
        writer.close();

        int responseCode = connection.getResponseCode();
        if (responseCode == 200) {
            String response = inputstreamToString(connection.getInputStream());
            System.out.println("Message sent to Firebase for delivery, response:");
            System.out.println(response);
        } else {
            System.out.println("Unable to send message to Firebase:");
            String response = inputstreamToString(connection.getErrorStream());
            System.out.println(response);
        }
    }

    /**
     * Send a message that uses the common FCM fields to send a notification message to all
     * platforms. Also platform specific overrides are used to customize how the message is
     * received on Android and iOS.
     *
     * @throws IOException
     */
    public void sendOverrideMessage(Context context) throws IOException {
        JsonObject overrideMessage = buildOverrideMessage();
        System.out.println("FCM request body for override message:");
        prettyPrint(overrideMessage);
        sendMessage(overrideMessage, context);
    }

    /**
     * Build the body of an FCM request. This body defines the common notification object
     * as well as platform specific customizations using the android and apns objects.
     *
     * @return JSON representation of the FCM request body.
     */
    public JsonObject buildOverrideMessage() {
        JsonObject jNotificationMessage = buildNotificationMessage();

        JsonObject messagePayload = jNotificationMessage.get(MESSAGE_KEY).getAsJsonObject();
        messagePayload.add("android", buildAndroidOverridePayload());

        JsonObject apnsPayload = new JsonObject();
        apnsPayload.add("headers", buildApnsHeadersOverridePayload());
        apnsPayload.add("payload", buildApsOverridePayload());

        messagePayload.add("apns", apnsPayload);

        jNotificationMessage.add(MESSAGE_KEY, messagePayload);

        return jNotificationMessage;
    }

    /**
     * Build the android payload that will customize how a message is received on Android.
     *
     * @return android payload of an FCM request.
     */
    public JsonObject buildAndroidOverridePayload() {
        JsonObject androidNotification = new JsonObject();
        androidNotification.addProperty("click_action", "android.intent.action.MAIN");

        JsonObject androidNotificationPayload = new JsonObject();
        androidNotificationPayload.add("notification", androidNotification);

        return androidNotificationPayload;
    }

    /**
     * Build the apns payload that will customize how a message is received on iOS.
     *
     * @return apns payload of an FCM request.
     */
    public JsonObject buildApnsHeadersOverridePayload() {
        JsonObject apnsHeaders = new JsonObject();
        apnsHeaders.addProperty("apns-priority", "10");

        return apnsHeaders;
    }

    /**
     * Build aps payload that will add a badge field to the message being sent to
     * iOS devices.
     *
     * @return JSON object with aps payload defined.
     */
    public JsonObject buildApsOverridePayload() {
        JsonObject badgePayload = new JsonObject();
        badgePayload.addProperty("badge", 1);

        JsonObject apsPayload = new JsonObject();
        apsPayload.add("aps", badgePayload);

        return apsPayload;
    }

    /**
     * Send notification message to FCM for delivery to registered devices.
     *
     * @throws IOException
     */
    public void sendCommonMessage(Context context) throws IOException {
        JsonObject notificationMessage = buildNotificationMessage();
        System.out.println("FCM request body for message using common notification object:");
        prettyPrint(notificationMessage);
        sendMessage(notificationMessage, context);
    }

    /**
     * Construct the body of a notification message request.
     *
     * @return JSON of notification message.
     */
    public JsonObject buildNotificationMessage() {
        JsonObject jNotification = new JsonObject();
        jNotification.addProperty("title", TITLE);
        jNotification.addProperty("body", BODY);

        JsonObject jMessage = new JsonObject();
        jMessage.add("notification", jNotification);
        jMessage.addProperty("topic", "allDevices");

        JsonObject jFcm = new JsonObject();
        jFcm.add(MESSAGE_KEY, jMessage);

        return jFcm;
    }

    /**
     * Read contents of InputStream into String.
     *
     * @param inputStream InputStream to read.
     * @return String containing contents of InputStream.
     * @throws IOException
     */
    public String inputstreamToString(InputStream inputStream) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        Scanner scanner = new Scanner(inputStream);
        while (scanner.hasNext()) {
            stringBuilder.append(scanner.nextLine());
        }
        return stringBuilder.toString();
    }

    /**
     * Pretty print a JsonObject.
     *
     * @param jsonObject JsonObject to pretty print.
     */
    public void prettyPrint(JsonObject jsonObject) {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(jsonObject) + "\n");
    }

    /*
    public static void main(String[] args) throws IOException {
        if (args.length == 1 && args[0].equals("common-message")) {
            sendCommonMessage();
        } else if (args.length == 1 && args[0].equals("override-message")) {
            sendOverrideMessage();
        } else {
            System.err.println("Invalid command. Please use one of the following commands:");
            // To send a simple notification message that is sent to all platforms using the common
            // fields.
            System.err.println("./gradlew run -Pmessage=common-message");
            // To send a simple notification message to all platforms using the common fields as well as
            // applying platform specific overrides.
            System.err.println("./gradlew run -Pmessage=override-message");
        }
    }
    */

}

소스의 내용중에 main 함수는 막았다. 왜냐면 android 내에서는 호출할 일이 없으니까. 다만 원래 있던 부분이니 혹시나 서버용으로 구성을 하는 경우 필요할 듯 하여...

이제 앱 수준의 gradle 파일에 추가를 해야 한다.

plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
}

android {
    compileSdk 30

    buildFeatures {
        viewBinding true
    }

    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/license.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/INDEX.LIST'
        exclude 'META-INF/notice.txt'
        exclude 'META-INF/ASL2.0'
    }

    aaptOptions {
        noCompress "json"
    }

}

dependencies {

    implementation 'com.google.auth:google-auth-library-oauth2-http:1.0.0'

    implementation platform('com.google.firebase:firebase-bom:28.2.1')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-analytics'

}

원래는 다른 것들도 많이 있지만, 이것들은 꼭 필요한 듯 하여...

aaptOptions 는 사용법이 파일 확장자를 등록하는 것인데, assets 폴더에 json 파일을 넣어주고 그걸 읽어오게 하려고 했더니 file not found 오류가 나서 찾다가 압축을 하지 않도록 설정하는 것이란다. 그래야 제대로 파일을 읽어올 수 있다고

그 assets 폴더에 들어가야 하는 파일은 인증을 시도하기 위해서 저장해야 하는 파일인데, 이건 messaging class 에 보면 getAccessToken 함수에서 사용하고 있으니 거길 보시면 되고 거기에 보면 openFd() 에 파일이름이 있는데, 원래 이름은 내려받은 파일 이름으로 작성하시면 됨 이파일은 

firebase console 에서 해당 프로젝트 설정 의 서비스 계정에 보면 새 비공개 키생성 이라고 있는데, 요기를 클릭하면 내려주는 json 파일인데, 이걸 받아서 이름을 변경하여 저장한 것을 내 프로젝트의 폴더에 담았다.

위치는 아래 그림 처럼 내 프로젝트 app/src/main/assets 폴더에 다가...

 

 

 

 

이제 준비는 된 것 같으니... MainActivity 에 호출하는 함수를 구현해 본다.

        new Thread() {
            @Override
            public void run() {
                super.run();
                try {

                    Messaging messaging = new Messaging(getApplicationContext());
                    messaging.sendCommonMessage(getApplicationContext());

                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }.start();

thread 을 사용하는 건 아시겠지만, android 가 http 통신을 하기 위해서는 비동기식으로 처리를 해야만 오류가 발생하지 않기 때문에 이렇게 처리를 하는 것이다.

혹시나 여기 까지를 했는데, 앱에서 수신을 하지 않는다면... Messaging 소스에서 topic 이라는 단어를 찾어 보시길, 이걸 설정하는 이유는 메시지 수신할 경우를 지정하기 위해서 인데, 앱에서 특정한 방법으로 메시지를 전송하려면 구현을 이런 방식으로 해 두는 것이 좋을 것 같기도 하고.  암튼 저 메시지가 수신되는 것으로 확인하고 싶으면 topic 을 먼저 구독 독 하도록 설정해야 하는데, 그건

        FirebaseMessaging.getInstance().subscribeToTopic("allDevices")
                .addOnCompleteListener(new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        Log.e(TAG, "allDevices subscribed ..." );
                    }
                });

MainActivity 의 onCreate 어딘가 쯤 위 코드를 넣어주면 앱이 시작 되면서 구독할 준비가 된다.

앱에서 메시지 수신에 대한 설명은 문서를 참조하여 작성하시길.  그건 아래 링크를 참조하시도록 남겨 놓는다.

https://firebase.google.com/docs/cloud-messaging/android/receive?authuser=0 

 

Android 앱에서 메시지 수신  |  Firebase

Firebase 알림의 동작은 수신하는 앱의 포그라운드/백그라운드 상태에 따라 달라집니다. 포그라운드 상태인 앱에서 알림 메시지 또는 데이터 메시지를 수신하려면 onMessageReceived 콜백을 처리하는

firebase.google.com

 

FCM 의 수신을 하는 경우 봐 두어야 하는 부분이 앱이 backgound 상태인 경우와 foreground 상태 메시지가 도달 했을 때 처리 되는 기준이 좀 다르다는 것을 알게 되었다. 위 링크에서 일부 설명은 되어 있지만 친절하게 설명이 되어 있지 않기 때문에 조금 준비를 하는 데 시간이 좀 필요하게 되었다. 

먼저 foreground 에서 수신 하는 경우에는  위 문서에서도 설명하고 있는 FirebaseMessagingService 에 onMessageReceived 을 통해 수신 되는 메시지를 이용해서 바로 보여 줄 수 있도록 구현을 하면 되었고,

@Override
    public void onMessageReceived(RemoteMessage remoteMessage) {

        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
        Log.e(TAG, "From: " + remoteMessage.getFrom());

        // Check if message contains a data payload.
        if (remoteMessage.getData().size() > 0) {
            Log.e(TAG, "Message data payload: " + remoteMessage.getData());

            sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);
            editor = sp.edit() ;

            Map<String, String> strMap = remoteMessage.getData();
            Log.e(TAG, "URL=" + strMap.get("URL"));
            Log.e(TAG, "BODY=" + strMap.get("BODY"));

            editor.putString("URL", strMap.get("URL").contains("http") ? strMap.get("URL") : "https://" + strMap.get("URL"));
            editor.putString("BODY", strMap.get("BODY"));
            editor.putBoolean("FcmTy", true) ;
            editor.commit();

            handleNow();

        }

        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.e(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
            sendNotification(remoteMessage.getNotification().getBody()) ;
        }

        onDeletedMessages();
    }
    
    private void handleNow() {

        Log.d(TAG, "Short lived task is done.");
        Intent intent = new Intent(FcmReceiveService.this, ViewActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);

    }

 

background 상태일 때는 앱의 시스템 알림으로 notify 을 하게 되는데, FirebaseMessagingService 에서는 아래 코드 처럼 notification 을 구현하게 되어 시스템 알림이 뜨게 된다 이  떄 알림을 클릭하면 

private void sendNotification(String messageBody) {
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        String channelId = getString(R.string.default_notification_channel_id);
        CharSequence channelName = getString(R.string.default_notification_channel_name);
        int importance = NotificationManager.IMPORTANCE_LOW;
        NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, importance);
        notificationChannel.enableLights(true);
        notificationChannel.setLightColor(Color.BLUE);
        notificationChannel.enableVibration(true);
        notificationChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
        notificationManager.createNotificationChannel(notificationChannel);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder =
                new NotificationCompat.Builder(this, channelId)
                        .setSmallIcon(R.drawable.ic_launcher_foreground)
                        .setContentTitle(getString(R.string.fcm_message))
                        .setContentText(messageBody)
                        .setAutoCancel(true)
                        .setSound(defaultSoundUri)
                        .setContentIntent(pendingIntent);

        // Since android Oreo notification channel is needed.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(channelId,
                    "Channel human readable title",
                    NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }

        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }

 

문서에 기술된 것 처럼 LAUNCHER activity 를 호출 하도록 되어 있다.

백그라운드 앱에서 알림 메시지 처리
앱이 백그라운드 상태이면 Android에서 알림 메시지를 작업 표시줄로 전송합니다. 사용자가 알림을 탭하면 기본적으로 앱 런처가 열립니다.

여기에는 알림과 데이터 페이로드가 모두 들어 있는 메시지 및 알림 콘솔에서 보낸 모든 메시지가 포함됩니다. 이러한 경우 알림은 기기의 작업 표시줄로 전송되고 데이터 페이로드는 런처 활동의 인텐트 부가 정보로 전송됩니다.

앱으로 전송된 메시지의 통계를 파악하려면, iOS 및 Android 기기에서 열린 전송 메시지 수와 Android 앱의 '노출수'(사용자에게 표시된 알림) 데이터가 기록된 FCM 보고 대시보드를 확인합니다.

 

그래서 Launcher 로 등록된 MainAcitivity 가 실행 될 때 받아온 수신 문구를 처리하도록 구성해 주어야 한다. 

    @Override
    protected void onStart() {
        super.onStart();

        Log.e(TAG, "onStart") ;

        if (getIntent().getExtras() != null && !sp.getBoolean("FcmTy", false)) {
            Bundle bundle = getIntent().getExtras();
            Log.e(TAG, "URL=" + bundle.getString("URL"));
            Log.e(TAG, "BODY=" + bundle.getString("BODY"));
            editor.putString("URL", bundle.getString("URL").contains("http") ? bundle.getString("URL") : "https://" + bundle.getString("URL"));
            editor.putString("BODY", bundle.getString("BODY"));
            editor.putBoolean("FcmTy", true) ;
            editor.commit();
            Intent intent1 = new Intent(this, ViewActivity.class);
            startActivity(intent1);
            finish();
        }
    }

 

onCreate 에 하지 않고 onStart 에서 처리 하는 것은 아무래도 activity 의 lifecycle 에 따른 조치라고 볼 수 있을 것 같다.

 

그럼... 오늘도 즐~

반응형