diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt index bc49d7ec..78b142ab 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt @@ -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.extensions.dpToPx 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.main.EventPopupDialogFragment import kr.co.vividnext.sodalive.message.MessageActivity @@ -107,6 +108,18 @@ class MainV2Activity : BaseActivity(ActivityMainV2Binding private var mediaControllerFuture: ListenableFuture? = null private val handler = Handler(Looper.getMainLooper()) 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 var playerStateJob: Job? = null private var isDeferredRouteLoading = false @@ -144,13 +157,7 @@ class MainV2Activity : BaseActivity(ActivityMainV2Binding trackAppLaunchIfNeeded() pushTokenUpdate() - handler.postDelayed({ - if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) { - executeDeeplink(intent) - } - isDeferredRouteLoading = false - updateRouteLoadingDialog() - }, 1000) + handler.postDelayed(deferredRouteRunnable, 1000) if (isV2AccessAllowed(AccessRequirement.Login)) { updatePidAndGaid() @@ -177,15 +184,27 @@ class MainV2Activity : BaseActivity(ActivityMainV2Binding override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + handler.removeCallbacks(deferredRouteRunnable) + isDeferredRouteLoading = false + updateRouteLoadingDialog() if (intent.hasExtra(EXTRA_CHAT_FILTER)) { openChatWithInitialFilter() return } - if (!handleAudioNotificationRoute(intent) && isV2AccessAllowed(AccessRequirement.Login)) { - executeDeeplink(intent) + if (handleAudioNotificationRoute(intent) || !isV2AccessAllowed(AccessRequirement.Login)) { + return } + + if (LiveRoomActivity.isForeground && isLiveRoomDeepLink(intent)) { + isDeferredRouteLoading = true + updateRouteLoadingDialog() + handler.postDelayed(deferredRouteRunnable, 1000) + return + } + + executeDeeplink(intent) } @SuppressLint("UnspecifiedRegisterReceiverFlag") @@ -212,6 +231,9 @@ class MainV2Activity : BaseActivity(ActivityMainV2Binding } override fun onDestroy() { + handler.removeCallbacks(deferredRouteRunnable) + isDeferredRouteLoading = false + routeLoadingDialog.dismiss() deInitMiniPlayer() playerStateJob?.cancel() super.onDestroy() @@ -759,19 +781,53 @@ class MainV2Activity : BaseActivity(ActivityMainV2Binding } 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 routeBundle = if (!deepLinkUrl.isNullOrBlank()) { + return if (!deepLinkUrl.isNullOrBlank()) { Bundle(bundle).apply { buildBundleFromDeepLinkUrl(deepLinkUrl)?.let { putAll(it) } } } else { 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? { diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt index d7a0c036..b386c6c3 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt @@ -87,6 +87,7 @@ class MainV2ActivitySourceTest { val observerSource = source.substringFrom("private fun setupLiveEntryObservers()") val updateSource = source.substringFrom("private fun updateRouteLoadingDialog()") 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("private lateinit var routeLoadingDialog: LoadingDialog")) @@ -104,7 +105,7 @@ class MainV2ActivitySourceTest { ) ) assertBefore(onCreateSource, "isDeferredRouteLoading =", "handler.postDelayed") - assertTrue(onCreateSource.contains("isDeferredRouteLoading = false")) + assertTrue(deferredRouteSource.contains("isDeferredRouteLoading = false")) assertTrue(updateSource.contains("if (isDeferredRouteLoading || isLiveEntryLoading)")) assertTrue(updateSource.contains("routeLoadingDialog.show(screenWidth)")) assertFalse(updateSource.contains("R.string.screen_live_loading")) @@ -112,6 +113,61 @@ class MainV2ActivitySourceTest { 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 fun `MainV2Activity는 Community post를 creator fallback보다 먼저 Action으로 라우팅한다`() { val source = projectFile("app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt").readText() diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2AudioNotificationRouteSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2AudioNotificationRouteSourceTest.kt index bae37f43..e81c563e 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2AudioNotificationRouteSourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2AudioNotificationRouteSourceTest.kt @@ -30,8 +30,10 @@ class MainV2AudioNotificationRouteSourceTest { assertBefore(onNewIntentSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)") val onCreateSource = source.substringFrom("override fun onCreate(savedInstanceState: Bundle?)") - assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "isV2AccessAllowed(AccessRequirement.Login)") - assertBefore(onCreateSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)") + val deferredRouteSource = source.substringFrom("private val deferredRouteRunnable = Runnable") + assertTrue(onCreateSource.contains("handler.postDelayed(deferredRouteRunnable, 1000)")) + assertBefore(deferredRouteSource, "handleAudioNotificationRoute(intent)", "isV2AccessAllowed(AccessRequirement.Login)") + assertBefore(deferredRouteSource, "handleAudioNotificationRoute(intent)", "executeDeeplink(intent)") } @Test