fix(deeplink): 딥링크 포그라운드 라우팅을 정비한다

This commit is contained in:
2026-03-06 16:54:35 +09:00
parent 93b620f4a8
commit 2b5240a565
9 changed files with 549 additions and 111 deletions

View File

@@ -85,6 +85,7 @@ object Constants {
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver" const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver"
const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver" const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver"
const val ACTION_LIVE_ROOM_DEEPLINK_CONFIRM = "soda_live_action_live_room_deeplink_confirm"
const val EXTRA_COMMUNITY_POST_ID = "community_post_id" const val EXTRA_COMMUNITY_POST_ID = "community_post_id"
const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id" const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id"

View File

@@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.app.SodaLiveApp import kr.co.vividnext.sodalive.app.SodaLiveApp
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.splash.SplashActivity import kr.co.vividnext.sodalive.main.DeepLinkActivity
class SodaFirebaseMessagingService : FirebaseMessagingService() { class SodaFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) { override fun onMessageReceived(remoteMessage: RemoteMessage) {
@@ -62,33 +62,22 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
val intent = Intent(this, SplashActivity::class.java) val intent = Intent(this, DeepLinkActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val roomId = messageData["room_id"] val deepLinkExtras = android.os.Bundle().apply {
if (roomId != null) { messageData["room_id"]?.let { putString("room_id", it) }
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId.toLong()) messageData["message_id"]?.let { putString("message_id", it) }
messageData["content_id"]?.let { putString("content_id", it) }
messageData["channel_id"]?.let { putString("channel_id", it) }
messageData["audition_id"]?.let { putString("audition_id", it) }
messageData["deep_link_value"]?.let { putString("deep_link_value", it) }
messageData["deep_link_sub5"]?.let { putString("deep_link_sub5", it) }
} }
val messageId = messageData["message_id"] if (!deepLinkExtras.isEmpty) {
if (messageId != null) { intent.putExtra(Constants.EXTRA_DATA, deepLinkExtras)
intent.putExtra(Constants.EXTRA_MESSAGE_ID, messageId.toLong())
}
val audioContentId = messageData["content_id"]
if (audioContentId != null) {
intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId.toLong())
}
val channelId = messageData["channel_id"]
if (channelId != null) {
intent.putExtra(Constants.EXTRA_USER_ID, channelId.toLong())
}
val auditionId = messageData["audition_id"]
if (auditionId != null) {
intent.putExtra(Constants.EXTRA_AUDITION_ID, auditionId.toLong())
} }
val pendingIntent = val pendingIntent =

View File

@@ -10,6 +10,10 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Matrix import android.graphics.Matrix
@@ -18,6 +22,7 @@ import android.graphics.Path
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
@@ -51,6 +56,7 @@ import androidx.core.graphics.withTranslation
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
@@ -109,6 +115,8 @@ import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
import kr.co.vividnext.sodalive.main.DeepLinkActivity
import kr.co.vividnext.sodalive.main.MainActivity
import kr.co.vividnext.sodalive.report.ProfileReportDialog import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog import kr.co.vividnext.sodalive.report.UserReportDialog
@@ -116,6 +124,7 @@ import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.settings.notification.MemberRole import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.json.JSONObject import org.json.JSONObject
import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.random.Random import kotlin.random.Random
@@ -242,6 +251,22 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
private val deepLinkConfirmReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val bundle = intent?.getBundleExtra(Constants.EXTRA_DATA) ?: return
val destinationPageName = resolveDestinationPageName(bundle)
showDeepLinkNavigationDialog(destinationPageName) {
val nextIntent = Intent(applicationContext, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(Constants.EXTRA_DATA, bundle)
}
startActivity(nextIntent)
finish()
}
}
}
// region lifecycle // region lifecycle
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
initAgora() initAgora()
@@ -265,6 +290,11 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
isForeground = true
LocalBroadcastManager.getInstance(this).registerReceiver(
deepLinkConfirmReceiver,
IntentFilter(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM)
)
if (this::layoutManager.isInitialized) { if (this::layoutManager.isInitialized) {
layoutManager.scrollToPosition(chatAdapter.itemCount - 1) layoutManager.scrollToPosition(chatAdapter.itemCount - 1)
@@ -284,6 +314,12 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
override fun onStop() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(deepLinkConfirmReceiver)
isForeground = false
super.onStop()
}
override fun onDestroy() { override fun onDestroy() {
cropper.cleanup() cropper.cleanup()
hideKeyboard { hideKeyboard {
@@ -297,6 +333,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
countDownTimer.cancel() countDownTimer.cancel()
super.onDestroy() super.onDestroy()
} }
// endregion // endregion
// region setupView // region setupView
@@ -1177,7 +1214,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
val clickableSpan = object : ClickableSpan() { val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) { override fun onClick(widget: View) {
val url = spannable.subSequence(start, end).toString() val url = spannable.subSequence(start, end).toString()
startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) handleNoticeUrlClick(url)
} }
} }
spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -1187,6 +1224,135 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
textView.movementMethod = LinkMovementMethod.getInstance() textView.movementMethod = LinkMovementMethod.getInstance()
} }
private fun handleNoticeUrlClick(url: String) {
val viewIntent = Intent(Intent.ACTION_VIEW, url.toUri())
if (isInAppDeepLinkIntent(viewIntent)) {
val deepLinkExtras = buildDeepLinkExtrasFromUri(viewIntent.data)
val destinationPageName = resolveDestinationPageName(deepLinkExtras)
showDeepLinkNavigationDialog(destinationPageName) {
startActivity(viewIntent)
finish()
}
return
}
startActivity(viewIntent)
}
private fun isInAppDeepLinkIntent(intent: Intent): Boolean {
val handlers = queryIntentActivitiesCompat(intent)
return handlers.any { resolveInfo ->
resolveInfo.activityInfo.packageName == packageName &&
resolveInfo.activityInfo.name == DeepLinkActivity::class.java.name
}
}
@Suppress("DEPRECATION")
private fun queryIntentActivitiesCompat(intent: Intent): List<ResolveInfo> {
val flags = PackageManager.MATCH_DEFAULT_ONLY
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.queryIntentActivities(
intent,
PackageManager.ResolveInfoFlags.of(flags.toLong())
)
} else {
packageManager.queryIntentActivities(intent, flags)
}
}
private fun buildDeepLinkExtrasFromUri(data: Uri?): Bundle {
val extras = Bundle()
if (data == null) {
return extras
}
fun putQuery(key: String) {
val value = data.getQueryParameter(key)
if (!value.isNullOrBlank()) {
extras.putString(key, value)
}
}
putQuery("room_id")
putQuery("channel_id")
putQuery("message_id")
putQuery("audition_id")
putQuery("content_id")
putQuery("deep_link_value")
putQuery("deep_link_sub5")
val deepLinkValue = extras.getString("deep_link_value")
val deepLinkValueId = extras.getString("deep_link_sub5")
if (!deepLinkValue.isNullOrBlank() && !deepLinkValueId.isNullOrBlank()) {
when (deepLinkValue.lowercase(Locale.ROOT)) {
"live" -> if (!extras.containsKey("room_id")) {
extras.putString("room_id", deepLinkValueId)
}
"channel" -> if (!extras.containsKey("channel_id")) {
extras.putString("channel_id", deepLinkValueId)
}
"content" -> if (!extras.containsKey("content_id")) {
extras.putString("content_id", deepLinkValueId)
}
"audition" -> if (!extras.containsKey("audition_id")) {
extras.putString("audition_id", deepLinkValueId)
}
else -> Unit
}
}
return extras
}
private fun resolveDestinationPageName(bundle: Bundle): String {
val roomId = bundle.getString("room_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
val channelId = bundle.getString("channel_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
val messageId = bundle.getString("message_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
val contentId = bundle.getString("content_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
return when {
roomId != null && roomId > 0 -> getString(R.string.screen_live_room_deeplink_target_live_room)
channelId != null && channelId > 0 -> getString(R.string.screen_live_room_deeplink_target_channel_profile)
contentId != null && contentId > 0 -> getString(R.string.screen_live_room_deeplink_target_content_detail)
messageId != null && messageId > 0 -> getString(R.string.screen_live_room_deeplink_target_message)
else -> {
when (bundle.getString("deep_link_value")?.lowercase(Locale.ROOT)) {
"live" -> getString(R.string.screen_live_room_deeplink_target_live_room)
"channel" -> getString(R.string.screen_live_room_deeplink_target_channel_profile)
"content" -> getString(R.string.screen_live_room_deeplink_target_content_detail)
"series" -> getString(R.string.screen_live_room_deeplink_target_series_detail)
"audition" -> getString(R.string.screen_live_room_deeplink_target_audition)
else -> getString(R.string.screen_live_room_deeplink_target_default)
}
}
}
}
private fun showDeepLinkNavigationDialog(destinationPageName: String, onConfirm: () -> Unit) {
SodaDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.screen_live_room_deeplink_move_title),
desc = getString(R.string.screen_live_room_deeplink_move_message, destinationPageName),
confirmButtonTitle = getString(R.string.screen_live_room_yes),
confirmButtonClick = {
onConfirm()
},
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}
private fun onClickQuit() { private fun onClickQuit() {
hideKeyboard { hideKeyboard {
if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) { if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) {
@@ -3583,5 +3749,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
companion object { companion object {
private const val NO_CHATTING_TIME = 180L private const val NO_CHATTING_TIME = 180L
var isForeground: Boolean = false
} }
} }

View File

@@ -3,9 +3,11 @@ package kr.co.vividnext.sodalive.main
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kr.co.vividnext.sodalive.app.SodaLiveApp
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
import kr.co.vividnext.sodalive.splash.SplashActivity import kr.co.vividnext.sodalive.splash.SplashActivity
@@ -14,6 +16,8 @@ class DeepLinkActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val data: Uri? = intent?.data val data: Uri? = intent?.data
val deepLinkExtras = buildDeepLinkExtras(intent)
if (data != null && data.scheme != null) { if (data != null && data.scheme != null) {
val host = data.host val host = data.host
val path = data.path val path = data.path
@@ -30,15 +34,136 @@ class DeepLinkActivity : AppCompatActivity() {
} }
} }
// 그 외 일반 딥링크는 기존처럼 Splash로 위임 if (SodaLiveApp.isAppInForeground && LiveRoomActivity.isForeground && deepLinkExtras != null) {
startActivity( LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(
Intent(applicationContext, SplashActivity::class.java).apply { Intent(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM).apply {
setData(intent.data) putExtra(Constants.EXTRA_DATA, deepLinkExtras)
} }
) )
Handler(Looper.getMainLooper()).postDelayed({
finish() finish()
}, 1000) return
}
if (SodaLiveApp.isAppInForeground) {
startActivity(
Intent(applicationContext, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
}
)
finish()
return
}
startActivity(
Intent(applicationContext, SplashActivity::class.java).apply {
setData(data)
deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) }
}
)
finish()
}
private fun buildDeepLinkExtras(intent: Intent): Bundle? {
val extras = Bundle()
val data = intent.data
if (data != null) {
fun putQuery(key: String) {
val value = data.getQueryParameter(key)
if (!value.isNullOrBlank()) {
extras.putString(key, value)
}
}
putQuery("room_id")
putQuery("channel_id")
putQuery("message_id")
putQuery("audition_id")
putQuery("content_id")
putQuery("deep_link_value")
putQuery("deep_link_sub5")
}
intent.getBundleExtra(Constants.EXTRA_DATA)?.let { source ->
fun copyString(key: String) {
val value = source.getString(key)
if (!value.isNullOrBlank()) {
extras.putString(key, value)
}
}
copyString("room_id")
copyString("channel_id")
copyString("message_id")
copyString("audition_id")
copyString("content_id")
copyString("deep_link_value")
copyString("deep_link_sub5")
source.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }?.let {
extras.putString("room_id", it.toString())
}
source.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }?.let {
extras.putString("channel_id", it.toString())
}
source.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }?.let {
extras.putString("message_id", it.toString())
}
source.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 }?.let {
extras.putString("audition_id", it.toString())
}
source.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }?.let {
extras.putString("content_id", it.toString())
}
}
intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0).takeIf { it > 0 }?.let {
extras.putString("room_id", it.toString())
}
intent.getLongExtra(Constants.EXTRA_USER_ID, 0).takeIf { it > 0 }?.let {
extras.putString("channel_id", it.toString())
}
intent.getLongExtra(Constants.EXTRA_MESSAGE_ID, 0).takeIf { it > 0 }?.let {
extras.putString("message_id", it.toString())
}
intent.getLongExtra(Constants.EXTRA_AUDITION_ID, 0).takeIf { it > 0 }?.let {
extras.putString("audition_id", it.toString())
}
intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0).takeIf { it > 0 }?.let {
extras.putString("content_id", it.toString())
}
val deepLinkValue = extras.getString("deep_link_value")
val deepLinkValueId = extras.getString("deep_link_sub5")
if (!deepLinkValue.isNullOrBlank() && !deepLinkValueId.isNullOrBlank()) {
when (deepLinkValue.lowercase()) {
"live" -> if (!extras.containsKey("room_id")) {
extras.putString("room_id", deepLinkValueId)
}
"channel" -> if (!extras.containsKey("channel_id")) {
extras.putString("channel_id", deepLinkValueId)
}
"content" -> if (!extras.containsKey("content_id")) {
extras.putString("content_id", deepLinkValueId)
}
"audition" -> if (!extras.containsKey("audition_id")) {
extras.putString("audition_id", deepLinkValueId)
}
else -> Unit
}
}
return if (extras.isEmpty) {
null
} else {
extras
}
} }
} }

View File

@@ -51,6 +51,7 @@ import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsDialog
import kr.co.vividnext.sodalive.user.login.LoginActivity import kr.co.vividnext.sodalive.user.login.LoginActivity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.Locale
@UnstableApi @UnstableApi
class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) { class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
@@ -267,26 +268,45 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
} }
private fun executeDeeplink(intent: Intent) { private fun executeDeeplink(intent: Intent) {
val isLoggedIn =
SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
if (!isLoggedIn) {
executeOneLink()
return
}
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) val bundle = intent.getBundleExtra(Constants.EXTRA_DATA)
if ( val isHandledFromBundle = if (bundle != null) {
SharedPreferenceManager.token.isNotBlank() &&
SharedPreferenceManager.token.length > 10 &&
bundle != null
) {
try { try {
val roomId = bundle.getString("room_id")?.toLong() executeBundleDeeplink(bundle)
?: bundle.getLong(Constants.EXTRA_ROOM_ID) } catch (_: IllegalStateException) {
val channelId = bundle.getString("channel_id")?.toLong() false
?: bundle.getLong(Constants.EXTRA_USER_ID) }
val messageId = bundle.getString("message_id")?.toLong() } else {
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID) false
val auditionId = bundle.getString("audition_id")?.toLong() }
?: bundle.getLong(Constants.EXTRA_AUDITION_ID)
val contentId = bundle.getString("content_id")?.toLong() if (isHandledFromBundle) {
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) clearDeferredDeepLink()
return
}
executeOneLink()
}
private fun executeBundleDeeplink(bundle: Bundle): Boolean {
val roomId = bundle.getString("room_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
val channelId = bundle.getString("channel_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 }
val messageId = bundle.getString("message_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 }
val contentId = bundle.getString("content_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }
val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE) val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE)
if (roomId > 0) { when {
roomId != null && roomId > 0 -> {
viewModel.clickTab(MainViewModel.CurrentTab.LIVE) viewModel.clickTab(MainViewModel.CurrentTab.LIVE)
handler.postDelayed({ handler.postDelayed({
@@ -296,26 +316,40 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
liveFragment.enterLiveRoom(roomId) liveFragment.enterLiveRoom(roomId)
} }
}, 500) }, 500)
} else if (channelId > 0) { return true
}
channelId != null && channelId > 0 -> {
val nextIntent = Intent(applicationContext, UserProfileActivity::class.java) val nextIntent = Intent(applicationContext, UserProfileActivity::class.java)
nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId) nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId)
startActivity(nextIntent) startActivity(nextIntent)
} else if (contentId > 0) { return true
}
contentId != null && contentId > 0 -> {
val nextIntent = Intent( val nextIntent = Intent(
applicationContext, applicationContext,
AudioContentDetailActivity::class.java AudioContentDetailActivity::class.java
) )
nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
startActivity(nextIntent) startActivity(nextIntent)
} else if (messageId > 0) { return true
startActivity(Intent(applicationContext, MessageActivity::class.java))
} else if (auditionId > 0) {
} }
} catch (_: IllegalStateException) {
messageId != null && messageId > 0 -> {
startActivity(Intent(applicationContext, MessageActivity::class.java))
return true
} }
} }
executeOneLink() val deepLinkValue = bundle.getString("deep_link_value")
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
if (!deepLinkValue.isNullOrBlank() && deepLinkValueId != null && deepLinkValueId > 0) {
return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
}
return false
} }
private fun executeOneLink() { private fun executeOneLink() {
@@ -323,13 +357,21 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId
if (deepLinkValue.isNotBlank() && deepLinkValueId > 0) { if (deepLinkValue.isNotBlank() && deepLinkValueId > 0) {
when (deepLinkValue) { routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId)
}
clearDeferredDeepLink()
}
private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long): Boolean {
return when (deepLinkValue.lowercase(Locale.ROOT)) {
"series" -> { "series" -> {
startActivity( startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply { Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId) putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId)
} }
) )
true
} }
"content" -> { "content" -> {
@@ -341,6 +383,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId) putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId)
} }
) )
true
} }
"channel" -> { "channel" -> {
@@ -349,6 +392,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
putExtra(Constants.EXTRA_USER_ID, deepLinkValueId) putExtra(Constants.EXTRA_USER_ID, deepLinkValueId)
} }
) )
true
} }
"live" -> { "live" -> {
@@ -357,15 +401,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
handler.postDelayed({ handler.postDelayed({
liveFragment.enterLiveRoom(deepLinkValueId) liveFragment.enterLiveRoom(deepLinkValueId)
}, 500) }, 500)
true
} }
else -> {} else -> false
} }
} }
clearDeferredDeepLink()
}
private fun clearDeferredDeepLink() { private fun clearDeferredDeepLink() {
SharedPreferenceManager.marketingUtmSource = "" SharedPreferenceManager.marketingUtmSource = ""
SharedPreferenceManager.marketingUtmMedium = "" SharedPreferenceManager.marketingUtmMedium = ""

View File

@@ -429,6 +429,15 @@
<string name="screen_live_room_end_message">End this live?\nChat history will not be saved and will disappear.\nParticipants will also be removed when the live ends.</string> <string name="screen_live_room_end_message">End this live?\nChat history will not be saved and will disappear.\nParticipants will also be removed when the live ends.</string>
<string name="screen_live_room_exit_title">Leave live</string> <string name="screen_live_room_exit_title">Leave live</string>
<string name="screen_live_room_exit_message">Leave this live room?</string> <string name="screen_live_room_exit_message">Leave this live room?</string>
<string name="screen_live_room_deeplink_move_title">Open deep link</string>
<string name="screen_live_room_deeplink_move_message">Close this page and move to %1$s?</string>
<string name="screen_live_room_deeplink_target_live_room">Live room page</string>
<string name="screen_live_room_deeplink_target_channel_profile">Channel profile page</string>
<string name="screen_live_room_deeplink_target_content_detail">Content detail page</string>
<string name="screen_live_room_deeplink_target_message">Message page</string>
<string name="screen_live_room_deeplink_target_series_detail">Series detail page</string>
<string name="screen_live_room_deeplink_target_audition">Audition page</string>
<string name="screen_live_room_deeplink_target_default">requested page</string>
<string name="screen_live_room_yes">Yes</string> <string name="screen_live_room_yes">Yes</string>
<string name="screen_live_room_no">No</string> <string name="screen_live_room_no">No</string>
<string name="screen_live_room_request_speaker">Speaker request sent.\nwait a moment.</string> <string name="screen_live_room_request_speaker">Speaker request sent.\nwait a moment.</string>

View File

@@ -428,6 +428,15 @@
<string name="screen_live_room_end_message">ライブを終了しますか?\n終了するとチャット内容は保存されず消えます。\nリスナーもライブ終了と共に\n強制退出となります。</string> <string name="screen_live_room_end_message">ライブを終了しますか?\n終了するとチャット内容は保存されず消えます。\nリスナーもライブ終了と共に\n強制退出となります。</string>
<string name="screen_live_room_exit_title">ライブ退出</string> <string name="screen_live_room_exit_title">ライブ退出</string>
<string name="screen_live_room_exit_message">ライブから退出しますか?</string> <string name="screen_live_room_exit_message">ライブから退出しますか?</string>
<string name="screen_live_room_deeplink_move_title">ディープリンク移動</string>
<string name="screen_live_room_deeplink_move_message">現在のページを終了して%1$sへ移動しますか</string>
<string name="screen_live_room_deeplink_target_live_room">ライブルームページ</string>
<string name="screen_live_room_deeplink_target_channel_profile">チャンネルプロフィールページ</string>
<string name="screen_live_room_deeplink_target_content_detail">コンテンツ詳細ページ</string>
<string name="screen_live_room_deeplink_target_message">メッセージページ</string>
<string name="screen_live_room_deeplink_target_series_detail">シリーズ詳細ページ</string>
<string name="screen_live_room_deeplink_target_audition">オーディションページ</string>
<string name="screen_live_room_deeplink_target_default">リクエストされたページ</string>
<string name="screen_live_room_yes">はい</string> <string name="screen_live_room_yes">はい</string>
<string name="screen_live_room_no">いいえ</string> <string name="screen_live_room_no">いいえ</string>
<string name="screen_live_room_request_speaker">スピーカーリクエストを送りました。\n少々お待ちください。</string> <string name="screen_live_room_request_speaker">スピーカーリクエストを送りました。\n少々お待ちください。</string>

View File

@@ -428,6 +428,15 @@
<string name="screen_live_room_end_message">라이브를 종료하시겠습니까?\n라이브를 종료하면 대화내용은\n저장되지 않고 사라집니다.\n참여자들 또한 라이브가 종료되어\n강제퇴장 됩니다.</string> <string name="screen_live_room_end_message">라이브를 종료하시겠습니까?\n라이브를 종료하면 대화내용은\n저장되지 않고 사라집니다.\n참여자들 또한 라이브가 종료되어\n강제퇴장 됩니다.</string>
<string name="screen_live_room_exit_title">라이브 나가기</string> <string name="screen_live_room_exit_title">라이브 나가기</string>
<string name="screen_live_room_exit_message">라이브에서 나가시겠습니까?</string> <string name="screen_live_room_exit_message">라이브에서 나가시겠습니까?</string>
<string name="screen_live_room_deeplink_move_title">딥링크 이동</string>
<string name="screen_live_room_deeplink_move_message">현재 페이지를 종료하고 %1$s로 이동하시겠습니까?</string>
<string name="screen_live_room_deeplink_target_live_room">라이브룸 페이지</string>
<string name="screen_live_room_deeplink_target_channel_profile">채널 프로필 페이지</string>
<string name="screen_live_room_deeplink_target_content_detail">콘텐츠 상세 페이지</string>
<string name="screen_live_room_deeplink_target_message">메시지 페이지</string>
<string name="screen_live_room_deeplink_target_series_detail">시리즈 상세 페이지</string>
<string name="screen_live_room_deeplink_target_audition">오디션 페이지</string>
<string name="screen_live_room_deeplink_target_default">요청한 페이지</string>
<string name="screen_live_room_yes"></string> <string name="screen_live_room_yes"></string>
<string name="screen_live_room_no">아니오</string> <string name="screen_live_room_no">아니오</string>
<string name="screen_live_room_request_speaker">스피커 요청을 보냈습니다.\n잠시만 기다려 주세요.</string> <string name="screen_live_room_request_speaker">스피커 요청을 보냈습니다.\n잠시만 기다려 주세요.</string>

View File

@@ -0,0 +1,87 @@
# 딥링크 실행 시 Splash 우회 및 LiveRoom 이동 확인 다이얼로그 구현
- [x] 요구사항 정리 및 영향 범위 확정 (`DeepLinkActivity`, `MainActivity`, `LiveRoomActivity`, `AndroidManifest.xml`, 문자열 리소스)
- [x] 딥링크 진입/라우팅 및 기존 다이얼로그 패턴 전수 탐색 (내부 검색 + 백그라운드 에이전트 병렬 탐색)
- [x] 앱 실행 중 딥링크 진입 시 `SplashActivity`를 거치지 않고 `MainActivity`로 직접 라우팅하도록 구현
- [x] 딥링크 파라미터를 `MainActivity.executeDeeplink`에서 즉시 처리할 수 있도록 전달/파싱 보강
- [x] `LiveRoomActivity`에서 앱 딥링크 실행 시 "현재 페이지 종료 후 이동" 확인 다이얼로그 추가 (확인 시 이동+현재 화면 종료, 취소 시 유지)
- [x] 신규 다이얼로그 문구 다국어 문자열(`values`, `values-en`, `values-ja`) 추가
- [x] 푸시 메시지 클릭 진입도 딥링크와 동일 라우팅 규칙 적용 (실행 중 Splash 우회)
- [x] 정적 진단/테스트/빌드 실행 후 결과를 검증 기록에 누적
## 검증 기록
### 2026-03-06 14:54 (KST)
- 무엇/왜/어떻게: 구현 착수 전 요구사항을 작업 단위로 분해하고, 딥링크 진입 경로와 현재 라우팅 구조를 기준으로 변경 포인트를 계획 문서에 확정했다.
- 전수 탐색: `explore` 3건 + `librarian` 2건 병렬 실행, `grep`/`ast-grep` 수행, `rg`는 로컬 미설치(`command not found`) 확인.
- 실행 명령 및 결과:
- `rg -n --hidden --glob '!**/build/**' "..." app/src/main` -> `command not found`
- 나머지 구현 검증 명령은 구현 완료 후 본 문서에 누적 기록 예정
### 2026-03-06 15:08 (KST)
- 무엇/왜/어떻게: `DeepLinkActivity`에서 앱 foreground 시 `MainActivity`로 직접 전달하도록 분기하고, URL query(`deep_link_value`, `deep_link_sub5`)를 `Constants.EXTRA_DATA`로 매핑해 `MainActivity.executeDeeplink`에서 즉시 처리되게 보강했다. 또한 `LiveRoomActivity` 공지 URL 클릭 시 앱 딥링크인 경우 `LiveDialog` 확인 후 이동/종료하도록 처리했다.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 실행 명령 및 결과:
- `lsp_diagnostics` (`DeepLinkActivity.kt`, `MainActivity.kt`, `LiveRoomActivity.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug :app:ktlintCheck` -> `:app:ktlintMainSourceSetCheck FAILED` (기존 누적 ktlint 위반 다수 + 기존 파일 이슈 포함)
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> `BUILD SUCCESSFUL`
### 2026-03-06 15:21 (KST)
- 무엇/왜/어떻게: 푸시 클릭 진입도 딥링크와 동일하게 처리하기 위해 `SodaFirebaseMessagingService`의 PendingIntent 타깃을 `DeepLinkActivity`로 통일하고, 푸시 payload를 `Constants.EXTRA_DATA` 번들(`room_id`, `channel_id`, `message_id`, `audition_id`, `content_id`)로 전달했다. 동시에 `DeepLinkActivity`에서 URI query + `EXTRA_DATA` + 레거시 long extras를 모두 병합 파싱하도록 보강해 warm 상태 Splash 우회 규칙을 푸시에도 적용했다.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
- 실행 명령 및 결과:
- `lsp_diagnostics` (`SodaFirebaseMessagingService.kt`, `DeepLinkActivity.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> `BUILD SUCCESSFUL`
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` (최종 수정 후 재실행) -> `BUILD SUCCESSFUL`
### 2026-03-06 15:29 (KST)
- 무엇/왜/어떻게: LiveRoom 화면 체류 중 외부 딥링크/푸시 탭 시 즉시 이동하지 않도록 `DeepLinkActivity`에서 `LiveRoomActivity.isForeground`를 확인해 확인 요청 브로드캐스트를 보내고 종료하도록 변경했다. `LiveRoomActivity`는 브로드캐스트 수신 시 `LiveDialog` 확인/취소를 표시하며, 확인 시 현재 화면을 종료하고 `MainActivity`로 딥링크 번들(`Constants.EXTRA_DATA`)을 전달해 목적지로 이동하고 취소 시 이동하지 않는다.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
### 2026-03-06 15:34 (KST)
- 실행 명령 및 결과:
- `lsp_diagnostics` (`Constants.kt`, `DeepLinkActivity.kt`, `LiveRoomActivity.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> 1차 컴파일 실패(`LiveRoomActivity` companion object 중복)
- companion 중복 선언 정리 후 `./gradlew :app:testDebugUnitTest :app:assembleDebug` 재실행 -> `BUILD SUCCESSFUL`
### 2026-03-06 15:43 (KST)
- 무엇/왜/어떻게: LiveRoom 체류 상태에서 다이얼로그가 누락되는 문제를 수정하기 위해, 브로드캐스트 방식 대신 `DeepLinkActivity`에서 직접 확인 다이얼로그(`AlertDialog`)를 표시하도록 전환했다. 조건은 `SodaLiveApp.isAppInForeground && LiveRoomActivity.isForeground`이며, 확인 시 `MainActivity``Constants.EXTRA_DATA`를 전달해 이동하고, 취소/백키/바깥영역 dismiss 시 `DeepLinkActivity`만 종료되어 이동하지 않는다. 또한 `LiveRoomActivity.isForeground` 판단을 `onStart/onStop` 기준으로 조정해 전환 순간에도 상태가 유지되도록 보강했다.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
- 실행 명령 및 결과:
- `lsp_diagnostics` (`DeepLinkActivity.kt`, `LiveRoomActivity.kt`, `Constants.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> `BUILD SUCCESSFUL`
### 2026-03-06 15:50 (KST)
- 무엇/왜/어떻게: 사용자 피드백(검은 배경 노출) 반영으로 `AlertDialog` 경유를 제거하고, LiveRoom 화면에서 직접 `SodaDialog`가 뜨도록 플로우를 조정했다. `DeepLinkActivity`는 LiveRoom 활성 시 `Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM` 브로드캐스트만 전송하고 종료하며, `LiveRoomActivity`는 이를 수신해 `SodaDialog`를 표시한다. 확인 시 `MainActivity`로 이동+현재 화면 종료, 취소 시 이동하지 않는다.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
- 실행 명령 및 결과:
- `lsp_diagnostics` (`DeepLinkActivity.kt`, `LiveRoomActivity.kt`, `Constants.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> `BUILD SUCCESSFUL`
### 2026-03-06 15:57 (KST)
- 무엇/왜/어떻게: LiveRoom 확인 다이얼로그 문구를 정적으로 `딥링크 목적지`라고 표시하던 방식에서, 실제 이동 대상 페이지명을 삽입하는 방식으로 변경했다. `LiveRoomActivity.resolveDestinationPageName`에서 `room_id/channel_id/content_id/message_id``deep_link_value`를 기반으로 목적지명을 결정하고, `SodaDialog` 설명에 `screen_live_room_deeplink_move_message(%1$s)` 포맷으로 주입한다. 예: 콘텐츠 등록 푸시 탭 시 `콘텐츠 상세 페이지`로 이동 문구 표시.
- 수정 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 실행 명령 및 결과:
- `lsp_diagnostics` (`LiveRoomActivity.kt`, `DeepLinkActivity.kt`) -> `.kt` 확장자 LSP 서버 미구성으로 실행 불가
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` -> `BUILD SUCCESSFUL`