@bigcrunch/react-native-ads 0.4.0 → 0.5.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.
- package/README.md +5 -5
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +434 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchBannerView.kt +484 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchInterstitial.kt +403 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchRewarded.kt +409 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +592 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +623 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +719 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +364 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/ConfigManager.kt +301 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +385 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/RewardedCallback.kt +42 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/core/SessionManager.kt +330 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/DeviceHelper.kt +60 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/HttpClient.kt +114 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Logger.kt +71 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/PrivacyStore.kt +125 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Storage.kt +88 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/BannerAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/InterstitialAdListener.kt +55 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/RewardedAdListener.kt +58 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +880 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/AppConfig.kt +90 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/DeviceData.kt +18 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +70 -0
- package/android/bigcrunch-ads/com/bigcrunch/ads/models/SessionInfo.kt +21 -0
- package/android/build.gradle +22 -10
- package/android/settings.gradle +2 -6
- package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
- package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
- package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
- package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
- package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
- package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
- package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
- package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
- package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
- package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
- package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
- package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
- package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
- package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
- package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
- package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
- package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
- package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
- package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
- package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
- package/ios/BigCrunchAdsModule.swift +0 -1
- package/ios/BigCrunchBannerViewManager.swift +0 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/package.json +8 -2
- package/react-native-bigcrunch-ads.podspec +0 -1
- package/scripts/inject-version.js +55 -0
- package/src/index.ts +3 -2
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
package com.bigcrunch.ads.core
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import com.bigcrunch.ads.adapters.BannerAdCallback
|
|
7
|
+
import com.bigcrunch.ads.adapters.GoogleAdsAdapter
|
|
8
|
+
import com.bigcrunch.ads.adapters.InterstitialAdCallback
|
|
9
|
+
import com.bigcrunch.ads.adapters.RewardedAdCallback
|
|
10
|
+
import com.bigcrunch.ads.internal.BCLogger
|
|
11
|
+
import com.bigcrunch.ads.models.PlacementConfig
|
|
12
|
+
import com.google.android.gms.ads.admanager.AdManagerAdRequest
|
|
13
|
+
import com.google.android.gms.ads.admanager.AdManagerAdView
|
|
14
|
+
import com.google.android.gms.ads.admanager.AdManagerInterstitialAd
|
|
15
|
+
import com.google.android.gms.ads.rewarded.RewardedAd
|
|
16
|
+
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.SupervisorJob
|
|
20
|
+
import kotlinx.coroutines.launch
|
|
21
|
+
import kotlinx.coroutines.sync.Mutex
|
|
22
|
+
import kotlinx.coroutines.sync.withLock
|
|
23
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback interface for banner ad events
|
|
27
|
+
*/
|
|
28
|
+
interface BannerCallback {
|
|
29
|
+
fun onAdLoaded()
|
|
30
|
+
fun onAdFailedToLoad(error: String)
|
|
31
|
+
fun onAdClicked()
|
|
32
|
+
fun onAdImpression()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Callback interface for interstitial ad events
|
|
37
|
+
*/
|
|
38
|
+
interface InterstitialCallback {
|
|
39
|
+
fun onAdLoaded()
|
|
40
|
+
fun onAdFailedToLoad(error: String)
|
|
41
|
+
fun onAdShowed()
|
|
42
|
+
fun onAdDismissed()
|
|
43
|
+
fun onAdClicked()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* AdOrchestrator - Central orchestration for ad loading lifecycle
|
|
48
|
+
*
|
|
49
|
+
* Coordinates the flow between ConfigManager, BidRequestClient, GoogleAdsAdapter,
|
|
50
|
+
* and AnalyticsClient to load and display ads.
|
|
51
|
+
*
|
|
52
|
+
* Flow:
|
|
53
|
+
* 1. Get PlacementConfig from ConfigManager
|
|
54
|
+
* 2. Track ad request via AnalyticsClient
|
|
55
|
+
* 3. Fetch S2S demand via BidRequestClient
|
|
56
|
+
* 4. Load Google ad via GoogleAdsAdapter (with targeting from S2S)
|
|
57
|
+
* 5. Wire up all callbacks for analytics
|
|
58
|
+
*/
|
|
59
|
+
internal class AdOrchestrator(
|
|
60
|
+
private val context: Context,
|
|
61
|
+
private val configManager: ConfigManager,
|
|
62
|
+
private val analyticsClient: AnalyticsClient,
|
|
63
|
+
private val bidRequestClient: BidRequestClient,
|
|
64
|
+
private val googleAdsAdapter: GoogleAdsAdapter
|
|
65
|
+
) {
|
|
66
|
+
private val TAG = "AdOrchestrator"
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Exception handler for coroutines - catches any uncaught exceptions
|
|
70
|
+
* to prevent SDK errors from crashing the host app.
|
|
71
|
+
*/
|
|
72
|
+
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
|
73
|
+
BCLogger.e(TAG, "Uncaught exception in SDK coroutine - SDK will not propagate this to prevent app crash", throwable)
|
|
74
|
+
// Don't rethrow - SDK errors should not crash the host app
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Coroutine scope with SupervisorJob (child failures don't cancel siblings)
|
|
79
|
+
* and exception handler (uncaught exceptions are logged, not propagated).
|
|
80
|
+
*/
|
|
81
|
+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob() + exceptionHandler)
|
|
82
|
+
|
|
83
|
+
// Cache for preloaded interstitial ads
|
|
84
|
+
private val interstitialCache = ConcurrentHashMap<String, AdManagerInterstitialAd>()
|
|
85
|
+
private val interstitialMutex = Mutex()
|
|
86
|
+
|
|
87
|
+
// Cache for preloaded rewarded ads
|
|
88
|
+
private val rewardedCache = ConcurrentHashMap<String, RewardedAd>()
|
|
89
|
+
private val rewardedMutex = Mutex()
|
|
90
|
+
|
|
91
|
+
// Track active banner views for cleanup
|
|
92
|
+
private val activeBanners = ConcurrentHashMap<String, AdManagerAdView>()
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Load a banner ad
|
|
96
|
+
*
|
|
97
|
+
* @param placementId The placement ID from the BigCrunch dashboard
|
|
98
|
+
* @param container The ViewGroup to add the banner to
|
|
99
|
+
* @param callback Callback for ad events
|
|
100
|
+
* @param adSizeOverride Optional size override (takes precedence over backend config)
|
|
101
|
+
*/
|
|
102
|
+
fun loadBannerAd(
|
|
103
|
+
placementId: String,
|
|
104
|
+
container: ViewGroup,
|
|
105
|
+
callback: BannerCallback,
|
|
106
|
+
adSizeOverride: com.bigcrunch.ads.models.AdSize? = null,
|
|
107
|
+
refreshCount: Int = 0
|
|
108
|
+
) {
|
|
109
|
+
BCLogger.d(TAG, "Loading banner ad: $placementId with adSizeOverride: $adSizeOverride")
|
|
110
|
+
|
|
111
|
+
// 1. Get placement config
|
|
112
|
+
val placement = configManager.getPlacement(placementId)
|
|
113
|
+
if (placement == null) {
|
|
114
|
+
BCLogger.e(TAG, "Placement not found: $placementId")
|
|
115
|
+
callback.onAdFailedToLoad("Placement not found: $placementId")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (placement.format != "banner") {
|
|
120
|
+
BCLogger.e(TAG, "Invalid format for banner: ${placement.format}")
|
|
121
|
+
callback.onAdFailedToLoad("Invalid placement format: ${placement.format}")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Track ad request
|
|
126
|
+
analyticsClient.trackAdRequest(placementId, placement.format)
|
|
127
|
+
|
|
128
|
+
// 3. Start the async ad loading flow
|
|
129
|
+
scope.launch {
|
|
130
|
+
loadBannerAsync(placement, container, callback, adSizeOverride, refreshCount)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private suspend fun loadBannerAsync(
|
|
135
|
+
placement: PlacementConfig,
|
|
136
|
+
container: ViewGroup,
|
|
137
|
+
callback: BannerCallback,
|
|
138
|
+
adSizeOverride: com.bigcrunch.ads.models.AdSize? = null,
|
|
139
|
+
refreshCount: Int = 0
|
|
140
|
+
) {
|
|
141
|
+
// Get GAM network code from config
|
|
142
|
+
val gamNetworkCode = configManager.getGamNetworkCode()
|
|
143
|
+
if (gamNetworkCode == null) {
|
|
144
|
+
BCLogger.e(TAG, "GAM network code not available")
|
|
145
|
+
callback.onAdFailedToLoad("Config not loaded")
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Resolve effective size for S2S bid request (adaptive → concrete dimensions)
|
|
150
|
+
val effectiveSize = adSizeOverride ?: placement.sizes?.firstOrNull()
|
|
151
|
+
val resolvedSize: com.bigcrunch.ads.models.AdSize? = if (effectiveSize?.isAdaptive == true) {
|
|
152
|
+
val displayMetrics = context.resources.displayMetrics
|
|
153
|
+
val widthDp = if (effectiveSize.width > 0) effectiveSize.width
|
|
154
|
+
else (displayMetrics.widthPixels / displayMetrics.density).toInt()
|
|
155
|
+
val googleAdSize = com.google.android.gms.ads.AdSize
|
|
156
|
+
.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, widthDp)
|
|
157
|
+
BCLogger.d(TAG, "Resolved adaptive size to ${googleAdSize.width}x${googleAdSize.height}")
|
|
158
|
+
com.bigcrunch.ads.models.AdSize(googleAdSize.width, googleAdSize.height)
|
|
159
|
+
} else {
|
|
160
|
+
effectiveSize
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create ad request builder
|
|
164
|
+
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
165
|
+
|
|
166
|
+
// 3. Fetch S2S demand (even if it fails, continue with Google)
|
|
167
|
+
val targeting = bidRequestClient.fetchDemand(placement)
|
|
168
|
+
if (targeting != null && targeting.isNotEmpty()) {
|
|
169
|
+
for ((key, value) in targeting) {
|
|
170
|
+
adRequestBuilder.addCustomTargeting(key, value)
|
|
171
|
+
}
|
|
172
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
173
|
+
} else {
|
|
174
|
+
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 4. Load Google ad with the (possibly enriched) ad request
|
|
178
|
+
val adRequest = adRequestBuilder.build()
|
|
179
|
+
val adapterCallback = object : BannerAdCallback {
|
|
180
|
+
override fun onAdLoaded() {
|
|
181
|
+
BCLogger.d(TAG, "Banner ad loaded: ${placement.placementId}")
|
|
182
|
+
callback.onAdLoaded()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
override fun onAdFailedToLoad(error: String) {
|
|
186
|
+
BCLogger.w(TAG, "Banner ad failed to load: ${placement.placementId} - $error")
|
|
187
|
+
callback.onAdFailedToLoad(error)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
override fun onAdClicked() {
|
|
191
|
+
BCLogger.d(TAG, "Banner ad clicked: ${placement.placementId}")
|
|
192
|
+
callback.onAdClicked()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
override fun onAdImpression() {
|
|
196
|
+
BCLogger.d(TAG, "Banner ad impression: ${placement.placementId}")
|
|
197
|
+
callback.onAdImpression()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get test ads flag from config
|
|
202
|
+
val useTestAds = configManager.shouldUseTestAds()
|
|
203
|
+
|
|
204
|
+
// Clean up existing banner for refresh
|
|
205
|
+
activeBanners.remove(placement.placementId)?.let { oldView ->
|
|
206
|
+
googleAdsAdapter.destroyBannerAd(oldView)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Load the ad through GoogleAdsAdapter
|
|
210
|
+
val adView = googleAdsAdapter.loadBannerAd(
|
|
211
|
+
placement,
|
|
212
|
+
gamNetworkCode,
|
|
213
|
+
adRequest,
|
|
214
|
+
container,
|
|
215
|
+
adapterCallback,
|
|
216
|
+
useTestAds,
|
|
217
|
+
adSizeOverride,
|
|
218
|
+
refreshCount
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Track the active banner for cleanup
|
|
222
|
+
activeBanners[placement.placementId] = adView
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Destroy a banner ad
|
|
227
|
+
*
|
|
228
|
+
* @param placementId The placement ID of the banner to destroy
|
|
229
|
+
*/
|
|
230
|
+
fun destroyBannerAd(placementId: String) {
|
|
231
|
+
BCLogger.d(TAG, "Destroying banner ad: $placementId")
|
|
232
|
+
activeBanners.remove(placementId)?.let { adView ->
|
|
233
|
+
googleAdsAdapter.destroyBannerAd(adView)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Preload an interstitial ad
|
|
239
|
+
*
|
|
240
|
+
* @param placementId The placement ID from the BigCrunch dashboard
|
|
241
|
+
* @param callback Callback for ad events
|
|
242
|
+
*/
|
|
243
|
+
fun preloadInterstitialAd(
|
|
244
|
+
placementId: String,
|
|
245
|
+
callback: InterstitialCallback
|
|
246
|
+
) {
|
|
247
|
+
BCLogger.d(TAG, "Preloading interstitial ad: $placementId")
|
|
248
|
+
|
|
249
|
+
// 1. Get placement config
|
|
250
|
+
val placement = configManager.getPlacement(placementId)
|
|
251
|
+
if (placement == null) {
|
|
252
|
+
BCLogger.e(TAG, "Placement not found: $placementId")
|
|
253
|
+
callback.onAdFailedToLoad("Placement not found: $placementId")
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (placement.format != "interstitial") {
|
|
258
|
+
BCLogger.e(TAG, "Invalid format for interstitial: ${placement.format}")
|
|
259
|
+
callback.onAdFailedToLoad("Invalid placement format: ${placement.format}")
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 2. Track ad request
|
|
264
|
+
analyticsClient.trackAdRequest(placementId, placement.format)
|
|
265
|
+
|
|
266
|
+
// 3. Start the async ad loading flow
|
|
267
|
+
scope.launch {
|
|
268
|
+
preloadInterstitialAsync(placement, callback)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private suspend fun preloadInterstitialAsync(
|
|
273
|
+
placement: PlacementConfig,
|
|
274
|
+
callback: InterstitialCallback
|
|
275
|
+
) = interstitialMutex.withLock {
|
|
276
|
+
// Check if already cached
|
|
277
|
+
if (interstitialCache.containsKey(placement.placementId)) {
|
|
278
|
+
BCLogger.d(TAG, "Interstitial already cached: ${placement.placementId}")
|
|
279
|
+
callback.onAdLoaded()
|
|
280
|
+
return@withLock
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get GAM network code from config
|
|
284
|
+
val gamNetworkCode = configManager.getGamNetworkCode()
|
|
285
|
+
if (gamNetworkCode == null) {
|
|
286
|
+
BCLogger.e(TAG, "GAM network code not available")
|
|
287
|
+
callback.onAdFailedToLoad("Config not loaded")
|
|
288
|
+
return@withLock
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Create ad request builder
|
|
292
|
+
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
293
|
+
|
|
294
|
+
// 3. Fetch S2S demand
|
|
295
|
+
val targeting = bidRequestClient.fetchDemand(placement)
|
|
296
|
+
if (targeting != null && targeting.isNotEmpty()) {
|
|
297
|
+
for ((key, value) in targeting) {
|
|
298
|
+
adRequestBuilder.addCustomTargeting(key, value)
|
|
299
|
+
}
|
|
300
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
301
|
+
} else {
|
|
302
|
+
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Get test ads flag from config
|
|
306
|
+
val useTestAds = configManager.shouldUseTestAds()
|
|
307
|
+
|
|
308
|
+
// 4. Load Google interstitial ad
|
|
309
|
+
val adRequest = adRequestBuilder.build()
|
|
310
|
+
val result = googleAdsAdapter.loadInterstitialAd(placement, gamNetworkCode, adRequest, useTestAds)
|
|
311
|
+
|
|
312
|
+
when {
|
|
313
|
+
result.isSuccess -> {
|
|
314
|
+
val interstitialAd = result.getOrNull()
|
|
315
|
+
if (interstitialAd == null) {
|
|
316
|
+
BCLogger.e(TAG, "Interstitial ad was null despite success result")
|
|
317
|
+
callback.onAdFailedToLoad("Internal error: ad was null")
|
|
318
|
+
return@withLock
|
|
319
|
+
}
|
|
320
|
+
interstitialCache[placement.placementId] = interstitialAd
|
|
321
|
+
BCLogger.d(TAG, "Interstitial ad preloaded: ${placement.placementId}")
|
|
322
|
+
callback.onAdLoaded()
|
|
323
|
+
}
|
|
324
|
+
else -> {
|
|
325
|
+
val error = result.exceptionOrNull()?.message ?: "Unknown error"
|
|
326
|
+
BCLogger.w(TAG, "Interstitial ad failed to load: ${placement.placementId} - $error")
|
|
327
|
+
callback.onAdFailedToLoad(error)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Show an interstitial ad
|
|
334
|
+
*
|
|
335
|
+
* @param activity The activity to show the interstitial from
|
|
336
|
+
* @param placementId The placement ID from the BigCrunch dashboard
|
|
337
|
+
* @param callback Callback for ad events
|
|
338
|
+
* @return true if an ad was available and shown, false otherwise
|
|
339
|
+
*/
|
|
340
|
+
fun showInterstitialAd(
|
|
341
|
+
activity: Activity,
|
|
342
|
+
placementId: String,
|
|
343
|
+
callback: InterstitialCallback
|
|
344
|
+
): Boolean {
|
|
345
|
+
BCLogger.d(TAG, "Showing interstitial ad: $placementId")
|
|
346
|
+
|
|
347
|
+
// Get placement config
|
|
348
|
+
val placement = configManager.getPlacement(placementId)
|
|
349
|
+
if (placement == null) {
|
|
350
|
+
BCLogger.e(TAG, "Placement not found: $placementId")
|
|
351
|
+
callback.onAdFailedToLoad("Placement not found: $placementId")
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Get cached interstitial
|
|
356
|
+
val interstitialAd = interstitialCache.remove(placementId)
|
|
357
|
+
if (interstitialAd == null) {
|
|
358
|
+
BCLogger.w(TAG, "No preloaded interstitial for: $placementId")
|
|
359
|
+
callback.onAdFailedToLoad("No preloaded ad available")
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Create adapter callback
|
|
364
|
+
val adapterCallback = object : InterstitialAdCallback {
|
|
365
|
+
override fun onAdLoaded() {
|
|
366
|
+
// Already loaded, this won't be called during show
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
override fun onAdFailedToLoad(error: String) {
|
|
370
|
+
callback.onAdFailedToLoad(error)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
override fun onAdShowed() {
|
|
374
|
+
callback.onAdShowed()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
override fun onAdDismissed() {
|
|
378
|
+
callback.onAdDismissed()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
override fun onAdClicked() {
|
|
382
|
+
callback.onAdClicked()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
override fun onAdImpression() {
|
|
386
|
+
// Handled by GoogleAdsAdapter analytics tracking
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Show the ad
|
|
391
|
+
googleAdsAdapter.showInterstitialAd(
|
|
392
|
+
interstitialAd,
|
|
393
|
+
activity,
|
|
394
|
+
placement,
|
|
395
|
+
adapterCallback
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check if an interstitial ad is ready to show
|
|
403
|
+
*
|
|
404
|
+
* @param placementId The placement ID to check
|
|
405
|
+
* @return true if an ad is preloaded and ready
|
|
406
|
+
*/
|
|
407
|
+
fun isInterstitialReady(placementId: String): Boolean {
|
|
408
|
+
return interstitialCache.containsKey(placementId)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Preload a rewarded ad
|
|
413
|
+
*
|
|
414
|
+
* @param placementId The placement ID from the BigCrunch dashboard
|
|
415
|
+
* @param callback Callback for ad events
|
|
416
|
+
*/
|
|
417
|
+
fun preloadRewardedAd(
|
|
418
|
+
placementId: String,
|
|
419
|
+
callback: RewardedCallback
|
|
420
|
+
) {
|
|
421
|
+
BCLogger.d(TAG, "Preloading rewarded ad: $placementId")
|
|
422
|
+
|
|
423
|
+
// 1. Get placement config
|
|
424
|
+
val placement = configManager.getPlacement(placementId)
|
|
425
|
+
if (placement == null) {
|
|
426
|
+
BCLogger.e(TAG, "Placement not found: $placementId")
|
|
427
|
+
callback.onAdFailedToLoad("Placement not found: $placementId")
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (placement.format != "rewarded") {
|
|
432
|
+
BCLogger.e(TAG, "Invalid format for rewarded: ${placement.format}")
|
|
433
|
+
callback.onAdFailedToLoad("Invalid placement format: ${placement.format}")
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. Track ad request
|
|
438
|
+
analyticsClient.trackAdRequest(placementId, placement.format)
|
|
439
|
+
|
|
440
|
+
// 3. Start the async ad loading flow
|
|
441
|
+
scope.launch {
|
|
442
|
+
preloadRewardedAsync(placement, callback)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private suspend fun preloadRewardedAsync(
|
|
447
|
+
placement: PlacementConfig,
|
|
448
|
+
callback: RewardedCallback
|
|
449
|
+
) = rewardedMutex.withLock {
|
|
450
|
+
// Check if already cached
|
|
451
|
+
if (rewardedCache.containsKey(placement.placementId)) {
|
|
452
|
+
BCLogger.d(TAG, "Rewarded ad already cached: ${placement.placementId}")
|
|
453
|
+
callback.onAdLoaded()
|
|
454
|
+
return@withLock
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get GAM network code from config
|
|
458
|
+
val gamNetworkCode = configManager.getGamNetworkCode()
|
|
459
|
+
if (gamNetworkCode == null) {
|
|
460
|
+
BCLogger.e(TAG, "GAM network code not available")
|
|
461
|
+
callback.onAdFailedToLoad("Config not loaded")
|
|
462
|
+
return@withLock
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Create ad request builder
|
|
466
|
+
val adRequestBuilder = AdManagerAdRequest.Builder()
|
|
467
|
+
|
|
468
|
+
// 3. Fetch S2S demand
|
|
469
|
+
val targeting = bidRequestClient.fetchDemand(placement)
|
|
470
|
+
if (targeting != null && targeting.isNotEmpty()) {
|
|
471
|
+
for ((key, value) in targeting) {
|
|
472
|
+
adRequestBuilder.addCustomTargeting(key, value)
|
|
473
|
+
}
|
|
474
|
+
BCLogger.d(TAG, "S2S demand fetched for: ${placement.placementId} (${targeting.size} keys)")
|
|
475
|
+
} else {
|
|
476
|
+
BCLogger.w(TAG, "S2S demand fetch returned no targeting, continuing with Google only")
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Get test ads flag from config
|
|
480
|
+
val useTestAds = configManager.shouldUseTestAds()
|
|
481
|
+
|
|
482
|
+
// 4. Load Google rewarded ad
|
|
483
|
+
val adRequest = adRequestBuilder.build()
|
|
484
|
+
val result = googleAdsAdapter.loadRewardedAd(placement, gamNetworkCode, adRequest, useTestAds)
|
|
485
|
+
|
|
486
|
+
when {
|
|
487
|
+
result.isSuccess -> {
|
|
488
|
+
val rewardedAd = result.getOrNull()
|
|
489
|
+
if (rewardedAd == null) {
|
|
490
|
+
BCLogger.e(TAG, "Rewarded ad was null despite success result")
|
|
491
|
+
callback.onAdFailedToLoad("Internal error: ad was null")
|
|
492
|
+
return@withLock
|
|
493
|
+
}
|
|
494
|
+
rewardedCache[placement.placementId] = rewardedAd
|
|
495
|
+
BCLogger.d(TAG, "Rewarded ad preloaded: ${placement.placementId}")
|
|
496
|
+
callback.onAdLoaded()
|
|
497
|
+
}
|
|
498
|
+
else -> {
|
|
499
|
+
val error = result.exceptionOrNull()?.message ?: "Unknown error"
|
|
500
|
+
BCLogger.w(TAG, "Rewarded ad failed to load: ${placement.placementId} - $error")
|
|
501
|
+
callback.onAdFailedToLoad(error)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Show a rewarded ad
|
|
508
|
+
*
|
|
509
|
+
* @param activity The activity to show the rewarded ad from
|
|
510
|
+
* @param placementId The placement ID from the BigCrunch dashboard
|
|
511
|
+
* @param callback Callback for ad events
|
|
512
|
+
* @return true if an ad was available and shown, false otherwise
|
|
513
|
+
*/
|
|
514
|
+
fun showRewardedAd(
|
|
515
|
+
activity: Activity,
|
|
516
|
+
placementId: String,
|
|
517
|
+
callback: RewardedCallback
|
|
518
|
+
): Boolean {
|
|
519
|
+
BCLogger.d(TAG, "Showing rewarded ad: $placementId")
|
|
520
|
+
|
|
521
|
+
// Get placement config
|
|
522
|
+
val placement = configManager.getPlacement(placementId)
|
|
523
|
+
if (placement == null) {
|
|
524
|
+
BCLogger.e(TAG, "Placement not found: $placementId")
|
|
525
|
+
callback.onAdFailedToLoad("Placement not found: $placementId")
|
|
526
|
+
return false
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Get cached rewarded ad
|
|
530
|
+
val rewardedAd = rewardedCache.remove(placementId)
|
|
531
|
+
if (rewardedAd == null) {
|
|
532
|
+
BCLogger.w(TAG, "No preloaded rewarded ad for: $placementId")
|
|
533
|
+
callback.onAdFailedToLoad("No preloaded ad available")
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Create adapter callback
|
|
538
|
+
val adapterCallback = object : RewardedAdCallback {
|
|
539
|
+
override fun onAdLoaded() {
|
|
540
|
+
// Already loaded, this won't be called during show
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
override fun onAdFailedToLoad(error: String) {
|
|
544
|
+
callback.onAdFailedToLoad(error)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
override fun onAdShowed() {
|
|
548
|
+
callback.onAdShowed()
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
override fun onAdDismissed() {
|
|
552
|
+
callback.onAdDismissed()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
override fun onAdClicked() {
|
|
556
|
+
callback.onAdClicked()
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
override fun onUserEarnedReward(type: String, amount: Int) {
|
|
560
|
+
callback.onUserEarnedReward(type, amount)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
override fun onAdImpression() {
|
|
564
|
+
// Handled by GoogleAdsAdapter analytics tracking
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Show the ad
|
|
569
|
+
googleAdsAdapter.showRewardedAd(
|
|
570
|
+
rewardedAd,
|
|
571
|
+
activity,
|
|
572
|
+
placement,
|
|
573
|
+
adapterCallback
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
return true
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Check if a rewarded ad is ready to show
|
|
581
|
+
*
|
|
582
|
+
* @param placementId The placement ID to check
|
|
583
|
+
* @return true if an ad is preloaded and ready
|
|
584
|
+
*/
|
|
585
|
+
fun isRewardedReady(placementId: String): Boolean {
|
|
586
|
+
return rewardedCache.containsKey(placementId)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Destroy a specific interstitial ad
|
|
591
|
+
*
|
|
592
|
+
* @param placementId The placement ID to destroy
|
|
593
|
+
*/
|
|
594
|
+
fun destroyInterstitialAd(placementId: String) {
|
|
595
|
+
BCLogger.d(TAG, "Destroying interstitial: $placementId")
|
|
596
|
+
interstitialCache.remove(placementId)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Destroy a specific rewarded ad
|
|
601
|
+
*
|
|
602
|
+
* @param placementId The placement ID to destroy
|
|
603
|
+
*/
|
|
604
|
+
fun destroyRewardedAd(placementId: String) {
|
|
605
|
+
BCLogger.d(TAG, "Destroying rewarded ad: $placementId")
|
|
606
|
+
rewardedCache.remove(placementId)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Clear all cached ads
|
|
611
|
+
*/
|
|
612
|
+
fun clearCache() {
|
|
613
|
+
BCLogger.d(TAG, "Clearing ad cache")
|
|
614
|
+
interstitialCache.clear()
|
|
615
|
+
rewardedCache.clear()
|
|
616
|
+
|
|
617
|
+
// Destroy all active banners
|
|
618
|
+
activeBanners.values.forEach { adView ->
|
|
619
|
+
googleAdsAdapter.destroyBannerAd(adView)
|
|
620
|
+
}
|
|
621
|
+
activeBanners.clear()
|
|
622
|
+
}
|
|
623
|
+
}
|