From f6a94e0f7c7e4dae1206a08b399bb259a94f5b6f Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 21 Apr 2026 14:11:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(live-room):=20=EB=AC=B4=EB=A3=8C=EB=B0=A9?= =?UTF-8?q?=20=EC=9E=85=EC=9E=A5=20=EC=A0=84=EB=A9=B4=20=EA=B4=91=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 + .../sodalive/live/room/LiveRoomActivity.kt | 101 ++++++++++++++++++ .../live/room/info/GetRoomInfoResponse.kt | 1 + 3 files changed, 104 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index dc072335..8dfaaf45 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,6 +75,7 @@ android { // release용 ad unit id는 배포 전 실제 값으로 교체한다. buildConfigField 'String', 'YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID', '"R-M-19140295-1"' + buildConfigField 'String', 'YANDEX_INTERSTITIAL_LIVE_ROOM_AD_UNIT_ID', '"R-M-19140295-2"' buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"' buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"' buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"' @@ -106,6 +107,7 @@ android { applicationIdSuffix '.debug' buildConfigField 'String', 'YANDEX_INLINE_BANNER_MYPAGE_AD_UNIT_ID', '"R-M-19140297-1"' + buildConfigField 'String', 'YANDEX_INTERSTITIAL_LIVE_ROOM_AD_UNIT_ID', '"R-M-19140297-2"' buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"' buildConfigField 'String', 'AGORA_API_BASE_URL', '"https://api.agora.io/api/speech-to-speech-translation/v2/"' buildConfigField 'String', 'AGORA_CUSTOMER_ID', '"de5dd9ea151f4a43ba1ad8411817b169"' 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 81715e73..84477435 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 @@ -82,6 +82,15 @@ import io.agora.rtm.RtmEventListener import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers +import com.yandex.mobile.ads.common.AdError +import com.yandex.mobile.ads.common.AdRequestConfiguration +import com.yandex.mobile.ads.common.AdRequestError +import com.yandex.mobile.ads.common.ImpressionData +import com.yandex.mobile.ads.interstitial.InterstitialAd +import com.yandex.mobile.ads.interstitial.InterstitialAdEventListener +import com.yandex.mobile.ads.interstitial.InterstitialAdLoadListener +import com.yandex.mobile.ads.interstitial.InterstitialAdLoader +import kr.co.vividnext.sodalive.BuildConfig import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.agora.Agora import kr.co.vividnext.sodalive.base.BaseActivity @@ -172,6 +181,13 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB private var isSpeaker = false private var hasKnownHostAbsence = false + private var isFreeRoomEntryInterstitialEligible = false + private var hasRequestedFreeRoomEntryInterstitialLoad = false + private var hasConsumedFreeRoomEntryInterstitialAttempt = false + private var isLiveRoomJoinCompleted = false + private var freeRoomEntryInterstitialAdLoader: InterstitialAdLoader? = null + private var freeRoomEntryInterstitialAd: InterstitialAd? = null + private var isCapturePrivacyMuted = false private var isScreenRecordingActive = false @@ -216,6 +232,35 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val content: String ) + private val freeRoomEntryInterstitialAdLoadListener = object : InterstitialAdLoadListener { + override fun onAdLoaded(interstitialAd: InterstitialAd) { + clearFreeRoomEntryInterstitialAd() + freeRoomEntryInterstitialAd = interstitialAd + maybeShowFreeRoomEntryInterstitial() + } + + override fun onAdFailedToLoad(error: AdRequestError) { + Logger.e("Free room interstitial failed to load: ${error.description}") + } + } + + private val freeRoomEntryInterstitialAdEventListener = object : InterstitialAdEventListener { + override fun onAdShown() = Unit + + override fun onAdFailedToShow(adError: AdError) { + Logger.e("Free room interstitial failed to show: ${adError.description}") + clearFreeRoomEntryInterstitialAd() + } + + override fun onAdDismissed() { + clearFreeRoomEntryInterstitialAd() + } + + override fun onAdClicked() = Unit + + override fun onAdImpression(impressionData: ImpressionData?) = Unit + } + // region 채팅 금지 private var isNoChatting = false private var isChatFrozen = false @@ -454,6 +499,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB override fun onDestroy() { // 액티비티 종료 전에 강제 음소거 상태를 원복한다. clearCapturePrivacyMuteState() + releaseFreeRoomEntryInterstitial() cropper.cleanup() hideKeyboard { viewModel.quitRoom(roomId) { @@ -1141,6 +1187,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } viewModel.roomInfoLiveData.observe(this) { response -> + syncFreeRoomEntryInterstitial(response) updateV2vAvailability(response) binding.ivShield.visibility = if (response.isAdult) { View.VISIBLE @@ -1510,6 +1557,58 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + private fun syncFreeRoomEntryInterstitial(roomInfo: GetRoomInfoResponse) { + isFreeRoomEntryInterstitialEligible = roomInfo.isFreeRoom + + if (!isFreeRoomEntryInterstitialEligible || hasRequestedFreeRoomEntryInterstitialLoad) { + return + } + + val adUnitId = BuildConfig.YANDEX_INTERSTITIAL_LIVE_ROOM_AD_UNIT_ID + if (adUnitId.isBlank()) { + Logger.e("Free room interstitial blocked: ad unit id is blank.") + return + } + + hasRequestedFreeRoomEntryInterstitialLoad = true + + freeRoomEntryInterstitialAdLoader = InterstitialAdLoader(this).apply { + setAdLoadListener(freeRoomEntryInterstitialAdLoadListener) + } + freeRoomEntryInterstitialAdLoader?.loadAd( + AdRequestConfiguration.Builder(adUnitId).build() + ) + } + + private fun maybeShowFreeRoomEntryInterstitial() { + if ( + !isFreeRoomEntryInterstitialEligible || + !isLiveRoomJoinCompleted || + hasConsumedFreeRoomEntryInterstitialAttempt || + !isForeground || + isFinishing || + isDestroyed + ) { + return + } + + val interstitialAd = freeRoomEntryInterstitialAd ?: return + hasConsumedFreeRoomEntryInterstitialAttempt = true + interstitialAd.setAdEventListener(freeRoomEntryInterstitialAdEventListener) + interstitialAd.show(this) + } + + private fun clearFreeRoomEntryInterstitialAd() { + freeRoomEntryInterstitialAd?.setAdEventListener(null) + freeRoomEntryInterstitialAd = null + } + + private fun releaseFreeRoomEntryInterstitial() { + freeRoomEntryInterstitialAdLoader?.setAdLoadListener(null) + freeRoomEntryInterstitialAdLoader = null + clearFreeRoomEntryInterstitialAd() + } + private fun hideKeyboard(onAfterExecute: () -> Unit) { handler.postDelayed({ imm.hideSoftInputFromWindow( @@ -2908,6 +3007,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB channelName = roomInfo.channelName, rtmChannelJoinSuccess = { isRtmJoined = true + isLiveRoomJoinCompleted = true // 두 채널 모두 연결 시 키보드 트릭 후 dismiss, 아니면 즉시 dismiss if (!tryForceLayoutRefresh()) { handler.post { loadingDialog.dismiss() } @@ -2936,6 +3036,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } setHeartButtonPosition() startPeriodicPlaybackValidation() + maybeShowFreeRoomEntryInterstitial() }, rtmChannelJoinFail = { agoraConnectFail() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt index a3b2f2c3..4106cf62 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt @@ -29,6 +29,7 @@ data class GetRoomInfoResponse( @SerializedName("isActiveRoulette") val isActiveRoulette: Boolean, @SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false, @SerializedName("isChatFrozen") val isChatFrozen: Boolean = false, + @SerializedName("isFreeRoom") val isFreeRoom: Boolean, @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean, @SerializedName("password") val password: String? = null )