내용
이제 FCM의 기초 개념과 안드로이드 플랫폼에서의 사용시 기초 설정에 대해 알아봤으니, 이제 이 FCM을 제대로 처리할 차례이다.
FirebaseMessagingService 확장
[Android/FCM] (4) Android 설정 시에 AndroidManifest.xml파일에 FirebaseMessagingService
를 상속받는 Service
하나를 등록했었다. 메시지를 수신하기 위해선, FirebaseMessagingService
를 extend해야 한다.
해당 파일을 확장시켜 Firebase에서 보낸 알림을 가져와 앱 내부에서 우리가 처리하고자 하는 로직대로 처리하기 위해 상속받은 메소드들을 오버라이딩 해야 한다.
기기의 Registration Token에 Access
앱을 처음 시작할 때, FCM SDK는 클라이언트 앱 Instance
에 대한 Reigstration Token
을 생성한다.
만약, 단일 기기를 targeting하거나, 기기 그룹을 만들기 위해선, FirebaseMessagingService
를 extending하고, onNewToken
을 오버라이딩해야 한다.
아래 내용은 토큰을 검색하는 방법과, 토큰 변경 사항을 모니터링 하는 방법을 설명한다.
Token
이 초기 시작후, rotate
될 수 있으므로, 가장 최근에 업데이트된 Registration token
을 검색하는 것이 좋다.
(참고) Registration Token이 변경될 수 있는 케이스
- 앱이 새로운 기기에서 복원되는 경우
- 유저가 앱을 제거/재설치 하는 경우
- 유저가 앱 데이터를 지우는 경우
1. 현재 registration token 검색
현재 토큰을 검색하는 경우에는 FirebaseMessaging.getInstance().getToken()
를 사용하면 된다.
FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
if (!task.isSuccessful) {
Log.w(TAG, "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
}
// Get new FCM registration token
val token = task.result
// Log and toast
val msg = getString(R.string.msg_token_fmt, token)
Log.d(TAG, msg)
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
})
2. Token 생성 모니터링
onNewToken
Callback은 새 Token
이 생성될 때마다 실행된다.
→ 때문에 앱에서 토큰이 갱신 될 경우 서버에 해당 토큰을 갱신됐다고 알려주는 콜백함수.
/**
* Called if the FCM registration token is updated. This may occur if the security of
* the previous token had been compromised. Note that this is called when the
* FCM registration token is initially generated so this is where you would retrieve the token.
*/
override fun onNewToken(token: String) {
Log.d(TAG, "Refreshed token: $token")
// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
// FCM registration token to your app server.
sendRegistrationToServer(token)
}
Token
을 얻은 후, 앱 서버로 보내고 원하는 방법을 사용해 저장할 수 있다.
Check for Google Play Services
Play Services SDK
에 의존하는 앱의 경우에는 Google Play 서비스 기능
에 액세스하기 전에 항상 기기에서 호환되는 Google Play services APK
를 확인해야 한다.
MainActivity
의 onCreate()
메소드와 onResume()
메소드의 두 위치에서 이 작업을 수행하는 것이 좋다.
The check in onCreate()
은 성공적인 check 없이 앱을 사용할 수 없도록 한다.
The check in onResume()
은 유저가 Back 버튼과 같은 다른 수단을 통해 실행중인 앱으로 돌아가는 경우에도 check가 계속 수행되도록 한다.
만약 기기에 호환되는 Google Play servies가 없는 경우, GoogleApiAvailability.makeGooglePlayServicesAvailable()
을 호출해, 사용자가 Play 스토어에서 Google Play Services를 다운로드할 수 있도록 할 수 있다.
Android에서의 메시지 수신
Firebase 알림은 앱의 Foreground
/Background
상태에 따라 다르게 동작한다.
→ 한 마디로, Foreground
에 있는 경우엔 onMessageReceived
에서, Background
에 있는 경우엔 Data Message
를 받은 경우에만 onMessageReceived
에서 처리하며,Notification Message
는 System Tray
에서 처리한다.
따라서, 앱이 notification message
또는 data message
를 수신하도록 하려면, onMessageReceived
callback을 처리하는 코드를 작성해야 한다.
만약 optional Data Payload
가 있는 Notification Message
를 Background
상태에서 수신하는 경우, Notifiction이 System Tray
를 통해 기기에 표시되고, onMessageReceived
를 통해 Data가 전달 되지는 않는다.
즉, Notification
필드가 있는 상태에서 Data
필드는 SDK
내에서만 처리하고, onMessageReceived
로는 제공하지 않는다.
따라서, 만약 Background
로 메시지를 보내는데 Data Payload
를 처리하고 싶다면,
(1) Optional Data payload
가 있는 Notification Message
형태로 구현하고자 하는 경우엔 Intent
를 통해서 원하는 로직이 구현되도록 처리하도록 하거나
(2) Notification Field
가 없는 Data Message
형태로 변경해 알림을 보내서 onMessageReceived
에서 원하는 로직이 구현되도록 처리되도록 하면 된다.
Handling Messages
앞서 언급한대로, 메시지를 수신하기 위해선 FirebaseMessagingService
를 extend해야 하는데, 여기서 onMessageReceived
와 onDeletedMessages
Callback을 오버라이드 시켜야한다.
또한 수신후 20초 이내에(마시멜로우 버전은 10초 이내에) 모든 메시지를 처리해야 한다.
time window
는 onMessageReceived
를 호출하기 전 발생하는 OS delay에 따라 더 짧아질 수 있다.
이후, Android O이후 버전의 백그라운스 실행 제한(background execution limits)과 같은 다양한 OS 동작들이 작업을 완료하는데 방해가 될 수 있다. (자세한 내용은 메시지 우선순위를 다시 참고하면 좋다)
onMessageReceived
onMessageReceived
는 아래의 예시들을 제외하고는 대부분의 메시지 유형에 provide된다.
- 앱이
Background
에 있는 경우에Notification messages
가 deliver 되는 상황 Background
에서 수신하는 경우에, 메시지가notification
과data payload
를 모두 포함하고 있는 상황
Override onMessageReceived
FirebaseMessagingService.onMessageReceived
를 오버라이딩하면, 수신된 RemoteMessage
객체를 기반으로 작업을 수행하고, 메시지 데이터를 가져올 수 있다.
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// 수신한 메시지를 처리하는 곳
// Not getting messages here? See why this may be: https://goo.gl/39bRNJ
Log.d(TAG, "From: ${remoteMessage.from}")
// 메시지가 data payload를 포함하고 있는 경우
if (remoteMessage.data.isNotEmpty()) {
Log.d(TAG, "Message data payload: ${remoteMessage.data}")
// 다음과 같이 remoteMessage 객체에 있는 data 필드의 "body" key를 이용해서 값을 가져올 수 있음
val body = remoteMessage.data["body"]
if (/* Check if data needs to be processed by long running job */ true) {
// For long-running tasks (10 seconds or more) use WorkManager.
// 오래 걸리는 작업인지를 판단하는 if문을 작성한 뒤, 오래 걸리는 경우엔 따로 처리
scheduleJob()
} else {
// 10초 이내에 핸들링 가능한 메시지만 처리
handleNow()
}
}
// notification payload를 가지고 있는 경우
remoteMessage.notification?.let {
Log.d(TAG, "Message Notification Body: ${it.body}")
}
// 위의 조건들을 통해 notification을 받아왔으면 sendNotification을 통해 알림을 띄워야 한다
// sendNotification(messageBody)
}
추가 구현된 메소드들
// 오래 걸리는 작업을 처리하는 경우
private fun scheduleJob() {
// [START dispatch_job]
val work = OneTimeWorkRequest.Builder(MyWorker::class.java)
.build()
WorkManager.getInstance(this)
.beginWith(work)
.enqueue()
// [END dispatch_job]
}
// 10초 이내에 처리되는 작업을 처리하는 경우
private fun handleNow() {
Log.d(TAG, "Short lived task is done.")
}
// onNewToken을 통해 새로운 토큰이 생성된 경우에 서버에도 해당 토큰을 등록하는 작업
private fun sendRegistrationToServer(token: String?) {
// TODO: Implement this method to send token to your app server.
Log.d(TAG, "sendRegistrationTokenToServer($token)")
}
// FCM으로부터 받은 알림을 통해 기기에 알림을 띄울 수 있도록 처리하는 작업
private fun sendNotification(messageBody: String) {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_IMMUTABLE)
val channelId = "fcm_default_channel"
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setContentTitle("FCM Message")
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(channelId,
"Channel human readable title",
NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build())
}
companion object {
private const val TAG = "MyFirebaseMsgService"
}
internal class MyWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
override fun doWork(): Result {
// TODO(developer): add long running task here.
return Result.success()
}
}
Override onDeletedMessages
경우에 따라, FCM이 메시지를 deliver하지 못할 수도 있다.
deliver하지 못하는 경우
1. 특정 기기에서 앱에 대해 대기중인 메시지가 너무 많거나 (100개 이상)
2. 기기가 한 달 이상 FCM에 연결되지 않은 경우에 발생한다
이 경우에는, FirebaseMessagingService.onDeletedMessages()
에 대한 Callback을 수신할 수 있다.
앱 Instance가 이 callback을 수신하면, 앱 서버와 full 동기화를 수행해야 한다.
만약, 4주동안 해당 기기의 앱에 메시지를 보내지 않은 경우엔, FCM 은 onDeletedMessages()
를 호출하지 않는다.
FirebaseMessagingService Method Overriding 예시코드
Handle Notification Message in Background
앱이 Background
에 있는 경우, 안드로이드는 Notification mesage
를 system tray로 보낸다. 유저가 알림을 탭하면, 기본적으로 앱 launcher가 열리게 된다.
여기에는 notifcation
및 data payload
가 모두 포함된 메시지(optional data payload
를 가진 notification message
)가 포함된다. 이 경우에는, Notification Payload
는 system tray
로 전달되고, data payload
는 launcher Activity의 intent의 extras
로 deliver된다.
앱의 메시지 delivery에 대한 insight는 안드로이드 앱에 대한 “impressions(노출)” 데이터와 함께 애플및 안드로이드 기기에서 전송되고, 열린 메시지 수를 기록하는 FCM reporting dashboard를 참고하면 된다.
Background Restricted Apps(Android P 이상)
FCM은 사용자가 백그라운드 제한으로 설정한 앱에는 메시지를 전달하지 않을 수 있다.
앱이 백그라운드 제한에서 제거되면, 이전과 같이 앱에 대한 새로운 메시지가 전달된다. 메시지 손실 및 백그라운드 제한 영향을 방지하기 위해선, Android vitals에 언급된 bad behaviors들을 피해야 한다.
해당 bad behaivors들은 안드로이드 기기에서 유저에게 앱을 백그라운드 제한하도록 권장할 수 있으므로, 주의해야 한다.
앱이 현재 백그라운드 제한 설정되어있는지 확인
isBackgroundRestricted()
을 통해 앱이 현재 백그라운드 제한 설정이 되어있는지를 확인할 수 있다.
Direct Boot Mode에서의 FCM 메시지 수신
기기가 잠금해제 되기전에, FCM 메시지를 보내고자 하는 경우에는, 기기가 direct boot mode
에 있을 때 안드로이드 앱이 메시지를 수신하도록 할 수 있다. 예를 들어 앱 사용자가 잠긴 기기에서도 알림을 받도록 할 수 있다.
이 경우에는, best practices and restrictions for direct boot mode을 참고하면 좋을 듯 하다.
특히, direct boot-enabled
메시지의 visibility
를 고려하는 것이 특히 중요하다. 기기에 액세스할 수 있는 모든 유저는 사용자 credential
을 입력하지 않고도 이러한 메시지를 볼 수 있다
전제조건
- 기기가
direct boot mode
로 설정되어야 한다 - 기기에 최신 버전의
Google Play Services
가 설치되어 있어야 한다 - FCM 메시지를 수신하기 위해서는 앱에서
FCM SDK
를 사용해야 한다
앱에서 dirct boot mode 메시지 handling
1. App 수준의 gradle 파일에 다음 추가
implementation 'com.google.firebase:firebase-messaging-directboot:20.2.0'
2. AndroidManifest
AndroidManifest
에 android:directBootAware="true"
속성을 추가해 앱의FirebaseMessagingService
가 직접 부팅을 인식하도록 함
<service
android:name=".java.MyFirebaseMessagingService"
android:exported="false"
android:directBootAware="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
이 때, 특히, FirebaseMessagingService
가 direct-boot mode
에서 실행될 수 있는지를 확인하는 것이 중요한데 그 확인 사항은 다음과 같다
1. 서비스가 direct boot mode
에서 실행되는 동안, credential protected storage
(자격 증명 보호 저장소)에 액세스해서는 안된다
2. 서비스는 direct boot mode
에서 실행되는 동안, direct boot
로 인식되지 않은 Activities
, BroadcastReceivers
, 혹은 다른 Services
와 같은 components을 사용하려고해서는 안된다
3. 서비스가 사용하는 모든 라이브러리는 자격 증명 보호 저장소에 액세스하거나 direct-boot mode
에서 실행하는 동안, non-directBootAware
구성요소를 호출해서는 안된다.예를 들어, Firebase SDK는 direct mode
로 작동하지만, 많은 Firesbase API는 direct boot mode
에서 호출되는 것을 지원하지 않는다.
4. 즉, 서비스에서 호출되는 앱이 사용하는 모든 라이브러리는 direct boot
를 인식해야 하거나, 앱이 direct boot mode
에서 실행중인지를 확인하고, 해당 모드에서 라이브러리를 호출하지 않아야 한다.
5. 앱이 사용자 지정 Application
을 사용하는 경우, Application
도 direct-boot를 인식해야 한다 (direct boot mode에서 자격 증명 보호 저장소에 액세스 불가)
Direct Boot mode에서 기기에 메시지 보내는 방법
알림 권한 요청
Android 13이상에서의 Runtime Notification 권한요청
Android 13부터 알림 표시를 위한 새로운 Runtime 권한이 도입되었다. 이는 FCM 알림을 사용하는 Android 13이상에서 실행되는 모든 앱에 영향을 미친다.
기본적으로 FCM SDK(버전 23.0.6 이상)에는 Manifest에 정의된 POST_NOTIFICATIONS 권한이 포함되어있다.
그러나, 앱은 android.permission.POST_NOTIFICATIONS
상수를 통해 이 권한의 runtime 버전도 요청해야 한다.
사용자가 이 권한을 허용하기 전 까지는 앱에서 알림을 표시할 수 없게 되는 것이다.
새 runtime 권한을 요청하는 예시 코드
// Declare the launcher at the top of your Activity/Fragment:
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// FCM SDK (and your app) can post notifications.
} else {
// TODO: Inform user that that your app will not show notifications.
}
}
private fun askNotificationPermission() {
// This is only necessary for API level >= 33 (TIRAMISU)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
// FCM SDK (and your app) can post notifications.
} else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
// TODO: display an educational UI explaining to the user the features that will be enabled
// by them granting the POST_NOTIFICATION permission. This UI should provide the user
// "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission.
// If the user selects "No thanks," allow the user to continue without notifications.
} else {
// Directly ask for the permission
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
일반적으로, 앱이 알림을 게시할 수 있는 권한을 부여하면 활성화될 기능을 사용자에게 설명하는 UI를 표시해야 하며, 해당 UI는 확인및 아니요 버튼과 같이 동의 또는 거부할 수 있는 사용자 옵션을 제공해야 한다.
여기서 확인을 선택하면 권한이 직접 요청하고, 아니요를 선택하면, 알림없이 continue 할 수 있어야 한다.
앱이 사용자에게 POST_NOTIFICATIONS
권한을 요청해야 하는 경우에 대한 모범 예시는 문서(Notification runtime permission)에 적혀져있다.
Android 12L(API Level 32) 이하에서의 알림 권한
안드로이드는 앱이 foreground
에 있는 한, Notification Channeld
을 처음 생성할 때 자동으로 사용자에게 권한을 요청한다. 그러나, channel 생성과 권한 요청 시기와 관련하여 중요한 주의사항이 있다.
1. 앱이 Background
에서 실행될 때, 1번째 Notification Channel
을 생성하는 경우(FCM SDK가 FCM 알림을 수신할 때 수행함) 안드로이드는 알림 표시를 허용하지 않고, 앱이 열릴 때까지 사용자에게 알림 권한을 묻는 메시지를 표시하지 않는다. 즉, 사용자가 앱이 열리고 권한을 허용하기 전까지의 모든 알림은 lost 됨을 의미한다
2. 플랫폼의 API를 활용해 권한을 요청하기 위해서는 안드로이드 13이상으로 앱을 target하여 업데이트하는 것이 좋다. 이것이 불가능한 경우에는, 앱은 알림 권한 dialog를 trigger하고 알림 손실되지 않도록 하려면, 앱에 알림을 보내기 전에 notification channels을 만들어야 한다. → 자세한 내용은 notification permission best practices을 확인한다.
(선택사항) POST_NOTIFICATION 권한 제거
기본적으로 FCM SDK에는 POST NOTIFICATIONS 권한이 포함되어 있다.
앱에서 notification message를 사용하지 않고 (FCM notification나, 다른 SDK, 앱에서 직접 post하는 등을 통하는 경우) 앱에 권한이 포함되는 것을 원하지 않는 경우에는 manifest merger의 remove
marker를 활용해 제거할 수 있다.
이 권한을 제거하면 FCM 알림 뿐만아니라, 모든 알림이 표시되지 않는다. 아래의 코드를 manifest 파일에 삽입하면 권한이 제거된다.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" tools:node="remove"/>
'IT > Android' 카테고리의 다른 글
[Android] RecyclerView Deep Dive - 2. RecyclerView에서의 활용과 최적화 (0) | 2023.07.10 |
---|---|
[Android] RecyclerView Deep Dive - 1. RecyclerView 정의와 동작원리 및 생명주기 (0) | 2023.07.05 |
[Android/FCM] (4) Android 설정 (0) | 2023.03.07 |
[Android/FCM] (3) Registration Token 관리 (0) | 2023.03.02 |
[Android/FCM] (2) 플랫폼 별 차이와 Delivery Options알림 (0) | 2023.03.01 |