Skip to content
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

[TNT-242] 알림 딥링크 구현 #115

Merged
merged 4 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification_logo" />

<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/ic_notification_background" />
</application>

</manifest>
96 changes: 94 additions & 2 deletions app/src/main/java/co/kr/tnt/service/MessagingService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package co.kr.tnt.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import co.kr.tnt.R
import co.kr.tnt.main.MainActivity
import com.google.firebase.messaging.Constants.MessageNotificationKeys
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -9,11 +22,90 @@ class MessagingService : FirebaseMessagingService() {

override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
// TODO

// TODO message.data 형태로 오도록 요청
launchNotification(
title = message.data["title"] ?: "트레이너 연결 알림",
content = message.data["content"] ?: "트레이니님과 연결되었어요!",
pendingIntent = createPendingIntent(message.data)
)
}

private fun createPendingIntent(
data: Map<String, String>,
): PendingIntent? {
val trainerId = data["trainerId"] ?: "0"
val traineeId = data["traineeId"] ?: "0"

val intent = Intent(this, MainActivity::class.java).apply {
this.data = "tnt-deeplink://trainee-connect-complete/$trainerId/$traineeId".toUri()
}

return TaskStackBuilder.create(this).run {
addNextIntentWithParentStack(intent)
getPendingIntent(
INTENT_REQUEST_CODE,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
}

override fun handleIntent(intent: Intent?) {
// Background 일 때 FCM notification 필드가 있으면
// data 필드를 수신할 수 없는 현상을 해결하기 위해,
// notification key 값 제거
val new = intent?.apply {
val temp = extras?.apply {
remove(MessageNotificationKeys.ENABLE_NOTIFICATION)
remove(keyWithOldPrefix(MessageNotificationKeys.ENABLE_NOTIFICATION))
}
replaceExtras(temp)
}
super.handleIntent(new)
}

private fun launchNotification(
title: String?,
content: String,
pendingIntent: PendingIntent?,
) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
val notificationManager: NotificationManager =
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
createNotificationChannel(channel)
}

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_logo)
.setColor(ContextCompat.getColor(this, R.color.ic_notification_background))
.setContentTitle(title)
.setContentText(content)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()

notificationManager.notify(System.currentTimeMillis().toInt(), notification)
}

private fun keyWithOldPrefix(key: String): String {
if (!key.startsWith(MessageNotificationKeys.NOTIFICATION_PREFIX)) {
return key
}

return key.replace(
MessageNotificationKeys.NOTIFICATION_PREFIX,
MessageNotificationKeys.NOTIFICATION_PREFIX_OLD
)
}

override fun onNewToken(token: String) {
super.onNewToken(token)
// TODO
Log.w("Messaging service", "token : $token") // TODO
}

companion object {
private const val CHANNEL_ID = "TNT_NOTIFICATION_ID"
private const val CHANNEL_NAME = "TNT_NOTIFICATION"
private const val INTENT_REQUEST_CODE = 100
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion app/src/main/res/values/ic_launcher_background.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FF472F</color>
</resources>
<color name="ic_notification_background">#FF472F</color>
</resources>
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
package co.kr.data.network.model

import co.kr.tnt.domain.model.ConnectedResult
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ConnectedTraineeResponse(
val trainerName: String,
val trainee: Trainee,
val trainer: Trainer,
)

@Serializable
data class Trainee(
val traineeName: String,
val trainerProfileImageUrl: String?,
val traineeProfileImageUrl: String?,
val traineeAge: Int,
val height: Double,
val weight: Double,
val ptGoal: String,
val traineeProfileImageUrl: String,
val cautionNote: String?,
val height: Double?,
val ptGoal: String,
val traineeAge: Int?,
val weight: Double?,
)

@Serializable
data class Trainer(
@SerialName("trainerName")
val trainerName: String,
@SerialName("trainerProfileImageUrl")
val trainerProfileImageUrl: String,
)

fun ConnectedTraineeResponse.toDomain(): ConnectedResult =
ConnectedResult(
trainerName = trainerName,
traineeName = traineeName,
trainerImage = trainerProfileImageUrl,
traineeImage = traineeProfileImageUrl,
age = traineeAge,
height = height,
weight = weight,
ptGoal = ptGoal,
cautionNote = cautionNote,
trainerName = trainer.trainerName,
traineeName = trainee.traineeName,
trainerImage = trainer.trainerProfileImageUrl,
traineeImage = trainee.traineeProfileImageUrl,
age = trainee.traineeAge,
height = trainee.height,
weight = trainee.weight,
ptGoal = trainee.ptGoal,
cautionNote = trainee.cautionNote,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ data class ConnectedResult(
val traineeName: String,
val trainerImage: String?,
val traineeImage: String?,
val age: Int,
val height: Double,
val weight: Double,
val age: Int?,
val height: Double?,
val weight: Double?,
val ptGoal: String,
val cautionNote: String?,
)
11 changes: 3 additions & 8 deletions domain/src/main/java/co/kr/tnt/domain/model/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,26 @@ sealed class User {
}
}

// TODO 도메인 모델 개선
data class Trainee(
override val id: String,
override val name: String,
override val image: String?,
val birthday: LocalDate?,
val age: Int? = 0,
val weight: Double?,
val height: Int?,
val ptPurpose: List<String>?,
val caution: String?,
val isConnected: Boolean,
) : User() {
/** 한국식 나이 */
val age: Int? =
if (birthday == null) {
null
} else {
LocalDate.now().year - birthday.year + 1
}

companion object {
val EMPTY = Trainee(
id = "",
name = "",
image = null,
birthday = null,
age = null,
weight = null,
height = null,
ptPurpose = emptyList(),
Expand Down
11 changes: 11 additions & 0 deletions feature/main/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="trainee-connect-complete"
android:scheme="tnt-deeplink"
android:pathPattern="/.*"/>
</intent-filter>
</activity>
</application>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ private fun TraineeMyPageScreenPreview() {
user = User.Trainee(
id = "",
name = "김헬스",
age = 0,
image = null,
birthday = LocalDate.of(2001, 1, 1),
weight = 10.0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,11 @@ internal fun TraineeProfilePage(
text = trainee.ptPurpose?.joinToString(", ") ?: "",
)
Spacer(Modifier.height(32.dp))
if (!trainee.caution.isNullOrEmpty()) {
TextWithBackground(
label = stringResource(R.string.caution),
text = trainee.caution ?: "",
modifier = Modifier.height(128.dp),
)
}
TextWithBackground(
label = stringResource(R.string.caution),
text = trainee.caution ?: "",
modifier = Modifier.height(128.dp),
)
}
}
TnTBottomButton(
Expand Down Expand Up @@ -207,9 +205,14 @@ private fun TextWithBackground(
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Text(
text = text,
text = text.ifEmpty { "미입력" },
style = TnTTheme.typography.label1Medium,
color = TnTTheme.colors.neutralColors.Neutral800,
color =
if (text.isEmpty()) {
TnTTheme.colors.neutralColors.Neutral400
} else {
TnTTheme.colors.neutralColors.Neutral800
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ internal class TrainerConnectViewModel @Inject constructor(
name = result.traineeName,
image = result.traineeImage,
birthday = null,
age = result.age,
weight = result.weight,
height = result.height.toInt(),
height = result.height?.toInt(),
ptPurpose = listOf(result.ptGoal),
caution = result.cautionNote,
isConnected = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import androidx.navigation.toRoute
import co.kr.tnt.navigation.Route
import co.kr.tnt.trainer.connect.TrainerConnectRoute
Expand All @@ -21,7 +22,13 @@ fun NavGraphBuilder.trainerConnectScreen(
navigateToPrevious: () -> Unit,
navigateToHome: (Boolean) -> Unit,
) {
composable<Route.TrainerConnect> { backstackEntry ->
composable<Route.TrainerConnect>(
deepLinks = listOf(
navDeepLink<Route.TrainerConnect>(
basePath = "tnt-deeplink://trainee-connect-complete",
),
),
) { backstackEntry ->
backstackEntry.toRoute<Route.TrainerConnect>().apply {
TrainerConnectRoute(
trainerId = trainerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiEvent
import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState
import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState.DialogState
import co.kr.tnt.ui.base.BaseViewModel
import com.kizitonwose.calendar.core.yearMonth
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
Expand All @@ -21,7 +22,6 @@ import java.time.LocalDate
import java.time.LocalDateTime
import java.time.YearMonth
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import javax.inject.Inject

const val DIALOG_HIDE_DURATION_HOURS = 72
Expand All @@ -32,9 +32,9 @@ internal class TrainerHomeViewModel @Inject constructor(
private val connectRepository: ConnectRepository,
) :
BaseViewModel<TrainerHomeUiState, TrainerHomeUiEvent, TrainerHomeSideEffect>(TrainerHomeUiState()) {
private val cachedMonthlyPtSessionCounts: ConcurrentMap<YearMonth, List<TrainerDailyPtSessionCount>> =
private val cachedMonthlyPtSessionCounts: ConcurrentHashMap<YearMonth, List<TrainerDailyPtSessionCount>> =
ConcurrentHashMap()
private val cachedDailyPtSession: ConcurrentMap<LocalDate, List<PtSession>> = ConcurrentHashMap()
private val cachedDailyPtSession: ConcurrentHashMap<LocalDate, List<PtSession>> = ConcurrentHashMap()

init {
selectDay(LocalDate.now())
Expand All @@ -44,7 +44,7 @@ internal class TrainerHomeViewModel @Inject constructor(
when (event) {
TrainerHomeUiEvent.OnScreen -> refresh()
TrainerHomeUiEvent.OnClickNotification -> sendEffect(TrainerHomeSideEffect.NavigateToNotification)
is TrainerHomeUiEvent.OnChangeVisibleMonth -> handleChangeVisibleMonth(event.yearMonth)
is TrainerHomeUiEvent.OnChangeVisibleMonth -> getMonthlySessionCounts(event.yearMonth)
is TrainerHomeUiEvent.OnClickDay -> selectDay(event.day)
TrainerHomeUiEvent.OnClickAddPtSession -> showConnectDialog(false)

Expand All @@ -55,7 +55,7 @@ internal class TrainerHomeViewModel @Inject constructor(
}
}

private fun handleChangeVisibleMonth(yearMonth: YearMonth) {
private fun getMonthlySessionCounts(yearMonth: YearMonth) {
// 현재 달을 기준으로 2개월 전부터 2개월 후까지의 데이터를 한 번에 요청합니다.
val targetRange = -2L..2L

Expand Down Expand Up @@ -142,6 +142,7 @@ internal class TrainerHomeViewModel @Inject constructor(
cachedMonthlyPtSessionCounts.clear()
cachedDailyPtSession.clear()
selectDay(currentState.selectedDay)
getMonthlySessionCounts(currentState.selectedDay.yearMonth)
showConnectDialog(true)
}

Expand Down