Skip to content

Commit

Permalink
Moloco Native Ad: Add support for native ads for the Moloco SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
yamir-godil committed Jan 29, 2025
1 parent 0e18547 commit 0634a9f
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,189 @@
package com.google.ads.mediation.moloco

import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.view.View
import androidx.annotation.VisibleForTesting
import com.google.android.gms.ads.AdError
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 com.moloco.sdk.publisher.AdLoad
import com.moloco.sdk.publisher.Moloco
import com.moloco.sdk.publisher.MolocoAd
import com.moloco.sdk.publisher.MolocoAdError
import com.moloco.sdk.publisher.NativeAd

/**
* Used to load Moloco native ads and mediate callbacks between Google Mobile Ads SDK and Moloco
* SDK.
*/
class MolocoNativeAd
private constructor(
private val context: Context,
private val adUnitId: String,
private val nativeAdOptions: NativeAdOptions, // TODO: Not sure where to use this?
private val bidResponse: String,
private val watermark: String,
private val mediationNativeAdLoadCallback:
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
// TODO: Add other parameters or remove unnecessary ones.
) : UnifiedNativeAdMapper() {
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
) : AdLoad.Listener, UnifiedNativeAdMapper() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var nativeAd: NativeAd? = null

fun loadAd() {
// TODO: Implement this method.
Moloco.createNativeAd(adUnitId, watermark) { returnedAd, adCreateError ->
if (adCreateError != null) {
val adError =
AdError(
adCreateError.errorCode,
adCreateError.description,
MolocoMediationAdapter.SDK_ERROR_DOMAIN,
)
mediationNativeAdLoadCallback.onFailure(adError)
return@createNativeAd
}

nativeAd = returnedAd

// Now that the ad object is created, load the bid response
nativeAd?.load(bidResponse, this)
}
}

override fun onAdLoadSuccess(molocoAd: MolocoAd) {
overrideClickHandling = true
// If nativeAd is null here, then that means the ad was destroyed before load was successful or there is a bug
// in the adapter
nativeAd?.apply {
assets?.apply {
// Admob first uses rating, if not present the it uses sponsorText and if that is not present it will use the store.
rating?.let { starRating = it.toDouble() }
sponsorText?.let { advertiser = it }
store = "Google Play"
title?.let { headline = it }
description?.let { body = it }
callToActionText?.let { callToAction = it }
iconUri?.let {
Drawable.createFromPath(it.toString())?.apply {
icon = MolocoNativeMappedImage(this)
}
}

val mediaView = this.mediaView

mediaView?.let {
it.tag = MEDIA_VIEW_TAG
setMediaView(it)
}
}
}

val showCallback = mediationNativeAdLoadCallback.onSuccess(this)
nativeAd?.interactionListener = object : NativeAd.InteractionListener {
/**
* Not needed as Admob handles impressions on its own.
*/
override fun onImpressionHandled() {}

/**
* When this Moloco function gets triggered, we inform Admob that ad click has occurred
*/
override fun onGeneralClickHandled() = showCallback.reportAdClicked()
}
}

override fun onAdLoadFailed(molocoAdError: MolocoAdError) {
val adError =
AdError(
molocoAdError.errorType.errorCode,
molocoAdError.errorType.description,
MolocoMediationAdapter.SDK_ERROR_DOMAIN,
)
mediationNativeAdLoadCallback.onFailure(adError)
}

/**
* Admob informs us that a click has happened to the view(s) they create.
* This excludes the view set in [setMediaView]. Click from said view must be handled elsewhere
*/
override fun handleClick(view: View) {
nativeAd?.handleGeneralAdClick()
}

/**
* Admob informs us that an impression happened
*/
override fun recordImpression() {
nativeAd?.handleImpression()
}

override fun trackViews(
containerView: View,
clickableAssetViews: MutableMap<String, View>,
nonClickableAssetViews: MutableMap<String, View>,
) {
// set the listener to Moloco's NativeAd object. Then we read it from the Moloco's `NativeAd.interactionListener` callback
containerView.setOnClickListener { nativeAd?.handleGeneralAdClick() }
clickableAssetViews.values.forEach {
it.setOnClickListener { nativeAd?.handleGeneralAdClick() }
}
}

/**
* To be called by the medation to destroy the ad object and any underlying references in the Moloco SDK. This should be called
* when the ad is permanently "hidden" / never going to be visible again.
*/
fun destroy() {
nativeAd?.destroy()
nativeAd = null
}

companion object {
fun newInstance(
mediationNativeAdConfiguration: MediationNativeAdConfiguration,
mediationNativeAdLoadCallback:
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback>,
): Result<MolocoNativeAd> {
val context = mediationNativeAdConfiguration.context
val serverParameters = mediationNativeAdConfiguration.serverParameters
val nativeAdOptions = mediationNativeAdConfiguration.nativeAdOptions

// TODO: Implement necessary initialization steps.
val adUnitId = serverParameters.getString(MolocoMediationAdapter.KEY_AD_UNIT_ID)
if (adUnitId.isNullOrEmpty()) {
val adError =
AdError(
MolocoMediationAdapter.ERROR_CODE_MISSING_AD_UNIT,
MolocoMediationAdapter.ERROR_MSG_MISSING_AD_UNIT,
MolocoMediationAdapter.ADAPTER_ERROR_DOMAIN,
)
mediationNativeAdLoadCallback.onFailure(adError)
return Result.failure(NoSuchElementException(adError.message))
}

val bidResponse = mediationNativeAdConfiguration.bidResponse
val watermark = mediationNativeAdConfiguration.watermark

return Result.success(MolocoNativeAd(context, mediationNativeAdLoadCallback))
return Result.success(MolocoNativeAd(adUnitId, nativeAdOptions, bidResponse, watermark, mediationNativeAdLoadCallback))
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
const val MEDIA_VIEW_TAG = "native_ad_media_view"
}

/**
* Admob adapter only needs the [drawable] parameter. The rest are optional
*/
@Suppress("DEPRECATION")
internal class MolocoNativeMappedImage(
private val drawable: Drawable,
private val uri: Uri = Uri.EMPTY,
private val scale: Double = 1.0,
) : com.google.android.gms.ads.formats.NativeAd.Image() { // Google deprecated the class, but didn't offer an alternative. So for now we *must* use the deprecated class.
override fun getScale() = scale
override fun getDrawable() = drawable
override fun getUri() = uri
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.google.ads.mediation.moloco

import android.content.Context
import androidx.core.os.bundleOf
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.ads.mediation.adaptertestkit.AdErrorMatcher
import com.google.ads.mediation.adaptertestkit.AdapterTestKitConstants.TEST_BID_RESPONSE
import com.google.ads.mediation.adaptertestkit.AdapterTestKitConstants.TEST_WATERMARK
import com.google.android.gms.ads.AdError
import com.google.android.gms.ads.RequestConfiguration
import com.google.android.gms.ads.mediation.MediationAdLoadCallback
import com.google.android.gms.ads.mediation.UnifiedNativeAdMapper
import com.google.android.gms.ads.mediation.MediationNativeAdCallback
import com.google.android.gms.ads.mediation.MediationNativeAdConfiguration
import com.moloco.sdk.publisher.MolocoAdError
import com.moloco.sdk.publisher.NativeAd
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argThat
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class MolocoNativeAdTest {
// Subject of tests
private lateinit var molocoNativeAd: MolocoNativeAd
private lateinit var mediationAdConfiguration: MediationNativeAdConfiguration

private val context = ApplicationProvider.getApplicationContext<Context>()
private val mockNativeAd = mock<NativeAd>()
private val mockMediationAdLoadCallback:
MediationAdLoadCallback<UnifiedNativeAdMapper, MediationNativeAdCallback> =
mock()
private val mockMediationAdCallback = mock<MediationNativeAdCallback>()

@Before
fun setUp() {
// Properly initialize molocoNativeAd
mediationAdConfiguration = createMediationNativeAdConfiguration()
MolocoNativeAd.newInstance(mediationAdConfiguration, mockMediationAdLoadCallback)
.onSuccess { molocoNativeAd = it }
whenever(mockMediationAdLoadCallback.onSuccess(molocoNativeAd)) doReturn
mockMediationAdCallback
}

@Test
fun onAdLoadFailed_invokesOnFailure() {
val testError =
MolocoAdError("testNetwork", "testAdUnit", MolocoAdError.ErrorType.UNKNOWN, "testDesc")
val expectedAdError =
AdError(
MolocoAdError.ErrorType.UNKNOWN.errorCode,
MolocoAdError.ErrorType.UNKNOWN.description,
MolocoMediationAdapter.SDK_ERROR_DOMAIN,
)

molocoNativeAd.onAdLoadFailed(testError)

verify(mockMediationAdLoadCallback).onFailure(argThat(AdErrorMatcher(expectedAdError)))
}

@Test
fun onAdLoadSuccess_invokesOnSuccess() {
molocoNativeAd.onAdLoadSuccess(mock())

verify(mockMediationAdLoadCallback).onSuccess(molocoNativeAd)
}

@Test
fun handleClick_invokesReportAdClicked() {
molocoNativeAd.nativeAd = mockNativeAd

molocoNativeAd.handleClick(mock())

verify(mockNativeAd).handleGeneralAdClick()
}

@Test
fun destroy_invokesOnAdClosed() {
// Arrange
molocoNativeAd.nativeAd = mockNativeAd

// Act
molocoNativeAd.destroy()

// Assert
verify(mockNativeAd).destroy()
assert(molocoNativeAd.nativeAd == null) {
"Expected nativeAd to be null after calling destroy"
}
}

private fun createMediationNativeAdConfiguration(): MediationNativeAdConfiguration {
val serverParameters = bundleOf(MolocoMediationAdapter.KEY_AD_UNIT_ID to TEST_AD_UNIT)
return MediationNativeAdConfiguration(
context,
TEST_BID_RESPONSE,
serverParameters,
/*mediationExtras=*/ bundleOf(),
/*isTesting=*/ true,
/*location=*/ null,
RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED,
RequestConfiguration.TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED,
/*maxAdContentRating=*/ "",
TEST_WATERMARK,
/*p10=*/ null
)
}

private companion object {
const val TEST_AD_UNIT = "testAdUnit"
}
}

0 comments on commit 0634a9f

Please sign in to comment.