Skip to content

Commit

Permalink
Implement RTB Native Ad loading for Line Mediation Adapter.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 693365672
  • Loading branch information
Mobile Ads Developer Relations authored and copybara-github committed Nov 5, 2024
1 parent 1154981 commit 57b6835
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 46 deletions.
1 change: 1 addition & 0 deletions ThirdPartyAdapters/line/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Implemented AdLoader to enable RTB for Banner Ads.
- Implemented AdLoader to enable RTB for Interstitial Ads.
- Implemented AdLoader to enable RTB for Rewarded Ads.
- Implemented AdLoader to enable RTB for Native Ads.

#### Version 2.8.20240827.0
- Verified compatibility with FiveAd SDK version 2.8.20240827.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.google.android.gms.ads.mediation.MediationNativeAdConfiguration
import com.google.android.gms.ads.mediation.MediationRewardedAd
import com.google.android.gms.ads.mediation.MediationRewardedAdCallback
import com.google.android.gms.ads.mediation.MediationRewardedAdConfiguration
import com.google.android.gms.ads.mediation.NativeAdMapper
import com.google.android.gms.ads.mediation.UnifiedNativeAdMapper
import com.google.android.gms.ads.mediation.rtb.RtbAdapter
import com.google.android.gms.ads.mediation.rtb.RtbSignalData
Expand Down Expand Up @@ -244,11 +243,14 @@ class LineMediationAdapter : RtbAdapter() {
}
}

override fun loadRtbNativeAdMapper(
override fun loadRtbNativeAd(
adConfiguration: MediationNativeAdConfiguration,
callback: MediationAdLoadCallback<NativeAdMapper, MediationNativeAdCallback>,
callback: MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
) {
super.loadRtbNativeAdMapper(adConfiguration, callback)
LineNativeAd.newInstance(adConfiguration, callback).onSuccess {
nativeAd = it
nativeAd.loadRtbAd()
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ import android.util.Log
import android.view.View
import android.widget.ImageView
import androidx.core.graphics.drawable.toDrawable
import com.five_corp.ad.AdLoader
import com.five_corp.ad.BidData
import com.five_corp.ad.FiveAdConfig
import com.five_corp.ad.FiveAdErrorCode
import com.five_corp.ad.FiveAdInterface
import com.five_corp.ad.FiveAdLoadListener
import com.five_corp.ad.FiveAdNative
import com.five_corp.ad.FiveAdNativeEventListener
import com.google.ads.mediation.line.LineMediationAdapter.Companion.SDK_ERROR_DOMAIN
import com.google.android.gms.ads.AdError
import com.google.android.gms.ads.formats.NativeAd
import com.google.android.gms.ads.mediation.MediationAdLoadCallback
import com.google.android.gms.ads.mediation.MediationNativeAdCallback
import com.google.android.gms.ads.mediation.MediationNativeAdConfiguration
import com.google.android.gms.ads.mediation.UnifiedNativeAdMapper
import com.google.android.gms.ads.nativead.NativeAdOptions
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
Expand All @@ -48,20 +53,67 @@ class LineNativeAd
private constructor(
private val context: Context,
private val appId: String,
private val slotId: String?,
private val bidResponse: String,
private val watermark: String,
private val nativeAdOptions: NativeAdOptions,
private val mediationNativeAdLoadCallback:
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
private val nativeAd: FiveAdNative,
private val adapterScope: CoroutineScope,
) : UnifiedNativeAdMapper(), FiveAdLoadListener, FiveAdNativeEventListener {

private var mediationNativeAdCallback: MediationNativeAdCallback? = null
private lateinit var nativeAd: FiveAdNative

fun loadAd() {
if (slotId.isNullOrEmpty()) {
val adError =
AdError(
LineMediationAdapter.ERROR_CODE_MISSING_SLOT_ID,
LineMediationAdapter.ERROR_MSG_MISSING_SLOT_ID,
LineMediationAdapter.ADAPTER_ERROR_DOMAIN,
)
mediationNativeAdLoadCallback.onFailure(adError)
return
}
LineInitializer.initialize(context, appId)
nativeAd = LineSdkFactory.delegate.createFiveAdNative(context, slotId)
val videoOptions = nativeAdOptions.videoOptions
if (videoOptions != null) {
nativeAd.enableSound(!videoOptions.startMuted)
}
nativeAd.setLoadListener(this)
nativeAd.loadAdAsync()
}

fun loadRtbAd() {
val fiveAdConfig = FiveAdConfig(appId)
val adLoader = AdLoader.getAdLoader(context, fiveAdConfig) ?: return
val bidData = BidData(bidResponse, watermark)
adLoader.loadNativeAd(
bidData,
object : AdLoader.LoadNativeAdCallback {
override fun onLoad(fiveAdNative: FiveAdNative) {
nativeAd = fiveAdNative
val videoOptions = nativeAdOptions.videoOptions
if (videoOptions != null) {
nativeAd.enableSound(!videoOptions.startMuted)
}
adapterScope.async {
mapNativeAd()
mediationNativeAdCallback = mediationNativeAdLoadCallback.onSuccess(this@LineNativeAd)
nativeAd.setEventListener(this@LineNativeAd)
}
}

override fun onError(adErrorCode: FiveAdErrorCode) {
val adError = AdError(adErrorCode.value, adErrorCode.name, SDK_ERROR_DOMAIN)
mediationNativeAdLoadCallback.onFailure(adError)
}
},
)
}

private suspend fun mapNativeAd() = coroutineScope {
headline = nativeAd.adTitle
body = nativeAd.descriptionText
Expand All @@ -77,7 +129,7 @@ private constructor(
AdError(
LineMediationAdapter.ERROR_CODE_MINIMUM_NATIVE_INFO_NOT_RECEIVED,
LineMediationAdapter.ERROR_MSG_MINIMUM_NATIVE_INFO_NOT_RECEIVED,
LineMediationAdapter.SDK_ERROR_DOMAIN,
SDK_ERROR_DOMAIN,
)
Log.w(TAG, adError.message)
mediationNativeAdLoadCallback.onFailure(adError)
Expand Down Expand Up @@ -112,6 +164,7 @@ private constructor(
}

override fun onFiveAdLoad(ad: FiveAdInterface) {
// This callback is used only in the waterfall flow
Log.d(TAG, "Finished loading Line Native Ad for slotId: ${ad.slotId}")
adapterScope.async {
mapNativeAd()
Expand All @@ -121,6 +174,7 @@ private constructor(
}

override fun onFiveAdLoadError(ad: FiveAdInterface, errorCode: FiveAdErrorCode) {
// This callback is used only in the waterfall flow
adapterScope.cancel()
val adError =
AdError(
Expand Down Expand Up @@ -205,28 +259,22 @@ private constructor(
}

val slotId = serverParameters.getString(LineMediationAdapter.KEY_SLOT_ID)
if (slotId.isNullOrEmpty()) {
val adError =
AdError(
LineMediationAdapter.ERROR_CODE_MISSING_SLOT_ID,
LineMediationAdapter.ERROR_MSG_MISSING_SLOT_ID,
LineMediationAdapter.ADAPTER_ERROR_DOMAIN,
)
mediationNativeAdLoadCallback.onFailure(adError)
return Result.failure(NoSuchElementException(adError.message))
}

val nativeAd = LineSdkFactory.delegate.createFiveAdNative(context, slotId)
val bidResponse = mediationNativeAdConfiguration.bidResponse
val watermark = mediationNativeAdConfiguration.watermark
val nativeAdOptions = mediationNativeAdConfiguration.nativeAdOptions
val videoOptions = nativeAdOptions.videoOptions
if (videoOptions != null) {
nativeAd.enableSound(!videoOptions.startMuted)
}

val adapterScope = CoroutineScope(coroutineContext)

val instance =
LineNativeAd(context, appId, mediationNativeAdLoadCallback, nativeAd, adapterScope)
LineNativeAd(
context,
appId,
slotId,
bidResponse,
watermark,
nativeAdOptions,
mediationNativeAdLoadCallback,
adapterScope,
)
return Result.success(instance)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1547,10 +1547,11 @@ class LineMediationAdapterTest {
private fun createMediationNativeAdConfiguration(
context: Context = this.context,
serverParameters: Bundle = Bundle(),
bidResponse: String = "",
) =
MediationNativeAdConfiguration(
context,
/*bidresponse=*/ "",
bidResponse,
serverParameters,
/*mediationExtras=*/ Bundle(),
/*isTesting=*/ true,
Expand All @@ -1564,6 +1565,115 @@ class LineMediationAdapterTest {

// endregion

// region RTB Native Ad Tests
@Test
fun loadRtbNativeAd_withNullAppId_invokesOnFailure() {
val serverParameters = bundleOf(KEY_SLOT_ID to TEST_SLOT_ID)
val mediationNativeAdConfiguration =
createMediationNativeAdConfiguration(
serverParameters = serverParameters,
bidResponse = TEST_BID_RESPONSE,
)
val adErrorCaptor = argumentCaptor<AdError>()

lineMediationAdapter.loadRtbNativeAd(
mediationNativeAdConfiguration,
mockMediationNativeAdLoadCallback,
)

verify(mockMediationNativeAdLoadCallback).onFailure(adErrorCaptor.capture())
val capturedError = adErrorCaptor.firstValue
assertThat(capturedError.code).isEqualTo(LineMediationAdapter.ERROR_CODE_MISSING_APP_ID)
assertThat(capturedError.message).isEqualTo(LineMediationAdapter.ERROR_MSG_MISSING_APP_ID)
assertThat(capturedError.domain).isEqualTo(ADAPTER_ERROR_DOMAIN)
}

@Test
fun loadRtbNativeAd_withEmptyAppId_invokesOnFailure() {
val serverParameters = bundleOf(KEY_APP_ID to "", KEY_SLOT_ID to TEST_SLOT_ID)
val mediationNativeAdConfiguration =
createMediationNativeAdConfiguration(
serverParameters = serverParameters,
bidResponse = TEST_BID_RESPONSE,
)
val adErrorCaptor = argumentCaptor<AdError>()

lineMediationAdapter.loadRtbNativeAd(
mediationNativeAdConfiguration,
mockMediationNativeAdLoadCallback,
)

verify(mockMediationNativeAdLoadCallback).onFailure(adErrorCaptor.capture())
val capturedError = adErrorCaptor.firstValue
assertThat(capturedError.code).isEqualTo(LineMediationAdapter.ERROR_CODE_MISSING_APP_ID)
assertThat(capturedError.message).isEqualTo(LineMediationAdapter.ERROR_MSG_MISSING_APP_ID)
assertThat(capturedError.domain).isEqualTo(ADAPTER_ERROR_DOMAIN)
}

@Test
fun loadRtbNativeAd_withMuteNativeAdOptions_setsEnableSound() {
mockStatic(AdLoader::class.java).use {
val mockAdLoader = mock<AdLoader>()
whenever(AdLoader.getAdLoader(eq(context), any())) doReturn mockAdLoader
val serverParameters = bundleOf(KEY_APP_ID to TEST_APP_ID_1, KEY_SLOT_ID to TEST_SLOT_ID)
val mediationNativeAdConfiguration =
spy(
createMediationNativeAdConfiguration(
serverParameters = serverParameters,
bidResponse = TEST_BID_RESPONSE,
)
)
val videoOptions = VideoOptions.Builder().setStartMuted(true).build()
val nativeAdOptions = NativeAdOptions.Builder().setVideoOptions(videoOptions).build()
whenever(mediationNativeAdConfiguration.nativeAdOptions) doReturn nativeAdOptions

lineMediationAdapter.loadRtbNativeAd(
mediationNativeAdConfiguration,
mockMediationNativeAdLoadCallback,
)

val loadCallbackCaptor = argumentCaptor<AdLoader.LoadNativeAdCallback>()
verify(mockAdLoader).loadNativeAd(any(), loadCallbackCaptor.capture())
val capturedCallback = loadCallbackCaptor.firstValue
capturedCallback.onLoad(mockFiveAdNative)
verify(mockFiveAdNative).enableSound(false)
}
}

@Test
fun loadRtbNativeAd_onErrorWhenLoading_invokesOnFailure() {
mockStatic(AdLoader::class.java).use {
val mockAdLoader = mock<AdLoader>()
whenever(AdLoader.getAdLoader(eq(context), any())) doReturn mockAdLoader
val serverParameters = bundleOf(KEY_APP_ID to TEST_APP_ID_1, KEY_SLOT_ID to TEST_SLOT_ID)
val mediationNativeAdConfiguration =
spy(
createMediationNativeAdConfiguration(
serverParameters = serverParameters,
bidResponse = TEST_BID_RESPONSE,
)
)
val videoOptions = VideoOptions.Builder().setStartMuted(true).build()
val nativeAdOptions = NativeAdOptions.Builder().setVideoOptions(videoOptions).build()
whenever(mediationNativeAdConfiguration.nativeAdOptions) doReturn nativeAdOptions

lineMediationAdapter.loadRtbNativeAd(
mediationNativeAdConfiguration,
mockMediationNativeAdLoadCallback,
)

val loadCallbackCaptor = argumentCaptor<AdLoader.LoadNativeAdCallback>()
verify(mockAdLoader).loadNativeAd(any(), loadCallbackCaptor.capture())
val capturedCallback = loadCallbackCaptor.firstValue
capturedCallback.onError(FiveAdErrorCode.NO_AD)
val expectedAdError =
AdError(FiveAdErrorCode.NO_AD.value, FiveAdErrorCode.NO_AD.name, SDK_ERROR_DOMAIN)
verify(mockMediationNativeAdLoadCallback).onFailure(argThat(AdErrorMatcher(expectedAdError)))
}
}

// endregion

private companion object {
const val MAJOR_VERSION = 4
const val MINOR_VERSION = 3
Expand Down
Loading

0 comments on commit 57b6835

Please sign in to comment.