diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index bc59949d..91ce1882 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -85,6 +85,7 @@ object Constants { const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2 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_LIVE_ROOM_DEEPLINK_CONFIRM = "soda_live_action_live_room_deeplink_confirm" const val EXTRA_COMMUNITY_POST_ID = "community_post_id" const val EXTRA_COMMUNITY_CREATOR_ID = "community_creator_id" diff --git a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt index edb0bec2..0887a851 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt @@ -14,7 +14,7 @@ import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.app.SodaLiveApp import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.SharedPreferenceManager -import kr.co.vividnext.sodalive.splash.SplashActivity +import kr.co.vividnext.sodalive.main.DeepLinkActivity class SodaFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { @@ -62,33 +62,22 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() { 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_SINGLE_TOP) - val roomId = messageData["room_id"] - if (roomId != null) { - intent.putExtra(Constants.EXTRA_ROOM_ID, roomId.toLong()) + val deepLinkExtras = android.os.Bundle().apply { + messageData["room_id"]?.let { putString("room_id", it) } + 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 (messageId != null) { - 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()) + if (!deepLinkExtras.isEmpty) { + intent.putExtra(Constants.EXTRA_DATA, deepLinkExtras) } val pendingIntent = diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index 0bedfa97..f6f24512 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -10,6 +10,10 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context 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.Canvas import android.graphics.Matrix @@ -18,6 +22,7 @@ import android.graphics.Path import android.graphics.Rect import android.graphics.Typeface import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.CountDownTimer @@ -51,6 +56,7 @@ import androidx.core.graphics.withTranslation import androidx.core.net.toUri import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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.RouletteSpinDialog 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.ReportType 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 org.koin.android.ext.android.inject import org.json.JSONObject +import java.util.Locale import java.util.concurrent.TimeUnit import java.util.regex.Pattern import kotlin.random.Random @@ -242,6 +251,22 @@ class LiveRoomActivity : BaseActivity(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 override fun onCreate(savedInstanceState: Bundle?) { initAgora() @@ -265,6 +290,11 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB override fun onStart() { super.onStart() + isForeground = true + LocalBroadcastManager.getInstance(this).registerReceiver( + deepLinkConfirmReceiver, + IntentFilter(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM) + ) if (this::layoutManager.isInitialized) { layoutManager.scrollToPosition(chatAdapter.itemCount - 1) @@ -284,6 +314,12 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + override fun onStop() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(deepLinkConfirmReceiver) + isForeground = false + super.onStop() + } + override fun onDestroy() { cropper.cleanup() hideKeyboard { @@ -297,6 +333,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB countDownTimer.cancel() super.onDestroy() } + // endregion // region setupView @@ -1177,7 +1214,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val clickableSpan = object : ClickableSpan() { override fun onClick(widget: View) { 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) @@ -1187,6 +1224,135 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB 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 { + 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() { hideKeyboard { if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) { @@ -3583,5 +3749,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB companion object { private const val NO_CHATTING_TIME = 180L + var isForeground: Boolean = false } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt index c9b9ea90..a6552cb8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt @@ -3,9 +3,11 @@ package kr.co.vividnext.sodalive.main import android.content.Intent import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper 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.splash.SplashActivity @@ -14,6 +16,8 @@ class DeepLinkActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val data: Uri? = intent?.data + val deepLinkExtras = buildDeepLinkExtras(intent) + if (data != null && data.scheme != null) { val host = data.host val path = data.path @@ -30,15 +34,136 @@ class DeepLinkActivity : AppCompatActivity() { } } - // 그 외 일반 딥링크는 기존처럼 Splash로 위임 + if (SodaLiveApp.isAppInForeground && LiveRoomActivity.isForeground && deepLinkExtras != null) { + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast( + Intent(Constants.ACTION_LIVE_ROOM_DEEPLINK_CONFIRM).apply { + putExtra(Constants.EXTRA_DATA, deepLinkExtras) + } + ) + finish() + 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(intent.data) + setData(data) + deepLinkExtras?.let { putExtra(Constants.EXTRA_DATA, it) } } ) + finish() + } - Handler(Looper.getMainLooper()).postDelayed({ - finish() - }, 1000) + 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 + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index 072046a8..3b192c32 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -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.user.login.LoginActivity import org.koin.android.ext.android.inject +import java.util.Locale @UnstableApi class MainActivity : BaseActivity(ActivityMainBinding::inflate) { @@ -267,105 +268,146 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl } 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) - if ( - SharedPreferenceManager.token.isNotBlank() && - SharedPreferenceManager.token.length > 10 && - bundle != null - ) { + val isHandledFromBundle = if (bundle != null) { try { - val roomId = bundle.getString("room_id")?.toLong() - ?: bundle.getLong(Constants.EXTRA_ROOM_ID) - val channelId = bundle.getString("channel_id")?.toLong() - ?: bundle.getLong(Constants.EXTRA_USER_ID) - val messageId = bundle.getString("message_id")?.toLong() - ?: bundle.getLong(Constants.EXTRA_MESSAGE_ID) - val auditionId = bundle.getString("audition_id")?.toLong() - ?: bundle.getLong(Constants.EXTRA_AUDITION_ID) - val contentId = bundle.getString("content_id")?.toLong() - ?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) - val isLiveReservation = bundle.getBoolean(Constants.EXTRA_LIVE_RESERVATION_RESPONSE) - - if (roomId > 0) { - viewModel.clickTab(MainViewModel.CurrentTab.LIVE) - - handler.postDelayed({ - if (isLiveReservation) { - liveFragment.reservationRoom(roomId) - } else { - liveFragment.enterLiveRoom(roomId) - } - }, 500) - } else if (channelId > 0) { - val nextIntent = Intent(applicationContext, UserProfileActivity::class.java) - nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId) - startActivity(nextIntent) - } else if (contentId > 0) { - val nextIntent = Intent( - applicationContext, - AudioContentDetailActivity::class.java - ) - nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) - startActivity(nextIntent) - } else if (messageId > 0) { - startActivity(Intent(applicationContext, MessageActivity::class.java)) - } else if (auditionId > 0) { - } + executeBundleDeeplink(bundle) } catch (_: IllegalStateException) { + false } + } else { + false + } + + if (isHandledFromBundle) { + 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) + + when { + roomId != null && roomId > 0 -> { + viewModel.clickTab(MainViewModel.CurrentTab.LIVE) + + handler.postDelayed({ + if (isLiveReservation) { + liveFragment.reservationRoom(roomId) + } else { + liveFragment.enterLiveRoom(roomId) + } + }, 500) + return true + } + + channelId != null && channelId > 0 -> { + val nextIntent = Intent(applicationContext, UserProfileActivity::class.java) + nextIntent.putExtra(Constants.EXTRA_USER_ID, channelId) + startActivity(nextIntent) + return true + } + + contentId != null && contentId > 0 -> { + val nextIntent = Intent( + applicationContext, + AudioContentDetailActivity::class.java + ) + nextIntent.putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) + startActivity(nextIntent) + return true + } + + messageId != null && messageId > 0 -> { + startActivity(Intent(applicationContext, MessageActivity::class.java)) + return true + } + } + + 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() { val deepLinkValue = SharedPreferenceManager.marketingLinkValue val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId if (deepLinkValue.isNotBlank() && deepLinkValueId > 0) { - when (deepLinkValue) { - "series" -> { - startActivity( - Intent(applicationContext, SeriesDetailActivity::class.java).apply { - putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId) - } - ) - } - - "content" -> { - startActivity( - Intent( - applicationContext, - AudioContentDetailActivity::class.java - ).apply { - putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId) - } - ) - } - - "channel" -> { - startActivity( - Intent(applicationContext, UserProfileActivity::class.java).apply { - putExtra(Constants.EXTRA_USER_ID, deepLinkValueId) - } - ) - } - - "live" -> { - viewModel.clickTab(MainViewModel.CurrentTab.LIVE) - - handler.postDelayed({ - liveFragment.enterLiveRoom(deepLinkValueId) - }, 500) - } - - else -> {} - } + routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId) } clearDeferredDeepLink() } + private fun routeByDeepLinkValue(deepLinkValue: String, deepLinkValueId: Long): Boolean { + return when (deepLinkValue.lowercase(Locale.ROOT)) { + "series" -> { + startActivity( + Intent(applicationContext, SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId) + } + ) + true + } + + "content" -> { + startActivity( + Intent( + applicationContext, + AudioContentDetailActivity::class.java + ).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId) + } + ) + true + } + + "channel" -> { + startActivity( + Intent(applicationContext, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, deepLinkValueId) + } + ) + true + } + + "live" -> { + viewModel.clickTab(MainViewModel.CurrentTab.LIVE) + + handler.postDelayed({ + liveFragment.enterLiveRoom(deepLinkValueId) + }, 500) + true + } + + else -> false + } + } + private fun clearDeferredDeepLink() { SharedPreferenceManager.marketingUtmSource = "" SharedPreferenceManager.marketingUtmMedium = "" diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 1d4c7bdc..da739512 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -429,6 +429,15 @@ End this live?\nChat history will not be saved and will disappear.\nParticipants will also be removed when the live ends. Leave live Leave this live room? + Open deep link + Close this page and move to %1$s? + Live room page + Channel profile page + Content detail page + Message page + Series detail page + Audition page + requested page Yes No Speaker request sent.\nwait a moment. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 57912526..2e75ddc9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -428,6 +428,15 @@ ライブを終了しますか?\n終了するとチャット内容は保存されず消えます。\nリスナーもライブ終了と共に\n強制退出となります。 ライブ退出 ライブから退出しますか? + ディープリンク移動 + 現在のページを終了して%1$sへ移動しますか? + ライブルームページ + チャンネルプロフィールページ + コンテンツ詳細ページ + メッセージページ + シリーズ詳細ページ + オーディションページ + リクエストされたページ はい いいえ スピーカーリクエストを送りました。\n少々お待ちください。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10d37b8f..fdb95cdb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,6 +428,15 @@ 라이브를 종료하시겠습니까?\n라이브를 종료하면 대화내용은\n저장되지 않고 사라집니다.\n참여자들 또한 라이브가 종료되어\n강제퇴장 됩니다. 라이브 나가기 라이브에서 나가시겠습니까? + 딥링크 이동 + 현재 페이지를 종료하고 %1$s로 이동하시겠습니까? + 라이브룸 페이지 + 채널 프로필 페이지 + 콘텐츠 상세 페이지 + 메시지 페이지 + 시리즈 상세 페이지 + 오디션 페이지 + 요청한 페이지 아니오 스피커 요청을 보냈습니다.\n잠시만 기다려 주세요. diff --git a/docs/20260306_딥링크스플래시우회및라이브룸확인다이얼로그.md b/docs/20260306_딥링크스플래시우회및라이브룸확인다이얼로그.md new file mode 100644 index 00000000..7c0abcf7 --- /dev/null +++ b/docs/20260306_딥링크스플래시우회및라이브룸확인다이얼로그.md @@ -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`