2022. 2. 4. 11:30ㆍProgramming/Flutter
꽤 예전에 FirebaseMessaging을 사용해서 전달받은 클라우드 메시지를, FlutterLocalNotifications를 사용해서 푸시 메시지로 띄우는 작업을 했었습니다. 이번에 맞닥뜨린 문제는 FlutterLocalNotifications를 사용해서 화면에 띄운 푸시메시지를 터치해서 앱이 실행됐을 때, FirebaseMassaging에서 제공하는 getInitialMessage()
의 실행 결과값이 null
로 반환되는 상황입니다. 물론 onMessageOpenedApp()
역시 기대한대로 동작하지 않습니다.
private Task<Map<String, Object>> getInitialMessage(Map<String, Object> arguments) {
return Tasks.call(
cachedThreadPool,
() -> {
if (initialMessage != null) {
Map<String, Object> remoteMessageMap =
FlutterFirebaseMessagingUtils.remoteMessageToMap(initialMessage);
initialMessage = null;
return remoteMessageMap;
}
if (mainActivity == null) {
return null;
}
Intent intent = mainActivity.getIntent();
if (intent == null || intent.getExtras() == null) {
return null;
}
// Remote Message ID can be either one of the following...
String messageId = intent.getExtras().getString("google.message_id");
if (messageId == null) messageId = intent.getExtras().getString("message_id");
// We only want to handle non-consumed initial messages.
if (messageId == null || consumedInitialMessages.get(messageId) != null) {
return null;
}
RemoteMessage remoteMessage =
FlutterFirebaseMessagingReceiver.notifications.get(messageId);
// If we can't find a copy of the remote message in memory then check from our persisted store.
if (remoteMessage == null) {
remoteMessage =
FlutterFirebaseMessagingStore.getInstance().getFirebaseMessage(messageId);
FlutterFirebaseMessagingStore.getInstance().removeFirebaseMessage(messageId);
}
if (remoteMessage == null) {
return null;
}
consumedInitialMessages.put(messageId, true);
return FlutterFirebaseMessagingUtils.remoteMessageToMap(remoteMessage);
});
}
FirebaseMessaging Github내의 getInitialMessage()
를 살펴봅시다. 대충 다음의 절차에 따라서 동작하는 걸 알 수 있습니다.
initialMessage
값이null
이 아니면,FlutterFirebaseMessagingUtils.remoteMessageToMap
에 저장된remoteMessage
를 반환합니다.mainActivity
의intent
를 얻어온 뒤, 이 intent 내에 저장된 google.message_id값을 읽어옵니다. 이후 위에서 읽어온messagId
를 기반으로FlutterFirebaseMessagingReceiver.notifications
에 저장된remoteMessage
를 반환합니다.- 메모리에서
remoteMessage
를 찾지 못했다면,FlutterFirebaseMessagingStore
에서messageId
를 키 값으로 하는,remoteMessage
를 찾아서 반환합니다.
브레이크 포인트를 걸어서 디버깅해보면 원인을 쉽게 찾을 수 있을 것도 같지만, 백그라운드에서 메시지를 수신했을 때 동작하지 않는 원인을 찾아야하다보니 생각보다 원인을 찾기 어렵습니다. 크흡...
@Override
public boolean onNewIntent(Intent intent) {
if (intent == null || intent.getExtras() == null) {
return false;
}
// Remote Message ID can be either one of the following...
String messageId = intent.getExtras().getString("google.message_id");
if (messageId == null) messageId = intent.getExtras().getString("message_id");
if (messageId == null) {
return false;
}
RemoteMessage remoteMessage = FlutterFirebaseMessagingReceiver.notifications.get(messageId);
// If we can't find a copy of the remote message in memory then check from our persisted store.
if (remoteMessage == null) {
remoteMessage = FlutterFirebaseMessagingStore.getInstance().getFirebaseMessage(messageId);
// Note we don't remove it here as the user may still call getInitialMessage.
}
if (remoteMessage == null) {
return false;
}
// Store this message for later use by getInitialMessage.
initialMessage = remoteMessage;
FlutterFirebaseMessagingReceiver.notifications.remove(messageId);
channel.invokeMethod(
"Messaging#onMessageOpenedApp",
FlutterFirebaseMessagingUtils.remoteMessageToMap(remoteMessage));
mainActivity.setIntent(intent);
return true;
}
initialMessage
를 대체 어디서 넣어주나 했더니 onNewIntent
에서 intent
내에 google.message_id
를 키값으로 하는 데이터를 찾아서 initialMessage
에 넣어줍니다. 여기서 onNewIntent
는 안드로이드에서 Activity
를 실행할 때 새로운 intent
가 넘어올 때 실행되는 녀석이죠. 즉 푸시 메시지를 터치해서 앱을 실행했을 때, intent
가 제대로 생성되지 않는다는 얘기가 됩니다.
AndroidManifest.xml
에 설정이 문제인가싶어 아래의 내용을 추가해줬지만, 역시 동작하지 않습니다.
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
여기까지 분석하다가 동작하지 않는 이유를 명확히 밝히기 위해서는, 라이프 사이클 및 코드 동작을 좀 더 면밀히 분석해봐야 할 것 같아서 패스. 예상할 수 있는 건 서버쪽에서 click_action
을 지정해주지 않았기 때문에, FirebaseMessaging
이 전달받은 클라우드 메시지를 intent
로 넘겨주지도, 저장해놓지도 않기 때문이 아닐까 싶습니다. 결국 푸시 메시지를 터치했을 때 원하는 항목만 payload로 받아오면 동일한 결과를 얻을 수 있으므로, FlutterLocalNotifications
패키지에 포함된 getNotificationAppLaunchDetails()
를 사용하여 대체 가능합니다. 물론 이건 FlutterLocalNotifications
패키지를 사용할 때만 해결 가능한 방법이니 참고해주세요. 서버쪽을 수정할 수 있다면 서버쪽을 수정하시는 게 좋...겠죠? 아무튼 저는 서버쪽은 건드릴 수 없는 상태이므로 FlutterLocalNotifications.getNotificationAppLaunchDetails()
를 사용합니다.
FlutterLocalNotifications
패키지의 getNotificationAppLaunchDetails()
의 안드로이드 쪽 내부 구현을 살펴보면 아래와 같습니다. 상수값 SELECT_NOTIFICATION
, NOTIFICATION_LAUNCHED_APP
, PAYLOAD
는 모두 문자열로 된 키값입니다.
private void getNotificationAppLaunchDetails(Result result) {
Map<String, Object> notificationAppLaunchDetails = new HashMap<>();
String payload = null;
Boolean notificationLaunchedApp =
mainActivity != null
&& SELECT_NOTIFICATION.equals(mainActivity.getIntent().getAction())
&& !launchedActivityFromHistory(mainActivity.getIntent());
notificationAppLaunchDetails.put(NOTIFICATION_LAUNCHED_APP, notificationLaunchedApp);
if (notificationLaunchedApp) {
payload = launchIntent.getStringExtra(PAYLOAD);
}
notificationAppLaunchDetails.put(PAYLOAD, payload);
result.success(notificationAppLaunchDetails);
}
보면 다음의 조건을 모두 만족할 때, notificationLaunchedApp
값을 true
로 판단합니다. 즉, 앱이 푸시 메시지를 터치해서 실행됐다고 판단한다는 얘기죠.
mainActivity
가null
이 아님mainActivity
의intent
내에action
값이“SELECT_NOTIFICATION”
임 (푸시 메시지를 선택함)mainActivity
의intent
를 확인해서, 다른Activity
로부터(Activity History
로부터) 실행되지 않음
위의 조건을 모두 만족하면 launchIntent
에서 “PAYLOAD”
키값으로 저장된 데이터를 가져와서, notificationLaunchedApp
값과 함께 HashMap
형태로 넘겨줍니다. Result.success
의 인자값으로 HashMap
인 notificationAppLaunchDetails
을 넘겨줬으니, 플러터 코드에서 notificationAppLaunchDetails
를 참조할 수 있겠죠?
사족을 붙이자면 launchedIntent
는 다음과 같이, 플러터 앱이 실행될 때 registerWith
에서 할당됩니다. 개인적으로는 deprecation 경고를 무시하도록 어노테이션을 걸어놓은게 쪼오끔 마음에 안들긴 합니다만, 고치자면 또 귀찮기 때문에 그러려니 합니다.
@SuppressWarnings("deprecation")
public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
FlutterLocalNotificationsPlugin plugin = new FlutterLocalNotificationsPlugin();
plugin.setActivity(registrar.activity());
registrar.addNewIntentListener(plugin);
plugin.onAttachedToEngine(registrar.context(), registrar.messenger());
}
private void setActivity(Activity flutterActivity) {
this.mainActivity = flutterActivity;
if (mainActivity != null) {
launchIntent = mainActivity.getIntent();
}
}
자 그럼, 왜 FirebaseMessaging
의 getInitialMessage()
는 동작하지 않지만, getNotificationAppLaunchDetails()
은 동작하는지, createNotification
을 살펴보면 알 수 있습니다.
protected static Notification createNotification(
Context context, NotificationDetails notificationDetails) {
NotificationChannelDetails notificationChannelDetails =
NotificationChannelDetails.fromNotificationDetails(notificationDetails);
if (canCreateNotificationChannel(context, notificationChannelDetails)) {
setupNotificationChannel(context, notificationChannelDetails);
}
Intent intent = getLaunchIntent(context);
intent.setAction(SELECT_NOTIFICATION);
intent.putExtra(PAYLOAD, notificationDetails.payload);
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (VERSION.SDK_INT >= VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent =
PendingIntent.getActivity(context, notificationDetails.id, intent, flags);
DefaultStyleInformation defaultStyleInformation =
(DefaultStyleInformation) notificationDetails.styleInformation;
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, notificationDetails.channelId)
.setContentTitle(
defaultStyleInformation.htmlFormatTitle
? fromHtml(notificationDetails.title)
: notificationDetails.title)
.setContentText(
defaultStyleInformation.htmlFormatBody
? fromHtml(notificationDetails.body)
: notificationDetails.body)
.setTicker(notificationDetails.ticker)
.setAutoCancel(BooleanUtils.getValue(notificationDetails.autoCancel))
.setContentIntent(pendingIntent)
.setPriority(notificationDetails.priority)
.setOngoing(BooleanUtils.getValue(notificationDetails.ongoing))
.setOnlyAlertOnce(BooleanUtils.getValue(notificationDetails.onlyAlertOnce));
...... (생략)
setVisibility(notificationDetails, builder);
applyGrouping(notificationDetails, builder);
setSound(context, notificationDetails, builder);
setVibrationPattern(notificationDetails, builder);
setLights(notificationDetails, builder);
setStyle(context, notificationDetails, builder);
setProgress(notificationDetails, builder);
setCategory(notificationDetails, builder);
setTimeoutAfter(notificationDetails, builder);
Notification notification = builder.build();
if (notificationDetails.additionalFlags != null
&& notificationDetails.additionalFlags.length > 0) {
for (int additionalFlag : notificationDetails.additionalFlags) {
notification.flags |= additionalFlag;
}
}
return notification;
}
대충 보면 아시겠지만 createNotification
에서 notification
을 만들 때, intent
내에 action
을 “SELECT_NOTIFICATION”
으로 설정하고, “PAYLOAD”
키 값에 payload
를 넣어주고 있습니다. 그러니까 결국 flutterLocalNotifications
패키지를 사용해서 띄운 푸시 메시지를 터치했을 때, intent
를 얻어오면 action
과 payload
값이 모두 flutterLocalNotifications
패키지가 의도한대로 값이 설정되어있으니 getNotificationAppLaunchDetails()
는 정상적으로 동작하는 셈이죠.
대충 앱이 백그라운드 상태일 때 FlutterLocalNotifications
패키지를 사용해서 푸시 메시지를 출력했을 때, 푸시 메시지를 터치해서 앱을 실행했음에도 불구하고 getInitialMessage()
가 동작하지 않아서, 삽질끝에 getNotificationAppLaunchDetails()
는 멀쩡히 실행되는 걸 보고 뭐가 다른가 살짝 뜯어본 글이었습니다. ‘ ㅈ’)/
'Programming > Flutter' 카테고리의 다른 글
최상위 Navigator와 MaterialApp, 그리고 Navigator.push()와 GetX.to() (0) | 2022.07.05 |
---|---|
[Flutter] isolate와 SharedPreferences, 그리고 파일에 대한 접근 2 (0) | 2022.06.10 |
[Flutter] isolate와 SharedPreferences, 그리고 파일에 대한 접근 (2) | 2021.11.19 |
[Flutter] Flutter에서 SharedPreferences에 저장한 값을, Android 네이티브 영역에서 참조해보자. (0) | 2021.10.28 |
[Flutter] firebase_messaging과 flutter_local_notifications을 사용한 푸시 알림 기능 구현 (0) | 2021.10.18 |