@capacitor-community/admob 8.2.1 → 8.3.0-beta.pr495.shaaa5aedc59a51
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 -1
- package/android/build.gradle +5 -1
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +15 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +257 -9
- package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +6 -0
- package/dist/esm/native-ads/native-ad-feed.d.ts +3 -3
- package/dist/esm/native-ads/native-ad-feed.js +107 -32
- package/dist/esm/native-ads/native-ad-feed.js.map +1 -1
- package/dist/esm/native-ads/native-ad-geometry.js +2 -1
- package/dist/esm/native-ads/native-ad-geometry.js.map +1 -1
- package/dist/esm/native-ads/native-ad-options.interface.d.ts +6 -2
- package/dist/esm/native-ads/native-ad-placement.interface.d.ts +2 -0
- package/dist/plugin.cjs.js +165 -33
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +165 -33
- package/dist/plugin.js.map +1 -1
- package/docs/native-ads.md +32 -6
- package/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerAdSizeEnum.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerExecutor.swift +4 -4
- package/ios/Sources/AdMobPlugin/Helper/AuthorizationStatusEnum.swift +3 -0
- package/ios/Sources/AdMobPlugin/Interstitial/InterstitialAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/Rewarded/RewardAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/RewardedInterstitial/RewardInterstitialAdPluginEvents.swift +3 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -190,7 +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
|
|
193
|
+
- [Native Ads](./docs/native-ads.md) — feed placement, styling, lifecycle, and events.
|
|
194
194
|
- Full-screen ads:
|
|
195
195
|
- [Interstitial Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/interstitial) — load, show, and multiple prepared ads.
|
|
196
196
|
- [Rewarded Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/rewarded) — rewarded video, rewarded interstitial, and server-side verification.
|
package/android/build.gradle
CHANGED
|
@@ -53,14 +53,18 @@ android {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
repositories {
|
|
56
|
+
if (project.hasProperty('nativeUIEngineRepository')) {
|
|
57
|
+
maven { url = uri(project.property('nativeUIEngineRepository')) }
|
|
58
|
+
}
|
|
56
59
|
google()
|
|
57
|
-
|
|
60
|
+
mavenCentral()
|
|
58
61
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
def testRunner = "1.7.0"
|
|
62
65
|
|
|
63
66
|
dependencies {
|
|
67
|
+
implementation "io.github.webview-native-ui:engine:${project.findProperty('nativeUIEngineVersion') ?: '[1.0.0,2.0.0)'}"
|
|
64
68
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
65
69
|
implementation project(':capacitor-android')
|
|
66
70
|
|
|
@@ -84,6 +84,11 @@ public class AdMob extends Plugin {
|
|
|
84
84
|
@Override
|
|
85
85
|
public void load() {
|
|
86
86
|
super.load();
|
|
87
|
+
try {
|
|
88
|
+
io.github.webview_native_ui.engine.NativeUIProjection.install(getBridge().getWebView());
|
|
89
|
+
} catch (Exception error) {
|
|
90
|
+
android.util.Log.w(getLogTag(), "Native UI channel unavailable; retaining native tracking", error);
|
|
91
|
+
}
|
|
87
92
|
if (
|
|
88
93
|
androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER) &&
|
|
89
94
|
!getBridge().getConfig().isUsingLegacyBridge()
|
|
@@ -276,6 +281,16 @@ public class AdMob extends Plugin {
|
|
|
276
281
|
nativeAdExecutor.remove(call);
|
|
277
282
|
}
|
|
278
283
|
|
|
284
|
+
@PluginMethod
|
|
285
|
+
public void captureNativeAdSnapshot(final PluginCall call) {
|
|
286
|
+
nativeAdExecutor.captureSnapshot(call);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@PluginMethod
|
|
290
|
+
public void prepareNativeAdSnapshot(final PluginCall call) {
|
|
291
|
+
nativeAdExecutor.prepareSnapshot(call);
|
|
292
|
+
}
|
|
293
|
+
|
|
279
294
|
// ---------------------------------------------------------
|
|
280
295
|
// INTERSTITIAL ADS
|
|
281
296
|
// ---------------------------------------------------------
|
package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt
CHANGED
|
@@ -2,8 +2,19 @@ package com.getcapacitor.community.admob.nativeads
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import io.github.webview_native_ui.engine.NativeUIProjection
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.Canvas
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.graphics.drawable.GradientDrawable
|
|
10
|
+
import android.os.Debug
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.os.SystemClock
|
|
14
|
+
import android.util.Base64
|
|
5
15
|
import android.view.View
|
|
6
16
|
import android.view.ViewGroup
|
|
17
|
+
import android.webkit.WebView
|
|
7
18
|
import android.widget.FrameLayout
|
|
8
19
|
import androidx.core.util.Supplier
|
|
9
20
|
import com.getcapacitor.JSArray
|
|
@@ -23,6 +34,7 @@ import com.google.android.gms.common.util.BiConsumer
|
|
|
23
34
|
import org.json.JSONException
|
|
24
35
|
import org.json.JSONObject
|
|
25
36
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
37
|
+
import java.io.ByteArrayOutputStream
|
|
26
38
|
import kotlin.math.roundToInt
|
|
27
39
|
|
|
28
40
|
class NativeAdExecutor(
|
|
@@ -39,8 +51,16 @@ class NativeAdExecutor(
|
|
|
39
51
|
val nativeAd: NativeAd,
|
|
40
52
|
val adView: NativeAdView,
|
|
41
53
|
val clippingContainer: FrameLayout,
|
|
54
|
+
val opaqueBackground: Boolean,
|
|
42
55
|
var generation: Int = -1,
|
|
43
|
-
|
|
56
|
+
var projection: NativeUIProjection? = null,
|
|
57
|
+
var projectionIdentity: JSObject? = null,
|
|
58
|
+
var projectionVisible: Boolean = true,
|
|
59
|
+
var deferredPlacement: JSONObject? = null,
|
|
60
|
+
|
|
61
|
+
) {
|
|
62
|
+
var placementSequence: Long = -1
|
|
63
|
+
}
|
|
44
64
|
|
|
45
65
|
private data class PendingLoad(
|
|
46
66
|
val call: PluginCall,
|
|
@@ -51,6 +71,7 @@ class NativeAdExecutor(
|
|
|
51
71
|
private val states = mutableMapOf<String, NativeAdState>()
|
|
52
72
|
private val pendingLoads = mutableMapOf<String, PendingLoad>()
|
|
53
73
|
private val feedSessions = NativeAdFeedSessions()
|
|
74
|
+
private val snapshotCapturePending = AtomicBoolean(false)
|
|
54
75
|
|
|
55
76
|
fun startFeed(call: PluginCall) {
|
|
56
77
|
val feedId = requiredString(call, "feedId") ?: return
|
|
@@ -142,26 +163,61 @@ class NativeAdExecutor(
|
|
|
142
163
|
requiredString(call, "feedId") ?: return
|
|
143
164
|
requiredString(call, "sessionId") ?: return
|
|
144
165
|
activitySupplier.get().runOnUiThread {
|
|
145
|
-
applyPlacements(call.data)
|
|
146
|
-
|
|
166
|
+
applyPlacements(call.data) { hidden ->
|
|
167
|
+
if (hidden) call.resolve() else call.reject("Native ad projection did not finish hiding")
|
|
168
|
+
}
|
|
147
169
|
}
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
private fun applyPlacements(batch: JSONObject) {
|
|
172
|
+
private fun applyPlacements(batch: JSONObject, complete: ((Boolean) -> Unit)? = null) {
|
|
151
173
|
val feedId = batch.optString("feedId")
|
|
152
174
|
val sessionId = batch.optString("sessionId")
|
|
153
|
-
if (!isCurrentSession(feedId, sessionId) || !feedSessions.accepts(feedId, batch.optLong("sequence", -1L))) return
|
|
175
|
+
if (!isCurrentSession(feedId, sessionId) || !feedSessions.accepts(feedId, batch.optLong("sequence", -1L))) { complete?.invoke(true); return }
|
|
176
|
+
val sequence = batch.optLong("sequence", -1L)
|
|
177
|
+
val eligible = states.filter { it.value.feedId == feedId && it.value.placementSequence < sequence }.keys.toSet()
|
|
154
178
|
val shown = mutableSetOf<String>()
|
|
179
|
+
val retained = mutableSetOf<String>()
|
|
155
180
|
val placements = batch.optJSONArray("placements") ?: JSArray()
|
|
156
181
|
repeat(placements.length()) { index ->
|
|
157
182
|
try {
|
|
158
|
-
|
|
183
|
+
val placement = placements.getJSONObject(index)
|
|
184
|
+
val key = stateKey(feedId, placement.optString("slotKey"))
|
|
185
|
+
if (key in eligible) {
|
|
186
|
+
if (placement.optBoolean("retain", false)) {
|
|
187
|
+
val state = states[key]
|
|
188
|
+
if (state != null && placement.optString("feedId") == feedId &&
|
|
189
|
+
placement.optInt("generation", -1) == state.generation && placement.optBoolean("visible", false) &&
|
|
190
|
+
state.projectionVisible && state.clippingContainer.visibility == View.VISIBLE) {
|
|
191
|
+
shown.add(key)
|
|
192
|
+
retained.add(key)
|
|
193
|
+
}
|
|
194
|
+
} else applyPlacement(feedId, placement)?.let { shown.add(it) }
|
|
195
|
+
}
|
|
159
196
|
} catch (_: JSONException) {
|
|
160
197
|
// Invalid entries fail closed below.
|
|
161
198
|
}
|
|
162
199
|
}
|
|
163
|
-
|
|
200
|
+
val hiding = mutableListOf<NativeUIProjection>()
|
|
201
|
+
// Retain carries no new geometry. A delayed retain must not supersede
|
|
202
|
+
// the final position arriving independently through the Engine channel.
|
|
203
|
+
(eligible - retained).forEach { states[it]?.placementSequence = sequence }
|
|
204
|
+
states.filter { it.key in eligible && it.key !in shown }.values.forEach {
|
|
205
|
+
it.deferredPlacement = null
|
|
164
206
|
it.clippingContainer.visibility = View.GONE
|
|
207
|
+
if (it.projectionVisible || complete != null) it.projection?.let(hiding::add)
|
|
208
|
+
if (it.projectionVisible) {
|
|
209
|
+
it.projectionVisible = false
|
|
210
|
+
it.projectionIdentity?.let { event -> notifyListeners("nativeAdProjectionHidden", event) }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (hiding.isEmpty()) complete?.invoke(true)
|
|
214
|
+
else {
|
|
215
|
+
var remaining = hiding.size
|
|
216
|
+
var succeeded = true
|
|
217
|
+
hiding.forEach { projection -> projection.hide { result ->
|
|
218
|
+
succeeded = succeeded && result
|
|
219
|
+
if (--remaining == 0) complete?.invoke(succeeded)
|
|
220
|
+
} }
|
|
165
221
|
}
|
|
166
222
|
}
|
|
167
223
|
|
|
@@ -228,9 +284,65 @@ class NativeAdExecutor(
|
|
|
228
284
|
return
|
|
229
285
|
}
|
|
230
286
|
|
|
231
|
-
val
|
|
287
|
+
val backgroundColor = (adView.background as? GradientDrawable)?.color?.defaultColor
|
|
288
|
+
val state = NativeAdState(
|
|
289
|
+
feedId, template == "small", pendingLoad.token, nativeAd, adView, clippingContainer,
|
|
290
|
+
backgroundColor != null && Color.alpha(backgroundColor) == 255,
|
|
291
|
+
)
|
|
232
292
|
states[stateKey] = state
|
|
233
293
|
overlayParent.addView(clippingContainer)
|
|
294
|
+
pendingLoad.call.getString("projectionSelector")?.let { selector ->
|
|
295
|
+
val event = identity(feedId, sessionId, slotKey).apply { put("loadId", java.util.UUID.randomUUID().toString()) }
|
|
296
|
+
state.projectionIdentity = event
|
|
297
|
+
val webView = webViewSupplier.get() as? WebView
|
|
298
|
+
if (webView != null) webView.post {
|
|
299
|
+
if (states[stateKey] !== state) return@post
|
|
300
|
+
state.projection = runCatching {
|
|
301
|
+
NativeUIProjection(
|
|
302
|
+
window = activitySupplier.get().window,
|
|
303
|
+
webView = webView,
|
|
304
|
+
plugin = "AdMob",
|
|
305
|
+
projectionSelector = selector,
|
|
306
|
+
receiveListeners = NativeUIProjection.ReceiveListeners(
|
|
307
|
+
update = listOf("nativeAdProjectionUpdated"),
|
|
308
|
+
projection = NativeUIProjection.ReceiveListeners.Projection(
|
|
309
|
+
native = listOf("nativeAdProjectionShown"), none = listOf("nativeAdProjectionHidden")
|
|
310
|
+
),
|
|
311
|
+
match = mapOf("feedId" to feedId, "sessionId" to sessionId, "slotKey" to slotKey, "loadId" to event.optString("loadId"))
|
|
312
|
+
),
|
|
313
|
+
failedFallback = NativeUIProjection.FailedFallback.NATIVE,
|
|
314
|
+
captureMode = NativeUIProjection.CaptureMode.VIEW,
|
|
315
|
+
updatePlacement = { placement ->
|
|
316
|
+
check(states[stateKey] === state && isCurrentSession(feedId, sessionId)) { "Native ad owner changed" }
|
|
317
|
+
check(placement.optString("feedId") == feedId && placement.optString("sessionId") == sessionId &&
|
|
318
|
+
placement.optString("slotKey") == slotKey) { "Native ad placement identity changed" }
|
|
319
|
+
check(placement.optInt("generation", -1) >= state.generation &&
|
|
320
|
+
placement.optLong("sequence", -1L) > state.placementSequence && state.projectionVisible) {
|
|
321
|
+
"Native ad placement is stale or hidden"
|
|
322
|
+
}
|
|
323
|
+
if (!placement.optBoolean("visible", false)) {
|
|
324
|
+
state.placementSequence = placement.optLong("sequence")
|
|
325
|
+
state.deferredPlacement = null
|
|
326
|
+
state.clippingContainer.visibility = View.GONE
|
|
327
|
+
state.projectionVisible = false
|
|
328
|
+
state.projection?.hide()
|
|
329
|
+
error("Native ad placement is not visible")
|
|
330
|
+
}
|
|
331
|
+
check(applyPlacement(feedId, placement, defer = false) == stateKey) { "Native ad placement is invalid" }
|
|
332
|
+
state.placementSequence = placement.optLong("sequence")
|
|
333
|
+
},
|
|
334
|
+
beforeNativeReveal = {
|
|
335
|
+
val placement = state.deferredPlacement
|
|
336
|
+
state.deferredPlacement = null
|
|
337
|
+
if (states[stateKey] === state && placement != null) applyPlacement(feedId, placement, defer = false)
|
|
338
|
+
},
|
|
339
|
+
canCapture = { states[stateKey] === state && state.opaqueBackground &&
|
|
340
|
+
nativeAd.mediaContent?.hasVideoContent() != true && isSnapshotSizeSupported(adView.width, adView.height) },
|
|
341
|
+
view = { if (states[stateKey] === state) adView else null }
|
|
342
|
+
)
|
|
343
|
+
}.onFailure { android.util.Log.w("AdMob", "Native ad projection unavailable; retaining native tracking", it) }.getOrNull()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
234
346
|
nativeAd.setOnPaidEventListener { adValue ->
|
|
235
347
|
if (states[stateKey] !== state) return@setOnPaidEventListener
|
|
236
348
|
val responseInfo = nativeAd.responseInfo
|
|
@@ -287,7 +399,7 @@ class NativeAdExecutor(
|
|
|
287
399
|
}
|
|
288
400
|
}
|
|
289
401
|
|
|
290
|
-
private fun applyPlacement(expectedFeedId: String, placement: JSONObject): String? {
|
|
402
|
+
private fun applyPlacement(expectedFeedId: String, placement: JSONObject, defer: Boolean = true): String? {
|
|
291
403
|
val density = contextSupplier.get().resources.displayMetrics.density
|
|
292
404
|
val value = NativeAdPlacementValue.parse(expectedFeedId, placement, density) ?: return null
|
|
293
405
|
val state = states[stateKey(value.feedId, value.slotKey)] ?: return null
|
|
@@ -296,6 +408,15 @@ class NativeAdExecutor(
|
|
|
296
408
|
val minimumHeight = (if (state.isSmall) 120 else 300) * density
|
|
297
409
|
if (value.rectWidth < minimumWidth.roundToInt() || value.rectHeight < minimumHeight.roundToInt()) return null
|
|
298
410
|
|
|
411
|
+
if (defer && state.projection?.isProjecting == true &&
|
|
412
|
+
state.adView.width == value.rectWidth && state.adView.height == value.rectHeight) {
|
|
413
|
+
state.deferredPlacement = JSONObject(placement.toString())
|
|
414
|
+
state.generation = value.generation
|
|
415
|
+
return stateKey(value.feedId, value.slotKey)
|
|
416
|
+
}
|
|
417
|
+
state.deferredPlacement = null
|
|
418
|
+
val resized = state.adView.width != value.rectWidth || state.adView.height != value.rectHeight
|
|
419
|
+
|
|
299
420
|
if (!state.isSmall) {
|
|
300
421
|
val adView = state.adView
|
|
301
422
|
if (adView.isLayoutRequested || adView.measuredWidth != value.rectWidth || adView.measuredHeight != value.rectHeight) {
|
|
@@ -329,13 +450,138 @@ class NativeAdExecutor(
|
|
|
329
450
|
state.generation = value.generation
|
|
330
451
|
state.clippingContainer.visibility = View.VISIBLE
|
|
331
452
|
if (!wasVisible) state.clippingContainer.bringToFront()
|
|
453
|
+
if (!state.projectionVisible) {
|
|
454
|
+
state.projection?.show()
|
|
455
|
+
state.projectionVisible = true
|
|
456
|
+
state.projectionIdentity?.let { notifyListeners("nativeAdProjectionShown", it) }
|
|
457
|
+
}
|
|
458
|
+
if (resized) state.projectionIdentity?.let { notifyListeners("nativeAdProjectionUpdated", it) }
|
|
332
459
|
return stateKey(value.feedId, value.slotKey)
|
|
333
460
|
}
|
|
334
461
|
|
|
335
462
|
private fun overlayParent(): ViewGroup? = webViewSupplier.get()?.parent as? ViewGroup
|
|
336
463
|
|
|
464
|
+
// Scroll snapshot proxy. SDK views remain responsible for all ad interactions.
|
|
465
|
+
fun captureSnapshot(call: PluginCall) {
|
|
466
|
+
val feedId = requiredString(call, "feedId") ?: return
|
|
467
|
+
val sessionId = requiredString(call, "sessionId") ?: return
|
|
468
|
+
val slotKey = requiredString(call, "slotKey") ?: return
|
|
469
|
+
activitySupplier.get().runOnUiThread {
|
|
470
|
+
val state = states[stateKey(feedId, slotKey)]
|
|
471
|
+
if (!isCurrentSession(feedId, sessionId) || state == null) {
|
|
472
|
+
call.reject("Snapshots require an active ad")
|
|
473
|
+
return@runOnUiThread
|
|
474
|
+
}
|
|
475
|
+
if (!state.opaqueBackground) {
|
|
476
|
+
call.reject("Snapshot underlays require an opaque native ad background")
|
|
477
|
+
return@runOnUiThread
|
|
478
|
+
}
|
|
479
|
+
if (state.nativeAd.mediaContent?.hasVideoContent() == true) {
|
|
480
|
+
call.reject("Video ads retain native tracking while scrolling")
|
|
481
|
+
return@runOnUiThread
|
|
482
|
+
}
|
|
483
|
+
val view = state.adView
|
|
484
|
+
if (!view.isShown || view.isLayoutRequested || !isSnapshotSizeSupported(view.width, view.height)) {
|
|
485
|
+
call.reject("Native ad is not ready for a bounded snapshot")
|
|
486
|
+
return@runOnUiThread
|
|
487
|
+
}
|
|
488
|
+
// One bitmap/encoder per executor, including concurrent feeds.
|
|
489
|
+
if (!snapshotCapturePending.compareAndSet(false, true)) {
|
|
490
|
+
call.reject("Another native ad snapshot is being encoded")
|
|
491
|
+
return@runOnUiThread
|
|
492
|
+
}
|
|
493
|
+
val started = SystemClock.elapsedRealtimeNanos()
|
|
494
|
+
val drawCpuStart = Debug.threadCpuTimeNanos()
|
|
495
|
+
val bitmap = try {
|
|
496
|
+
Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
|
|
497
|
+
} catch (error: Exception) {
|
|
498
|
+
snapshotCapturePending.set(false)
|
|
499
|
+
call.reject("Could not allocate native ad snapshot", error)
|
|
500
|
+
return@runOnUiThread
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
view.draw(Canvas(bitmap))
|
|
504
|
+
val drawn = SystemClock.elapsedRealtimeNanos()
|
|
505
|
+
val drawCpuMs = (Debug.threadCpuTimeNanos() - drawCpuStart) / 1_000_000.0
|
|
506
|
+
// Compression must not block the UI thread when the next gesture begins.
|
|
507
|
+
Thread {
|
|
508
|
+
try {
|
|
509
|
+
val encodeStart = SystemClock.elapsedRealtimeNanos()
|
|
510
|
+
val encodeCpuStart = Debug.threadCpuTimeNanos()
|
|
511
|
+
val bytes = ByteArrayOutputStream()
|
|
512
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bytes)
|
|
513
|
+
val dataUrl = "data:image/png;base64," + Base64.encodeToString(bytes.toByteArray(), Base64.NO_WRAP)
|
|
514
|
+
val timings = JSObject().apply {
|
|
515
|
+
put("drawMs", (drawn - started) / 1_000_000.0)
|
|
516
|
+
put("drawCpuMs", drawCpuMs)
|
|
517
|
+
put("workerWaitMs", (encodeStart - drawn) / 1_000_000.0)
|
|
518
|
+
put("encodeMs", (SystemClock.elapsedRealtimeNanos() - encodeStart) / 1_000_000.0)
|
|
519
|
+
put("encodeCpuMs", (Debug.threadCpuTimeNanos() - encodeCpuStart) / 1_000_000.0)
|
|
520
|
+
put("bitmapBytes", bitmap.byteCount)
|
|
521
|
+
put("pngBytes", bytes.size())
|
|
522
|
+
}
|
|
523
|
+
call.resolve(JSObject().put("dataUrl", dataUrl).put("timings", timings))
|
|
524
|
+
} catch (error: Exception) {
|
|
525
|
+
call.reject("Could not encode native ad snapshot", error)
|
|
526
|
+
} finally {
|
|
527
|
+
bitmap.recycle()
|
|
528
|
+
snapshotCapturePending.set(false)
|
|
529
|
+
}
|
|
530
|
+
}.start()
|
|
531
|
+
} catch (error: Exception) {
|
|
532
|
+
bitmap.recycle()
|
|
533
|
+
snapshotCapturePending.set(false)
|
|
534
|
+
call.reject("Could not draw native ad snapshot", error)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fun prepareSnapshot(call: PluginCall) {
|
|
540
|
+
val feedId = requiredString(call, "feedId") ?: return
|
|
541
|
+
val sessionId = requiredString(call, "sessionId") ?: return
|
|
542
|
+
val slotKey = requiredString(call, "slotKey") ?: return
|
|
543
|
+
activitySupplier.get().runOnUiThread {
|
|
544
|
+
val key = stateKey(feedId, slotKey)
|
|
545
|
+
val state = states[key]
|
|
546
|
+
val webView = webViewSupplier.get() as? WebView
|
|
547
|
+
if (!isCurrentSession(feedId, sessionId) || state == null ||
|
|
548
|
+
webView == null || !webView.isAttachedToWindow || !webView.isShown) {
|
|
549
|
+
call.reject("Native ad snapshot WebView is no longer visible")
|
|
550
|
+
return@runOnUiThread
|
|
551
|
+
}
|
|
552
|
+
val completed = AtomicBoolean(false)
|
|
553
|
+
val handler = Handler(Looper.getMainLooper())
|
|
554
|
+
val timeout = Runnable {
|
|
555
|
+
if (completed.compareAndSet(false, true)) call.reject("Native ad snapshot visual state timed out")
|
|
556
|
+
}
|
|
557
|
+
handler.postDelayed(timeout, 2000)
|
|
558
|
+
try {
|
|
559
|
+
webView.postVisualStateCallback(0, object : WebView.VisualStateCallback() {
|
|
560
|
+
override fun onComplete(requestId: Long) {
|
|
561
|
+
if (!completed.compareAndSet(false, true)) return
|
|
562
|
+
handler.removeCallbacks(timeout)
|
|
563
|
+
if (!isCurrentSession(feedId, sessionId) || states[key] !== state ||
|
|
564
|
+
!webView.isAttachedToWindow || !webView.isShown) {
|
|
565
|
+
call.reject("Native ad snapshot was invalidated")
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
// Ready for the next WebView draw, not a claim that pixels have been presented.
|
|
569
|
+
webView.invalidate()
|
|
570
|
+
call.resolve()
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
} catch (error: Exception) {
|
|
574
|
+
if (completed.compareAndSet(false, true)) {
|
|
575
|
+
handler.removeCallbacks(timeout)
|
|
576
|
+
call.reject("Could not prepare native ad snapshot", error)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
337
582
|
private fun destroyState(stateKey: String) {
|
|
338
583
|
val state = states.remove(stateKey) ?: return
|
|
584
|
+
state.projection?.close()
|
|
339
585
|
(state.clippingContainer.parent as? ViewGroup)?.removeView(state.clippingContainer)
|
|
340
586
|
state.adView.destroy()
|
|
341
587
|
state.nativeAd.destroy()
|
|
@@ -374,5 +620,7 @@ class NativeAdExecutor(
|
|
|
374
620
|
companion object {
|
|
375
621
|
const val NATIVE_TESTER_ID = "ca-app-pub-3940256099942544/2247696110"
|
|
376
622
|
private const val MAX_ADS_PER_FEED = 3
|
|
623
|
+
internal fun isSnapshotSizeSupported(width: Int, height: Int): Boolean =
|
|
624
|
+
width > 0 && height > 0 && width.toLong() * height <= 2_000_000
|
|
377
625
|
}
|
|
378
626
|
}
|
|
@@ -10,5 +10,11 @@ export interface NativeAdDefinitions {
|
|
|
10
10
|
loadNativeAd(options: NativeAdLoadOptions): Promise<void>;
|
|
11
11
|
updateNativeAdPlacements(options: NativeAdPlacementBatch): Promise<void>;
|
|
12
12
|
removeNativeAd(options: NativeAdCommandIdentity): Promise<void>;
|
|
13
|
+
/** Android-only snapshot; does not change native visibility. */
|
|
14
|
+
captureNativeAdSnapshot?(options: NativeAdCommandIdentity): Promise<{
|
|
15
|
+
dataUrl: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Android-only: confirm that the appended image is ready for the next WebView draw. */
|
|
18
|
+
prepareNativeAdSnapshot?(options: NativeAdCommandIdentity): Promise<void>;
|
|
13
19
|
addListener(eventName: NativeAdPluginEvents, listenerFunc: (info: (NativeAdEvent | NativeAdErrorEvent | NativeAdRevenueEvent) & NativeAdFeedSession) => void): Promise<PluginListenerHandle>;
|
|
14
20
|
}
|
|
@@ -4,12 +4,10 @@ import type { NativeAdEvent, NativeAdErrorEvent, NativeAdRevenueEvent } from './
|
|
|
4
4
|
import type { NativeAdFeedOptions } from './native-ad-options.interface';
|
|
5
5
|
import type { NativeAdPluginEvents } from './native-ad-plugin-events.enum';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Manages native ad placement and lifecycle in a scrolling feed.
|
|
8
8
|
*
|
|
9
9
|
* A stable `slotKey`, rather than the element or array index, owns the native
|
|
10
10
|
* ad lifecycle. Virtual-scroller compatibility has not yet been validated.
|
|
11
|
-
*
|
|
12
|
-
* @experimental Native overlay gesture routing is still being validated on devices.
|
|
13
11
|
*/
|
|
14
12
|
export declare class NativeAdFeed {
|
|
15
13
|
private readonly options;
|
|
@@ -65,6 +63,8 @@ export declare class NativeAdFeed {
|
|
|
65
63
|
private removeStateAd;
|
|
66
64
|
private runOperation;
|
|
67
65
|
private destroyResources;
|
|
66
|
+
private reuseProjectedPlacement;
|
|
67
|
+
private measurePlacement;
|
|
68
68
|
private requestPlacementUpdate;
|
|
69
69
|
private flushPlacementUpdates;
|
|
70
70
|
private assertActive;
|