Things take time

[Android] 푸시 Notification 만들기, 노티 빌더(Builder), 그리고 클릭 PendingIntent 본문

Android(기능)

[Android] 푸시 Notification 만들기, 노티 빌더(Builder), 그리고 클릭 PendingIntent

겸손할 겸 2018. 11. 27. 13:15

[노티피케이션]


실제 푸시를 발송하면 FCM 파이어베이스 서버에서 데이터를 받고, 그 데이터를 바탕으로 보낼 대상과 그 대상에게 전달할 메시지를 전송한다. (registration_ids : 토큰 리스트, data : 보낼 데이터)


이 때, 안드로이드에서는 FirebaseMessagingService를 상속받는 서비스의 onMessageReceived 함수에서 넘겨받는다.

이 함수에서는 서버에서 넘겨준 데이터를 바탕으로 실제 사용자가 푸시를 받았다는 느낌을 줄 수 있도록 UI를 만들고, 이벤트를 설정한다.



[UI]


여기서 UI를 만든다는 작업을 하는 것이 Notification이다.

NotificationCompat.Builder 클래스 혹은 Notification 클래스를 사용할 수 있는데, NotificationCompat.Builder 클래스를 사용할 것이다.

https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ko

NotificationCompat.Builder 클래스는 build()메소드에서 알림이란 것을 생성하고, 생성된 알림을 사용자에게 보여줄 때는 notify()라는 메소드를 사용한다.


Notification 객체는 3가지의 필수 정보를 담는다. setSmallIcon()과 setContentTitle(), setContentText()로 구분된다. 그러나 실제 setContentText()는 빼도 오류가 출력되지 않는다(?)


가장 기본적인 Noti는 아래와 같다. 채널은 앱 target이 Oreo 이상이면 해야 한다.


NotificationManager 클래스 변수를 하나 생성한다. 이 객체는 실제 사용자에게 UI를 그리라고 알려주는(notify) 역할이다.

NotificationChannel 클래스 변수를 하나 생성한다. 이 객체는 오레오 이상을 target하는 앱이라면 필수적으로 구현해야하는 푸시 전용 채널이며, 이 채널은 그룹핑할 수 있어 사용자가 직접 푸시 그룹들로 묶인 채널을 받을지 안받을지 제어할 수 있도록 하는 역할이다.

NotificationCompat.Builder 클래스 변수는 설명 생략한다.


            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationChannel notificationChannel = new NotificationChannel("channel_id", "channel_name", NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(notificationChannel);
            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, notificationChannel.getId());
            notificationBuilder.setAutoCancel(true)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentTitle("제목")
                    .setContentText("세부내용")
                    .setContentIntent(pendingIntent);
        notificationManager.notify(1
                // ID of notification
                , notificationBuilder.build());

그런데 저기서 보이는 setContentIntent(PendingIntent 타입)이 바로 이벤트다.



[이벤트]


푸시를 받는것에서 끝난다면 위의 setContentIntent를 할 필요가 없다. 즉, 선택사항인 항목이지만 대부분 앱들에서 사용하는 기능이다. 하이브리드앱을 사용하는 가장 기본적인 이유는 푸시 때문이라 할 수 있다. 모바일 웹페이지만으로는 푸시를 보낼 토큰 값을 생성할 수 없다. 위치 정보, 파일 업로드같은 네이티브의 강한 특성도 이제는 웹에서도 구현이 되지만, 푸시만큼은 구현할 방법이 없기 때문이다. 그렇기 때문에 단순히 푸시를 보내는 것에서 끝나지 않고 푸시를 클릭했을 때, 그 이후에 동작하는 '이벤트'를 설정하는 것도 중요한 기능이다.



PendingIntent란?


인텐트를 포함하는 인텐트, 사용하는 목적은 현재 앱이 아닌 외부의 앱(노티피케이션, 알람 등)이 현재 내가 개발한 앱을 열 수 있도록 허락할 수 있는 인텐트이며, 그 펜딩 인텐트 안에는 실제 데이터를 갖고, 열 액티비티를 저장한 인텐트를 갖고 있는 것과 같다.


내가 개발한 앱안에서 A라는 액티비티에서 B라는 액티비티를 열려면, Intent intent = new Intent(A.this, B.class)로 하여 startActivity(intent)지만, 외부에서는 이 intent를 포함하고 있는 PendingIntent를 선언하여 intent를 품게 한뒤 사용하게 하는 것이다.



간단히 말해서 외부(ex : 디바이스 상단 알림창)에서 내가 개발중인 앱을 실행하려면 Pending Intent를 통해서 열 수 있다는 것, 웹에서 앱을 실행할 때 사용하는 스키마도 어찌보면 비슷할지도.



[푸시 중첩? 개별?]


푸시를 받아서 노티를 그릴때, 상단을 보면 푸시를 개별적으로 다 보여줄 것인지, 아니면 하나의 노티에 쌓이게 할 것인지를 정해야할 때가 있다.


이때 중요한게 두 코드

PendingIntent notifyPendingIntent =
        PendingIntent.getActivity(
        this,
        0,
        notifyIntent,
        PendingIntent.FLAG_UPDATE_CURRENT
);

여기서 0의 값과

NotificationManager mNotificationManager =
    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Builds an anonymous Notification object from the builder, and
// passes it to the NotificationManager
mNotificationManager.notify(id, builder.build());

위의 id 값이다.


즉, 0과 id의 값이 각 노티마다 부여한다면, 앱은 별개의 푸시로 오게되고, 위의 예제처럼 0이란 하나의 값으로만 설정한다면 푸시는 중첩되게 된다.



[정규 액티비티 vs 특수 액티비티]


이 말이 무슨말인가 하니


예를 들어, 특정 게시글의 링크가 데이터로 담긴 푸시메시지를 받았고, 그를 인텐트로 만들어 펜딩안에 넣었다고 가정하자.

그 푸시를 누르면 앱이켜지면서 특정 게시글의 링크로 이동해야 한다. 근데 이 상황에서 뒤로가기 버튼을 눌렀다면 어떻게 해야하는가?

앱이 꺼지는 것인지, 아니면 앱을 키고 -> 게시글의 링크가 이동이 되는 일반적인 루트처럼, 앱을 켜진 초기화면 액티비티가 되어야 하는지 말이다.


전자가 특수 액티비티, 후자가 정규 액티비티다.


클릭이벤트에 덧붙여 나는 액티비티를 어떻게 쌓을지를 결정하는 것이다.



[정규 액티비티 / 특수 액티비티] 


번역본은 정규라는데, 뭐 일반적으로 사용하는 거겠지.


액티비티는 따로 설정하지 않는한 기본적으로 스택형태로 쌓인다. A이 앱이 열릴 때 처음 열린고 B가 호출이 된다면 스택에는 A위에 B가 있게 된다.


푸시를 눌렀을 때 B라는 액티비티를 키게 한다면, A를 실질적으로 들어가서 킨 것이 아니라 스택에 없게 된다. 이때는 A라는 액티비티를 B라는 액티비티의 PARENT로 설정해야 스택에 쌓이게 된다.


이 방법은 매니페스트, 그리고 클래스 두 곳 모두 해야한다. 


여기서 A는 Main Activity, B는 Result Activity라고 대체한다.


매니페스트 안에 설정은 다음과 같다.

<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ResultActivity"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>

클래스에서는 아래처럼 사용한다. 그리고 resultPendingIntent는 notificationBuilder의 setContentIntent()에 넣으면 된다.

        Intent resultIntent = new Intent(this, ResultActivity.class);
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addParentStack( ResultActivity.class );
        stackBuilder.addNextIntent(resultIntent);
        PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

잠깐 해석을 하자면,

매니페스트 설정파일에서는 MainActivity가 MAIN, LAUNCHER 속성을 가지므로 앱이 켜질때 먼저 호출되는 액티비티임을 알 수 있고, Result Activity는 MainActivity라는 부모 액티비티를 갖고 있는 액티비티라고 하는 것이다.


그리고 액티비티 클래스에서는 실제 푸시를 눌렀을 때 이동할 액티비티를 resultIntent로 설정했고(만약 putExtra처럼 데이터를 전달할 거면 이 인텐트에 넣어야 함) stackBuilder라는 클래스 변수를 통해 스택을 개발자가 직접 정의했다. ResultActivity라는 클래스를 Parent로 한다는게 아니라, ResultActivity의 Parent 스택을 쌓는데 그 스택에는 매니페스트의 설정에 있는 값, MainActivity를 넣겠다는 의미로 해석해야한다. addParentStack(스택에 쌓을 대상 액티비티)가 아니라, addParentStack(스택의 기본이 되는 액티비티)로 되어, 매니페스트의 설정 값을 불러온다고 생각하자.


여기서 한가지 더,


나의 경우에는 보통 3개의 액티비티를 사용한다.

A액티비티는 SplashActivity라고 하여 실제 B액티비티를 호출하기 전에, 인트로 이미지를 1-2초간 묶고 있는 액티비티가 있다. 공유할 데이터, 외부 스키마 등 처리도 하고 말이다.

B액티비티가 실제 MainActivity이며, 대부분의 작업을 수행한다.

C액티비티는 MainActivity에서 호출되는 상세페이지인데, 푸시를 눌렀을 때는 C액티비티를 오기 바란다.


이때, 그럼 C의 Parent는 A, B 둘 다 인가, A인가 B인가.

B로만 설정해 주면 된다. 위의 매니페스트 소스에서 MAIN, LAUNCHER를 A에 선언하는 것으로 바꾸고 B에는 아무것도 없이, C는 그대로 B(MainActivity)를 ParentActivity로 설정해주면 된다.


그리고 푸시를 누르게 되면 아래의 ResultActivity가 나오면서 상단 왼편에 <- 화살표가 자동적으로 생긴다. <- 화살표를 누르면 MainActivity로 이동한다.



이제 특수 액티비티다.

실제 예제로 테스트해본 결과 이 액티비티는 푸시 전용 뷰어 액티비티라고 생각하면 된다.

앱의 최근 사용목록에도 남지 않으며, 백버튼 클릭시 앱이 종료되기 때문이다.


매니페스트엔 아래처럼 등록한다. 나같은 경우 테스트를 위해 Result2Activity로 만들었다.


<activity android:name=".Result2Activity"
android:launchMode="singleTask"
android:taskAffinity=""
android:excludeFromRecents="true">
</activity>

taskAffinity는 인텐트에 들어가는 플래그 옵션중 FLAG_ACTIVITY_NEW_TASK 플래그를 사용해야 하며, 앱의 기본 실행 작업과는 별개의 독립성을 보장해주는 것이다.

excludeFromRecents는 최근 실행중인 앱 목록에서 나타나지 않게 하는 옵션이다.

        Intent notifyIntent = new Intent(this, Result2Activity.class);
        notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent notifyPendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);

그리고 액티비티 클래스 에서는 위처럼 코드를 작성한다. 그리고 setContetnIntent(notifyPendingIntent)를 한 뒤, 푸시를 받으면 아래와 같다.



그리고 해당 화면에서 뒤로가기를 하면 앱은 종료되고, 실행중인 목록에서도 해당 앱은 나타나지 않게 된다.



[Q. 정규 액티비티 방법으로 뒤로가기를 눌렀는데 앱이 종료된다.]


스택 오버플로에서도 봤고, 실제 나한테도 발생했던 문제인데.. 아무리 컴파일을 새로해도 계속 뒤로가기 하면 종료가 된다. 분명 Parent Activity인 MainActivity로 이동해야하는데..


=> 앱 삭제 후 재 설치할 것