-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] FCM 푸쉬알림 / 푸쉬알림 구현 #229
Changes from all commits
4662a19
2b1738e
0dcb0a2
32fd741
a1ce97b
f6c64f1
6f52257
73fa15c
d90a1b5
c3cdaa0
01918bb
9eebf99
1869607
46949f1
a40c575
6228ae9
768942c
4f3623a
30f3eab
2c35766
9ea1a46
38b24d3
8f30fc2
52c2089
173d49f
cb34398
01d7885
39ba5d9
bc1835a
fa5332a
4235bb2
643ef55
a9c5f7a
c8ee570
a4312b0
c098203
a66700f
dd0c643
bc88e5f
d119f8a
ff8ba91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package org.go.sopt.winey | ||
|
||
import android.app.Activity | ||
import android.app.Application | ||
import android.os.Bundle | ||
|
||
class ActivityLifecycleHandler(private val application: Application) : | ||
Application.ActivityLifecycleCallbacks { | ||
override fun onActivityCreated(p0: Activity, p1: Bundle?) { | ||
} | ||
|
||
override fun onActivityStarted(p0: Activity) { | ||
} | ||
|
||
override fun onActivityResumed(p0: Activity) { | ||
isAppInForeground = true | ||
} | ||
|
||
override fun onActivityPaused(p0: Activity) { | ||
isAppInForeground = false | ||
} | ||
|
||
override fun onActivityStopped(p0: Activity) { | ||
} | ||
|
||
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { | ||
} | ||
|
||
override fun onActivityDestroyed(p0: Activity) { | ||
} | ||
|
||
companion object { | ||
var isAppInForeground = false | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package org.go.sopt.winey.configuration | ||
|
||
import android.app.NotificationChannel | ||
import android.app.NotificationManager | ||
import android.app.PendingIntent | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.media.RingtoneManager | ||
import android.net.Uri | ||
import android.os.Build | ||
import androidx.core.app.NotificationCompat | ||
import com.google.firebase.messaging.FirebaseMessagingService | ||
import com.google.firebase.messaging.RemoteMessage | ||
import dagger.hilt.android.AndroidEntryPoint | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.launch | ||
import org.go.sopt.winey.ActivityLifecycleHandler | ||
import org.go.sopt.winey.R | ||
import org.go.sopt.winey.domain.repository.DataStoreRepository | ||
import org.go.sopt.winey.presentation.splash.SplashActivity | ||
import javax.inject.Inject | ||
|
||
@AndroidEntryPoint | ||
class WineyMessagingService : FirebaseMessagingService() { | ||
|
||
@Inject | ||
lateinit var dataStoreRepository: DataStoreRepository | ||
|
||
override fun onNewToken(token: String) { | ||
super.onNewToken(token) | ||
|
||
CoroutineScope(Dispatchers.IO).launch { dataStoreRepository.saveDeviceToken(token) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dispatchers.IO 으로 지정되어야하는 이유가 있을까요 ? FCM 자체가 백그라운드 스레드에서 호출된다고 알고 있어서 별도 스코프를 지정하지 않아도 될까하는데 궁금합니다 ! 자세히 공부해본 적은 없어서 아니라면 알려주세요 😮
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞습니다! 별도의 백그라운드 스레드에서 실행되기때문에 스코프 지정이 필요없습니다. 그런데 Dispatcher.default는 최대 12개의 스레드, 즉 12개의 작업만 한번에 할 수 있는 반면, Dispatcher.io는 최대 64개의 스레드를 사용한다고 합니다. 따라서 한 가지의 무거운 작업을 할때는 Dispatcher.default, 대기시간이 있는 가벼운 입출력 작업을 할때는 Dispatcher.io가 성능향상에 도움이 된다고 합니다. |
||
} | ||
|
||
override fun onMessageReceived(remoteMessage: RemoteMessage) { | ||
super.onMessageReceived(remoteMessage) | ||
if (remoteMessage.data.isNotEmpty() && !ActivityLifecycleHandler.isAppInForeground) { | ||
sendNotification(remoteMessage) | ||
} | ||
} | ||
|
||
private fun createNotificationIntent(remoteMessage: RemoteMessage): Intent { | ||
return Intent(this, SplashActivity::class.java).apply { | ||
putExtra(KEY_NOTI_TYPE, remoteMessage.data[KEY_NOTI_TYPE]) | ||
putExtra(KEY_FEED_ID, remoteMessage.data[KEY_FEED_ID]) | ||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||
} | ||
} | ||
|
||
private fun createPendingIntent(intent: Intent, uniqueIdentifier: Int): PendingIntent { | ||
return PendingIntent.getActivity( | ||
this, | ||
uniqueIdentifier, | ||
intent, | ||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 글을 통해 Intent vs. PendingIntent 차이점을 알 수 있었네요! 참고하면 좋을 거 같아요~! |
||
} | ||
|
||
private fun getSoundUri(): Uri { | ||
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) | ||
} | ||
|
||
private fun generateUniqueIdentifier(): Int { | ||
return (System.currentTimeMillis() / 7).toInt() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 7로 나누는 이유가 있을까요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 특별한 이유는 없습니다 ! 유니크한 고유 식별자를 만들기위해서 사용한 방식입니다. |
||
} | ||
|
||
private fun createNotificationBuilder(remoteMessage: RemoteMessage, pendingIntent: PendingIntent): NotificationCompat.Builder { | ||
val soundUri = getSoundUri() | ||
|
||
return NotificationCompat.Builder(this, CHANNEL_ID) | ||
.setSmallIcon(R.mipmap.ic_launcher) | ||
.setContentTitle(remoteMessage.data[KEY_TITLE]) | ||
.setContentText(remoteMessage.data[KEY_MESSAGE]) | ||
.setAutoCancel(true) | ||
.setSound(soundUri) | ||
.setContentIntent(pendingIntent) | ||
} | ||
|
||
private fun showNotification(notificationBuilder: NotificationCompat.Builder, uniqueIdentifier: Int) { | ||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) | ||
notificationManager.createNotificationChannel(channel) | ||
} | ||
|
||
notificationManager.notify(uniqueIdentifier, notificationBuilder.build()) | ||
} | ||
|
||
private fun sendNotification(remoteMessage: RemoteMessage) { | ||
val uniqueIdentifier = generateUniqueIdentifier() | ||
val intent = createNotificationIntent(remoteMessage) | ||
val pendingIntent = createPendingIntent(intent, uniqueIdentifier) | ||
val notification = createNotificationBuilder(remoteMessage, pendingIntent) | ||
|
||
showNotification(notification, uniqueIdentifier) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sendNotification 함수 안에서 스플래쉬 화면 띄우기, Notification 띄우기 등 여러 동작을 수행하고 있어서 |
||
|
||
companion object { | ||
private const val KEY_FEED_ID = "feedId" | ||
private const val KEY_NOTI_TYPE = "notiType" | ||
private const val KEY_TITLE = "title" | ||
private const val KEY_MESSAGE = "message" | ||
private const val CHANNEL_NAME = "Notice" | ||
private const val CHANNEL_ID = "channel" | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 상수화 👍👍 근데 NOTICE는 어떤 문자열인지 구분이 좀 어려워서 이름을 좀 더 구체적으로 적어도 좋을 거 같아요~! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.go.sopt.winey.data.model.remote.request | ||
|
||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class RequestPatchAllowedNotificationDto( | ||
@SerialName("allowedPush") | ||
val allowedPush: Boolean | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.go.sopt.winey.data.model.remote.request | ||
|
||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class RequestPatchFcmTokenDto( | ||
@SerialName("token") | ||
val fcmToken: String | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.go.sopt.winey.data.model.remote.response | ||
|
||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
|
||
@Serializable | ||
data class ResponsePatchAllowedNotificationDto( | ||
@SerialName("isAllowed") | ||
val isAllowed: Boolean | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
생성자의 인자로 application을 넘겨줘야 하는 이유가 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
불필요한 인자네요 수정하겠습니다 !