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