feat(main): 라이브룸 딥링크 지연 처리를 추가한다

This commit is contained in:
2026-09-18 14:48:47 +09:00
parent 5e114e18a0
commit 086af8a366
3 changed files with 130 additions and 16 deletions
@@ -55,6 +55,7 @@ import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.LiveViewModel import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
import kr.co.vividnext.sodalive.message.MessageActivity import kr.co.vividnext.sodalive.message.MessageActivity
@@ -107,6 +108,18 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
private var mediaControllerFuture: ListenableFuture<MediaController>? = null private var mediaControllerFuture: ListenableFuture<MediaController>? = null
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() } private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
private val deferredRouteRunnable = Runnable {
if (isFinishing || isDestroyed) return@Runnable
try {
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent)
}
} finally {
isDeferredRouteLoading = false
updateRouteLoadingDialog()
}
}
private val audioContentReceiver = AudioContentReceiver() private val audioContentReceiver = AudioContentReceiver()
private var playerStateJob: Job? = null private var playerStateJob: Job? = null
private var isDeferredRouteLoading = false private var isDeferredRouteLoading = false
@@ -144,13 +157,7 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
trackAppLaunchIfNeeded() trackAppLaunchIfNeeded()
pushTokenUpdate() pushTokenUpdate()
handler.postDelayed({ handler.postDelayed(deferredRouteRunnable, 1000)
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent)
}
isDeferredRouteLoading = false
updateRouteLoadingDialog()
}, 1000)
if (isV2AccessAllowed(AccessRequirement.Login)) { if (isV2AccessAllowed(AccessRequirement.Login)) {
updatePidAndGaid() updatePidAndGaid()
@@ -177,15 +184,27 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
handler.removeCallbacks(deferredRouteRunnable)
isDeferredRouteLoading = false
updateRouteLoadingDialog()
if (intent.hasExtra(EXTRA_CHAT_FILTER)) { if (intent.hasExtra(EXTRA_CHAT_FILTER)) {
openChatWithInitialFilter() openChatWithInitialFilter()
return return
} }
if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) { if (handleAudioNotificationRoute(intent) || !isV2AccessAllowed(AccessRequirement.Login)) {
executeDeeplink(intent) return
} }
if (LiveRoomActivity.isForeground && isLiveRoomDeepLink(intent)) {
isDeferredRouteLoading = true
updateRouteLoadingDialog()
handler.postDelayed(deferredRouteRunnable, 1000)
return
}
executeDeeplink(intent)
} }
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
@@ -212,6 +231,9 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
} }
override fun onDestroy() { override fun onDestroy() {
handler.removeCallbacks(deferredRouteRunnable)
isDeferredRouteLoading = false
routeLoadingDialog.dismiss()
deInitMiniPlayer() deInitMiniPlayer()
playerStateJob?.cancel() playerStateJob?.cancel()
super.onDestroy() super.onDestroy()
@@ -759,19 +781,53 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
} }
private fun executeDeeplink(intent: Intent) { private fun executeDeeplink(intent: Intent) {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return val routeBundle = resolveDeepLinkBundle(intent) ?: return
if (executeBundleRoute(routeBundle)) {
clearDeferredDeepLink()
}
}
private fun resolveDeepLinkBundle(intent: Intent): Bundle? {
val bundle = intent.getBundleExtra(Constants.EXTRA_DATA) ?: return null
val deepLinkUrl = bundle.getString("deep_link") val deepLinkUrl = bundle.getString("deep_link")
val routeBundle = if (!deepLinkUrl.isNullOrBlank()) { return if (!deepLinkUrl.isNullOrBlank()) {
Bundle(bundle).apply { Bundle(bundle).apply {
buildBundleFromDeepLinkUrl(deepLinkUrl)?.let { putAll(it) } buildBundleFromDeepLinkUrl(deepLinkUrl)?.let { putAll(it) }
} }
} else { } else {
bundle bundle
} }
if (executeBundleRoute(routeBundle)) {
clearDeferredDeepLink()
} }
private fun isLiveRoomDeepLink(intent: Intent): Boolean {
val bundle = resolveDeepLinkBundle(intent) ?: return false
if (isDmChatDeepLink(bundle)) return false
val roomId = bundle.getString("room_id")?.toLongOrNull()
?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }
if (roomId != null && roomId > 0) return true
if (hasDirectNonLiveRoute(bundle)) return false
val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull()
return bundle.getString("deep_link_value")?.lowercase(Locale.ROOT) == "live" &&
deepLinkValueId != null && deepLinkValueId > 0
}
private fun hasDirectNonLiveRoute(bundle: Bundle): Boolean {
fun hasPositiveId(stringKey: String, longKey: String): Boolean {
return (
bundle.getString(stringKey)?.toLongOrNull()
?: bundle.getLong(longKey).takeIf { it > 0 }
)?.let { it > 0 } == true
}
return hasPositiveId(Constants.EXTRA_COMMUNITY_POST_ID, Constants.EXTRA_COMMUNITY_POST_ID) ||
hasPositiveId("channel_id", Constants.EXTRA_USER_ID) ||
hasPositiveId("content_id", Constants.EXTRA_AUDIO_CONTENT_ID) ||
hasPositiveId("message_id", Constants.EXTRA_MESSAGE_ID) ||
hasPositiveId(Constants.EXTRA_COMMUNITY_CREATOR_ID, Constants.EXTRA_COMMUNITY_CREATOR_ID) ||
hasPositiveId("audition_id", Constants.EXTRA_AUDITION_ID)
} }
private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? { private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? {
@@ -87,6 +87,7 @@ class MainV2ActivitySourceTest {
val observerSource = source.substringFrom("private fun setupLiveEntryObservers()") val observerSource = source.substringFrom("private fun setupLiveEntryObservers()")
val updateSource = source.substringFrom("private fun updateRouteLoadingDialog()") val updateSource = source.substringFrom("private fun updateRouteLoadingDialog()")
val setupViewSource = source.substringFrom("override fun setupView()") val setupViewSource = source.substringFrom("override fun setupView()")
val deferredRouteSource = source.substringFrom("private val deferredRouteRunnable = Runnable")
assertTrue(source.contains("import kr.co.vividnext.sodalive.common.LoadingDialog")) assertTrue(source.contains("import kr.co.vividnext.sodalive.common.LoadingDialog"))
assertTrue(source.contains("private lateinit var routeLoadingDialog: LoadingDialog")) assertTrue(source.contains("private lateinit var routeLoadingDialog: LoadingDialog"))
@@ -104,7 +105,7 @@ class MainV2ActivitySourceTest {
) )
) )
assertBefore(onCreateSource, "isDeferredRouteLoading =", "handler.postDelayed") assertBefore(onCreateSource, "isDeferredRouteLoading =", "handler.postDelayed")
assertTrue(onCreateSource.contains("isDeferredRouteLoading = false")) assertTrue(deferredRouteSource.contains("isDeferredRouteLoading = false"))
assertTrue(updateSource.contains("if (isDeferredRouteLoading || isLiveEntryLoading)")) assertTrue(updateSource.contains("if (isDeferredRouteLoading || isLiveEntryLoading)"))
assertTrue(updateSource.contains("routeLoadingDialog.show(screenWidth)")) assertTrue(updateSource.contains("routeLoadingDialog.show(screenWidth)"))
assertFalse(updateSource.contains("R.string.screen_live_loading")) assertFalse(updateSource.contains("R.string.screen_live_loading"))
@@ -112,6 +113,61 @@ class MainV2ActivitySourceTest {
assertTrue(setupViewSource.contains("setupLiveEntryObservers()")) assertTrue(setupViewSource.contains("setupLiveEntryObservers()"))
} }
@Test
fun `MainV2Activity는 foreground 라이브 딥링크만 로딩과 함께 지연한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
val onNewIntentSource = source.substringFrom("override fun onNewIntent(intent: Intent)")
val liveRouteSource = source.substringFrom("private fun isLiveRoomDeepLink(intent: Intent): Boolean")
assertTrue(source.contains("import kr.co.vividnext.sodalive.live.room.LiveRoomActivity"))
assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "isLiveRoomDeepLink(intent)")
assertTrue(
onNewIntentSource.contains(
"LiveRoomActivity.isForeground && isLiveRoomDeepLink(intent)"
)
)
assertBefore(onNewIntentSource, "isDeferredRouteLoading = true", "updateRouteLoadingDialog()")
assertBefore(onNewIntentSource, "updateRouteLoadingDialog()", "handler.postDelayed(deferredRouteRunnable, 1000)")
assertBefore(onNewIntentSource, "handler.postDelayed(deferredRouteRunnable, 1000)", "return")
assertBefore(onNewIntentSource, "return", "executeDeeplink(intent)")
assertTrue(liveRouteSource.contains("val bundle = resolveDeepLinkBundle(intent) ?: return false"))
assertTrue(liveRouteSource.contains("if (isDmChatDeepLink(bundle)) return false"))
assertTrue(liveRouteSource.contains("if (roomId != null && roomId > 0) return true"))
assertTrue(liveRouteSource.contains("if (hasDirectNonLiveRoute(bundle)) return false"))
assertTrue(liveRouteSource.contains("bundle.getString(\"deep_link_value\")?.lowercase(Locale.ROOT) == \"live\""))
assertTrue(liveRouteSource.contains("deepLinkValueId != null && deepLinkValueId > 0"))
}
@Test
fun `MainV2Activity는 종료할 때 지연 딥링크와 로딩을 정리한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
val onCreateSource = source.substringFrom("override fun onCreate(savedInstanceState: Bundle?)")
val onDestroySource = source.substringFrom("override fun onDestroy()")
val runnableSource = source.substringFrom("private val deferredRouteRunnable = Runnable")
assertTrue(onCreateSource.contains("handler.postDelayed(deferredRouteRunnable, 1000)"))
assertTrue(runnableSource.contains("if (isFinishing || isDestroyed) return@Runnable"))
assertBefore(runnableSource, "executeDeeplink(intent)", "isDeferredRouteLoading = false")
assertBefore(runnableSource, "isDeferredRouteLoading = false", "updateRouteLoadingDialog()")
assertTrue(onDestroySource.contains("handler.removeCallbacks(deferredRouteRunnable)"))
assertTrue(onDestroySource.contains("isDeferredRouteLoading = false"))
assertTrue(onDestroySource.contains("routeLoadingDialog.dismiss()"))
}
@Test
fun `MainV2Activity의 비라이브 ID 판별은 실제 route와 같은 fallback 순서를 사용한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
val routeSource = source.substringFrom("private fun hasDirectNonLiveRoute(bundle: Bundle): Boolean")
assertBefore(
routeSource,
"bundle.getString(stringKey)?.toLongOrNull()",
"?: bundle.getLong(longKey).takeIf { it > 0 }"
)
assertFalse(routeSource.contains("== true ||"))
}
@Test @Test
fun `MainV2Activity는 Community post를 creator fallback보다 먼저 Action으로 라우팅한다`() { fun `MainV2Activity는 Community post를 creator fallback보다 먼저 Action으로 라우팅한다`() {
val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText() val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText()
@@ -30,8 +30,10 @@ class MainV2AudioNotificationRouteSourceTest {
assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)") assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)")
val onCreateSource = source.substringFrom("override fun onCreate(savedInstanceState: Bundle?)") val onCreateSource = source.substringFrom("override fun onCreate(savedInstanceState: Bundle?)")
assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "isV2AccessAllowed(AccessRequirement.Login)") val deferredRouteSource = source.substringFrom("private val deferredRouteRunnable = Runnable")
assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)") assertTrue(onCreateSource.contains("handler.postDelayed(deferredRouteRunnable, 1000)"))
assertBefore(deferredRouteSource, "handleAudioNotificationRoute(intent)", "isV2AccessAllowed(AccessRequirement.Login)")
assertBefore(deferredRouteSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)")
} }
@Test @Test