cloudx-react-native 3.10.0 → 3.10.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/cloudxreactnative/CloudXAdEvents.kt +10 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +40 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +718 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +119 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXPackage.kt +4 -1
- package/android/src/main/res/layout/cloudx_rn_native_media_banner_template.xml +100 -0
- package/android/src/main/res/layout/cloudx_rn_native_medium_template.xml +99 -0
- package/android/src/main/res/layout/cloudx_rn_native_small_template.xml +72 -0
- package/android/src/main/res/layout/cloudx_rn_native_vertical_media_template.xml +117 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +86 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +118 -0
- package/ios/CloudX.h +8 -0
- package/ios/CloudX.mm +159 -29
- package/ios/CloudXNativeAdView.h +75 -0
- package/ios/CloudXNativeAdView.mm +979 -0
- package/ios/CloudXNativeAdViewManager.h +12 -0
- package/ios/CloudXNativeAdViewManager.mm +119 -0
- package/lib/commonjs/CloudXAdViewBase.js +8 -97
- package/lib/commonjs/CloudXAdViewBase.js.map +1 -1
- package/lib/commonjs/CloudXNativeAdView.js +331 -0
- package/lib/commonjs/CloudXNativeAdView.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js +252 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js +117 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/commonjs/extraParameters.js +52 -0
- package/lib/commonjs/extraParameters.js.map +1 -0
- package/lib/commonjs/hooks.js +34 -0
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +72 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +41 -0
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/commonjs/types/NativeAd.js +70 -0
- package/lib/commonjs/types/NativeAd.js.map +1 -0
- package/lib/commonjs/types/index.js +13 -0
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/commonjs/useCloudXSdkInitialization.js +75 -0
- package/lib/commonjs/useCloudXSdkInitialization.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/module/CloudXAdViewBase.js +7 -96
- package/lib/module/CloudXAdViewBase.js.map +1 -1
- package/lib/module/CloudXNativeAdView.js +324 -0
- package/lib/module/CloudXNativeAdView.js.map +1 -0
- package/lib/module/CloudXNativeAdViewComponents.js +240 -0
- package/lib/module/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/module/CloudXNativeAdViewProvider.js +110 -0
- package/lib/module/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/module/extraParameters.js +45 -0
- package/lib/module/extraParameters.js.map +1 -0
- package/lib/module/hooks.js +34 -0
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +35 -0
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/module/types/NativeAd.js +68 -0
- package/lib/module/types/NativeAd.js.map +1 -0
- package/lib/module/types/index.js +1 -0
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/useCloudXSdkInitialization.js +68 -0
- package/lib/module/useCloudXSdkInitialization.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/CloudXAdViewBase.d.ts.map +1 -1
- package/lib/typescript/src/CloudXNativeAdView.d.ts +24 -0
- package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts +67 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts +72 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts.map +1 -0
- package/lib/typescript/src/extraParameters.d.ts +4 -0
- package/lib/typescript/src/extraParameters.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +103 -0
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types/AdInfo.d.ts +4 -1
- package/lib/typescript/src/types/AdInfo.d.ts.map +1 -1
- package/lib/typescript/src/types/NativeAd.d.ts +114 -0
- package/lib/typescript/src/types/NativeAd.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +2 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts +16 -0
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts.map +1 -0
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/CloudXAdViewBase.tsx +8 -125
- package/src/CloudXNativeAdView.tsx +419 -0
- package/src/CloudXNativeAdViewComponents.tsx +203 -0
- package/src/CloudXNativeAdViewProvider.tsx +149 -0
- package/src/extraParameters.ts +65 -0
- package/src/hooks.ts +38 -0
- package/src/index.ts +24 -0
- package/src/specs/CloudXNativeAdViewNativeComponent.ts +137 -0
- package/src/types/AdInfo.ts +5 -1
- package/src/types/NativeAd.ts +125 -0
- package/src/types/index.ts +7 -0
- package/src/useCloudXSdkInitialization.ts +73 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
package com.cloudxreactnative
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.BitmapDrawable
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.View.MeasureSpec
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.ImageView
|
|
9
|
+
import com.facebook.react.bridge.Arguments
|
|
10
|
+
import com.facebook.react.bridge.WritableMap
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
13
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
14
|
+
import io.cloudx.sdk.CloudX
|
|
15
|
+
import io.cloudx.sdk.CloudXAd
|
|
16
|
+
import io.cloudx.sdk.CloudXAdRevenueListener
|
|
17
|
+
import io.cloudx.sdk.CloudXError
|
|
18
|
+
import io.cloudx.sdk.CloudXErrorCode
|
|
19
|
+
import io.cloudx.sdk.CloudXNativeAd
|
|
20
|
+
import io.cloudx.sdk.CloudXNativeAdListener
|
|
21
|
+
import io.cloudx.sdk.CloudXNativeAdLoader
|
|
22
|
+
import io.cloudx.sdk.CloudXNativeAdView as SdkNativeAdView
|
|
23
|
+
import io.cloudx.sdk.CloudXNativeAdViewBinder
|
|
24
|
+
import io.cloudx.sdk.toCloudXError
|
|
25
|
+
import org.json.JSONObject
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* In-tree native ad view component. Supports two render modes:
|
|
29
|
+
*
|
|
30
|
+
* - Template (default): the bundled SDK templates (small / medium / media-banner
|
|
31
|
+
* / vertical-media) are inflated and populated by the SDK.
|
|
32
|
+
* - Manual: when the JS layer binds asset-view refs via `updateAssetView` and
|
|
33
|
+
* calls `renderNativeAd`, this view populates the publisher-supplied views
|
|
34
|
+
* from `ad.nativeAd.*` getters and calls
|
|
35
|
+
* `ad.nativeAd.prepareForInteraction(clickableViews, this)` so impression
|
|
36
|
+
* and click tracking are wired up against the publisher's own layout.
|
|
37
|
+
*
|
|
38
|
+
* Extends [ReactViewGroup] (not [FrameLayout], the base used by the in-tree
|
|
39
|
+
* banner/MREC [CloudXAdView]) because, unlike a banner, this component can
|
|
40
|
+
* host real React-declared children in manual mode: `ReactViewGroup` makes
|
|
41
|
+
* `onLayout`/`requestLayout` no-ops so React Native's `NativeViewHierarchyManager`
|
|
42
|
+
* keeps authority over their Yoga-computed frames. Extending a plain `ViewGroup`
|
|
43
|
+
* here would let Android's own layout pass resize every RN-managed asset view
|
|
44
|
+
* (title, icon, media, CTA) to `MATCH_PARENT` on the next re-layout, stacking
|
|
45
|
+
* them at identical bounds and burying the CTA.
|
|
46
|
+
*
|
|
47
|
+
* A dedicated [CloudXNativeAdLoader] is created per view instance rather than
|
|
48
|
+
* shared per ad unit. Sharing a loader across concurrent mounts for the same ad
|
|
49
|
+
* unit produces two defects: (1) delegate reassignment — each new view's
|
|
50
|
+
* `loader.nativeAdListener = this` overwrites the previous view's registration,
|
|
51
|
+
* so callbacks are delivered to whichever view attached most recently; (2)
|
|
52
|
+
* duplicate-load rejection — the SDK serializes `loadAd` on a single loader, so
|
|
53
|
+
* concurrent mounts beyond the first get an immediate "load already in
|
|
54
|
+
* progress" failure and stay stuck.
|
|
55
|
+
*/
|
|
56
|
+
class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext), CloudXNativeAdListener, CloudXAdRevenueListener {
|
|
57
|
+
|
|
58
|
+
private companion object {
|
|
59
|
+
const val TAG = "CloudXNativeAdView"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Delay before the second sizing pass on an inserted media view. Some
|
|
63
|
+
* networks attach their media surface asynchronously after the binder
|
|
64
|
+
* returns, so the next-frame pass can still measure an empty view.
|
|
65
|
+
*/
|
|
66
|
+
const val MEDIA_VIEW_RESIZE_RETRY_MS = 500L
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var adUnitId: String? = null
|
|
70
|
+
set(value) {
|
|
71
|
+
if (field == value) return
|
|
72
|
+
field = value
|
|
73
|
+
// The @ReactProp setters for `adUnitId` and `manualRender` fire in
|
|
74
|
+
// declaration order, so eagerly loading from this setter would run
|
|
75
|
+
// before `manualRender` has been applied — locking the view into
|
|
76
|
+
// the template load path even when the publisher mounted asset-view
|
|
77
|
+
// children. Defer to `onAttachedToWindow`, which RN guarantees to
|
|
78
|
+
// fire after all props are set.
|
|
79
|
+
//
|
|
80
|
+
// A change AFTER a loader exists is a live re-target: the old
|
|
81
|
+
// loader belongs to the old ad unit and must not keep serving
|
|
82
|
+
// callbacks for this view.
|
|
83
|
+
if (attachedLoader != null) {
|
|
84
|
+
releaseLoaderState()
|
|
85
|
+
// The same ordering trap described above applies to the
|
|
86
|
+
// re-target: `placement`, `customData`, the video flags and the
|
|
87
|
+
// extra parameters are applied AFTER `adUnitId`, so loading
|
|
88
|
+
// synchronously here configured the new loader from the
|
|
89
|
+
// previous unit's values. Post it instead, so the load runs
|
|
90
|
+
// once the whole prop update has been applied.
|
|
91
|
+
val target = value
|
|
92
|
+
if (isAttachedToWindow) {
|
|
93
|
+
post { if (adUnitId == target && isAttachedToWindow) loadIfPossible() }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var templateName: String? = null
|
|
99
|
+
|
|
100
|
+
var placement: String? = null
|
|
101
|
+
set(value) {
|
|
102
|
+
field = value
|
|
103
|
+
currentLoader()?.setPlacement(value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var customData: String? = null
|
|
107
|
+
set(value) {
|
|
108
|
+
field = value
|
|
109
|
+
currentLoader()?.setCustomData(value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var disableVideoFullScreen: Boolean = false
|
|
113
|
+
set(value) {
|
|
114
|
+
field = value
|
|
115
|
+
currentLoader()?.isVideoFullScreenDisabled = value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var startVideoUnmuted: Boolean = false
|
|
119
|
+
set(value) {
|
|
120
|
+
field = value
|
|
121
|
+
currentLoader()?.isStartVideoUnmuted = value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var hideVideoMediaControls: Boolean = false
|
|
125
|
+
set(value) {
|
|
126
|
+
field = value
|
|
127
|
+
currentLoader()?.isVideoMediaControlsHidden = value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Controls how the loaded ad is rendered. When true, this view skips the
|
|
132
|
+
* template binder and lets the JS layer drive layout through
|
|
133
|
+
* `updateAssetView` + `renderNativeAd`. Auto-load itself runs in both
|
|
134
|
+
* render modes.
|
|
135
|
+
*/
|
|
136
|
+
var manualRender: Boolean = false
|
|
137
|
+
|
|
138
|
+
var extraParameters: Map<String, Any?> = emptyMap()
|
|
139
|
+
set(value) {
|
|
140
|
+
field = value
|
|
141
|
+
applyExtraParametersIfReady()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var localExtraParameters: Map<String, Any?> = emptyMap()
|
|
145
|
+
set(value) {
|
|
146
|
+
field = value
|
|
147
|
+
applyExtraParametersIfReady()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private var appliedExtraParameterKeys: Set<String> = emptySet()
|
|
151
|
+
private var appliedLocalExtraParameterKeys: Set<String> = emptySet()
|
|
152
|
+
|
|
153
|
+
private var attachedLoader: CloudXNativeAdLoader? = null
|
|
154
|
+
private var hostedAdView: SdkNativeAdView? = null
|
|
155
|
+
private var loadRequested: Boolean = false
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Ad delivered by the current load cycle, if any. Used to distinguish
|
|
159
|
+
* post-load failures (render/adapter errors after a successful load,
|
|
160
|
+
* surfaced to JS as `onAdDisplayFailedEvent`) from genuine load failures
|
|
161
|
+
* (`onAdLoadFailedEvent`).
|
|
162
|
+
*/
|
|
163
|
+
private var loadedCycleAd: CloudXAd? = null
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Most recently loaded ad in manual mode. Held so `renderNativeAd` can
|
|
167
|
+
* register click+impression tracking once the JS layer finishes binding
|
|
168
|
+
* asset views.
|
|
169
|
+
*/
|
|
170
|
+
private var pendingManualAd: CloudXAd? = null
|
|
171
|
+
|
|
172
|
+
/** Publisher-supplied clickable subviews, collected by `updateAssetView`. */
|
|
173
|
+
private val clickableViews: MutableList<View> = mutableListOf()
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Maps asset role name to the publisher-supplied view. Used so a re-bind
|
|
177
|
+
* of the same role replaces the prior entry without leaving stale
|
|
178
|
+
* references in `clickableViews`.
|
|
179
|
+
*/
|
|
180
|
+
private val assetViewsByRole: MutableMap<String, View> = mutableMapOf()
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* SDK-owned media/options views inserted into the publisher's asset
|
|
184
|
+
* containers during manual binding. Tracked so a reload or a destroy can
|
|
185
|
+
* pull them back out; without this, successive load cycles stack multiple
|
|
186
|
+
* SDK views inside the same container. Mirrors iOS's `insertedSDKSubviews`.
|
|
187
|
+
*/
|
|
188
|
+
private val insertedSdkViews: MutableList<View> = mutableListOf()
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* React-declared children only. The SDK ad view is attached as a physical
|
|
192
|
+
* `ViewGroup` child but deliberately kept out of this list so React's
|
|
193
|
+
* `manageChildren` reconciliation never touches it. Mirrors
|
|
194
|
+
* [CloudXNativeAdViewManagerImpl]'s shadowed child accessors.
|
|
195
|
+
*/
|
|
196
|
+
private val reactChildren: MutableList<View> = mutableListOf()
|
|
197
|
+
|
|
198
|
+
fun reactChildCount(): Int = reactChildren.size
|
|
199
|
+
|
|
200
|
+
fun reactChildAt(index: Int): View? = reactChildren.getOrNull(index)
|
|
201
|
+
|
|
202
|
+
fun addReactChild(child: View, index: Int) {
|
|
203
|
+
val clampedIndex = index.coerceIn(0, reactChildren.size)
|
|
204
|
+
reactChildren.add(clampedIndex, child)
|
|
205
|
+
super.addView(child, clampedIndex)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fun removeReactChildAt(index: Int) {
|
|
209
|
+
if (index !in reactChildren.indices) return
|
|
210
|
+
val child = reactChildren.removeAt(index)
|
|
211
|
+
super.removeView(child)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fun removeAllReactChildren() {
|
|
215
|
+
for (child in reactChildren) {
|
|
216
|
+
super.removeView(child)
|
|
217
|
+
}
|
|
218
|
+
reactChildren.clear()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A dedicated loader per view. Sharing a loader across views for the same
|
|
223
|
+
* ad unit causes delegate reassignment and duplicate-load rejection (see
|
|
224
|
+
* the class doc). Reads the view's current field values so a loader
|
|
225
|
+
* created lazily by any setter (or by `onAttachedToWindow`) always starts
|
|
226
|
+
* fully configured, regardless of which prop landed first.
|
|
227
|
+
*/
|
|
228
|
+
private fun currentLoader(): CloudXNativeAdLoader? {
|
|
229
|
+
attachedLoader?.let { return it }
|
|
230
|
+
val id = adUnitId ?: return null
|
|
231
|
+
val loader = CloudX.createNativeAdLoader(reactContext, id)
|
|
232
|
+
attachedLoader = loader
|
|
233
|
+
loader.nativeAdListener = this
|
|
234
|
+
loader.revenueListener = this
|
|
235
|
+
loader.setPlacement(placement)
|
|
236
|
+
loader.setCustomData(customData)
|
|
237
|
+
loader.isVideoFullScreenDisabled = disableVideoFullScreen
|
|
238
|
+
loader.isStartVideoUnmuted = startVideoUnmuted
|
|
239
|
+
loader.isVideoMediaControlsHidden = hideVideoMediaControls
|
|
240
|
+
appliedExtraParameterKeys = emptySet()
|
|
241
|
+
appliedLocalExtraParameterKeys = emptySet()
|
|
242
|
+
applyExtraParametersIfReady()
|
|
243
|
+
return loader
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private fun applyExtraParametersIfReady() {
|
|
247
|
+
val loader = attachedLoader ?: return
|
|
248
|
+
appliedExtraParameterKeys = applyEntries(loader::setExtraParameter, extraParameters, appliedExtraParameterKeys)
|
|
249
|
+
appliedLocalExtraParameterKeys = applyEntries(loader::setLocalExtraParameter, localExtraParameters, appliedLocalExtraParameterKeys)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun applyEntries(setter: (String, Any?) -> Unit, entries: Map<String, Any?>, previousKeys: Set<String>): Set<String> {
|
|
253
|
+
for ((key, value) in entries) setter(key, value)
|
|
254
|
+
for (key in previousKeys - entries.keys) setter(key, null)
|
|
255
|
+
return entries.keys
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun buildBinder(): CloudXNativeAdViewBinder {
|
|
259
|
+
// Every bundled layout uses the same resource IDs for each slot so we
|
|
260
|
+
// only need to swap the layout resource based on `templateName`.
|
|
261
|
+
// Unknown values fall back to the medium layout.
|
|
262
|
+
val layoutRes = when (templateName) {
|
|
263
|
+
"small_template" -> R.layout.cloudx_rn_native_small_template
|
|
264
|
+
"media_banner_template" -> R.layout.cloudx_rn_native_media_banner_template
|
|
265
|
+
"vertical_media_template" -> R.layout.cloudx_rn_native_vertical_media_template
|
|
266
|
+
else -> R.layout.cloudx_rn_native_medium_template
|
|
267
|
+
}
|
|
268
|
+
return CloudXNativeAdViewBinder.Builder(layoutRes)
|
|
269
|
+
.setTitleTextViewId(R.id.cloudx_rn_native_title)
|
|
270
|
+
.setBodyTextViewId(R.id.cloudx_rn_native_body)
|
|
271
|
+
.setCallToActionButtonId(R.id.cloudx_rn_native_cta)
|
|
272
|
+
.setIconImageViewId(R.id.cloudx_rn_native_icon)
|
|
273
|
+
.setMediaContentViewGroupId(R.id.cloudx_rn_native_media)
|
|
274
|
+
.setOptionsContentViewGroupId(R.id.cloudx_rn_native_options)
|
|
275
|
+
.setAdvertiserTextViewId(R.id.cloudx_rn_native_advertiser)
|
|
276
|
+
.setStarRatingContentViewGroupId(R.id.cloudx_rn_native_rating)
|
|
277
|
+
.build()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private fun loadIfPossible() {
|
|
281
|
+
if (loadRequested) return
|
|
282
|
+
val loader = currentLoader() ?: return
|
|
283
|
+
loadRequested = true
|
|
284
|
+
loadedCycleAd = null
|
|
285
|
+
|
|
286
|
+
if (manualRender) {
|
|
287
|
+
// Flow C (late binding): no pre-built binder view. The publisher
|
|
288
|
+
// children drive layout; `onNativeAdLoaded` will stash the ad and
|
|
289
|
+
// the subsequent `updateAssetView` + `renderNativeAd` commands
|
|
290
|
+
// wire up content and click tracking.
|
|
291
|
+
pendingManualAd = null
|
|
292
|
+
clickableViews.clear()
|
|
293
|
+
assetViewsByRole.clear()
|
|
294
|
+
loader.loadAd()
|
|
295
|
+
} else {
|
|
296
|
+
// Flow B (pre-built binder): the SDK populates the bundled
|
|
297
|
+
// template view. Attach before loading so the SDK's impression
|
|
298
|
+
// observer sees a window-backed view when it binds the creative;
|
|
299
|
+
// attaching only from `onNativeAdLoaded` can miss the first
|
|
300
|
+
// impression and its revenue callback.
|
|
301
|
+
val adView = SdkNativeAdView(context, buildBinder())
|
|
302
|
+
attachAdView(adView)
|
|
303
|
+
loader.loadAd(adView)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
override fun onAttachedToWindow() {
|
|
308
|
+
super.onAttachedToWindow()
|
|
309
|
+
loadIfPossible()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fun teardown() {
|
|
313
|
+
releaseLoaderState()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Destroys the owned loader and resets all per-loader state. The loader
|
|
318
|
+
* is owned by this view: destroying it releases the SDK-side ad pipeline
|
|
319
|
+
* so nothing calls back into this view afterwards. Used both when React
|
|
320
|
+
* drops the view instance and when `adUnitId` is re-targeted live.
|
|
321
|
+
*/
|
|
322
|
+
private fun releaseLoaderState() {
|
|
323
|
+
attachedLoader?.let {
|
|
324
|
+
it.nativeAdListener = null
|
|
325
|
+
it.revenueListener = null
|
|
326
|
+
it.destroy()
|
|
327
|
+
}
|
|
328
|
+
attachedLoader = null
|
|
329
|
+
removeInsertedSdkViews()
|
|
330
|
+
hostedAdView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
331
|
+
hostedAdView = null
|
|
332
|
+
pendingManualAd = null
|
|
333
|
+
loadedCycleAd = null
|
|
334
|
+
loadRequested = false
|
|
335
|
+
clickableViews.clear()
|
|
336
|
+
assetViewsByRole.clear()
|
|
337
|
+
appliedExtraParameterKeys = emptySet()
|
|
338
|
+
appliedLocalExtraParameterKeys = emptySet()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Pulls SDK-owned views back out of the publisher's containers. The
|
|
343
|
+
* containers themselves are React children and are left mounted.
|
|
344
|
+
*/
|
|
345
|
+
private fun removeInsertedSdkViews() {
|
|
346
|
+
for (sdkView in insertedSdkViews) {
|
|
347
|
+
(sdkView.parent as? ViewGroup)?.removeView(sdkView)
|
|
348
|
+
}
|
|
349
|
+
insertedSdkViews.clear()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private fun attachAdView(adView: SdkNativeAdView) {
|
|
353
|
+
// If a previous SDK ad view was attached for this instance, detach it
|
|
354
|
+
// first. React-managed children (manual mode's asset views) are never
|
|
355
|
+
// touched here — they belong to the JS tree and must survive reloads.
|
|
356
|
+
hostedAdView?.takeIf { it !== adView }?.let { previous ->
|
|
357
|
+
(previous.parent as? ViewGroup)?.removeView(previous)
|
|
358
|
+
}
|
|
359
|
+
hostedAdView = adView
|
|
360
|
+
if (adView.parent !== this) {
|
|
361
|
+
(adView.parent as? ViewGroup)?.removeView(adView)
|
|
362
|
+
// `super.addView` routes around the React-child shadow list so the
|
|
363
|
+
// SDK ad view is invisible to React's `manageChildren`.
|
|
364
|
+
super.addView(adView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
365
|
+
}
|
|
366
|
+
layoutAdViewToBounds()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Explicitly sizes the SDK ad view to fill this view. React Native drives
|
|
371
|
+
* layout through Yoga and does not measure/lay out children it did not
|
|
372
|
+
* declare, so the SDK ad view — added as a physical child but hidden from
|
|
373
|
+
* React's shadow tree — would otherwise stay at 0x0 regardless of its
|
|
374
|
+
* MATCH_PARENT layout params.
|
|
375
|
+
*/
|
|
376
|
+
private fun layoutAdViewToBounds() {
|
|
377
|
+
val ad = hostedAdView ?: return
|
|
378
|
+
val w = width
|
|
379
|
+
val h = height
|
|
380
|
+
if (w <= 0 || h <= 0) return
|
|
381
|
+
ad.measure(
|
|
382
|
+
MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
|
|
383
|
+
MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY),
|
|
384
|
+
)
|
|
385
|
+
ad.layout(0, 0, w, h)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
389
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
390
|
+
layoutAdViewToBounds()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Sizes one SDK-inserted asset view to its React-managed container.
|
|
395
|
+
*
|
|
396
|
+
* [layoutAdViewToBounds] covers the hosted ad view in template mode. Manual
|
|
397
|
+
* mode instead inserts the SDK's own options and media views underneath the
|
|
398
|
+
* publisher's React containers, and those containers never measure children
|
|
399
|
+
* they did not declare — their `requestLayout`/`onLayout` are no-ops. An
|
|
400
|
+
* inserted view therefore stays 0x0 and never draws.
|
|
401
|
+
*/
|
|
402
|
+
private fun sizeInsertedViewToParent(child: View, parent: ViewGroup) {
|
|
403
|
+
val w = parent.width
|
|
404
|
+
val h = parent.height
|
|
405
|
+
if (w <= 0 || h <= 0) return
|
|
406
|
+
child.measure(
|
|
407
|
+
MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
|
|
408
|
+
MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY),
|
|
409
|
+
)
|
|
410
|
+
child.layout(0, 0, w, h)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Sizes [child] on the next frame, and again after [retryDelayMs] when that
|
|
415
|
+
* is non-zero. A single pass is not enough: the container's own bounds can
|
|
416
|
+
* still be settling when the asset mounts, and some networks attach their
|
|
417
|
+
* media surface asynchronously after the binder returns.
|
|
418
|
+
*/
|
|
419
|
+
private fun scheduleInsertedViewSizing(child: View, parent: ViewGroup, retryDelayMs: Long) {
|
|
420
|
+
parent.post { sizeInsertedViewToParent(child, parent) }
|
|
421
|
+
if (retryDelayMs > 0L) {
|
|
422
|
+
parent.postDelayed({ sizeInsertedViewToParent(child, parent) }, retryDelayMs)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Fabric-compatible event dispatch, routed through the same
|
|
428
|
+
* `UIManagerHelper` / `EventDispatcher` pathway (and shared [CloudXAdViewEvent]
|
|
429
|
+
* carrier) as the in-tree banner/MREC component.
|
|
430
|
+
*/
|
|
431
|
+
private fun dispatch(eventName: String, payload: WritableMap) {
|
|
432
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
|
|
433
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
434
|
+
dispatcher.dispatchEvent(CloudXAdViewEvent(surfaceId, id, eventName, payload))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Commands
|
|
438
|
+
|
|
439
|
+
fun commandLoadAd() {
|
|
440
|
+
loadRequested = false
|
|
441
|
+
loadIfPossible()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fun commandUpdateAssetView(assetViewTag: Int, assetViewName: String) {
|
|
445
|
+
val ad = pendingManualAd ?: return
|
|
446
|
+
val nativeAd = ad.nativeAd ?: return
|
|
447
|
+
val view = findViewById<View>(assetViewTag) ?: return
|
|
448
|
+
|
|
449
|
+
// Dropping the prior view for this role ensures only the current one
|
|
450
|
+
// is registered for clicks; otherwise a re-mount (e.g. in a feed)
|
|
451
|
+
// would leave stale references in the list.
|
|
452
|
+
assetViewsByRole[assetViewName]?.let { clickableViews.remove(it) }
|
|
453
|
+
assetViewsByRole[assetViewName] = view
|
|
454
|
+
|
|
455
|
+
when (assetViewName) {
|
|
456
|
+
"TitleView" -> {
|
|
457
|
+
// No text write: JS already renders the title from the
|
|
458
|
+
// dispatched metadata. Assigning TextView.text here replaces
|
|
459
|
+
// React's styled spannable with a plain string and loses the
|
|
460
|
+
// publisher's color and weight.
|
|
461
|
+
view.isClickable = true
|
|
462
|
+
if (!nativeAd.title.isNullOrEmpty()) clickableViews.add(view)
|
|
463
|
+
}
|
|
464
|
+
"AdvertiserView" -> {
|
|
465
|
+
view.isClickable = true
|
|
466
|
+
if (!nativeAd.advertiser.isNullOrEmpty()) clickableViews.add(view)
|
|
467
|
+
}
|
|
468
|
+
"BodyView" -> {
|
|
469
|
+
view.isClickable = true
|
|
470
|
+
if (!nativeAd.body.isNullOrEmpty()) clickableViews.add(view)
|
|
471
|
+
}
|
|
472
|
+
"CallToActionView" -> {
|
|
473
|
+
view.isClickable = true
|
|
474
|
+
if (!nativeAd.callToAction.isNullOrEmpty()) clickableViews.add(view)
|
|
475
|
+
}
|
|
476
|
+
"IconView" -> {
|
|
477
|
+
val icon = nativeAd.icon
|
|
478
|
+
if (icon != null && view is ImageView) {
|
|
479
|
+
// URL-based icons are loaded asynchronously in JS via the
|
|
480
|
+
// dispatched metadata; drawable-based icons must be set
|
|
481
|
+
// here because the raw bytes never cross the bridge.
|
|
482
|
+
if (icon.uri == null && icon.drawable is BitmapDrawable) {
|
|
483
|
+
view.setImageDrawable(icon.drawable)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
view.isClickable = true
|
|
487
|
+
clickableViews.add(view)
|
|
488
|
+
}
|
|
489
|
+
"OptionsView" -> {
|
|
490
|
+
val optionsView = nativeAd.optionsView
|
|
491
|
+
if (optionsView != null && view is ViewGroup) {
|
|
492
|
+
(optionsView.parent as? ViewGroup)?.takeIf { it !== view }?.removeView(optionsView)
|
|
493
|
+
if (optionsView.parent == null) {
|
|
494
|
+
view.addView(optionsView)
|
|
495
|
+
insertedSdkViews.add(optionsView)
|
|
496
|
+
scheduleInsertedViewSizing(optionsView, view, 0L)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
"MediaView" -> {
|
|
501
|
+
val mediaView = nativeAd.mediaView
|
|
502
|
+
if (mediaView != null && view is ViewGroup) {
|
|
503
|
+
(mediaView.parent as? ViewGroup)?.takeIf { it !== view }?.removeView(mediaView)
|
|
504
|
+
if (mediaView.parent == null) {
|
|
505
|
+
view.addView(mediaView)
|
|
506
|
+
insertedSdkViews.add(mediaView)
|
|
507
|
+
scheduleInsertedViewSizing(mediaView, view, MEDIA_VIEW_RESIZE_RETRY_MS)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
view.isClickable = true
|
|
511
|
+
clickableViews.add(view)
|
|
512
|
+
}
|
|
513
|
+
"StarRatingView" -> {
|
|
514
|
+
// Decorative; not clickable. JS handles rendering.
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fun commandRenderNativeAd() {
|
|
520
|
+
// Each guard is a display failure, not a silent no-op: the publisher's
|
|
521
|
+
// onAdDisplayFailedEvent handler must hear about it so the QA harness
|
|
522
|
+
// and publisher UI don't hang waiting for a callback that never comes.
|
|
523
|
+
val ad = pendingManualAd
|
|
524
|
+
if (ad == null) {
|
|
525
|
+
val error = CloudXErrorCode.ADAPTER_INTERNAL_ERROR.toCloudXError("renderNativeAd called before ad loaded")
|
|
526
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(adUnitId ?: "", error))
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
val nativeAd = ad.nativeAd
|
|
530
|
+
if (nativeAd == null) {
|
|
531
|
+
val error = CloudXErrorCode.ADAPTER_INTERNAL_ERROR.toCloudXError("renderNativeAd: native ad not available")
|
|
532
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(ad.adUnitId, error))
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (nativeAd.isExpired) {
|
|
536
|
+
val error = CloudXErrorCode.ADAPTER_AD_EXPIRED.toCloudXError("renderNativeAd: ad expired")
|
|
537
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(ad.adUnitId, error))
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
val registered = nativeAd.prepareForInteraction(clickableViews.toList(), this)
|
|
541
|
+
if (!registered) {
|
|
542
|
+
// A load that cannot register click/impression tracking is a
|
|
543
|
+
// display failure: the ad renders but never produces revenue.
|
|
544
|
+
val error = CloudXErrorCode.ADAPTER_INTERNAL_ERROR.toCloudXError("Native ad failed to register click/impression tracking")
|
|
545
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(ad.adUnitId, error))
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fun commandDestroyAd() {
|
|
550
|
+
// Destroy whichever ad this cycle holds: manual mode stashes it in
|
|
551
|
+
// pendingManualAd, template mode only in loadedCycleAd. Leaving the
|
|
552
|
+
// template-mode ad alive would leak SDK-side resources.
|
|
553
|
+
val ad = pendingManualAd ?: loadedCycleAd
|
|
554
|
+
if (ad != null) {
|
|
555
|
+
attachedLoader?.destroy(ad)
|
|
556
|
+
}
|
|
557
|
+
// Tear the loader down rather than only clearing per-ad state. When
|
|
558
|
+
// destroyAd arrives before onNativeAdLoaded there is no ad to destroy
|
|
559
|
+
// above, and leaving the loader attached kept its listener wired: the
|
|
560
|
+
// in-flight ad was then delivered into a view the JS layer had already
|
|
561
|
+
// destroyed. Next loadAd rebuilds the loader through currentLoader().
|
|
562
|
+
releaseLoaderState()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Drops the per-ad mutable state and detaches the SDK-owned ad view, so an
|
|
567
|
+
* invalidated ad can't leak its state into the next load cycle. Used by the
|
|
568
|
+
* SDK-initiated expiry/close callbacks, which leave the loader alive
|
|
569
|
+
* deliberately: the ad is gone but the publisher may still reload this
|
|
570
|
+
* view. [commandDestroyAd] wants the stronger [releaseLoaderState], which
|
|
571
|
+
* also cancels a load still in flight.
|
|
572
|
+
*/
|
|
573
|
+
private fun clearAdState() {
|
|
574
|
+
pendingManualAd = null
|
|
575
|
+
loadedCycleAd = null
|
|
576
|
+
clickableViews.clear()
|
|
577
|
+
assetViewsByRole.clear()
|
|
578
|
+
removeInsertedSdkViews()
|
|
579
|
+
// Only detach the SDK-owned ad view. React-managed asset-view children
|
|
580
|
+
// stay mounted — they belong to the JS tree and will be removed by
|
|
581
|
+
// React itself when the publisher unmounts them.
|
|
582
|
+
hostedAdView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
583
|
+
hostedAdView = null
|
|
584
|
+
loadRequested = false
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// CloudXNativeAdListener
|
|
588
|
+
|
|
589
|
+
override fun onNativeAdLoaded(adView: SdkNativeAdView?, ad: CloudXAd) {
|
|
590
|
+
loadedCycleAd = ad
|
|
591
|
+
if (manualRender) {
|
|
592
|
+
pendingManualAd = ad
|
|
593
|
+
} else {
|
|
594
|
+
adView?.let { attachAdView(it) }
|
|
595
|
+
}
|
|
596
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOADED_EVENT, infoPayload(ad))
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
override fun onNativeAdLoadFailed(adUnitId: String, error: CloudXError) {
|
|
600
|
+
loadRequested = false
|
|
601
|
+
// The SDK funnels post-load adapter/render errors through the same
|
|
602
|
+
// listener callback as load failures. If this cycle already delivered
|
|
603
|
+
// an ad, the failure happened while displaying it — surface it as a
|
|
604
|
+
// display failure so JS sees the correct lifecycle event.
|
|
605
|
+
val displayedAd = loadedCycleAd
|
|
606
|
+
if (displayedAd != null) {
|
|
607
|
+
loadedCycleAd = null
|
|
608
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(displayedAd.adUnitId, error))
|
|
609
|
+
} else {
|
|
610
|
+
// Template mode attaches the ad view before loadAd, so a genuine
|
|
611
|
+
// load failure would otherwise leave a blank attached view.
|
|
612
|
+
hostedAdView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
|
613
|
+
hostedAdView = null
|
|
614
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOAD_FAILED_EVENT, loadFailedPayload(adUnitId, error))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
override fun onNativeAdClicked(ad: CloudXAd) {
|
|
619
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLICKED_EVENT, infoPayload(ad))
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
override fun onNativeAdExpired(ad: CloudXAd) {
|
|
623
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_EXPIRED_EVENT, infoPayload(ad))
|
|
624
|
+
// The ad is invalidated: release SDK-side resources and drop our
|
|
625
|
+
// per-ad refs so the expired state can't leak into the next load
|
|
626
|
+
// cycle. The loader itself stays alive for reuse.
|
|
627
|
+
attachedLoader?.destroy(ad)
|
|
628
|
+
clearAdState()
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
override fun onNativeAdClosed(ad: CloudXAd) {
|
|
632
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLOSED_EVENT, infoPayload(ad))
|
|
633
|
+
// Mirrors onNativeAdExpired: the ad is done, so detach its view and
|
|
634
|
+
// clear per-ad refs. Loader stays alive for the next load cycle.
|
|
635
|
+
attachedLoader?.destroy(ad)
|
|
636
|
+
clearAdState()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// CloudXAdRevenueListener
|
|
640
|
+
|
|
641
|
+
override fun onAdRevenuePaid(cloudXAd: CloudXAd) {
|
|
642
|
+
dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_REVENUE_PAID_EVENT, infoPayload(cloudXAd))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/*
|
|
646
|
+
* Codegen cannot express a nested/heterogeneous native-ad payload as a
|
|
647
|
+
* typed prop, so both `adValues` and the loaded native ad's own metadata
|
|
648
|
+
* are serialized to JSON strings here and re-parsed by the JS wrapper
|
|
649
|
+
* (mirrors CloudXAdView's adValuesJson for the banner/MREC component).
|
|
650
|
+
*/
|
|
651
|
+
private fun infoPayload(ad: CloudXAd): WritableMap {
|
|
652
|
+
val map = Arguments.createMap()
|
|
653
|
+
map.putString("adUnitId", ad.adUnitId)
|
|
654
|
+
map.putString("adFormat", ad.adFormat.name)
|
|
655
|
+
map.putString("networkName", ad.networkName)
|
|
656
|
+
map.putString("networkPlacement", ad.networkPlacement ?: "")
|
|
657
|
+
map.putString("placement", ad.placement ?: "")
|
|
658
|
+
map.putString("creativeId", ad.creativeId ?: "")
|
|
659
|
+
map.putDouble("revenue", ad.revenue)
|
|
660
|
+
val adValuesJson = try {
|
|
661
|
+
JSONObject(ad.adValues as Map<*, *>).toString()
|
|
662
|
+
} catch (e: Exception) {
|
|
663
|
+
Log.w(TAG, "Failed to serialize adValues; delivering empty object", e)
|
|
664
|
+
"{}"
|
|
665
|
+
}
|
|
666
|
+
map.putString("adValuesJson", adValuesJson)
|
|
667
|
+
ad.nativeAd?.let { map.putString("nativeAdJson", nativeAdMetadataJson(it)) }
|
|
668
|
+
return map
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private fun loadFailedPayload(unitId: String, error: CloudXError): WritableMap {
|
|
672
|
+
val map = Arguments.createMap()
|
|
673
|
+
map.putString("adUnitId", unitId)
|
|
674
|
+
// Codegen declares `code` as Double; emit a double to match (and to
|
|
675
|
+
// stay consistent with `revenue` and the iOS side).
|
|
676
|
+
map.putDouble("code", error.code.code.toDouble())
|
|
677
|
+
map.putString("message", error.message)
|
|
678
|
+
return map
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* JSON-encoded native ad metadata for the codegen-typed `nativeAdJson`
|
|
683
|
+
* string field (codegen cannot express a nested map here). Mirrors the
|
|
684
|
+
* same field set as CloudXAdInfo.kt's imperative-event metadata map.
|
|
685
|
+
*/
|
|
686
|
+
private fun nativeAdMetadataJson(nativeAd: CloudXNativeAd): String {
|
|
687
|
+
val iconImageSource = nativeAd.icon?.let { icon ->
|
|
688
|
+
if (icon.uri == null) {
|
|
689
|
+
(icon.drawable as? BitmapDrawable)?.bitmap?.let(::bitmapToBase64Png)
|
|
690
|
+
} else {
|
|
691
|
+
null
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
val json = JSONObject()
|
|
695
|
+
json.put("title", nativeAd.title ?: JSONObject.NULL)
|
|
696
|
+
json.put("advertiser", nativeAd.advertiser ?: JSONObject.NULL)
|
|
697
|
+
json.put("body", nativeAd.body ?: JSONObject.NULL)
|
|
698
|
+
json.put("callToAction", nativeAd.callToAction ?: JSONObject.NULL)
|
|
699
|
+
nativeAd.starRating?.let { json.put("starRating", it) }
|
|
700
|
+
if (nativeAd.mediaContentAspectRatio > 0f) {
|
|
701
|
+
json.put("mediaContentAspectRatio", nativeAd.mediaContentAspectRatio.toDouble())
|
|
702
|
+
}
|
|
703
|
+
val iconAvailable = nativeAd.icon != null
|
|
704
|
+
json.put("isIconImageAvailable", iconAvailable)
|
|
705
|
+
json.put("isOptionsViewAvailable", nativeAd.optionsView != null)
|
|
706
|
+
json.put("isMediaViewAvailable", nativeAd.mediaView != null)
|
|
707
|
+
nativeAd.icon?.let { icon ->
|
|
708
|
+
icon.uri?.let { json.put("url", it.toString()) }
|
|
709
|
+
if (icon.uri == null) {
|
|
710
|
+
iconImageSource?.let { json.put("imageSource", it) }
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
json.put("isVideoContent", nativeAd.isVideoContent)
|
|
714
|
+
json.put("videoDuration", nativeAd.videoDuration)
|
|
715
|
+
json.put("isExpired", nativeAd.isExpired)
|
|
716
|
+
return json.toString()
|
|
717
|
+
}
|
|
718
|
+
}
|