@capacitor-community/admob 8.1.0 → 8.2.0-beta.pr454.sha49386d40323a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CapacitorCommunityAdmob.podspec +1 -1
  2. package/Package.swift +1 -1
  3. package/README.md +129 -323
  4. package/android/build.gradle +1 -1
  5. package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +86 -3
  6. package/android/src/main/java/com/getcapacitor/community/admob/banner/BannerExecutor.java +261 -141
  7. package/android/src/main/java/com/getcapacitor/community/admob/helpers/FullscreenPluginCallback.kt +5 -1
  8. package/android/src/main/java/com/getcapacitor/community/admob/interstitial/AdInterstitialExecutor.java +27 -31
  9. package/android/src/main/java/com/getcapacitor/community/admob/interstitial/InterstitialAdCallbackAndListeners.kt +1 -1
  10. package/android/src/main/java/com/getcapacitor/community/admob/models/LoadPluginEventNames.kt +2 -1
  11. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +378 -0
  12. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdFeedSessions.kt +35 -0
  13. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPlacementValue.kt +44 -0
  14. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPluginEvents.kt +11 -0
  15. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/PluginNativeAdView.kt +187 -0
  16. package/android/src/main/java/com/getcapacitor/community/admob/rewarded/AdRewardExecutor.java +29 -33
  17. package/android/src/main/java/com/getcapacitor/community/admob/rewarded/RewardAdPluginEvents.kt +2 -1
  18. package/android/src/main/java/com/getcapacitor/community/admob/rewarded/RewardedAdCallbackAndListeners.kt +1 -1
  19. package/android/src/main/java/com/getcapacitor/community/admob/rewardedinterstitial/AdRewardInterstitialExecutor.java +29 -37
  20. package/android/src/main/java/com/getcapacitor/community/admob/rewardedinterstitial/RewardedInterstitialAdCallbackAndListeners.kt +1 -1
  21. package/dist/docs.json +33 -3
  22. package/dist/esm/index.d.ts +1 -0
  23. package/dist/esm/index.js +5 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/interstitial/interstitial-definitions.interface.d.ts +3 -0
  26. package/dist/esm/native-ads/index.d.ts +6 -0
  27. package/dist/esm/native-ads/index.js +4 -0
  28. package/dist/esm/native-ads/index.js.map +1 -0
  29. package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +14 -0
  30. package/dist/esm/native-ads/native-ad-definitions.interface.js +2 -0
  31. package/dist/esm/native-ads/native-ad-definitions.interface.js.map +1 -0
  32. package/dist/esm/native-ads/native-ad-element.d.ts +9 -0
  33. package/dist/esm/native-ads/native-ad-element.js +53 -0
  34. package/dist/esm/native-ads/native-ad-element.js.map +1 -0
  35. package/dist/esm/native-ads/native-ad-event.interface.d.ts +5 -0
  36. package/dist/esm/native-ads/native-ad-event.interface.js +2 -0
  37. package/dist/esm/native-ads/native-ad-event.interface.js.map +1 -0
  38. package/dist/esm/native-ads/native-ad-feed.d.ts +72 -0
  39. package/dist/esm/native-ads/native-ad-feed.js +568 -0
  40. package/dist/esm/native-ads/native-ad-feed.js.map +1 -0
  41. package/dist/esm/native-ads/native-ad-geometry.d.ts +8 -0
  42. package/dist/esm/native-ads/native-ad-geometry.js +91 -0
  43. package/dist/esm/native-ads/native-ad-geometry.js.map +1 -0
  44. package/dist/esm/native-ads/native-ad-options.interface.d.ts +36 -0
  45. package/dist/esm/native-ads/native-ad-options.interface.js +2 -0
  46. package/dist/esm/native-ads/native-ad-options.interface.js.map +1 -0
  47. package/dist/esm/native-ads/native-ad-placement.interface.d.ts +26 -0
  48. package/dist/esm/native-ads/native-ad-placement.interface.js +2 -0
  49. package/dist/esm/native-ads/native-ad-placement.interface.js.map +1 -0
  50. package/dist/esm/native-ads/native-ad-plugin-events.enum.d.ts +9 -0
  51. package/dist/esm/native-ads/native-ad-plugin-events.enum.js +11 -0
  52. package/dist/esm/native-ads/native-ad-plugin-events.enum.js.map +1 -0
  53. package/dist/esm/native-ads/native-ad-style.interface.d.ts +30 -0
  54. package/dist/esm/native-ads/native-ad-style.interface.js +2 -0
  55. package/dist/esm/native-ads/native-ad-style.interface.js.map +1 -0
  56. package/dist/esm/native-ads/native-ad-template.enum.d.ts +11 -0
  57. package/dist/esm/native-ads/native-ad-template.enum.js +13 -0
  58. package/dist/esm/native-ads/native-ad-template.enum.js.map +1 -0
  59. package/dist/esm/reward/reward-ad-plugin-events.enum.d.ts +4 -0
  60. package/dist/esm/reward/reward-ad-plugin-events.enum.js +4 -0
  61. package/dist/esm/reward/reward-ad-plugin-events.enum.js.map +1 -1
  62. package/dist/esm/reward/reward-definitions.interface.d.ts +8 -0
  63. package/dist/esm/reward-interstitial/reward-interstitial-definitions.interface.d.ts +3 -0
  64. package/dist/esm/web.d.ts +9 -1
  65. package/dist/esm/web.js +15 -0
  66. package/dist/esm/web.js.map +1 -1
  67. package/dist/plugin.cjs.js +755 -1
  68. package/dist/plugin.cjs.js.map +1 -1
  69. package/dist/plugin.js +755 -1
  70. package/dist/plugin.js.map +1 -1
  71. package/docs/app-open.md +53 -0
  72. package/docs/banner.md +100 -0
  73. package/docs/configuration.md +30 -0
  74. package/docs/consent.md +80 -0
  75. package/docs/events.md +76 -0
  76. package/docs/interstitial.md +47 -0
  77. package/docs/migration.md +146 -0
  78. package/docs/native-ads.md +161 -0
  79. package/docs/rewarded.md +148 -0
  80. package/docs/testing.md +67 -0
  81. package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +106 -41
  82. package/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdManager.swift +1 -1
  83. package/ios/Sources/AdMobPlugin/Banner/BannerExecutor.swift +1 -1
  84. package/ios/Sources/AdMobPlugin/Interstitial/AdInterstitialExecutor.swift +6 -3
  85. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdExecutor.swift +354 -0
  86. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdFeedSessions.swift +34 -0
  87. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPlacementValue.swift +50 -0
  88. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPluginEvents.swift +11 -0
  89. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdScrollTracker.swift +66 -0
  90. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdValues.swift +27 -0
  91. package/ios/Sources/AdMobPlugin/NativeAds/PluginNativeAdView.swift +187 -0
  92. package/ios/Sources/AdMobPlugin/Rewarded/AdRewardExecutor.swift +10 -3
  93. package/ios/Sources/AdMobPlugin/Rewarded/RewardAdPluginEvents.swift +1 -0
  94. package/ios/Sources/AdMobPlugin/RewardedInterstitial/AdRewardInterstitialExecutor.swift +6 -3
  95. package/ios/Tests/AdMobPluginTests/AdMobPluginTests.swift +82 -10
  96. package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
  97. package/package.json +4 -1
@@ -40,18 +40,16 @@ public class AdInterstitialExecutor extends Executor {
40
40
  final AdOptions adOptions = factory.createInterstitialOptions(call);
41
41
 
42
42
  try {
43
- activitySupplier
44
- .get()
45
- .runOnUiThread(() -> {
46
- final AdRequest adRequest = RequestHelper.createRequest(adOptions);
47
- final String id = AdViewIdHelper.getFinalAdId(adOptions, adRequest, logTag, contextSupplier.get());
48
- InterstitialAd.load(
49
- activitySupplier.get(),
50
- id,
51
- adRequest,
52
- adCallbackAndListeners.getInterstitialAdLoadCallback(call, notifyListenersFunction)
53
- );
54
- });
43
+ activitySupplier.get().runOnUiThread(() -> {
44
+ final AdRequest adRequest = RequestHelper.createRequest(adOptions);
45
+ final String id = AdViewIdHelper.getFinalAdId(adOptions, adRequest, logTag, contextSupplier.get());
46
+ InterstitialAd.load(
47
+ activitySupplier.get(),
48
+ id,
49
+ adRequest,
50
+ adCallbackAndListeners.getInterstitialAdLoadCallback(call, notifyListenersFunction)
51
+ );
52
+ });
55
53
  } catch (Exception ex) {
56
54
  call.reject(ex.getLocalizedMessage(), ex);
57
55
  }
@@ -71,24 +69,22 @@ public class AdInterstitialExecutor extends Executor {
71
69
  }
72
70
 
73
71
  final InterstitialAd adToShow = ad;
74
- activitySupplier
75
- .get()
76
- .runOnUiThread(() -> {
77
- try {
78
- adToShow.setFullScreenContentCallback(
79
- new FullscreenPluginCallback(InterstitialAdPluginPluginEvent.INSTANCE, notifyListenersFunction, () -> {
80
- preparedAds.remove(adId);
81
- if (adId != null && adId.equals(lastPreparedAdId)) {
82
- lastPreparedAdId = null;
83
- for (String remainingAdId : preparedAds.keySet()) lastPreparedAdId = remainingAdId;
84
- }
85
- })
86
- );
87
- adToShow.show(activitySupplier.get());
88
- call.resolve();
89
- } catch (Exception ex) {
90
- call.reject(ex.getLocalizedMessage(), ex);
91
- }
92
- });
72
+ activitySupplier.get().runOnUiThread(() -> {
73
+ try {
74
+ adToShow.setFullScreenContentCallback(
75
+ new FullscreenPluginCallback(InterstitialAdPluginPluginEvent.INSTANCE, notifyListenersFunction, () -> {
76
+ preparedAds.remove(adId);
77
+ if (adId != null && adId.equals(lastPreparedAdId)) {
78
+ lastPreparedAdId = null;
79
+ for (String remainingAdId : preparedAds.keySet()) lastPreparedAdId = remainingAdId;
80
+ }
81
+ })
82
+ );
83
+ adToShow.show(activitySupplier.get());
84
+ call.resolve();
85
+ } catch (Exception ex) {
86
+ call.reject(ex.getLocalizedMessage(), ex);
87
+ }
88
+ });
93
89
  }
94
90
  }
@@ -43,7 +43,7 @@ object InterstitialAdCallbackAndListeners {
43
43
  val adMobError = AdMobPluginError(adError)
44
44
 
45
45
  notifyListenersFunction.accept(InterstitialAdPluginPluginEvent.FailedToLoad, adMobError)
46
- call.reject(adError.message)
46
+ call.reject(adError.message, adError.code.toString())
47
47
  }
48
48
  }
49
49
  }
@@ -1,7 +1,8 @@
1
1
  package com.getcapacitor.community.admob.models
2
2
 
3
3
  interface LoadPluginEventNames {
4
+ val adClicked: String? get() = null
4
5
  val Showed: String
5
6
  val FailedToShow: String
6
7
  val Dismissed: String
7
- }
8
+ }
@@ -0,0 +1,378 @@
1
+ package com.getcapacitor.community.admob.nativeads
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.view.View
6
+ import android.view.ViewGroup
7
+ import android.widget.FrameLayout
8
+ import androidx.core.util.Supplier
9
+ import com.getcapacitor.JSArray
10
+ import com.getcapacitor.JSObject
11
+ import com.getcapacitor.PluginCall
12
+ import com.getcapacitor.community.admob.helpers.RequestHelper
13
+ import com.getcapacitor.community.admob.models.AdMobRevenueData
14
+ import com.getcapacitor.community.admob.models.AdOptions
15
+ import com.getcapacitor.community.admob.models.Executor
16
+ import com.google.android.gms.ads.AdListener
17
+ import com.google.android.gms.ads.AdLoader
18
+ import com.google.android.gms.ads.LoadAdError
19
+ import com.google.android.gms.ads.nativead.NativeAd
20
+ import com.google.android.gms.ads.nativead.NativeAdOptions
21
+ import com.google.android.gms.ads.nativead.NativeAdView
22
+ import com.google.android.gms.common.util.BiConsumer
23
+ import org.json.JSONException
24
+ import org.json.JSONObject
25
+ import java.util.concurrent.atomic.AtomicBoolean
26
+ import kotlin.math.roundToInt
27
+
28
+ class NativeAdExecutor(
29
+ contextSupplier: Supplier<Context>,
30
+ activitySupplier: Supplier<Activity>,
31
+ private val webViewSupplier: Supplier<View>,
32
+ notifyListenersFunction: BiConsumer<String, JSObject>,
33
+ pluginLogTag: String,
34
+ ) : Executor(contextSupplier, activitySupplier, notifyListenersFunction, pluginLogTag, "NativeAdExecutor") {
35
+ private data class NativeAdState(
36
+ val feedId: String,
37
+ val isSmall: Boolean,
38
+ val loadToken: Any,
39
+ val nativeAd: NativeAd,
40
+ val adView: NativeAdView,
41
+ val clippingContainer: FrameLayout,
42
+ var generation: Int = -1,
43
+ )
44
+
45
+ private data class PendingLoad(
46
+ val call: PluginCall,
47
+ val feedId: String,
48
+ val token: Any = Any(),
49
+ )
50
+
51
+ private val states = mutableMapOf<String, NativeAdState>()
52
+ private val pendingLoads = mutableMapOf<String, PendingLoad>()
53
+ private val feedSessions = NativeAdFeedSessions()
54
+
55
+ fun startFeed(call: PluginCall) {
56
+ val feedId = requiredString(call, "feedId") ?: return
57
+ val sessionId = requiredString(call, "sessionId") ?: return
58
+ activitySupplier.get().runOnUiThread {
59
+ if (!feedSessions.isCurrent(feedId, sessionId)) clearFeed(feedId)
60
+ if (!feedSessions.start(feedId, sessionId)) {
61
+ call.reject("At most two native ad feeds can be active")
62
+ return@runOnUiThread
63
+ }
64
+ call.resolve()
65
+ }
66
+ }
67
+
68
+ fun destroyFeed(call: PluginCall) {
69
+ val feedId = requiredString(call, "feedId") ?: return
70
+ val sessionId = requiredString(call, "sessionId") ?: return
71
+ activitySupplier.get().runOnUiThread {
72
+ if (feedSessions.isCurrent(feedId, sessionId)) {
73
+ clearFeed(feedId)
74
+ feedSessions.remove(feedId)
75
+ }
76
+ call.resolve()
77
+ }
78
+ }
79
+
80
+ fun load(call: PluginCall) {
81
+ val feedId = requiredString(call, "feedId") ?: return
82
+ val sessionId = requiredString(call, "sessionId") ?: return
83
+ val slotKey = requiredString(call, "slotKey") ?: return
84
+ val options = AdOptions.getFactory().createGenericOptions(call, NATIVE_TESTER_ID)
85
+ val adId = if (options.isTesting) NATIVE_TESTER_ID else requiredString(call, "adId") ?: return
86
+ val template = call.getString("template", "medium") ?: "medium"
87
+ val style = call.getObject("style", JSObject()) ?: JSObject()
88
+ val stateKey = stateKey(feedId, slotKey)
89
+ val finished = AtomicBoolean(false)
90
+ val pendingLoad = PendingLoad(call, feedId)
91
+
92
+ activitySupplier.get().runOnUiThread {
93
+ if (!isCurrentSession(feedId, sessionId)) {
94
+ call.reject("Native ad feed session is no longer active")
95
+ return@runOnUiThread
96
+ }
97
+ destroyState(stateKey)
98
+ cancelPendingLoad(stateKey)
99
+ val feedAdCount = states.values.count { it.feedId == feedId } + pendingLoads.values.count { it.feedId == feedId }
100
+ if (feedAdCount >= MAX_ADS_PER_FEED) {
101
+ call.reject("At most three native ads can be active in a feed")
102
+ return@runOnUiThread
103
+ }
104
+ pendingLoads[stateKey] = pendingLoad
105
+ val adLoader = AdLoader.Builder(contextSupplier.get(), adId)
106
+ .forNativeAd { nativeAd ->
107
+ handleLoadedAd(
108
+ nativeAd = nativeAd,
109
+ adId = adId,
110
+ stateKey = stateKey,
111
+ pendingLoad = pendingLoad,
112
+ finished = finished,
113
+ feedId = feedId,
114
+ sessionId = sessionId,
115
+ slotKey = slotKey,
116
+ template = template,
117
+ style = style,
118
+ )
119
+ }
120
+ .withNativeAdOptions(
121
+ NativeAdOptions.Builder()
122
+ .setAdChoicesPlacement(NativeAdOptions.ADCHOICES_TOP_RIGHT)
123
+ .build(),
124
+ )
125
+ .withAdListener(adListener(stateKey, pendingLoad, finished, feedId, sessionId, slotKey))
126
+ .build()
127
+
128
+ adLoader.loadAd(RequestHelper.createRequest(options))
129
+ }
130
+ }
131
+
132
+ // WebMessageListener delivers on the UI thread; lifecycle calls still use PluginCall.
133
+ fun updateDirectPlacements(raw: String?) {
134
+ try {
135
+ applyPlacements(JSONObject(raw ?: return))
136
+ } catch (_: JSONException) {
137
+ // Ignore malformed messages without affecting another feed.
138
+ }
139
+ }
140
+
141
+ fun updatePlacements(call: PluginCall) {
142
+ requiredString(call, "feedId") ?: return
143
+ requiredString(call, "sessionId") ?: return
144
+ activitySupplier.get().runOnUiThread {
145
+ applyPlacements(call.data)
146
+ call.resolve()
147
+ }
148
+ }
149
+
150
+ private fun applyPlacements(batch: JSONObject) {
151
+ val feedId = batch.optString("feedId")
152
+ val sessionId = batch.optString("sessionId")
153
+ if (!isCurrentSession(feedId, sessionId) || !feedSessions.accepts(feedId, batch.optLong("sequence", -1L))) return
154
+ val shown = mutableSetOf<String>()
155
+ val placements = batch.optJSONArray("placements") ?: JSArray()
156
+ repeat(placements.length()) { index ->
157
+ try {
158
+ applyPlacement(feedId, placements.getJSONObject(index))?.let { shown.add(it) }
159
+ } catch (_: JSONException) {
160
+ // Invalid entries fail closed below.
161
+ }
162
+ }
163
+ states.filter { it.value.feedId == feedId && it.key !in shown }.values.forEach {
164
+ it.clippingContainer.visibility = View.GONE
165
+ }
166
+ }
167
+
168
+ fun remove(call: PluginCall) {
169
+ val feedId = requiredString(call, "feedId") ?: return
170
+ val sessionId = requiredString(call, "sessionId") ?: return
171
+ val slotKey = requiredString(call, "slotKey") ?: return
172
+ activitySupplier.get().runOnUiThread {
173
+ if (isCurrentSession(feedId, sessionId)) {
174
+ val key = stateKey(feedId, slotKey)
175
+ cancelPendingLoad(key)
176
+ destroyState(key)
177
+ }
178
+ call.resolve()
179
+ }
180
+ }
181
+
182
+ fun destroyAll() {
183
+ pendingLoads.keys.toList().forEach(::cancelPendingLoad)
184
+ states.keys.toList().forEach(::destroyState)
185
+ feedSessions.clear()
186
+ }
187
+
188
+ private fun handleLoadedAd(
189
+ nativeAd: NativeAd,
190
+ adId: String,
191
+ stateKey: String,
192
+ pendingLoad: PendingLoad,
193
+ finished: AtomicBoolean,
194
+ feedId: String,
195
+ sessionId: String,
196
+ slotKey: String,
197
+ template: String,
198
+ style: JSObject,
199
+ ) {
200
+ if (pendingLoads[stateKey] !== pendingLoad || !finished.compareAndSet(false, true)) {
201
+ nativeAd.destroy()
202
+ return
203
+ }
204
+ pendingLoads.remove(stateKey)
205
+ if (template == "small" && nativeAd.mediaContent?.hasVideoContent() == true) {
206
+ nativeAd.destroy()
207
+ val message = "Small native ad template does not support video ads; use Medium"
208
+ val event = identity(feedId, sessionId, slotKey).apply {
209
+ put("code", -1)
210
+ put("message", message)
211
+ }
212
+ notifyListeners(NativeAdPluginEvents.FAILED_TO_LOAD, event)
213
+ pendingLoad.call.reject(message, "-1", event)
214
+ return
215
+ }
216
+ val adView = PluginNativeAdView.create(contextSupplier.get(), nativeAd, template, style)
217
+ val clippingContainer = FrameLayout(contextSupplier.get()).apply {
218
+ clipChildren = true
219
+ clipToPadding = true
220
+ visibility = View.GONE
221
+ addView(adView)
222
+ }
223
+ val overlayParent = overlayParent()
224
+ if (overlayParent == null) {
225
+ adView.destroy()
226
+ nativeAd.destroy()
227
+ pendingLoad.call.reject("AdMob WebView parent is unavailable")
228
+ return
229
+ }
230
+
231
+ val state = NativeAdState(feedId, template == "small", pendingLoad.token, nativeAd, adView, clippingContainer)
232
+ states[stateKey] = state
233
+ overlayParent.addView(clippingContainer)
234
+ nativeAd.setOnPaidEventListener { adValue ->
235
+ if (states[stateKey] !== state) return@setOnPaidEventListener
236
+ val responseInfo = nativeAd.responseInfo
237
+ val data = AdMobRevenueData(
238
+ adValue,
239
+ adId,
240
+ responseInfo?.mediationAdapterClassName ?: "",
241
+ responseInfo?.responseId ?: "",
242
+ ).apply {
243
+ put("feedId", feedId)
244
+ put("slotKey", slotKey)
245
+ put("sessionId", sessionId)
246
+ }
247
+ notifyListeners(NativeAdPluginEvents.AD_PAID, data)
248
+ }
249
+
250
+ notifyListeners(NativeAdPluginEvents.LOADED, identity(feedId, sessionId, slotKey))
251
+ pendingLoad.call.resolve()
252
+ }
253
+
254
+ private fun adListener(
255
+ stateKey: String,
256
+ pendingLoad: PendingLoad,
257
+ finished: AtomicBoolean,
258
+ feedId: String,
259
+ sessionId: String,
260
+ slotKey: String,
261
+ ): AdListener = object : AdListener() {
262
+ override fun onAdFailedToLoad(error: LoadAdError) {
263
+ if (pendingLoads[stateKey] !== pendingLoad) return
264
+ pendingLoads.remove(stateKey)
265
+ val event = identity(feedId, sessionId, slotKey).apply {
266
+ put("code", error.code)
267
+ put("message", error.message)
268
+ }
269
+ notifyListeners(NativeAdPluginEvents.FAILED_TO_LOAD, event)
270
+ if (finished.compareAndSet(false, true)) {
271
+ pendingLoad.call.reject(error.message, error.code.toString(), event)
272
+ }
273
+ }
274
+
275
+ override fun onAdClicked() = notifyIfCurrent(NativeAdPluginEvents.CLICKED)
276
+
277
+ override fun onAdImpression() = notifyIfCurrent(NativeAdPluginEvents.IMPRESSION)
278
+
279
+ override fun onAdOpened() = notifyIfCurrent(NativeAdPluginEvents.OPENED)
280
+
281
+ override fun onAdClosed() = notifyIfCurrent(NativeAdPluginEvents.CLOSED)
282
+
283
+ private fun notifyIfCurrent(eventName: String) {
284
+ if (isCurrent(stateKey, pendingLoad.token)) {
285
+ notifyListeners(eventName, identity(feedId, sessionId, slotKey))
286
+ }
287
+ }
288
+ }
289
+
290
+ private fun applyPlacement(expectedFeedId: String, placement: JSONObject): String? {
291
+ val density = contextSupplier.get().resources.displayMetrics.density
292
+ val value = NativeAdPlacementValue.parse(expectedFeedId, placement, density) ?: return null
293
+ val state = states[stateKey(value.feedId, value.slotKey)] ?: return null
294
+ if (value.generation < state.generation) return null
295
+ val minimumWidth = (if (state.isSmall) 120 else 144) * density
296
+ val minimumHeight = (if (state.isSmall) 120 else 300) * density
297
+ if (value.rectWidth < minimumWidth.roundToInt() || value.rectHeight < minimumHeight.roundToInt()) return null
298
+
299
+ if (!state.isSmall) {
300
+ val adView = state.adView
301
+ if (adView.isLayoutRequested || adView.measuredWidth != value.rectWidth || adView.measuredHeight != value.rectHeight) {
302
+ adView.measure(
303
+ View.MeasureSpec.makeMeasureSpec(value.rectWidth, View.MeasureSpec.EXACTLY),
304
+ View.MeasureSpec.makeMeasureSpec(value.rectHeight, View.MeasureSpec.EXACTLY),
305
+ )
306
+ adView.layout(0, 0, value.rectWidth, value.rectHeight)
307
+ }
308
+ val media = adView.mediaView ?: return null
309
+ if (media.measuredWidth < 120 * density || media.measuredHeight < 120 * density) return null
310
+ }
311
+
312
+ val webView = webViewSupplier.get() ?: return null
313
+ val wasVisible = state.clippingContainer.visibility == View.VISIBLE
314
+ val clipParams = state.clippingContainer.layoutParams
315
+ if (clipParams.width != value.clipWidth || clipParams.height != value.clipHeight) {
316
+ state.clippingContainer.layoutParams = clipParams.apply {
317
+ width = value.clipWidth
318
+ height = value.clipHeight
319
+ }
320
+ }
321
+ state.clippingContainer.x = webView.x + value.clipX
322
+ state.clippingContainer.y = webView.y + value.clipY
323
+ val adParams = state.adView.layoutParams
324
+ if (adParams.width != value.rectWidth || adParams.height != value.rectHeight) {
325
+ state.adView.layoutParams = FrameLayout.LayoutParams(value.rectWidth, value.rectHeight)
326
+ }
327
+ state.adView.x = (value.rectX - value.clipX).toFloat()
328
+ state.adView.y = (value.rectY - value.clipY).toFloat()
329
+ state.generation = value.generation
330
+ state.clippingContainer.visibility = View.VISIBLE
331
+ if (!wasVisible) state.clippingContainer.bringToFront()
332
+ return stateKey(value.feedId, value.slotKey)
333
+ }
334
+
335
+ private fun overlayParent(): ViewGroup? = webViewSupplier.get()?.parent as? ViewGroup
336
+
337
+ private fun destroyState(stateKey: String) {
338
+ val state = states.remove(stateKey) ?: return
339
+ (state.clippingContainer.parent as? ViewGroup)?.removeView(state.clippingContainer)
340
+ state.adView.destroy()
341
+ state.nativeAd.destroy()
342
+ }
343
+
344
+ private fun cancelPendingLoad(stateKey: String) {
345
+ pendingLoads.remove(stateKey)?.call?.reject("Native ad load was cancelled")
346
+ }
347
+
348
+ private fun isCurrent(stateKey: String, loadToken: Any): Boolean = states[stateKey]?.loadToken === loadToken
349
+
350
+ private fun isCurrentSession(feedId: String, sessionId: String): Boolean = feedSessions.isCurrent(feedId, sessionId)
351
+
352
+ private fun clearFeed(feedId: String) {
353
+ pendingLoads.filterValues { it.feedId == feedId }.keys.toList().forEach(::cancelPendingLoad)
354
+ states.filterValues { it.feedId == feedId }.keys.toList().forEach(::destroyState)
355
+ }
356
+
357
+ private fun requiredString(call: PluginCall, name: String): String? {
358
+ val value = call.getString(name)?.trim()
359
+ if (value.isNullOrEmpty()) {
360
+ call.reject("$name must not be empty")
361
+ return null
362
+ }
363
+ return value
364
+ }
365
+
366
+ private fun stateKey(feedId: String, slotKey: String): String = "$feedId\u0000$slotKey"
367
+
368
+ private fun identity(feedId: String, sessionId: String, slotKey: String): JSObject = JSObject().apply {
369
+ put("feedId", feedId)
370
+ put("sessionId", sessionId)
371
+ put("slotKey", slotKey)
372
+ }
373
+
374
+ companion object {
375
+ const val NATIVE_TESTER_ID = "ca-app-pub-3940256099942544/2247696110"
376
+ private const val MAX_ADS_PER_FEED = 3
377
+ }
378
+ }
@@ -0,0 +1,35 @@
1
+ package com.getcapacitor.community.admob.nativeads
2
+
3
+ internal class NativeAdFeedSessions {
4
+ private val sessions = mutableMapOf<String, String>()
5
+ private val placementSequences = mutableMapOf<String, Long>()
6
+
7
+ fun start(feedId: String, sessionId: String): Boolean {
8
+ if (feedId !in sessions && sessions.size >= MAX_FEEDS) return false
9
+ sessions[feedId] = sessionId
10
+ placementSequences[feedId] = -1
11
+ return true
12
+ }
13
+
14
+ fun isCurrent(feedId: String, sessionId: String): Boolean = sessions[feedId] == sessionId
15
+
16
+ fun accepts(feedId: String, sequence: Long): Boolean {
17
+ if (sequence <= placementSequences.getOrDefault(feedId, -1)) return false
18
+ placementSequences[feedId] = sequence
19
+ return true
20
+ }
21
+
22
+ fun remove(feedId: String) {
23
+ sessions.remove(feedId)
24
+ placementSequences.remove(feedId)
25
+ }
26
+
27
+ fun clear() {
28
+ sessions.clear()
29
+ placementSequences.clear()
30
+ }
31
+
32
+ private companion object {
33
+ const val MAX_FEEDS = 2
34
+ }
35
+ }
@@ -0,0 +1,44 @@
1
+ package com.getcapacitor.community.admob.nativeads
2
+
3
+ import org.json.JSONObject
4
+ import kotlin.math.roundToInt
5
+
6
+ internal class NativeAdPlacementValue private constructor(
7
+ val feedId: String,
8
+ val slotKey: String,
9
+ val generation: Int,
10
+ val rectX: Int,
11
+ val rectY: Int,
12
+ val rectWidth: Int,
13
+ val rectHeight: Int,
14
+ val clipX: Int,
15
+ val clipY: Int,
16
+ val clipWidth: Int,
17
+ val clipHeight: Int,
18
+ ) {
19
+ private fun hasArea(): Boolean = rectWidth > 0 && rectHeight > 0 && clipWidth > 0 && clipHeight > 0
20
+
21
+ companion object {
22
+ fun parse(expectedFeedId: String, value: JSONObject, density: Float): NativeAdPlacementValue? {
23
+ if (!value.optBoolean("visible", false)) return null
24
+ val rect = value.getJSONObject("rect")
25
+ val clip = value.getJSONObject("clipRect")
26
+ val placement = NativeAdPlacementValue(
27
+ feedId = value.getString("feedId"),
28
+ slotKey = value.getString("slotKey"),
29
+ generation = value.optInt("generation", -1),
30
+ rectX = rect.pixels("x", density),
31
+ rectY = rect.pixels("y", density),
32
+ rectWidth = rect.pixels("width", density),
33
+ rectHeight = rect.pixels("height", density),
34
+ clipX = clip.pixels("x", density),
35
+ clipY = clip.pixels("y", density),
36
+ clipWidth = clip.pixels("width", density),
37
+ clipHeight = clip.pixels("height", density),
38
+ )
39
+ return placement.takeIf { it.feedId == expectedFeedId && it.hasArea() }
40
+ }
41
+
42
+ private fun JSONObject.pixels(name: String, density: Float): Int = (getDouble(name) * density).roundToInt()
43
+ }
44
+ }
@@ -0,0 +1,11 @@
1
+ package com.getcapacitor.community.admob.nativeads
2
+
3
+ internal object NativeAdPluginEvents {
4
+ const val LOADED = "nativeAdLoaded"
5
+ const val FAILED_TO_LOAD = "nativeAdFailedToLoad"
6
+ const val CLICKED = "nativeAdClicked"
7
+ const val IMPRESSION = "nativeAdImpression"
8
+ const val OPENED = "nativeAdOpened"
9
+ const val CLOSED = "nativeAdClosed"
10
+ const val AD_PAID = "nativeAdPaid"
11
+ }