@capacitor-community/admob 8.2.0-beta.pr484.shaa6be63853bbd → 8.2.1
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.
- package/README.md +1 -0
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +73 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +378 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdFeedSessions.kt +35 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPlacementValue.kt +44 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPluginEvents.kt +11 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/PluginNativeAdView.kt +187 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/native-ads/index.d.ts +6 -0
- package/dist/esm/native-ads/index.js +4 -0
- package/dist/esm/native-ads/index.js.map +1 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +14 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-element.d.ts +9 -0
- package/dist/esm/native-ads/native-ad-element.js +53 -0
- package/dist/esm/native-ads/native-ad-element.js.map +1 -0
- package/dist/esm/native-ads/native-ad-event.interface.d.ts +5 -0
- package/dist/esm/native-ads/native-ad-event.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-event.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-feed.d.ts +72 -0
- package/dist/esm/native-ads/native-ad-feed.js +568 -0
- package/dist/esm/native-ads/native-ad-feed.js.map +1 -0
- package/dist/esm/native-ads/native-ad-geometry.d.ts +8 -0
- package/dist/esm/native-ads/native-ad-geometry.js +91 -0
- package/dist/esm/native-ads/native-ad-geometry.js.map +1 -0
- package/dist/esm/native-ads/native-ad-options.interface.d.ts +36 -0
- package/dist/esm/native-ads/native-ad-options.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-options.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-placement.interface.d.ts +26 -0
- package/dist/esm/native-ads/native-ad-placement.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-placement.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.d.ts +9 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.js +11 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.js.map +1 -0
- package/dist/esm/native-ads/native-ad-style.interface.d.ts +30 -0
- package/dist/esm/native-ads/native-ad-style.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-style.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-template.enum.d.ts +11 -0
- package/dist/esm/native-ads/native-ad-template.enum.js +13 -0
- package/dist/esm/native-ads/native-ad-template.enum.js.map +1 -0
- package/dist/esm/web.d.ts +9 -1
- package/dist/esm/web.js +15 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +751 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +751 -1
- package/dist/plugin.js.map +1 -1
- package/docs/native-ads.md +161 -0
- package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +76 -44
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdExecutor.swift +354 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdFeedSessions.swift +34 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPlacementValue.swift +50 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPluginEvents.swift +11 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdScrollTracker.swift +66 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdValues.swift +27 -0
- package/ios/Sources/AdMobPlugin/NativeAds/PluginNativeAdView.swift +187 -0
- package/ios/Tests/AdMobPluginTests/AdMobPluginTests.swift +37 -0
- package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,6 +190,7 @@ Start with [Installation](#installation) above, then [Configuration](https://doc
|
|
|
190
190
|
- [Consent](https://docs.rdlabo.dev/projects/capacitor-admob/docs/consent) — privacy consent and iOS tracking authorization.
|
|
191
191
|
- [Testing](https://docs.rdlabo.dev/projects/capacitor-admob/docs/testing) — demo ad units, test devices, and consent testing.
|
|
192
192
|
- [Banner Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/banner) — banner options, lifecycle, and events.
|
|
193
|
+
- [Native Ads (Preview)](./docs/native-ads.md) — available for testing and production use. Its API may change in a minor release while in Preview.
|
|
193
194
|
- Full-screen ads:
|
|
194
195
|
- [Interstitial Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/interstitial) — load, show, and multiple prepared ads.
|
|
195
196
|
- [Rewarded Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/rewarded) — rewarded video, rewarded interstitial, and server-side verification.
|
|
@@ -17,6 +17,7 @@ import com.getcapacitor.community.admob.consent.AdConsentExecutor;
|
|
|
17
17
|
import com.getcapacitor.community.admob.helpers.AuthorizationStatusEnum;
|
|
18
18
|
import com.getcapacitor.community.admob.interstitial.AdInterstitialExecutor;
|
|
19
19
|
import com.getcapacitor.community.admob.interstitial.InterstitialAdCallbackAndListeners;
|
|
20
|
+
import com.getcapacitor.community.admob.nativeads.NativeAdExecutor;
|
|
20
21
|
import com.getcapacitor.community.admob.rewarded.AdRewardExecutor;
|
|
21
22
|
import com.getcapacitor.community.admob.rewardedinterstitial.AdRewardInterstitialExecutor;
|
|
22
23
|
import com.google.android.gms.ads.MobileAds;
|
|
@@ -70,6 +71,39 @@ public class AdMob extends Plugin {
|
|
|
70
71
|
|
|
71
72
|
private final AppOpenAdPlugin appOpenAdPlugin = new AppOpenAdPlugin();
|
|
72
73
|
|
|
74
|
+
private final NativeAdExecutor nativeAdExecutor = new NativeAdExecutor(
|
|
75
|
+
this::getContext,
|
|
76
|
+
this::getActivity,
|
|
77
|
+
() -> getBridge().getWebView(),
|
|
78
|
+
this::notifyListeners,
|
|
79
|
+
getLogTag()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
private boolean nativeAdPlacementChannelRegistered;
|
|
83
|
+
|
|
84
|
+
@Override
|
|
85
|
+
public void load() {
|
|
86
|
+
super.load();
|
|
87
|
+
if (
|
|
88
|
+
androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER) &&
|
|
89
|
+
!getBridge().getConfig().isUsingLegacyBridge()
|
|
90
|
+
) {
|
|
91
|
+
try {
|
|
92
|
+
androidx.webkit.WebViewCompat.addWebMessageListener(
|
|
93
|
+
getBridge().getWebView(),
|
|
94
|
+
"capacitorAdMobPlacements",
|
|
95
|
+
getBridge().getAllowedOriginRules(),
|
|
96
|
+
(view, message, origin, mainFrame, reply) -> {
|
|
97
|
+
if (mainFrame) nativeAdExecutor.updateDirectPlacements(message.getData());
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
nativeAdPlacementChannelRegistered = true;
|
|
101
|
+
} catch (RuntimeException ignored) {
|
|
102
|
+
// Placement updates retain the regular Capacitor bridge as a fallback.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
@PluginMethod
|
|
74
108
|
public void loadAppOpen(final PluginCall call) {
|
|
75
109
|
appOpenAdPlugin.loadAppOpen(getContext(), getActivity(), call, this::notifyListeners);
|
|
@@ -213,6 +247,35 @@ public class AdMob extends Plugin {
|
|
|
213
247
|
bannerExecutor.removeBanner(call);
|
|
214
248
|
}
|
|
215
249
|
|
|
250
|
+
// ---------------------------------------------------------
|
|
251
|
+
// NATIVE ADS
|
|
252
|
+
// ---------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
@PluginMethod
|
|
255
|
+
public void startNativeAdFeed(final PluginCall call) {
|
|
256
|
+
nativeAdExecutor.startFeed(call);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@PluginMethod
|
|
260
|
+
public void destroyNativeAdFeed(final PluginCall call) {
|
|
261
|
+
nativeAdExecutor.destroyFeed(call);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@PluginMethod
|
|
265
|
+
public void loadNativeAd(final PluginCall call) {
|
|
266
|
+
nativeAdExecutor.load(call);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@PluginMethod
|
|
270
|
+
public void updateNativeAdPlacements(final PluginCall call) {
|
|
271
|
+
nativeAdExecutor.updatePlacements(call);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@PluginMethod
|
|
275
|
+
public void removeNativeAd(final PluginCall call) {
|
|
276
|
+
nativeAdExecutor.remove(call);
|
|
277
|
+
}
|
|
278
|
+
|
|
216
279
|
// ---------------------------------------------------------
|
|
217
280
|
// INTERSTITIAL ADS
|
|
218
281
|
// ---------------------------------------------------------
|
|
@@ -319,4 +382,14 @@ public class AdMob extends Plugin {
|
|
|
319
382
|
call.reject(error.toString());
|
|
320
383
|
}
|
|
321
384
|
}
|
|
385
|
+
|
|
386
|
+
@Override
|
|
387
|
+
protected void handleOnDestroy() {
|
|
388
|
+
if (nativeAdPlacementChannelRegistered) {
|
|
389
|
+
androidx.webkit.WebViewCompat.removeWebMessageListener(getBridge().getWebView(), "capacitorAdMobPlacements");
|
|
390
|
+
nativeAdPlacementChannelRegistered = false;
|
|
391
|
+
}
|
|
392
|
+
nativeAdExecutor.destroyAll();
|
|
393
|
+
super.handleOnDestroy();
|
|
394
|
+
}
|
|
322
395
|
}
|
|
@@ -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
|
+
}
|
package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdFeedSessions.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPlacementValue.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPluginEvents.kt
ADDED
|
@@ -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
|
+
}
|