cloudx-react-native 3.6.1 → 3.8.0

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 (38) hide show
  1. package/android/build.gradle +92 -2
  2. package/android/src/main/java/com/cloudxreactnative/CloudXAdViewEvent.kt +2 -2
  3. package/android/src/main/java/com/cloudxreactnative/CloudXAdViewManagerImpl.kt +6 -6
  4. package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +208 -102
  5. package/android/src/test/java/com/cloudxreactnative/CloudXModuleImplArbiterBidsTest.kt +254 -0
  6. package/android/src/test/java/com/cloudxreactnative/CloudXModuleImplThreadingTest.kt +170 -0
  7. package/android/src/test/java/com/cloudxreactnative/FakeReadable.kt +101 -0
  8. package/cloudx-react-native.podspec +1 -1
  9. package/ios/CloudX.mm +62 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/types/Arbiter.js +27 -0
  12. package/lib/commonjs/types/Arbiter.js.map +1 -1
  13. package/lib/commonjs/types/RevenueData.js +1 -0
  14. package/lib/commonjs/types/RevenueData.js.map +1 -1
  15. package/lib/commonjs/types/index.js.map +1 -1
  16. package/lib/commonjs/version.js +1 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/types/Arbiter.js +27 -0
  19. package/lib/module/types/Arbiter.js.map +1 -1
  20. package/lib/module/types/RevenueData.js +1 -0
  21. package/lib/module/types/RevenueData.js.map +1 -1
  22. package/lib/module/types/index.js.map +1 -1
  23. package/lib/module/version.js +1 -1
  24. package/lib/typescript/src/index.d.ts +1 -1
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/types/Arbiter.d.ts +45 -2
  27. package/lib/typescript/src/types/Arbiter.d.ts.map +1 -1
  28. package/lib/typescript/src/types/RevenueData.d.ts +1 -0
  29. package/lib/typescript/src/types/RevenueData.d.ts.map +1 -1
  30. package/lib/typescript/src/types/index.d.ts +1 -1
  31. package/lib/typescript/src/types/index.d.ts.map +1 -1
  32. package/lib/typescript/src/version.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/src/index.ts +2 -0
  35. package/src/types/Arbiter.ts +61 -2
  36. package/src/types/RevenueData.ts +1 -0
  37. package/src/types/index.ts +2 -0
  38. package/src/version.ts +1 -1
@@ -9,6 +9,7 @@ import android.view.View
9
9
  import android.view.ViewGroup
10
10
  import android.widget.LinearLayout
11
11
  import android.widget.RelativeLayout
12
+ import androidx.annotation.VisibleForTesting
12
13
  import com.facebook.react.bridge.Arguments
13
14
  import com.facebook.react.bridge.LifecycleEventListener
14
15
  import com.facebook.react.bridge.Promise
@@ -71,6 +72,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
71
72
 
72
73
  companion object {
73
74
  private const val TAG = "CloudX"
75
+ private const val ADMOB_DEFAULT_NETWORK_NAME = "admob"
76
+ private const val GAM_DEFAULT_NETWORK_NAME = "gam"
74
77
  const val MODULE_NAME = "CloudX"
75
78
  }
76
79
 
@@ -85,42 +88,67 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
85
88
  return currentActivity
86
89
  }
87
90
 
91
+ // =========================================================================
92
+ // Threading
93
+ // =========================================================================
94
+
95
+ /*
96
+ * Every public module method runs its whole body on the UI thread, including reads of the
97
+ * module's own state. React Native invokes Android native-module methods on a shared
98
+ * background thread, so a method that did work inline could execute before a previously
99
+ * posted one (e.g. loadInterstitial() running on an instance that a queued
100
+ * destroyInterstitial() then destroys). Posting everything here keeps JS call order intact,
101
+ * makes the lazy retrieveX() create paths single-threaded, and matches the iOS module, whose
102
+ * methodQueue is the main queue.
103
+ */
104
+ private inline fun onUiThread(crossinline block: () -> Unit) {
105
+ reactApplicationContext.runOnUiQueueThread { block() }
106
+ }
107
+
88
108
  // =========================================================================
89
109
  // Initialization
90
110
  // =========================================================================
91
111
 
92
112
  fun isInitialized(promise: Promise) {
93
- promise.resolve(CloudX.isInitialized())
113
+ onUiThread {
114
+ promise.resolve(CloudX.isInitialized())
115
+ }
94
116
  }
95
117
 
96
118
  fun initialize(pluginVersion: String, appKey: String, promise: Promise) {
97
- val config = CloudXInitializationConfiguration.builder(appKey)
98
- .setPluginVersion(pluginVersion)
99
- .build()
100
-
101
- CloudX.initialize(reactApplicationContext, config, object : CloudXInitializationListener {
102
- override fun onInitialized(configuration: CloudXSdkConfiguration) {
103
- setupOrientationListener()
104
- val result = Arguments.createMap()
105
- result.putBoolean("success", true)
106
- result.putString("message", "SDK initialized successfully")
107
- promise.resolve(result)
108
- }
119
+ onUiThread {
120
+ val config = CloudXInitializationConfiguration.builder(appKey)
121
+ .setPluginVersion(pluginVersion)
122
+ .build()
123
+
124
+ CloudX.initialize(reactApplicationContext, config, object : CloudXInitializationListener {
125
+ override fun onInitialized(configuration: CloudXSdkConfiguration) {
126
+ setupOrientationListener()
127
+ val result = Arguments.createMap()
128
+ result.putBoolean("success", true)
129
+ result.putString("message", "SDK initialized successfully")
130
+ promise.resolve(result)
131
+ }
109
132
 
110
- override fun onInitializationFailed(cloudXError: CloudXError) {
111
- Log.e(TAG, "CloudX SDK initialization failed: ${cloudXError.message}")
112
- promise.reject(cloudXError.code.name, cloudXError.message, null)
113
- }
114
- })
133
+ override fun onInitializationFailed(cloudXError: CloudXError) {
134
+ Log.e(TAG, "CloudX SDK initialization failed: ${cloudXError.message}")
135
+ promise.reject(cloudXError.code.name, cloudXError.message, null)
136
+ }
137
+ })
138
+ }
115
139
  }
116
140
 
117
141
  fun isTablet(promise: Promise) {
118
- val isTablet = reactApplicationContext.resources.configuration.smallestScreenWidthDp >= 600
119
- promise.resolve(isTablet)
142
+ onUiThread {
143
+ val isTablet = reactApplicationContext.resources.configuration.smallestScreenWidthDp >= 600
144
+ promise.resolve(isTablet)
145
+ }
120
146
  }
121
147
 
122
148
  fun getVersion(promise: Promise) {
123
- promise.resolve(CloudX.getVersion())
149
+ onUiThread {
150
+ promise.resolve(CloudX.getVersion())
151
+ }
124
152
  }
125
153
 
126
154
  // =========================================================================
@@ -128,16 +156,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
128
156
  // =========================================================================
129
157
 
130
158
  fun setMinLogLevel(level: String) {
131
- val logLevel = when (level.lowercase()) {
132
- "verbose" -> CloudXLogLevel.VERBOSE
133
- "debug" -> CloudXLogLevel.DEBUG
134
- "info" -> CloudXLogLevel.INFO
135
- "warn" -> CloudXLogLevel.WARN
136
- "error" -> CloudXLogLevel.ERROR
137
- "none" -> CloudXLogLevel.NONE
138
- else -> CloudXLogLevel.INFO
159
+ onUiThread {
160
+ val logLevel = when (level.lowercase()) {
161
+ "verbose" -> CloudXLogLevel.VERBOSE
162
+ "debug" -> CloudXLogLevel.DEBUG
163
+ "info" -> CloudXLogLevel.INFO
164
+ "warn" -> CloudXLogLevel.WARN
165
+ "error" -> CloudXLogLevel.ERROR
166
+ "none" -> CloudXLogLevel.NONE
167
+ else -> CloudXLogLevel.INFO
168
+ }
169
+ CloudX.setMinLogLevel(logLevel)
139
170
  }
140
- CloudX.setMinLogLevel(logLevel)
141
171
  }
142
172
 
143
173
  fun setVisualDebuggingEnabled(enabled: Boolean) {
@@ -149,19 +179,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
149
179
  // =========================================================================
150
180
 
151
181
  fun setHashedUserID(hashedUserID: String) {
152
- CloudX.setHashedUserId(hashedUserID)
182
+ onUiThread {
183
+ CloudX.setHashedUserId(hashedUserID)
184
+ }
153
185
  }
154
186
 
155
187
  fun setUserKeyValue(key: String, value: String) {
156
- CloudX.setUserKeyValue(key, value)
188
+ onUiThread {
189
+ CloudX.setUserKeyValue(key, value)
190
+ }
157
191
  }
158
192
 
159
193
  fun setAppKeyValue(key: String, value: String) {
160
- CloudX.setAppKeyValue(key, value)
194
+ onUiThread {
195
+ CloudX.setAppKeyValue(key, value)
196
+ }
161
197
  }
162
198
 
163
199
  fun clearAllKeyValues() {
164
- CloudX.clearAllKeyValues()
200
+ onUiThread {
201
+ CloudX.clearAllKeyValues()
202
+ }
165
203
  }
166
204
 
167
205
  // =========================================================================
@@ -169,11 +207,15 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
169
207
  // =========================================================================
170
208
 
171
209
  fun setHasUserConsent(hasUserConsent: Boolean?) {
172
- CloudX.setHasUserConsent(hasUserConsent)
210
+ onUiThread {
211
+ CloudX.setHasUserConsent(hasUserConsent)
212
+ }
173
213
  }
174
214
 
175
215
  fun setDoNotSell(isDoNotSell: Boolean?) {
176
- CloudX.setDoNotSell(isDoNotSell)
216
+ onUiThread {
217
+ CloudX.setDoNotSell(isDoNotSell)
218
+ }
177
219
  }
178
220
 
179
221
  // =========================================================================
@@ -181,27 +223,31 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
181
223
  // =========================================================================
182
224
 
183
225
  fun arbiter(configuration: ReadableMap, promise: Promise) {
184
- val bids = arbiterBids(configuration)
185
- if (bids.isEmpty()) {
186
- promise.resolve(emptyArbiterResult())
187
- return
188
- }
189
-
190
- val arbiterConfiguration = CloudXArbiterConfiguration.builder(bids).build()
191
- CloudX.arbiter(arbiterConfiguration, object : CloudXArbiterListener {
192
- override fun onCompleted(result: CloudXArbiterResult) {
193
- promise.resolve(result.toWritableMap())
226
+ onUiThread {
227
+ val bids = arbiterBids(configuration)
228
+ if (bids.isEmpty()) {
229
+ promise.resolve(emptyArbiterResult())
230
+ return@onUiThread
194
231
  }
195
- })
232
+
233
+ val arbiterConfiguration = CloudXArbiterConfiguration.builder(bids).build()
234
+ CloudX.arbiter(arbiterConfiguration, object : CloudXArbiterListener {
235
+ override fun onCompleted(result: CloudXArbiterResult) {
236
+ promise.resolve(result.toWritableMap())
237
+ }
238
+ })
239
+ }
196
240
  }
197
241
 
198
242
  fun reportRevenueData(data: ReadableMap, promise: Promise) {
199
- val revenueData = data.toRevenueData()
200
- if (revenueData == null) {
201
- promise.resolve(false)
202
- return
243
+ onUiThread {
244
+ val revenueData = data.toRevenueData()
245
+ if (revenueData == null) {
246
+ promise.resolve(false)
247
+ return@onUiThread
248
+ }
249
+ promise.resolve(CloudX.reportRevenueData(revenueData))
203
250
  }
204
- promise.resolve(CloudX.reportRevenueData(revenueData))
205
251
  }
206
252
 
207
253
  // =========================================================================
@@ -214,8 +260,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
214
260
  fun setBannerPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.BANNER, placement)
215
261
  fun setBannerCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.BANNER, customData)
216
262
  fun setBannerExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.BANNER, extraParameters)
217
- fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position, adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP)
218
- fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, adViewPositions[adUnitId], Pair(xOffset.toFloat(), yOffset.toFloat()))
263
+ fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position, null)
264
+ fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
219
265
  fun startBannerAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
220
266
  fun stopBannerAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
221
267
  fun showBanner(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.BANNER)
@@ -232,8 +278,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
232
278
  fun setMRECPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.MREC, placement)
233
279
  fun setMRECCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.MREC, customData)
234
280
  fun setMRECExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.MREC, extraParameters)
235
- fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position, adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP)
236
- fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, adViewPositions[adUnitId], Pair(xOffset.toFloat(), yOffset.toFloat()))
281
+ fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position, null)
282
+ fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
237
283
  fun startMRECAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
238
284
  fun stopMRECAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
239
285
  fun showMREC(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.MREC)
@@ -245,21 +291,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
245
291
  // =========================================================================
246
292
 
247
293
  fun loadInterstitial(adUnitId: String) {
248
- retrieveInterstitial(adUnitId).load()
294
+ onUiThread {
295
+ retrieveInterstitial(adUnitId).load()
296
+ }
249
297
  }
250
298
 
251
299
  fun setInterstitialExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
252
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
253
- retrieveInterstitial(adUnitId).setExtraParameter(key, value)
300
+ onUiThread {
301
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
302
+ retrieveInterstitial(adUnitId).setExtraParameter(key, value)
303
+ }
254
304
  }
255
305
  }
256
306
 
257
307
  fun isInterstitialReady(adUnitId: String, promise: Promise) {
258
- promise.resolve(retrieveInterstitial(adUnitId).isAdReady)
308
+ onUiThread {
309
+ promise.resolve(retrieveInterstitial(adUnitId).isAdReady)
310
+ }
259
311
  }
260
312
 
261
313
  fun showInterstitial(adUnitId: String, placement: String?, customData: String?) {
262
- reactApplicationContext.runOnUiQueueThread {
314
+ onUiThread {
263
315
  val interstitial = retrieveInterstitial(adUnitId)
264
316
  val activity = maybeGetCurrentActivity()
265
317
  if (activity == null) {
@@ -268,14 +320,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
268
320
  CloudXAdEvents.ON_INTERSTITIAL_DISPLAY_FAILED_EVENT,
269
321
  getNoActivityDisplayFailedInfo(adUnitId),
270
322
  )
271
- return@runOnUiQueueThread
323
+ return@onUiThread
272
324
  }
273
325
  interstitial.show(activity, placement, customData)
274
326
  }
275
327
  }
276
328
 
277
329
  fun destroyInterstitial(adUnitId: String) {
278
- reactApplicationContext.runOnUiQueueThread {
330
+ onUiThread {
279
331
  interstitials.remove(adUnitId)?.let {
280
332
  it.listener = null
281
333
  it.revenueListener = null
@@ -289,21 +341,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
289
341
  // =========================================================================
290
342
 
291
343
  fun loadAppOpen(adUnitId: String) {
292
- retrieveAppOpen(adUnitId).load()
344
+ onUiThread {
345
+ retrieveAppOpen(adUnitId).load()
346
+ }
293
347
  }
294
348
 
295
349
  fun setAppOpenExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
296
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
297
- retrieveAppOpen(adUnitId).setExtraParameter(key, value)
350
+ onUiThread {
351
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
352
+ retrieveAppOpen(adUnitId).setExtraParameter(key, value)
353
+ }
298
354
  }
299
355
  }
300
356
 
301
357
  fun isAppOpenReady(adUnitId: String, promise: Promise) {
302
- promise.resolve(retrieveAppOpen(adUnitId).isAdReady)
358
+ onUiThread {
359
+ promise.resolve(retrieveAppOpen(adUnitId).isAdReady)
360
+ }
303
361
  }
304
362
 
305
363
  fun showAppOpen(adUnitId: String, placement: String?, customData: String?) {
306
- reactApplicationContext.runOnUiQueueThread {
364
+ onUiThread {
307
365
  val appOpen = retrieveAppOpen(adUnitId)
308
366
  val activity = maybeGetCurrentActivity()
309
367
  if (activity == null) {
@@ -312,14 +370,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
312
370
  CloudXAdEvents.ON_APPOPEN_DISPLAY_FAILED_EVENT,
313
371
  getNoActivityDisplayFailedInfo(adUnitId),
314
372
  )
315
- return@runOnUiQueueThread
373
+ return@onUiThread
316
374
  }
317
375
  appOpen.show(activity, placement, customData)
318
376
  }
319
377
  }
320
378
 
321
379
  fun destroyAppOpen(adUnitId: String) {
322
- reactApplicationContext.runOnUiQueueThread {
380
+ onUiThread {
323
381
  appOpenAds.remove(adUnitId)?.let {
324
382
  it.listener = null
325
383
  it.revenueListener = null
@@ -333,21 +391,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
333
391
  // =========================================================================
334
392
 
335
393
  fun loadRewarded(adUnitId: String) {
336
- retrieveRewarded(adUnitId).load()
394
+ onUiThread {
395
+ retrieveRewarded(adUnitId).load()
396
+ }
337
397
  }
338
398
 
339
399
  fun setRewardedExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
340
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
341
- retrieveRewarded(adUnitId).setExtraParameter(key, value)
400
+ onUiThread {
401
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
402
+ retrieveRewarded(adUnitId).setExtraParameter(key, value)
403
+ }
342
404
  }
343
405
  }
344
406
 
345
407
  fun isRewardedReady(adUnitId: String, promise: Promise) {
346
- promise.resolve(retrieveRewarded(adUnitId).isAdReady)
408
+ onUiThread {
409
+ promise.resolve(retrieveRewarded(adUnitId).isAdReady)
410
+ }
347
411
  }
348
412
 
349
413
  fun showRewarded(adUnitId: String, placement: String?, customData: String?) {
350
- reactApplicationContext.runOnUiQueueThread {
414
+ onUiThread {
351
415
  val rewarded = retrieveRewarded(adUnitId)
352
416
  val activity = maybeGetCurrentActivity()
353
417
  if (activity == null) {
@@ -356,14 +420,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
356
420
  CloudXAdEvents.ON_REWARDED_AD_DISPLAY_FAILED_EVENT,
357
421
  getNoActivityDisplayFailedInfo(adUnitId),
358
422
  )
359
- return@runOnUiQueueThread
423
+ return@onUiThread
360
424
  }
361
425
  rewarded.show(activity, placement, customData)
362
426
  }
363
427
  }
364
428
 
365
429
  fun destroyRewarded(adUnitId: String) {
366
- reactApplicationContext.runOnUiQueueThread {
430
+ onUiThread {
367
431
  rewardedAds.remove(adUnitId)?.let {
368
432
  it.listener = null
369
433
  it.revenueListener = null
@@ -511,7 +575,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
511
575
 
512
576
  private fun createAdView(adUnitId: String, adFormat: CloudXAdFormat, position: String, xOffset: Double, yOffset: Double) {
513
577
  // Run on main thread to ensure there are no concurrency issues with other ad view methods
514
- reactApplicationContext.runOnUiQueueThread {
578
+ onUiThread {
515
579
  // Intentionally load the ad view while detached from any window.
516
580
  // Attaching-then-hiding-then-loading causes the underlying banner
517
581
  // to take a prefetch code path whose hidden->visible transition
@@ -520,7 +584,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
520
584
  // (~30s). Detached loads use the "display when added to a window"
521
585
  // path, so showAdView attaches and displays immediately.
522
586
  val adView = retrieveAdView(adUnitId, adFormat, position, Pair(xOffset.toFloat(), yOffset.toFloat()))
523
- ?: return@runOnUiQueueThread
587
+ ?: return@onUiThread
524
588
 
525
589
  adViewAdFormats[adUnitId] = adFormat
526
590
  applyPendingExtraParameters(adUnitId, adView)
@@ -536,27 +600,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
536
600
  }
537
601
 
538
602
  private fun setAdViewPlacement(adUnitId: String, adFormat: CloudXAdFormat, placement: String?) {
539
- reactApplicationContext.runOnUiQueueThread {
540
- val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@runOnUiQueueThread
603
+ onUiThread {
604
+ val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
541
605
  adView.setPlacement(placement)
542
606
  }
543
607
  }
544
608
 
545
609
  private fun setAdViewCustomData(adUnitId: String, adFormat: CloudXAdFormat, customData: String?) {
546
- reactApplicationContext.runOnUiQueueThread {
547
- val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@runOnUiQueueThread
610
+ onUiThread {
611
+ val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
548
612
  adView.setCustomData(customData)
549
613
  }
550
614
  }
551
615
 
552
616
  private fun setAdViewExtraParameters(adUnitId: String, adFormat: CloudXAdFormat, extraParameters: ReadableMap) {
553
- reactApplicationContext.runOnUiQueueThread {
617
+ onUiThread {
554
618
  val adView = retrieveAdView(adUnitId, adFormat)
555
619
  if (adView != null) {
556
620
  CloudXExtraParameters.forEach(extraParameters) { key, value ->
557
621
  adView.setExtraParameter(key, value)
558
622
  }
559
- return@runOnUiQueueThread
623
+ return@onUiThread
560
624
  }
561
625
 
562
626
  val pending = adViewExtraParameters.getOrPut(adUnitId) { ConcurrentHashMap() }
@@ -575,7 +639,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
575
639
  CloudXExtraParameters.applyToAdView(adView, pending)
576
640
  }
577
641
 
578
- private fun arbiterBids(configuration: ReadableMap): List<CloudXArbiterBid> {
642
+ // internal rather than private so unit tests can exercise bid parsing directly.
643
+ internal fun arbiterBids(configuration: ReadableMap): List<CloudXArbiterBid> {
579
644
  if (!configuration.hasKey("bids") || configuration.getType("bids") != ReadableType.Array) {
580
645
  return emptyList()
581
646
  }
@@ -603,6 +668,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
603
668
  "LEVELPLAY" -> levelPlayArbiterBid(index)
604
669
  "PUBMATIC" -> pubMaticArbiterBid(index)
605
670
  "CUSTOM" -> customArbiterBid(index)
671
+ "ADMOB" -> adMobArbiterBid(index)
672
+ "GAM" -> gamArbiterBid(index)
606
673
  else -> {
607
674
  Log.w(TAG, "arbiter: ignoring bid with unsupported platform=$platform at index=$index")
608
675
  null
@@ -687,6 +754,36 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
687
754
  )
688
755
  }
689
756
 
757
+ private fun ReadableMap.adMobArbiterBid(index: Int): CloudXArbiterBid? {
758
+ val adUnitId = stringValue("adUnitId")
759
+ if (adUnitId.isNullOrBlank()) {
760
+ Log.w(TAG, "arbiter: ignoring ADMOB bid with missing adUnitId at index=$index")
761
+ return null
762
+ }
763
+
764
+ return CloudXArbiterBid.adMob(
765
+ adUnitId = adUnitId,
766
+ networkName = optionalStringValue("networkName") ?: ADMOB_DEFAULT_NETWORK_NAME,
767
+ manualRevenuePerImpressionUSD = doubleValue("manualRevenuePerImpressionUSD"),
768
+ extras = stringMapValue("extras"),
769
+ )
770
+ }
771
+
772
+ private fun ReadableMap.gamArbiterBid(index: Int): CloudXArbiterBid? {
773
+ val adUnitId = stringValue("adUnitId")
774
+ if (adUnitId.isNullOrBlank()) {
775
+ Log.w(TAG, "arbiter: ignoring GAM bid with missing adUnitId at index=$index")
776
+ return null
777
+ }
778
+
779
+ return CloudXArbiterBid.gam(
780
+ adUnitId = adUnitId,
781
+ networkName = optionalStringValue("networkName") ?: GAM_DEFAULT_NETWORK_NAME,
782
+ manualRevenuePerImpressionUSD = doubleValue("manualRevenuePerImpressionUSD"),
783
+ extras = stringMapValue("extras"),
784
+ )
785
+ }
786
+
690
787
  private fun ReadableMap.toRevenueData(): CloudXRevenueData? {
691
788
  val platform = stringValue("platform")
692
789
  val revenuePlatform = platform?.toRevenuePlatformOrNull()
@@ -723,6 +820,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
723
820
  if (platform.isBlank()) return null
724
821
  return when {
725
822
  platform.equals("AdMob", ignoreCase = true) -> CloudXRevenuePlatform.ADMOB
823
+ platform.equals("GAM", ignoreCase = true) -> CloudXRevenuePlatform.GAM
726
824
  platform.equals("InMobi", ignoreCase = true) -> CloudXRevenuePlatform.INMOBI
727
825
  else -> CloudXRevenuePlatform.custom(platform)
728
826
  }
@@ -804,21 +902,29 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
804
902
  override val adValues: Map<String, String>,
805
903
  ) : CloudXAd
806
904
 
807
- private fun updateAdViewPosition(adUnitId: String, adFormat: CloudXAdFormat, position: String?, offsetsDp: Pair<Float, Float>) {
808
- reactApplicationContext.runOnUiQueueThread {
809
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
810
- adViewPositions[adUnitId] = position ?: return@runOnUiQueueThread
811
- adViewOffsetsDp[adUnitId] = offsetsDp
905
+ /*
906
+ * A null [position] or [offsetsDp] means "keep the current value". The current value is read
907
+ * inside the posted block, so a position update followed by an offsets update from JS cannot
908
+ * overwrite the first call's position with the value that was current before it ran.
909
+ */
910
+ private fun updateAdViewPosition(adUnitId: String, adFormat: CloudXAdFormat, position: String?, offsetsDp: Pair<Float, Float>?) {
911
+ onUiThread {
912
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
913
+ adViewPositions[adUnitId] = position ?: adViewPositions[adUnitId] ?: return@onUiThread
914
+ adViewOffsetsDp[adUnitId] = offsetsDp ?: adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP
812
915
  positionAdView(adUnitId, adFormat)
813
916
  }
814
917
  }
815
918
 
919
+ @VisibleForTesting
920
+ internal fun adViewPositionForTesting(adUnitId: String): String? = adViewPositions[adUnitId]
921
+
816
922
  private fun showAdView(adUnitId: String, adFormat: CloudXAdFormat) {
817
- reactApplicationContext.runOnUiQueueThread {
923
+ onUiThread {
818
924
  val adView = retrieveAdView(adUnitId, adFormat)
819
925
  if (adView == null) {
820
926
  adUnitIdsToShowAfterCreate.add(adUnitId)
821
- return@runOnUiQueueThread
927
+ return@onUiThread
822
928
  }
823
929
 
824
930
  // Lazily attach and position on first show. Attaching here — not
@@ -836,18 +942,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
836
942
  }
837
943
 
838
944
  private fun hideAdView(adUnitId: String, adFormat: CloudXAdFormat) {
839
- reactApplicationContext.runOnUiQueueThread {
945
+ onUiThread {
840
946
  adUnitIdsToShowAfterCreate.remove(adUnitId)
841
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
947
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
842
948
  adView.visibility = View.GONE
843
949
  adView.stopAutoRefresh()
844
950
  }
845
951
  }
846
952
 
847
953
  private fun destroyAdView(adUnitId: String, adFormat: CloudXAdFormat) {
848
- reactApplicationContext.runOnUiQueueThread {
954
+ onUiThread {
849
955
  val adView = retrieveAdView(adUnitId, adFormat)
850
- if (adView == null) return@runOnUiQueueThread
956
+ if (adView == null) return@onUiThread
851
957
 
852
958
  val parent = adView.parent
853
959
  if (parent is ViewGroup) {
@@ -868,8 +974,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
868
974
  }
869
975
 
870
976
  private fun setAdViewBackgroundColor(adUnitId: String, adFormat: CloudXAdFormat, hexColorString: String) {
871
- reactApplicationContext.runOnUiQueueThread {
872
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
977
+ onUiThread {
978
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
873
979
  try {
874
980
  adView.setBackgroundColor(Color.parseColor(hexColorString))
875
981
  } catch (e: IllegalArgumentException) {
@@ -879,11 +985,11 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
879
985
  }
880
986
 
881
987
  private fun startAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
882
- reactApplicationContext.runOnUiQueueThread { retrieveAdView(adUnitId, adFormat)?.startAutoRefresh() }
988
+ onUiThread { retrieveAdView(adUnitId, adFormat)?.startAutoRefresh() }
883
989
  }
884
990
 
885
991
  private fun stopAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
886
- reactApplicationContext.runOnUiQueueThread { retrieveAdView(adUnitId, adFormat)?.stopAutoRefresh() }
992
+ onUiThread { retrieveAdView(adUnitId, adFormat)?.stopAutoRefresh() }
887
993
  }
888
994
 
889
995
  // =========================================================================
@@ -1069,7 +1175,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
1069
1175
  destroyAdView(adUnitId, adFormat)
1070
1176
  }
1071
1177
 
1072
- reactApplicationContext.runOnUiQueueThread {
1178
+ onUiThread {
1073
1179
  for (interstitial in interstitials.values) {
1074
1180
  interstitial.destroy()
1075
1181
  }