diff --git a/ads-admob/.gitignore b/ads-admob/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/ads-admob/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ads-admob/build.gradle.kts b/ads-admob/build.gradle.kts new file mode 100644 index 0000000..cf5a0f0 --- /dev/null +++ b/ads-admob/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) +} + +apply(from = "../gradlescripts/android-library.gradle") + +val artifactGroupId by extra("io.voodoo.apps") +val artifactId by extra("ads-admob") +val artifactVersion by extra(rootProject.extra.get("SDK_VER")) + +android { + namespace = "io.voodoo.apps.ads.admob" +} + +dependencies { + implementation(project(":ads-api")) + implementation(libs.play.services.ads) + +} + +apply(from = "../gradlescripts/publisher.gradle") diff --git a/ads-admob/consumer-rules.pro b/ads-admob/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/ads-admob/proguard-rules.pro b/ads-admob/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/ads-admob/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/ads-admob/src/main/AndroidManifest.xml b/ads-admob/src/main/AndroidManifest.xml new file mode 100644 index 0000000..19d2638 --- /dev/null +++ b/ads-admob/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/exception/AdMobAdLoadException.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/exception/AdMobAdLoadException.kt new file mode 100644 index 0000000..027e5db --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/exception/AdMobAdLoadException.kt @@ -0,0 +1,6 @@ +package io.voodoo.apps.ads.admob.exception + +import com.google.android.gms.ads.LoadAdError +import java.io.IOException + +class AdMobAdLoadException(val error: LoadAdError) : IOException(error.message) diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/AdMobNativeAdViewListener.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/AdMobNativeAdViewListener.kt new file mode 100644 index 0000000..6a83674 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/AdMobNativeAdViewListener.kt @@ -0,0 +1,20 @@ +package io.voodoo.apps.ads.admob.listener + +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd + +interface AdMobNativeAdViewListener { + fun onAdClicked(ad: NativeAd?) + + fun onAdClosed(ad: NativeAd?) + + fun onAdFailedToLoad(error: LoadAdError?) + + fun onAdImpression(ad: NativeAd?) + + fun onAdLoaded(ad: NativeAd?) + + fun onAdOpened(ad: NativeAd?) + + fun onAdSwipeGestureClicked(ad: NativeAd?) +} \ No newline at end of file diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/DefaultAdMobNativeAdViewListener.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/DefaultAdMobNativeAdViewListener.kt new file mode 100644 index 0000000..c401b6d --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/DefaultAdMobNativeAdViewListener.kt @@ -0,0 +1,20 @@ +package io.voodoo.apps.ads.admob.listener + +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd + +abstract class DefaultAdMobNativeAdViewListener : AdMobNativeAdViewListener { + override fun onAdClicked(ad: NativeAd?) {} + + override fun onAdClosed(ad: NativeAd?) {} + + override fun onAdFailedToLoad(error: LoadAdError?) {} + + override fun onAdImpression(ad: NativeAd?) {} + + override fun onAdLoaded(ad: NativeAd?) {} + + override fun onAdOpened(ad: NativeAd?) {} + + override fun onAdSwipeGestureClicked(ad: NativeAd?) {} +} \ No newline at end of file diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/MultiAdMobNativeAdViewListener.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/MultiAdMobNativeAdViewListener.kt new file mode 100644 index 0000000..a06d2c0 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/listener/MultiAdMobNativeAdViewListener.kt @@ -0,0 +1,47 @@ +package io.voodoo.apps.ads.admob.listener + +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd +import java.util.concurrent.CopyOnWriteArraySet + +internal class MultiAdMobNativeAdViewListener : AdMobNativeAdViewListener { + + // TODO: check the implementation, we're re-creating the backing list for every request (because we add a listener) + private val delegates = CopyOnWriteArraySet() + + fun add(listener: AdMobNativeAdViewListener) { + delegates.add(listener) + } + + fun remove(listener: AdMobNativeAdViewListener) { + delegates.remove(listener) + } + + override fun onAdClosed(ad: NativeAd?) { + delegates.forEach { it.onAdClosed(ad) } + } + + override fun onAdFailedToLoad(error: LoadAdError?) { + delegates.forEach { it.onAdFailedToLoad(error) } + } + + override fun onAdImpression(ad: NativeAd?) { + delegates.forEach { it.onAdImpression(ad) } + } + + override fun onAdOpened(ad: NativeAd?) { + delegates.forEach { it.onAdOpened(ad) } + } + + override fun onAdClicked(ad: NativeAd?) { + delegates.forEach { it.onAdClicked(ad) } + } + + override fun onAdLoaded(ad: NativeAd?) { + delegates.forEach { it.onAdLoaded(ad) } + } + + override fun onAdSwipeGestureClicked(ad: NativeAd?) { + delegates.forEach { it.onAdSwipeGestureClicked(ad) } + } +} diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdClient.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdClient.kt new file mode 100644 index 0000000..ffeb336 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdClient.kt @@ -0,0 +1,264 @@ +package io.voodoo.apps.ads.admob.nativ + +import android.app.Activity +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import com.google.android.gms.ads.AdListener +import com.google.android.gms.ads.AdLoader +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.nativead.NativeAd +import io.voodoo.apps.ads.admob.exception.AdMobAdLoadException +import io.voodoo.apps.ads.admob.listener.AdMobNativeAdViewListener +import io.voodoo.apps.ads.admob.listener.MultiAdMobNativeAdViewListener +import io.voodoo.apps.ads.api.AdClient +import io.voodoo.apps.ads.api.BaseAdClient +import io.voodoo.apps.ads.api.LocalExtrasProvider +import io.voodoo.apps.ads.api.model.Ad +import kotlinx.coroutines.CompletableDeferred +import java.util.Date + +private sealed interface LoadingAd { + data class Success(val ad: NativeAd) : LoadingAd + data class Failure(val error: LoadAdError) : LoadingAd +} + +class AdMobNativeAdClient( + config: AdClient.Config, + private val activity: Activity, + adViewFactory: AdMobNativeAdViewFactory, + private val adViewRenderer: AdMobNativeAdViewRenderer, + //private val renderListener: MaxNativeAdRenderListener? = null, + localExtrasProviders: List = emptyList(), +) : BaseAdClient(config = config) { + + override val adType: Ad.Type = Ad.Type.NATIVE + + private val adMobNativeAdListener = MultiAdMobNativeAdViewListener() + + private val adViewPool = AdMobNativeAdViewPool( + adViewFactory, + ) + + private val localExtrasProviders = localExtrasProviders.toList() + + init { + /* + require(appLovinSdk.isInitialized) { "AppLovin instance not initialized" } + loader.setNativeAdListener(maxNativeAdListener) + loader.setRevenueListener { ad -> + val adWrapper = findOrCreateAdWrapper(ad) + runRevenueListener { it.onAdRevenuePaid(this, adWrapper) } + } + + maxNativeAdListener.add(object : MaxNativeAdListener() { + override fun onNativeAdExpired(ad: MaxAd) { + // ad expired, can't be served anymore + checkAndNotifyAvailableAdCountChanges() + } + + override fun onNativeAdClicked(ad: MaxAd) { + val adWrapper = findOrCreateAdWrapper(ad) + runClickListener { it.onAdClick(this@MaxNativeAdClient, adWrapper) } + } + }) + */ + + (activity as? LifecycleOwner)?.lifecycle?.let(::registerToLifecycle) + // config.placement?.let { loader.placement = it } + } + + fun addAdMobNativeAdViewListener(listener: AdMobNativeAdViewListener) { + adMobNativeAdListener.add(listener) + } + + fun removeAdMobNativeAdViewListener(listener: AdMobNativeAdViewListener) { + adMobNativeAdListener.remove(listener) + } + + override fun close() { + super.close() + //loader.destroy() + } + + override fun destroyAd(ad: AdmobNativeAdWrapper) { + Log.w("AdClient", "destroyAd ${ad.id}") + ad.ad.destroy() + } + + /** see https://developers.applovin.com/en/android/ad-formats/native-ads#templates */ + override suspend fun fetchAdSafe(vararg localExtras: Pair): AdmobNativeAdWrapper { + runLoadingListeners { it.onAdLoadingStarted(this) } + + var _currentAdWrapper: AdmobNativeAdWrapper? = null + + val loadingAdLocal = CompletableDeferred() + + lateinit var loader: AdLoader + loader = AdLoader.Builder( + activity, + config.adUnit, + ).forNativeAd { ad: NativeAd -> + if (activity.isDestroyed) { + ad.destroy() + return@forNativeAd + } else if (!loader.isLoading) { + loadingAdLocal.complete(LoadingAd.Success(ad)) + } + }.withAdListener(object : AdListener() { + override fun onAdClicked() { + _currentAdWrapper?.let { adWrapper -> + adMobNativeAdListener.onAdClicked(adWrapper.ad) + runClickListener { it.onAdClick(this@AdMobNativeAdClient, adWrapper) } + } + } + + override fun onAdClosed() { + adMobNativeAdListener.onAdClosed(_currentAdWrapper?.ad) + } + + override fun onAdImpression() { + adMobNativeAdListener.onAdImpression(_currentAdWrapper?.ad) + } + + override fun onAdLoaded() { + // will be called with deffered + } + + override fun onAdOpened() { + adMobNativeAdListener.onAdOpened(_currentAdWrapper?.ad) + } + + override fun onAdSwipeGestureClicked() { + adMobNativeAdListener.onAdSwipeGestureClicked(_currentAdWrapper?.ad) + } + + override fun onAdFailedToLoad(adError: LoadAdError) { + loadingAdLocal.complete(LoadingAd.Failure(adError)) + } + }) + //.withNativeAdOptions( + // NativeAdOptions.Builder() + // // Methods in the NativeAdOptions.Builder class can be + // // used here to specify individual options settings. + // .build() + //) + .build() + + runLoadingListeners { it.onAdLoadingStarted(this@AdMobNativeAdClient) } + + loader.loadAd( + AdRequest.Builder() + .apply { + //this.setNeighboringContentUrls() TODO + }.build() + ) + + val result = loadingAdLocal.await() + + /* + + val providersExtras = localExtrasProviders.flatMap { it.getLocalExtras() } + val ad = withContext(Dispatchers.IO) { + try { + // Wrap ad loading into a coroutine + suspendCancellableCoroutine { continuation -> + val callback = object : MaxNativeAdListener() { + override fun onNativeAdLoaded(view: MaxNativeAdView?, ad: MaxAd) { + maxNativeAdListener.remove(this) + val adWrapper = AdmobNativeAdWrapper( + ad = ad, + loadedAt = Date(), + loader = loader, + renderListener = renderListener, + viewPool = adViewPool, + apphrbrModerationResult = if (AppHarbr.isInitialized()) { + ad.getNativeAdModerationResult() + } else { + null + } + ) + try { + continuation.resume(adWrapper) + } catch (e: Exception) { + // Avoid crashes if callback is called multiple times + Log.e("MaxNativeAdClient", "Failed to notify fetchAd", e) + } + } + + override fun onNativeAdLoadFailed(adUnitId: String, error: MaxError) { + maxNativeAdListener.remove(this) + try { + continuation.resumeWithException(MaxAdLoadException(error)) + } catch (e: Exception) { + // Avoid crashes if callback is called multiple times + Log.e("MaxNativeAdClient", "Failed to notify fetchAd error", e) + } + } + } + + Log.i("MaxNativeAdClient", "fetchAd") + maxNativeAdListener.add(callback) + providersExtras.forEach { (key, value) -> + loader.setLocalExtraParameter(key, value) + } + localExtras.forEach { (key, value) -> + loader.setLocalExtraParameter(key, value) + } + loader.loadAd() + + continuation.invokeOnCancellation { + maxNativeAdListener.remove(callback) + } + } + } catch (e: MaxAdLoadException) { + Log.e("MaxNativeAdClient", "Failed to load ad", e) + runLoadingListeners { it.onAdLoadingFailed(this@MaxNativeAdClient, e) } + + // Keep reused ad instead of destroying it + reusedAd?.let { addLoadedAd(it, isAlreadyServed = true) } + + throw e + } + } + */ + + //reusedAd?.let(::destroyAd) + + //if (ad.isBlocked) { + // runModerationListener { it.onAdBlocked(this, ad) } + //} + when (result) { + is LoadingAd.Failure -> { + _currentAdWrapper = null + val error = AdMobAdLoadException(result.error) + runLoadingListeners { it.onAdLoadingFailed(this@AdMobNativeAdClient, error) } + adMobNativeAdListener.onAdFailedToLoad(result.error) + throw error + } + + is LoadingAd.Success -> { + Log.i("AdMobNativeAdClient", "fetchAd success") + + val adWrapper = AdmobNativeAdWrapper( + adUnit = config.adUnit, + ad = result.ad, + loadedAt = Date(), + viewPool = adViewPool, + adViewRenderer = adViewRenderer, + ) + + adWrapper.ad.setOnPaidEventListener { adValue -> + adWrapper.revenue = adValue + runRevenueListener { it.onAdRevenuePaid(this@AdMobNativeAdClient, adWrapper) } + } + + _currentAdWrapper = adWrapper + + runLoadingListeners { it.onAdLoadingFinished(this, adWrapper) } + addLoadedAd(adWrapper) + return adWrapper + } + } + } +} \ No newline at end of file diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewFactory.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewFactory.kt new file mode 100644 index 0000000..6a7e8ab --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewFactory.kt @@ -0,0 +1,12 @@ +package io.voodoo.apps.ads.admob.nativ + +import android.content.Context +import androidx.annotation.UiThread +import com.google.android.gms.ads.nativead.NativeAdView + +interface AdMobNativeAdViewFactory { + + @UiThread + fun create(context: Context): NativeAdView +} + diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewPool.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewPool.kt new file mode 100644 index 0000000..6a8393e --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewPool.kt @@ -0,0 +1,14 @@ +package io.voodoo.apps.ads.admob.nativ + +import android.content.Context +import com.google.android.gms.ads.nativead.NativeAdView +import io.voodoo.apps.ads.admob.util.ViewPool + +internal class AdMobNativeAdViewPool( + private val factory: AdMobNativeAdViewFactory, +) : ViewPool() { + + override fun createView(context: Context): NativeAdView { + return factory.create(context) + } +} diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewRenderer.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewRenderer.kt new file mode 100644 index 0000000..f612cf0 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdMobNativeAdViewRenderer.kt @@ -0,0 +1,8 @@ +package io.voodoo.apps.ads.admob.nativ + +import com.google.android.gms.ads.nativead.NativeAd +import com.google.android.gms.ads.nativead.NativeAdView + +interface AdMobNativeAdViewRenderer { + fun render(nativeAdView: NativeAdView, nativeAd: NativeAd) +} \ No newline at end of file diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdmobNativeAdWrapper.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdmobNativeAdWrapper.kt new file mode 100644 index 0000000..4019d78 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/nativ/AdmobNativeAdWrapper.kt @@ -0,0 +1,61 @@ +package io.voodoo.apps.ads.admob.nativ + +import android.view.View +import android.view.ViewGroup +import com.google.android.gms.ads.AdValue +import com.google.android.gms.ads.nativead.NativeAd +import com.google.android.gms.ads.nativead.NativeAdView +import io.voodoo.apps.ads.api.model.Ad +import io.voodoo.apps.ads.admob.util.buildInfo +import io.voodoo.apps.ads.admob.util.id +import io.voodoo.apps.ads.admob.util.removeFromParent +import java.util.Date + +class AdmobNativeAdWrapper internal constructor( + val ad: NativeAd, + private val adUnit: String, + internal val viewPool: AdMobNativeAdViewPool, + internal val adViewRenderer: AdMobNativeAdViewRenderer, + override val loadedAt: Date, +) : Ad.Native() { + + internal var revenue: AdValue? = null + + override val id: Id = ad.id + override val info: Info = ad.buildInfo( + adUnit = adUnit, + revenue = revenue, + ) + + override val moderationResult: ModerationResult = ModerationResult.UNKNOWN // TODO + + override val isExpired: Boolean = false // TODO + + internal var view: NativeAdView? = null + private set + + override fun render(parent: View) { + // safety in case the view is already render (shouldn't happen, but be safe) + release() + require(parent is ViewGroup) { "parent is not a ViewGroup" } + + val view = viewPool.getOrCreate(parent.context) + .also { this.view = it } + + adViewRenderer.render(view, ad) + + parent.addView(view) + + markAsRendered() + } + + internal fun markAsPaidInternal() { + super.markAsRevenuePaid() + } + + override fun release() { + view?.removeFromParent() + view?.let(viewPool::release) + view = null + } +} diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/NativeAd.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/NativeAd.kt new file mode 100644 index 0000000..d975cbd --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/NativeAd.kt @@ -0,0 +1,25 @@ +package io.voodoo.apps.ads.admob.util + +import com.google.android.gms.ads.AdValue +import com.google.android.gms.ads.nativead.NativeAd +import io.voodoo.apps.ads.api.model.Ad + +val NativeAd.id: Ad.Id get() = Ad.Id(System.identityHashCode(this).toString()) + +fun NativeAd.buildInfo( + revenue: AdValue?, + adUnit: String, +): Ad.Info { + return Ad.Info( + adUnit = adUnit, + network = "", + revenue = revenue?.valueMicros?.toDouble() ?: 0.0, + revenuePrecision = revenue?.precisionType?.toString() ?: "", + cohortId = null, + creativeId = null, + placement = null, + reviewCreativeId = null, + formatLabel = null, + requestLatencyMillis = 0, + ) +} \ No newline at end of file diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/View.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/View.kt new file mode 100644 index 0000000..39769d2 --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/View.kt @@ -0,0 +1,8 @@ +package io.voodoo.apps.ads.admob.util + +import android.view.View +import android.view.ViewGroup + +internal fun View.removeFromParent() { + (parent as? ViewGroup)?.removeView(this) +} diff --git a/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/ViewPool.kt b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/ViewPool.kt new file mode 100644 index 0000000..f65999c --- /dev/null +++ b/ads-admob/src/main/java/io/voodoo/apps/ads/admob/util/ViewPool.kt @@ -0,0 +1,59 @@ +package io.voodoo.apps.ads.admob.util + +import android.content.Context +import android.view.View +import androidx.annotation.CallSuper +import androidx.annotation.UiThread + +internal abstract class ViewPool { + + var maxSize: Int = 5 + set(value) { + field = value + ensureSize() + } + private val pool = mutableListOf() + + fun getOrNull(): T? = synchronized(pool) { pool.removeFirstOrNull() } + + @UiThread + fun getOrCreate(context: Context): T { + return getOrNull() + ?: createView(context) + .apply { + // State should not be saved for pooled views + isSaveEnabled = false + isSaveFromParentEnabled = false + } + } + + @CallSuper + open fun release(view: T) { + check(view.parent == null) { "View must be detached from parent before releasing it" } + synchronized(this) { + pool.add(view) + ensureSize() + } + } + + @CallSuper + open fun clear() { + synchronized(pool) { + pool.forEach { destroy(it) } + pool.clear() + } + } + + abstract fun createView(context: Context): T + + protected open fun destroy(view: T) {} + + private fun ensureSize() { + synchronized(pool) { + while (pool.size > maxSize) { + destroy(pool.removeAt(0)) + } + } + } +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7103e44..223b437 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ androidx-compose-compiler = "1.5.1" androidx-core = "1.13.1" androidx-lifecycle = "2.8.1" androidx-navigation = "2.7.7" +playServicesAds = "23.2.0" retrofit = "2.11.0" [libraries] @@ -39,6 +40,7 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose androidx-material3 = { group = "androidx.compose.material3", name = "material3" } # Misc for demo app +play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" } retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-converter-serializer = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } coil = "io.coil-kt:coil-compose:2.6.0" diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 8a37049..b9915a1 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -85,6 +85,7 @@ dependencies { if (true) { implementation(project(":ads-api")) implementation(project(":ads-applovin")) + implementation(project(":ads-admob")) implementation(project(":ads-compose")) implementation(project(":ads-applovin-plugin-amazon")) } else { diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 7d43422..fc415fa 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -4,6 +4,11 @@ + + + + + { + return AdMobNativeAdClient( + config = AdClient.Config( + adCacheSize = 1, + adUnit = MockData.ADMOB_TEST_AD, + placement = "feed" + ), + activity = activity, + adViewFactory = MyAdMobNativeAdViewFactory(), + adViewRenderer = MyAdMobNativeAdViewRenderer(), + // Provide extras via here if more convenient than the UI + localExtrasProviders = emptyList(), + ) + } + private fun createNativeClient(activity: Activity): AdClient { return MaxNativeAdClient( config = AdClient.Config( diff --git a/sample/src/main/java/io/voodoo/apps/ads/feature/ads/nativ/admob/MyAdMobNativeAdViewFactory.kt b/sample/src/main/java/io/voodoo/apps/ads/feature/ads/nativ/admob/MyAdMobNativeAdViewFactory.kt new file mode 100644 index 0000000..464ddc0 --- /dev/null +++ b/sample/src/main/java/io/voodoo/apps/ads/feature/ads/nativ/admob/MyAdMobNativeAdViewFactory.kt @@ -0,0 +1,36 @@ +package io.voodoo.apps.ads.feature.ads.nativ.admob + +import android.content.Context +import android.view.LayoutInflater +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import com.google.android.gms.ads.nativead.MediaView +import com.google.android.gms.ads.nativead.NativeAdView +import io.voodoo.apps.ads.R +import io.voodoo.apps.ads.admob.nativ.AdMobNativeAdViewFactory + +class MyAdMobNativeAdViewFactory : AdMobNativeAdViewFactory { + + + override fun create(context: Context): NativeAdView { + val inflater = LayoutInflater.from(context) + val nativeAdView = + inflater.inflate(R.layout.layout_admob_feed_ad_item, null) as NativeAdView + + // Set the media view. + nativeAdView.mediaView = nativeAdView.findViewById(R.id.media_view_container) + // Set other ad assets. + nativeAdView.bodyView = nativeAdView.findViewById(R.id.body_text_view) + nativeAdView.callToActionView = nativeAdView.findViewById