@capgo/capacitor-admob 8.1.21 → 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.
Files changed (44) hide show
  1. package/CapgoCapacitorAdmob.podspec +1 -0
  2. package/Package.swift +4 -2
  3. package/README.md +327 -8
  4. package/android/build.gradle +3 -2
  5. package/android/src/main/kotlin/admob/plus/capacitor/AdMobPlusPlugin.kt +121 -0
  6. package/android/src/main/kotlin/admob/plus/capacitor/ConsentHelper.kt +140 -0
  7. package/android/src/main/kotlin/admob/plus/capacitor/Generated.kt +7 -0
  8. package/android/src/main/kotlin/admob/plus/capacitor/feed/FeedAdManager.kt +512 -0
  9. package/android/src/main/kotlin/admob/plus/core/Ad.kt +2 -4
  10. package/android/src/main/kotlin/admob/plus/core/Helper.kt +18 -1
  11. package/dist/docs.json +543 -5
  12. package/dist/esm/definitions.d.ts +284 -1
  13. package/dist/esm/definitions.js +55 -0
  14. package/dist/esm/definitions.js.map +1 -1
  15. package/dist/esm/events.d.ts +127 -0
  16. package/dist/esm/events.js +50 -0
  17. package/dist/esm/events.js.map +1 -0
  18. package/dist/esm/feed-ad.d.ts +32 -0
  19. package/dist/esm/feed-ad.js +133 -0
  20. package/dist/esm/feed-ad.js.map +1 -0
  21. package/dist/esm/index.d.ts +4 -2
  22. package/dist/esm/index.js +4 -32
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/plugin-instance.d.ts +3 -0
  25. package/dist/esm/plugin-instance.js +35 -0
  26. package/dist/esm/plugin-instance.js.map +1 -0
  27. package/dist/esm/web.d.ts +11 -1
  28. package/dist/esm/web.js +40 -0
  29. package/dist/esm/web.js.map +1 -1
  30. package/dist/plugin.cjs.js +313 -31
  31. package/dist/plugin.cjs.js.map +1 -1
  32. package/dist/plugin.js +313 -31
  33. package/dist/plugin.js.map +1 -1
  34. package/ios/Sources/AdmobPlusPlugin/AdmobPlusPlugin.swift +131 -1
  35. package/ios/Sources/AdmobPlusPlugin/BannerAd.swift +6 -9
  36. package/ios/Sources/AdmobPlusPlugin/ConsentExecutor.swift +100 -0
  37. package/ios/Sources/AdmobPlusPlugin/FeedAdManager.swift +435 -0
  38. package/ios/Sources/AdmobPlusPlugin/Generated.swift +7 -0
  39. package/ios/Sources/AdmobPlusPlugin/Helper.swift +29 -0
  40. package/ios/Sources/AdmobPlusPlugin/InterstitialAd.swift +7 -13
  41. package/ios/Sources/AdmobPlusPlugin/RewardedAd.swift +13 -20
  42. package/ios/Sources/AdmobPlusPlugin/RewardedInterstitialAd.swift +13 -20
  43. package/ios/Tests/AdmobPlusPluginTests/AdmobPlusPluginTests.swift +3 -6
  44. package/package.json +2 -2
@@ -0,0 +1,140 @@
1
+ package admob.plus.capacitor
2
+
3
+ import android.app.Activity
4
+ import com.getcapacitor.JSArray
5
+ import com.getcapacitor.JSObject
6
+ import com.getcapacitor.PluginCall
7
+ import com.google.android.ump.ConsentDebugSettings
8
+ import com.google.android.ump.ConsentInformation
9
+ import com.google.android.ump.ConsentRequestParameters
10
+ import com.google.android.ump.UserMessagingPlatform
11
+
12
+ class ConsentHelper(private val plugin: AdMobPlusPlugin) {
13
+ private var consentInformation: ConsentInformation? = null
14
+
15
+ fun requestConsentInfo(call: PluginCall) {
16
+ try {
17
+ val consentInfo = ensureConsentInfo()
18
+ val activity = plugin.activity
19
+ if (activity == null) {
20
+ call.reject("Trying to request consent info but the Activity is null")
21
+ return
22
+ }
23
+
24
+ val debugSettingsBuilder = ConsentDebugSettings.Builder(plugin.context)
25
+ val data = call.data
26
+
27
+ if (data.has("testDeviceIdentifiers")) {
28
+ val devices = call.getArray("testDeviceIdentifiers") ?: JSArray()
29
+ for (i in 0 until devices.length()) {
30
+ debugSettingsBuilder.addTestDeviceHashedId(devices.getString(i))
31
+ }
32
+ }
33
+
34
+ if (data.has("debugGeography")) {
35
+ debugSettingsBuilder.setDebugGeography(data.getInt("debugGeography"))
36
+ }
37
+
38
+ val paramsBuilder = ConsentRequestParameters.Builder()
39
+ .setConsentDebugSettings(debugSettingsBuilder.build())
40
+
41
+ if (data.has("tagForUnderAgeOfConsent")) {
42
+ paramsBuilder.setTagForUnderAgeOfConsent(data.getBoolean("tagForUnderAgeOfConsent"))
43
+ }
44
+
45
+ consentInfo.requestConsentInfoUpdate(
46
+ activity,
47
+ paramsBuilder.build(),
48
+ {
49
+ call.resolve(buildConsentInfo(consentInfo))
50
+ },
51
+ { formError ->
52
+ call.reject(formError.message)
53
+ },
54
+ )
55
+ } catch (ex: Exception) {
56
+ call.reject(ex.localizedMessage, ex)
57
+ }
58
+ }
59
+
60
+ fun showPrivacyOptionsForm(call: PluginCall) {
61
+ try {
62
+ val activity = plugin.activity
63
+ if (activity == null) {
64
+ call.reject("Trying to show the privacy options form but the Activity is null")
65
+ return
66
+ }
67
+
68
+ ensureConsentInfo()
69
+ activity.runOnUiThread {
70
+ UserMessagingPlatform.showPrivacyOptionsForm(activity) { formError ->
71
+ if (formError != null) {
72
+ call.reject("Error when show privacy form", formError.message)
73
+ } else {
74
+ call.resolve()
75
+ }
76
+ }
77
+ }
78
+ } catch (ex: Exception) {
79
+ call.reject(ex.localizedMessage, ex)
80
+ }
81
+ }
82
+
83
+ fun showConsentForm(call: PluginCall) {
84
+ try {
85
+ val activity = plugin.activity
86
+ if (activity == null) {
87
+ call.reject("Trying to show the consent form but the Activity is null")
88
+ return
89
+ }
90
+
91
+ val consentInfo = ensureConsentInfo()
92
+ activity.runOnUiThread {
93
+ UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError ->
94
+ if (formError != null) {
95
+ call.reject("Error when show consent form", formError.message)
96
+ return@loadAndShowConsentFormIfRequired
97
+ }
98
+
99
+ call.resolve(buildConsentInfo(consentInfo, includeFormAvailability = false))
100
+ }
101
+ }
102
+ } catch (ex: Exception) {
103
+ call.reject(ex.localizedMessage, ex)
104
+ }
105
+ }
106
+
107
+ private fun ensureConsentInfo(): ConsentInformation {
108
+ if (consentInformation == null) {
109
+ consentInformation = UserMessagingPlatform.getConsentInformation(plugin.context)
110
+ }
111
+ return consentInformation!!
112
+ }
113
+
114
+ private fun buildConsentInfo(
115
+ consentInfo: ConsentInformation,
116
+ includeFormAvailability: Boolean = true,
117
+ ): JSObject {
118
+ val result = JSObject()
119
+ result.put("status", getConsentStatusString(consentInfo.consentStatus))
120
+ if (includeFormAvailability) {
121
+ result.put("isConsentFormAvailable", consentInfo.isConsentFormAvailable)
122
+ }
123
+ result.put("canRequestAds", consentInfo.canRequestAds())
124
+ result.put(
125
+ "privacyOptionsRequirementStatus",
126
+ consentInfo.privacyOptionsRequirementStatus.name,
127
+ )
128
+ return result
129
+ }
130
+
131
+ private fun getConsentStatusString(consentStatus: Int): String {
132
+ return when (consentStatus) {
133
+ ConsentInformation.ConsentStatus.REQUIRED -> "REQUIRED"
134
+ ConsentInformation.ConsentStatus.NOT_REQUIRED -> "NOT_REQUIRED"
135
+ ConsentInformation.ConsentStatus.OBTAINED -> "OBTAINED"
136
+ ConsentInformation.ConsentStatus.UNKNOWN -> "UNKNOWN"
137
+ else -> "UNKNOWN"
138
+ }
139
+ }
140
+ }
@@ -38,5 +38,12 @@ class Generated {
38
38
  const val REWARDED_REWARD = "rewarded.reward"
39
39
  const val REWARDED_SHOW = "rewarded.show"
40
40
  const val REWARDED_SHOW_FAIL = "rewarded.showfail"
41
+ const val FEED_CLICK = "feed.click"
42
+ const val FEED_CLOSE = "feed.close"
43
+ const val FEED_IMPRESSION = "feed.impression"
44
+ const val FEED_LOAD = "feed.load"
45
+ const val FEED_LOAD_FAIL = "feed.loadfail"
46
+ const val FEED_OPEN = "feed.open"
47
+ const val FEED_REFRESH = "feed.refresh"
41
48
  }
42
49
  }
@@ -0,0 +1,512 @@
1
+ package admob.plus.capacitor.feed
2
+
3
+ import admob.plus.capacitor.AdMobPlusPlugin
4
+ import admob.plus.capacitor.Generated
5
+ import android.content.pm.ApplicationInfo
6
+ import android.graphics.Bitmap
7
+ import android.graphics.Canvas
8
+ import android.graphics.Color
9
+ import android.util.Log
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import android.util.TypedValue
13
+ import android.view.Gravity
14
+ import android.view.View
15
+ import android.view.ViewGroup
16
+ import android.webkit.WebView
17
+ import java.io.File
18
+ import java.io.FileOutputStream
19
+ import android.widget.Button
20
+ import android.widget.FrameLayout
21
+ import android.widget.LinearLayout
22
+ import android.widget.TextView
23
+ import com.getcapacitor.JSObject
24
+ import com.google.android.libraries.ads.mobile.sdk.banner.AdSize
25
+ import com.google.android.libraries.ads.mobile.sdk.banner.AdView
26
+ import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd
27
+ import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback
28
+ import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback
29
+ import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError
30
+ import com.google.android.libraries.ads.mobile.sdk.nativead.MediaView
31
+ import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd
32
+ import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader
33
+ import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback
34
+ import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest
35
+ import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView
36
+ import kotlin.math.max
37
+
38
+ private const val MIN_AUTO_REFRESH_MS = 30_000L
39
+ private const val FEED_SCREENSHOT_LOG_TAG = "CapgoAdmobFeed"
40
+ private const val MAX_NATIVE_NO_FILL_RETRIES = 0
41
+ private const val NATIVE_NO_FILL_RETRY_DELAY_MS = 2_000L
42
+
43
+ class FeedAdManager(private val plugin: AdMobPlusPlugin) {
44
+ private val entries = mutableMapOf<Int, FeedAdEntry>()
45
+ private var nextId = 1
46
+ private val mainHandler = Handler(Looper.getMainLooper())
47
+
48
+ fun create(
49
+ format: String,
50
+ adUnitId: String,
51
+ positionKey: String?,
52
+ autoRefreshMs: Int?,
53
+ nativeStyle: Map<String, String>,
54
+ ): Int {
55
+ val id = nextId++
56
+ val entry = FeedAdEntry(
57
+ id = id,
58
+ format = if (format == "banner") FeedAdFormat.BANNER else FeedAdFormat.NATIVE,
59
+ adUnitId = adUnitId,
60
+ positionKey = positionKey,
61
+ nativeStyle = nativeStyle,
62
+ plugin = plugin,
63
+ mainHandler = mainHandler,
64
+ )
65
+ autoRefreshMs?.let { entry.setAutoRefresh(it) }
66
+ entries[id] = entry
67
+ return id
68
+ }
69
+
70
+ fun destroy(id: Int) {
71
+ entries.remove(id)?.destroy()
72
+ }
73
+
74
+ fun load(id: Int, onComplete: (String?) -> Unit) {
75
+ val entry = entries[id]
76
+ if (entry == null) {
77
+ onComplete("Feed ad not found")
78
+ return
79
+ }
80
+ entry.load(onComplete)
81
+ }
82
+
83
+ fun isLoaded(id: Int): Boolean = entries[id]?.isLoaded == true
84
+
85
+ fun updateBounds(
86
+ id: Int,
87
+ x: Double,
88
+ y: Double,
89
+ width: Double,
90
+ height: Double,
91
+ visible: Boolean,
92
+ ) {
93
+ entries[id]?.updateBounds(x, y, width, height, visible)
94
+ }
95
+
96
+ fun setAutoRefresh(id: Int, autoRefreshMs: Int?) {
97
+ entries[id]?.setAutoRefresh(autoRefreshMs)
98
+ }
99
+ }
100
+
101
+ private enum class FeedAdFormat {
102
+ NATIVE,
103
+ BANNER,
104
+ }
105
+
106
+ private class FeedAdEntry(
107
+ val id: Int,
108
+ val format: FeedAdFormat,
109
+ val adUnitId: String,
110
+ val positionKey: String?,
111
+ val nativeStyle: Map<String, String>,
112
+ private val plugin: AdMobPlusPlugin,
113
+ private val mainHandler: Handler,
114
+ ) {
115
+ private var adHost: View? = null
116
+ private var nativeAd: NativeAd? = null
117
+ private var bannerAdView: AdView? = null
118
+ private var loaded = false
119
+ private var refreshRunnable: Runnable? = null
120
+ private var loadCallback: ((String?) -> Unit)? = null
121
+ private var nativeNoFillRetries = 0
122
+ private var nativeNoFillRetryRunnable: Runnable? = null
123
+ private var overlayVisibleLogged = false
124
+ private var ciBannerSnapshotWritten = false
125
+ private val ciBannerSnapshotToken = Any()
126
+
127
+ val isLoaded: Boolean
128
+ get() = loaded
129
+
130
+ fun destroy() {
131
+ mainHandler.removeCallbacksAndMessages(ciBannerSnapshotToken)
132
+ refreshRunnable?.let { mainHandler.removeCallbacks(it) }
133
+ refreshRunnable = null
134
+ nativeNoFillRetryRunnable?.let { mainHandler.removeCallbacks(it) }
135
+ nativeNoFillRetryRunnable = null
136
+ nativeAd?.destroy()
137
+ nativeAd = null
138
+ adHost?.let { (it.parent as? ViewGroup)?.removeView(it) }
139
+ adHost = null
140
+ bannerAdView?.destroy()
141
+ bannerAdView = null
142
+ loaded = false
143
+ }
144
+
145
+ fun setAutoRefresh(autoRefreshMs: Int?) {
146
+ refreshRunnable?.let { mainHandler.removeCallbacks(it) }
147
+ refreshRunnable = null
148
+ if (autoRefreshMs == null || autoRefreshMs < MIN_AUTO_REFRESH_MS) {
149
+ return
150
+ }
151
+ val runnable = object : Runnable {
152
+ override fun run() {
153
+ emitFeed(Generated.Events.FEED_REFRESH, null)
154
+ load { }
155
+ mainHandler.postDelayed(this, autoRefreshMs.toLong())
156
+ }
157
+ }
158
+ refreshRunnable = runnable
159
+ mainHandler.postDelayed(runnable, autoRefreshMs.toLong())
160
+ }
161
+
162
+ fun load(onComplete: (String?) -> Unit) {
163
+ loadCallback = onComplete
164
+ mainHandler.post {
165
+ when (format) {
166
+ FeedAdFormat.NATIVE -> loadNative()
167
+ FeedAdFormat.BANNER -> loadBanner()
168
+ }
169
+ }
170
+ }
171
+
172
+ fun updateBounds(x: Double, y: Double, width: Double, height: Double, visible: Boolean) {
173
+ mainHandler.post {
174
+ val webView = plugin.bridge.webView
175
+ val host = adHost ?: return@post
176
+ val parent = webView.parent as? ViewGroup ?: return@post
177
+ if (host.parent == null) {
178
+ parent.addView(host)
179
+ }
180
+ val density = webView.resources.displayMetrics.density
181
+ val left = (x * density).toInt()
182
+ val top = (y * density).toInt() + webView.top
183
+ val w = max(1, (width * density).toInt())
184
+ val h = max(1, (height * density).toInt())
185
+ val params = ViewGroup.MarginLayoutParams(w, h).apply {
186
+ leftMargin = left
187
+ topMargin = top
188
+ }
189
+ host.layoutParams = params
190
+ host.visibility = if (visible && loaded) View.VISIBLE else View.INVISIBLE
191
+ host.bringToFront()
192
+ if (visible && loaded && !overlayVisibleLogged && w > 40 && h > 20) {
193
+ overlayVisibleLogged = true
194
+ val formatLabel = if (format == FeedAdFormat.BANNER) "banner" else "native"
195
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "overlay_visible id=$id format=$formatLabel")
196
+ if (format == FeedAdFormat.BANNER) {
197
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_slot_ready id=$id")
198
+ writeCiBannerOverlaySnapshotNow(host, w, h)
199
+ }
200
+ } else if (
201
+ visible &&
202
+ loaded &&
203
+ format == FeedAdFormat.BANNER &&
204
+ !ciBannerSnapshotWritten &&
205
+ w > 40 &&
206
+ h > 20
207
+ ) {
208
+ writeCiBannerOverlaySnapshotNow(host, w, h)
209
+ }
210
+ }
211
+ }
212
+
213
+ private fun scheduleCiBannerOverlaySnapshotAfterLoad() {
214
+ if (ciBannerSnapshotWritten || format != FeedAdFormat.BANNER) {
215
+ return
216
+ }
217
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_schedule id=$id")
218
+ adHost?.let { host ->
219
+ val lp = host.layoutParams
220
+ val w = host.width.takeIf { it > 20 } ?: lp?.width ?: 0
221
+ val h = host.height.takeIf { it > 10 } ?: lp?.height ?: 0
222
+ if (w > 20 && h > 10) {
223
+ mainHandler.postDelayed(
224
+ {
225
+ if (!ciBannerSnapshotWritten) {
226
+ writeCiBannerOverlaySnapshotNow(host, w, h)
227
+ }
228
+ },
229
+ ciBannerSnapshotToken,
230
+ 0L,
231
+ )
232
+ }
233
+ }
234
+ val delaysMs = longArrayOf(150L, 400L, 900L, 1_600L, 2_400L)
235
+ for (delayMs in delaysMs) {
236
+ mainHandler.postDelayed(
237
+ {
238
+ if (!ciBannerSnapshotWritten) {
239
+ adHost?.let { host ->
240
+ val lp = host.layoutParams
241
+ val w = host.width.takeIf { it > 20 } ?: lp?.width ?: 0
242
+ val h = host.height.takeIf { it > 10 } ?: lp?.height ?: 0
243
+ writeCiBannerOverlaySnapshotNow(host, w, h)
244
+ }
245
+ }
246
+ },
247
+ ciBannerSnapshotToken,
248
+ delayMs,
249
+ )
250
+ }
251
+ }
252
+
253
+ private fun writeCiBannerOverlaySnapshotNow(host: View, layoutW: Int, layoutH: Int) {
254
+ var width = if (host.width >= 20) host.width else layoutW
255
+ var height = if (host.height >= 10) host.height else layoutH
256
+ if (width < 20 || height < 10) {
257
+ Log.i(
258
+ FEED_SCREENSHOT_LOG_TAG,
259
+ "ci_banner_snapshot_failed message=host_not_laid_out w=${host.width} h=${host.height} lw=$layoutW lh=$layoutH",
260
+ )
261
+ return
262
+ }
263
+ if (host.width < 20 || host.height < 10) {
264
+ host.measure(
265
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
266
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
267
+ )
268
+ host.layout(0, 0, width, height)
269
+ }
270
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_attempt id=$id")
271
+ var bitmap: Bitmap? = null
272
+ try {
273
+ width = host.width.coerceAtLeast(width).coerceAtLeast(1)
274
+ height = host.height.coerceAtLeast(height).coerceAtLeast(1)
275
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
276
+ val canvas = Canvas(bitmap)
277
+ canvas.drawColor(Color.WHITE)
278
+ host.draw(canvas)
279
+ val file = File(plugin.context.cacheDir, "ci_feed_banner.png")
280
+ FileOutputStream(file).use { out ->
281
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
282
+ }
283
+ ciBannerSnapshotWritten = true
284
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "ci_banner_snapshot_written path=${file.absolutePath}")
285
+ } catch (e: Exception) {
286
+ Log.i(
287
+ FEED_SCREENSHOT_LOG_TAG,
288
+ "ci_banner_snapshot_failed message=${e.message}",
289
+ )
290
+ } finally {
291
+ bitmap?.recycle()
292
+ }
293
+ }
294
+
295
+ private fun finishLoad(errorMessage: String?) {
296
+ val cb = loadCallback
297
+ loadCallback = null
298
+ cb?.invoke(errorMessage)
299
+ }
300
+
301
+ private fun loadNative() {
302
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load_start id=$id format=native")
303
+ val request = NativeAdRequest.Builder(adUnitId, listOf(NativeAd.NativeAdType.NATIVE)).build()
304
+ NativeAdLoader.load(
305
+ request,
306
+ object : NativeAdLoaderCallback {
307
+ override fun onNativeAdLoaded(ad: NativeAd) {
308
+ mainHandler.post {
309
+ displayNativeAd(ad)
310
+ }
311
+ }
312
+
313
+ override fun onAdFailedToLoad(adError: LoadAdError) {
314
+ Log.i(
315
+ FEED_SCREENSHOT_LOG_TAG,
316
+ "feed_load_fail id=$id format=native code=${adError.code.value} message=${adError.message}",
317
+ )
318
+ if (shouldRetryNativeNoFill(adError)) {
319
+ nativeNoFillRetries += 1
320
+ nativeNoFillRetryRunnable?.let { mainHandler.removeCallbacks(it) }
321
+ val retry = Runnable { loadNative() }
322
+ nativeNoFillRetryRunnable = retry
323
+ mainHandler.postDelayed(retry, NATIVE_NO_FILL_RETRY_DELAY_MS)
324
+ return
325
+ }
326
+ emitFeedFail(adError)
327
+ loaded = false
328
+ finishLoad(adError.message)
329
+ }
330
+ },
331
+ )
332
+ }
333
+
334
+ private fun displayNativeAd(ad: NativeAd) {
335
+ val oldHost = adHost
336
+ val oldParent = oldHost?.parent as? ViewGroup
337
+ oldParent?.removeView(oldHost)
338
+ nativeAd?.destroy()
339
+ nativeAd = ad
340
+ val nativeAdView = buildNativeAdView(ad)
341
+ adHost = nativeAdView
342
+ oldHost?.layoutParams?.let { nativeAdView.layoutParams = it }
343
+ oldParent?.addView(nativeAdView)
344
+ loaded = true
345
+ nativeAdView.visibility = View.VISIBLE
346
+
347
+ val assets = JSObject()
348
+ assets.put("headline", ad.headline ?: "")
349
+ assets.put("body", ad.body ?: "")
350
+ assets.put("callToAction", ad.callToAction ?: "")
351
+ assets.put("advertiser", ad.advertiser ?: "")
352
+ assets.put("price", ad.price ?: "")
353
+ assets.put("store", ad.store ?: "")
354
+ ad.starRating?.let { assets.put("starRating", it) }
355
+ ad.mediaContent?.aspectRatio?.let { assets.put("mediaAspectRatio", it) }
356
+ nativeNoFillRetries = 0
357
+ emitFeed(Generated.Events.FEED_LOAD, assets)
358
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load id=$id format=native")
359
+ finishLoad(null)
360
+ }
361
+
362
+ private fun shouldRetryNativeNoFill(adError: LoadAdError): Boolean {
363
+ val isDebuggable =
364
+ (plugin.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
365
+ return isDebuggable &&
366
+ adError.message.equals("No fill.", ignoreCase = true) &&
367
+ nativeNoFillRetries < MAX_NATIVE_NO_FILL_RETRIES
368
+ }
369
+
370
+ private fun buildNativeAdView(ad: NativeAd): NativeAdView {
371
+ val context = plugin.context
372
+ val nativeAdView = NativeAdView(context)
373
+ nativeAdView.setBackgroundColor(parseColor(nativeStyle["backgroundColor"]) ?: Color.parseColor("#F2F2F2"))
374
+
375
+ val column = LinearLayout(context)
376
+ column.orientation = LinearLayout.VERTICAL
377
+ column.setPadding(24, 24, 24, 24)
378
+
379
+ val attribution = TextView(context)
380
+ attribution.text = "Ad"
381
+ attribution.gravity = Gravity.END
382
+ attribution.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
383
+ column.addView(attribution)
384
+
385
+ val mediaView = MediaView(context)
386
+ mediaView.layoutParams = LinearLayout.LayoutParams(
387
+ ViewGroup.LayoutParams.MATCH_PARENT,
388
+ (180 * context.resources.displayMetrics.density).toInt(),
389
+ )
390
+ column.addView(mediaView)
391
+
392
+ val headline = TextView(context)
393
+ headline.text = ad.headline
394
+ headline.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
395
+ headline.setTextColor(parseColor(nativeStyle["headlineTextColor"]) ?: Color.BLACK)
396
+ nativeAdView.headlineView = headline
397
+ column.addView(headline)
398
+
399
+ if (!ad.body.isNullOrBlank()) {
400
+ val body = TextView(context)
401
+ body.text = ad.body
402
+ body.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
403
+ body.setTextColor(parseColor(nativeStyle["bodyTextColor"]) ?: Color.DKGRAY)
404
+ nativeAdView.bodyView = body
405
+ column.addView(body)
406
+ }
407
+
408
+ val cta = Button(context)
409
+ cta.text = ad.callToAction
410
+ cta.isClickable = false
411
+ cta.isEnabled = false
412
+ cta.setBackgroundColor(parseColor(nativeStyle["ctaBackgroundColor"]) ?: Color.parseColor("#2563EB"))
413
+ cta.setTextColor(parseColor(nativeStyle["ctaTextColor"]) ?: Color.WHITE)
414
+ nativeAdView.callToActionView = cta
415
+ column.addView(cta)
416
+
417
+ nativeAdView.addView(
418
+ column,
419
+ ViewGroup.LayoutParams(
420
+ ViewGroup.LayoutParams.MATCH_PARENT,
421
+ ViewGroup.LayoutParams.WRAP_CONTENT,
422
+ ),
423
+ )
424
+ nativeAdView.registerNativeAd(ad, mediaView)
425
+ return nativeAdView
426
+ }
427
+
428
+ private fun loadBanner() {
429
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load_start id=$id format=banner")
430
+ val activity = plugin.activity
431
+ val webView = plugin.bridge.webView
432
+ if (bannerAdView == null) {
433
+ bannerAdView = AdView(activity)
434
+ adHost = bannerAdView
435
+ }
436
+ loaded = false
437
+ val adWidth = (webView.width / webView.resources.displayMetrics.density).toInt().coerceAtLeast(320)
438
+ val adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(activity, adWidth)
439
+ bannerAdView!!.loadAd(
440
+ com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest.Builder(adUnitId, adSize).build(),
441
+ object : AdLoadCallback<BannerAd> {
442
+ override fun onAdLoaded(bannerAd: BannerAd) {
443
+ loaded = true
444
+ bannerAd.adEventCallback = object : BannerAdEventCallback {
445
+ override fun onAdClicked() {
446
+ emitFeed(Generated.Events.FEED_CLICK, null)
447
+ }
448
+
449
+ override fun onAdDismissedFullScreenContent() {
450
+ emitFeed(Generated.Events.FEED_CLOSE, null)
451
+ }
452
+
453
+ override fun onAdImpression() {
454
+ emitFeed(Generated.Events.FEED_IMPRESSION, null)
455
+ }
456
+
457
+ override fun onAdShowedFullScreenContent() {
458
+ emitFeed(Generated.Events.FEED_OPEN, null)
459
+ }
460
+ }
461
+ val assets = JSObject()
462
+ assets.put("width", adSize.width)
463
+ assets.put("height", adSize.height)
464
+ emitFeed(Generated.Events.FEED_LOAD, assets)
465
+ Log.i(FEED_SCREENSHOT_LOG_TAG, "feed_load id=$id format=banner")
466
+ scheduleCiBannerOverlaySnapshotAfterLoad()
467
+ finishLoad(null)
468
+ }
469
+
470
+ override fun onAdFailedToLoad(loadAdError: LoadAdError) {
471
+ Log.i(
472
+ FEED_SCREENSHOT_LOG_TAG,
473
+ "feed_load_fail id=$id format=banner code=${loadAdError.code.value} message=${loadAdError.message}",
474
+ )
475
+ emitFeedFail(loadAdError)
476
+ loaded = false
477
+ finishLoad(loadAdError.message)
478
+ }
479
+ },
480
+ )
481
+ }
482
+
483
+ private fun emitFeed(event: String, assets: JSObject?) {
484
+ val payload = JSObject()
485
+ payload.put("feedAdId", id)
486
+ positionKey?.let { payload.put("positionKey", it) }
487
+ payload.put("format", if (format == FeedAdFormat.BANNER) "banner" else "native")
488
+ assets?.let { payload.put("assets", it) }
489
+ plugin.emit(event, payload)
490
+ }
491
+
492
+ private fun emitFeedFail(error: LoadAdError) {
493
+ val payload = JSObject()
494
+ payload.put("feedAdId", id)
495
+ positionKey?.let { payload.put("positionKey", it) }
496
+ payload.put("format", if (format == FeedAdFormat.BANNER) "banner" else "native")
497
+ payload.put("code", error.code.value)
498
+ payload.put("message", error.message)
499
+ plugin.emit(Generated.Events.FEED_LOAD_FAIL, payload)
500
+ }
501
+
502
+ private fun parseColor(hex: String?): Int? {
503
+ if (hex.isNullOrBlank() || !hex.startsWith("#") || hex.length != 7) {
504
+ return null
505
+ }
506
+ return try {
507
+ Color.parseColor(hex)
508
+ } catch (_: IllegalArgumentException) {
509
+ null
510
+ }
511
+ }
512
+ }
@@ -6,16 +6,14 @@ import android.view.ViewGroup
6
6
  import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError
7
7
  import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError
8
8
  import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardItem
9
- import java.util.Objects
10
-
11
9
  abstract class Ad(val id: Int, val adUnitId: String) {
12
10
  init {
13
11
  Helper.ads.put(id, this)
14
12
  }
15
13
 
16
14
  constructor(ctx: Context) : this(
17
- Objects.requireNonNull<Int?>(ctx.optId()),
18
- Objects.requireNonNull<String?>(ctx.optAdUnitID())
15
+ requireNotNull(ctx.optId()),
16
+ requireNotNull(ctx.optAdUnitID()),
19
17
  )
20
18
 
21
19
  open fun destroy() {