diff --git a/app/build.gradle b/app/build.gradle index 8dfaaf45..0d701ff2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,10 @@ 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', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140295-3"' + buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140295-4"' + buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140295-6"' + buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140295-5"' 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"' @@ -108,6 +112,10 @@ android { 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', 'YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID', '"R-M-19140297-3"' + buildConfigField 'String', 'YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID', '"R-M-19140297-4"' + buildConfigField 'String', 'YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID', '"R-M-19140297-6"' + buildConfigField 'String', 'YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID', '"R-M-19140297-5"' 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/audio_content/detail/AudioContentDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt index d998b3b7..8a7430b8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/detail/AudioContentDetailActivity.kt @@ -28,6 +28,18 @@ import coil.transform.RoundedCornersTransformation import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.gson.Gson +import com.orhanobut.logger.Logger +import com.yandex.mobile.ads.banner.BannerAdSize +import com.yandex.mobile.ads.common.AdError +import com.yandex.mobile.ads.common.AdRequest +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.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.PurchaseOption @@ -60,6 +72,7 @@ import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent import kr.co.vividnext.sodalive.report.ReportType import org.koin.android.ext.android.inject import kotlin.math.ceil +import kotlin.math.roundToInt @UnstableApi class AudioContentDetailActivity : BaseActivity( @@ -88,6 +101,42 @@ class AudioContentDetailActivity : BaseActivity Unit)? = null + private var hasConsumedAudioContentPlayInterstitialAttempt = false + private var isAudioContentPlaying = false + private var isAudioContentInterstitialEligible = false + private var audioContentStartPlaybackAction: (() -> Unit)? = null + + private val audioContentPlayInterstitialAdLoadListener = object : InterstitialAdLoadListener { + override fun onAdLoaded(interstitialAd: InterstitialAd) { + clearAudioContentPlayInterstitialAd() + audioContentPlayInterstitialAd = interstitialAd + } + + override fun onAdFailedToLoad(error: AdRequestError) { + Logger.e("Audio content interstitial failed to load: ${error.description}") + } + } + + private val audioContentPlayInterstitialAdEventListener = object : InterstitialAdEventListener { + override fun onAdShown() = Unit + + override fun onAdFailedToShow(adError: AdError) { + Logger.e("Audio content interstitial failed to show: ${adError.description}") + continuePendingAudioContentPlayAction() + } + + override fun onAdDismissed() { + continuePendingAudioContentPlayAction() + } + + override fun onAdClicked() = Unit + + override fun onAdImpression(impressionData: ImpressionData?) = Unit + } + @SuppressLint("SetTextI18n") override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -105,6 +154,9 @@ class AudioContentDetailActivity : BaseActivity 0 } ?: screenWidth + val adWidthDp = (adWidthPixels / density).roundToInt() + val maxAdHeightDp = 90 + + binding.yandexInlineBannerView.apply { + setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_AUDIO_CONTENT_DETAIL_AD_UNIT_ID) + setAdSize( + BannerAdSize.inlineSize( + this@AudioContentDetailActivity, + adWidthDp, + maxAdHeightDp + ) + ) + loadAd(AdRequest.Builder().build()) + } + } + } + + private fun setupAudioContentPlayInterstitial() { + val adUnitId = BuildConfig.YANDEX_INTERSTITIAL_AUDIO_CONTENT_PLAY_AD_UNIT_ID + if (adUnitId.isBlank()) { + Logger.e("Audio content interstitial blocked: ad unit id is blank.") + return + } + + audioContentPlayInterstitialAdLoader = InterstitialAdLoader(this).apply { + setAdLoadListener(audioContentPlayInterstitialAdLoadListener) + } + audioContentPlayInterstitialAdLoader?.loadAd( + AdRequestConfiguration.Builder(adUnitId).build() + ) + } + + private fun playAudioContentWithInterstitialIfAvailable(playAction: () -> Unit) { + if (hasConsumedAudioContentPlayInterstitialAttempt || isFinishing || isDestroyed) { + playAction() + return + } + + val interstitialAd = audioContentPlayInterstitialAd ?: run { + playAction() + return + } + + hasConsumedAudioContentPlayInterstitialAttempt = true + pendingAudioContentPlayAction = playAction + interstitialAd.setAdEventListener(audioContentPlayInterstitialAdEventListener) + runCatching { + interstitialAd.show(this) + }.onFailure { + Logger.e("Audio content interstitial failed to show: ${it.message}") + continuePendingAudioContentPlayAction() + } + } + + private fun continuePendingAudioContentPlayAction() { + val playAction = pendingAudioContentPlayAction + pendingAudioContentPlayAction = null + clearAudioContentPlayInterstitialAd() + if (isFinishing || isDestroyed) { + return + } + playAction?.invoke() + } + + private fun clearAudioContentPlayInterstitialAd() { + audioContentPlayInterstitialAd?.setAdEventListener(null) + audioContentPlayInterstitialAd = null + } + + private fun releaseAudioContentPlayInterstitial() { + audioContentPlayInterstitialAdLoader?.setAdLoadListener(null) + audioContentPlayInterstitialAdLoader = null + pendingAudioContentPlayAction = null + clearAudioContentPlayInterstitialAd() + } + + private fun pauseAudioContentPlayback() { + startService( + Intent(this, AudioContentPlayService::class.java).apply { + action = AudioContentPlayService.MusicAction.PAUSE.name + } + ) + } + + private fun updateAudioContentPlayOrPauseControls() { + val startPlaybackAction = audioContentStartPlaybackAction ?: return + + if (isAudioContentPlaying) { + binding.ivPlayOrPause.visibility = View.VISIBLE + binding.llPreview.visibility = View.GONE + binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_pause) + binding.ivPlayOrPause.setOnClickListener { pauseAudioContentPlayback() } + binding.llPreview.setOnClickListener(null) + return + } + + binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play) + + val startPlaybackClickListener = View.OnClickListener { + if (isAudioContentInterstitialEligible) { + playAudioContentWithInterstitialIfAvailable(startPlaybackAction) + } else { + startPlaybackAction() + } + } + + binding.ivPlayOrPause.setOnClickListener(startPlaybackClickListener) + binding.llPreview.setOnClickListener(startPlaybackClickListener) + } + private fun setupBuyerList() { val recyclerView = binding.rvBuyer contentBuyerAdapter = AudioContentBuyerAdapter() @@ -775,10 +943,13 @@ class AudioContentDetailActivity : BaseActivity 0 + isAudioContentInterstitialEligible = response.price <= 0 || isAlertPreview if ( response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered && @@ -798,7 +969,7 @@ class AudioContentDetailActivity : BaseActivity Unit = { startService( Intent( applicationContext, @@ -842,9 +1013,10 @@ class AudioContentDetailActivity : BaseActivity(FragmentLiveBinding::infl } override fun onDestroyView() { + binding.yandexInlineBannerView.destroy() super.onDestroyView() } @@ -158,9 +162,31 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl setupRecommendChannel() setupLatestFinishedLiveChannel() setupLiveReplay() + setupLiveTabInlineBanner() setupLiveReservation() } + private fun setupLiveTabInlineBanner() { + binding.yandexInlineBannerView.post { + val density = resources.displayMetrics.density + val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth + val adWidthDp = (adWidthPixels / density).roundToInt() + val maxAdHeightDp = 90 + + binding.yandexInlineBannerView.apply { + setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_LIVE_TAB_AD_UNIT_ID) + setAdSize( + BannerAdSize.inlineSize( + requireContext(), + adWidthDp, + maxAdHeightDp + ) + ) + loadAd(AdRequest.Builder().build()) + } + } + } + private fun renderMakeLiveByRole(role: String) { if (role == MemberRole.CREATOR.name) { binding.llMakeLive.visibility = View.VISIBLE diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt index 57fe2367..012ee676 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt @@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.live.room.detail import android.annotation.SuppressLint import android.content.Intent import android.graphics.Rect -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -20,6 +19,9 @@ import coil.transform.CircleCropTransformation import coil.transform.RoundedCornersTransformation import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.yandex.mobile.ads.banner.BannerAdSize +import com.yandex.mobile.ads.common.AdRequest +import kr.co.vividnext.sodalive.BuildConfig import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog @@ -35,6 +37,7 @@ import org.koin.android.ext.android.inject import java.util.Locale import java.util.TimeZone import androidx.core.net.toUri +import kotlin.math.roundToInt class LiveRoomDetailFragment( private val roomId: Long, @@ -79,11 +82,39 @@ class LiveRoomDetailFragment( behavior.state = BottomSheetBehavior.STATE_EXPANDED setupAdapter() + setupLiveRoomDetailInlineBanner() bindData() binding.ivClose.setOnClickListener { dismiss() } viewModel.getDetail(roomId) { dismiss() } } + override fun onDestroyView() { + binding.yandexInlineBannerView.destroy() + super.onDestroyView() + } + + private fun setupLiveRoomDetailInlineBanner() { + binding.yandexInlineBannerView.post { + val density = resources.displayMetrics.density + val screenWidth = resources.displayMetrics.widthPixels + val adWidthPixels = binding.yandexInlineBannerView.width.takeIf { it > 0 } ?: screenWidth + val adWidthDp = (adWidthPixels / density).roundToInt() + val maxAdHeightDp = 90 + + binding.yandexInlineBannerView.apply { + setAdUnitId(BuildConfig.YANDEX_INLINE_BANNER_LIVE_ROOM_DETAIL_AD_UNIT_ID) + setAdSize( + BannerAdSize.inlineSize( + requireContext(), + adWidthDp, + maxAdHeightDp + ) + ) + loadAd(AdRequest.Builder().build()) + } + } + } + private fun setupAdapter() { val recyclerView = binding.rvParticipate adapter = LiveRoomDetailAdapter {} @@ -384,7 +415,7 @@ class LiveRoomDetailFragment( viewModel.shareRoomLink( response.roomId, response.isPrivateRoom, - response.password, + response.password ) { val intent = Intent(Intent.ACTION_SEND) intent.type = "text/plain" diff --git a/app/src/main/res/layout/activity_audio_content_detail.xml b/app/src/main/res/layout/activity_audio_content_detail.xml index d5b71c57..51e01d5a 100644 --- a/app/src/main/res/layout/activity_audio_content_detail.xml +++ b/app/src/main/res/layout/activity_audio_content_detail.xml @@ -396,6 +396,14 @@ android:visibility="gone" /> + + + + + +