@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,330 @@
|
|
|
1
|
+
package com.bigcrunch.ads.core
|
|
2
|
+
|
|
3
|
+
import com.bigcrunch.ads.internal.BCLogger
|
|
4
|
+
import com.bigcrunch.ads.internal.KeyValueStore
|
|
5
|
+
import java.text.SimpleDateFormat
|
|
6
|
+
import java.util.Date
|
|
7
|
+
import java.util.Locale
|
|
8
|
+
import java.util.TimeZone
|
|
9
|
+
import java.util.UUID
|
|
10
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
11
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
12
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* SessionManager handles session and user identification for analytics
|
|
16
|
+
*
|
|
17
|
+
* Manages:
|
|
18
|
+
* - user_id: Persistent UUID stored across app launches (identifies a user/device)
|
|
19
|
+
* - session_id: New UUID generated per app launch (identifies a session)
|
|
20
|
+
* - session_depth: Counter incremented per screen view within a session
|
|
21
|
+
* - page_id: New UUID generated per screen/page view
|
|
22
|
+
*
|
|
23
|
+
* Thread-safe implementation ensures consistent IDs across all analytics events.
|
|
24
|
+
*/
|
|
25
|
+
internal class SessionManager private constructor(private val storage: KeyValueStore) {
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
private const val TAG = "SessionManager"
|
|
29
|
+
|
|
30
|
+
private const val KEY_USER_ID = "user_id"
|
|
31
|
+
private const val KEY_SESSION_COUNT = "session_count"
|
|
32
|
+
private const val KEY_SESSION_START_TIME = "session_start_time"
|
|
33
|
+
private const val KEY_UTM_SOURCE = "utm_source"
|
|
34
|
+
private const val KEY_UTM_MEDIUM = "utm_medium"
|
|
35
|
+
private const val KEY_UTM_CAMPAIGN = "utm_campaign"
|
|
36
|
+
private const val KEY_UTM_TERM = "utm_term"
|
|
37
|
+
private const val KEY_UTM_CONTENT = "utm_content"
|
|
38
|
+
|
|
39
|
+
@Volatile
|
|
40
|
+
private var instance: SessionManager? = null
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize the SessionManager singleton
|
|
44
|
+
*
|
|
45
|
+
* Must be called once during SDK initialization.
|
|
46
|
+
*
|
|
47
|
+
* @param storage Key-value storage for persistence
|
|
48
|
+
*/
|
|
49
|
+
fun initialize(storage: KeyValueStore) {
|
|
50
|
+
if (instance == null) {
|
|
51
|
+
synchronized(this) {
|
|
52
|
+
if (instance == null) {
|
|
53
|
+
instance = SessionManager(storage)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the SessionManager instance
|
|
61
|
+
*
|
|
62
|
+
* @return The singleton instance
|
|
63
|
+
* @throws IllegalStateException if not initialized
|
|
64
|
+
*/
|
|
65
|
+
fun getInstance(): SessionManager {
|
|
66
|
+
return instance ?: throw IllegalStateException(
|
|
67
|
+
"SessionManager not initialized. Call SessionManager.initialize() first."
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Static accessors for tracking counters
|
|
72
|
+
fun getSessionId(): String = getInstance().sessionId
|
|
73
|
+
fun getSessionStartTime(): String = getInstance().sessionStartTime
|
|
74
|
+
fun getScreenViewCount(): Int = getInstance().screenViewCounter.get()
|
|
75
|
+
fun getAdRequestCount(): Int = getInstance().adRequestCounter.get()
|
|
76
|
+
fun getAdImpressionCount(): Int = getInstance().adImpressionCounter.get()
|
|
77
|
+
fun getTotalRevenueMicros(): Long = getInstance().totalRevenueMicrosCounter.get()
|
|
78
|
+
|
|
79
|
+
fun incrementScreenViewCount() {
|
|
80
|
+
getInstance().screenViewCounter.incrementAndGet()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fun incrementAdRequestCount() {
|
|
84
|
+
getInstance().adRequestCounter.incrementAndGet()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fun incrementAdImpressionCount() {
|
|
88
|
+
getInstance().adImpressionCounter.incrementAndGet()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fun addRevenueMicros(micros: Long) {
|
|
92
|
+
getInstance().totalRevenueMicrosCounter.addAndGet(micros)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun startNewSession() {
|
|
96
|
+
getInstance().resetSessionCounters()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reset the singleton instance (for testing only)
|
|
101
|
+
*/
|
|
102
|
+
internal fun resetForTesting() {
|
|
103
|
+
synchronized(this) {
|
|
104
|
+
instance?.resetState()
|
|
105
|
+
instance = null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Properties
|
|
111
|
+
|
|
112
|
+
/** Persistent user ID (stored across app launches) */
|
|
113
|
+
val userId: String
|
|
114
|
+
|
|
115
|
+
/** Current session ID (new per app launch) */
|
|
116
|
+
val sessionId: String
|
|
117
|
+
|
|
118
|
+
/** Session start time (ISO 8601 string) */
|
|
119
|
+
val sessionStartTime: String
|
|
120
|
+
|
|
121
|
+
/** Total number of sessions for this user (across all time) */
|
|
122
|
+
val totalSessionCount: Int
|
|
123
|
+
|
|
124
|
+
/** True if this is the user's first session */
|
|
125
|
+
val isNewUser: Boolean
|
|
126
|
+
get() = totalSessionCount == 1
|
|
127
|
+
|
|
128
|
+
/** UTM parameters (from deep links or attribution) */
|
|
129
|
+
var utmSource: String? = null
|
|
130
|
+
private set
|
|
131
|
+
var utmMedium: String? = null
|
|
132
|
+
private set
|
|
133
|
+
var utmCampaign: String? = null
|
|
134
|
+
private set
|
|
135
|
+
var utmTerm: String? = null
|
|
136
|
+
private set
|
|
137
|
+
var utmContent: String? = null
|
|
138
|
+
private set
|
|
139
|
+
|
|
140
|
+
/** Session attribution source (derived from UTM or defaults to "direct") */
|
|
141
|
+
val sessionSource: String
|
|
142
|
+
get() = utmSource ?: "direct"
|
|
143
|
+
|
|
144
|
+
/** Session attribution medium (derived from UTM or defaults to "none") */
|
|
145
|
+
val sessionMedium: String
|
|
146
|
+
get() = utmMedium ?: "none"
|
|
147
|
+
|
|
148
|
+
/** Number of page views in current session */
|
|
149
|
+
private val sessionDepthCounter = AtomicInteger(0)
|
|
150
|
+
val sessionDepth: Int
|
|
151
|
+
get() = sessionDepthCounter.get()
|
|
152
|
+
|
|
153
|
+
/** Current page ID (new per page/screen view) */
|
|
154
|
+
private val currentPageIdRef = AtomicReference<String?>(null)
|
|
155
|
+
val currentPageId: String?
|
|
156
|
+
get() = currentPageIdRef.get()
|
|
157
|
+
|
|
158
|
+
/** Current screen name (for use in impression events) */
|
|
159
|
+
private val currentScreenNameRef = AtomicReference<String?>(null)
|
|
160
|
+
val currentScreenName: String?
|
|
161
|
+
get() = currentScreenNameRef.get()
|
|
162
|
+
|
|
163
|
+
// Analytics tracking counters
|
|
164
|
+
internal val screenViewCounter = AtomicInteger(0)
|
|
165
|
+
internal val adRequestCounter = AtomicInteger(0)
|
|
166
|
+
internal val adImpressionCounter = AtomicInteger(0)
|
|
167
|
+
internal val totalRevenueMicrosCounter = AtomicLong(0)
|
|
168
|
+
|
|
169
|
+
init {
|
|
170
|
+
// Load or generate user_id
|
|
171
|
+
val existingUserId = storage.getString(KEY_USER_ID)
|
|
172
|
+
if (existingUserId != null) {
|
|
173
|
+
userId = existingUserId
|
|
174
|
+
BCLogger.d(TAG, "Loaded existing user_id: $existingUserId")
|
|
175
|
+
} else {
|
|
176
|
+
val newUserId = UUID.randomUUID().toString()
|
|
177
|
+
storage.putString(KEY_USER_ID, newUserId)
|
|
178
|
+
userId = newUserId
|
|
179
|
+
BCLogger.d(TAG, "Generated new user_id: $newUserId")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Load session count and increment
|
|
183
|
+
val sessionCountStr = storage.getString(KEY_SESSION_COUNT) ?: "0"
|
|
184
|
+
val previousCount = sessionCountStr.toIntOrNull() ?: 0
|
|
185
|
+
totalSessionCount = previousCount + 1
|
|
186
|
+
storage.putString(KEY_SESSION_COUNT, totalSessionCount.toString())
|
|
187
|
+
|
|
188
|
+
// Generate new session_id for this app launch
|
|
189
|
+
sessionId = UUID.randomUUID().toString()
|
|
190
|
+
|
|
191
|
+
// Set session start time (ISO 8601 format)
|
|
192
|
+
sessionStartTime = getCurrentTimestamp()
|
|
193
|
+
storage.putString(KEY_SESSION_START_TIME, sessionStartTime)
|
|
194
|
+
|
|
195
|
+
// Load UTM parameters (if set from deep link)
|
|
196
|
+
utmSource = storage.getString(KEY_UTM_SOURCE)
|
|
197
|
+
utmMedium = storage.getString(KEY_UTM_MEDIUM)
|
|
198
|
+
utmCampaign = storage.getString(KEY_UTM_CAMPAIGN)
|
|
199
|
+
utmTerm = storage.getString(KEY_UTM_TERM)
|
|
200
|
+
utmContent = storage.getString(KEY_UTM_CONTENT)
|
|
201
|
+
|
|
202
|
+
BCLogger.d(TAG, "Started session $totalSessionCount with session_id: $sessionId")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Page Tracking
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start a new page view
|
|
209
|
+
*
|
|
210
|
+
* Generates a new page_id and increments session_depth.
|
|
211
|
+
* Should be called at the start of each screen/page view.
|
|
212
|
+
*
|
|
213
|
+
* @param screenName Optional screen name to store for impression tracking
|
|
214
|
+
* @return The new page_id
|
|
215
|
+
*/
|
|
216
|
+
fun startPageView(screenName: String? = null): String {
|
|
217
|
+
val depth = sessionDepthCounter.incrementAndGet()
|
|
218
|
+
val pageId = UUID.randomUUID().toString()
|
|
219
|
+
currentPageIdRef.set(pageId)
|
|
220
|
+
screenName?.let { currentScreenNameRef.set(it) }
|
|
221
|
+
|
|
222
|
+
BCLogger.d(TAG, "Started page view $depth with page_id: $pageId, screenName: $screenName")
|
|
223
|
+
|
|
224
|
+
return pageId
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the current page ID, generating one if needed
|
|
229
|
+
*
|
|
230
|
+
* @return The current page_id (creates one if none exists)
|
|
231
|
+
*/
|
|
232
|
+
fun getOrCreatePageId(): String {
|
|
233
|
+
var pageId = currentPageIdRef.get()
|
|
234
|
+
if (pageId == null) {
|
|
235
|
+
// Auto-generate if no page view has been started
|
|
236
|
+
sessionDepthCounter.incrementAndGet()
|
|
237
|
+
pageId = UUID.randomUUID().toString()
|
|
238
|
+
currentPageIdRef.set(pageId)
|
|
239
|
+
}
|
|
240
|
+
return pageId
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// MARK: - Reset
|
|
244
|
+
|
|
245
|
+
/** Reset session counters (when starting a new session) */
|
|
246
|
+
internal fun resetSessionCounters() {
|
|
247
|
+
screenViewCounter.set(0)
|
|
248
|
+
adRequestCounter.set(0)
|
|
249
|
+
adImpressionCounter.set(0)
|
|
250
|
+
totalRevenueMicrosCounter.set(0)
|
|
251
|
+
sessionDepthCounter.set(0)
|
|
252
|
+
currentPageIdRef.set(null)
|
|
253
|
+
BCLogger.d(TAG, "Session counters reset")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Reset all session state (for testing only) */
|
|
257
|
+
private fun resetState() {
|
|
258
|
+
storage.clear()
|
|
259
|
+
sessionDepthCounter.set(0)
|
|
260
|
+
currentPageIdRef.set(null)
|
|
261
|
+
resetSessionCounters()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// MARK: - UTM Parameter Management
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Set UTM parameters (typically from deep link)
|
|
268
|
+
*
|
|
269
|
+
* This persists UTM parameters for attribution tracking. These will be
|
|
270
|
+
* loaded for future sessions until cleared.
|
|
271
|
+
*
|
|
272
|
+
* @param source UTM source (e.g., "google", "facebook")
|
|
273
|
+
* @param medium UTM medium (e.g., "cpc", "email")
|
|
274
|
+
* @param campaign UTM campaign (e.g., "summer_sale")
|
|
275
|
+
* @param term UTM term (optional keyword)
|
|
276
|
+
* @param content UTM content (optional content variant)
|
|
277
|
+
*/
|
|
278
|
+
fun setUTMParameters(
|
|
279
|
+
source: String? = null,
|
|
280
|
+
medium: String? = null,
|
|
281
|
+
campaign: String? = null,
|
|
282
|
+
term: String? = null,
|
|
283
|
+
content: String? = null
|
|
284
|
+
) {
|
|
285
|
+
utmSource = source
|
|
286
|
+
utmMedium = medium
|
|
287
|
+
utmCampaign = campaign
|
|
288
|
+
utmTerm = term
|
|
289
|
+
utmContent = content
|
|
290
|
+
|
|
291
|
+
// Persist to storage
|
|
292
|
+
source?.let { storage.putString(KEY_UTM_SOURCE, it) }
|
|
293
|
+
medium?.let { storage.putString(KEY_UTM_MEDIUM, it) }
|
|
294
|
+
campaign?.let { storage.putString(KEY_UTM_CAMPAIGN, it) }
|
|
295
|
+
term?.let { storage.putString(KEY_UTM_TERM, it) }
|
|
296
|
+
content?.let { storage.putString(KEY_UTM_CONTENT, it) }
|
|
297
|
+
|
|
298
|
+
BCLogger.d(TAG, "Set UTM parameters - source: $source, medium: $medium, campaign: $campaign")
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Clear UTM parameters
|
|
303
|
+
*/
|
|
304
|
+
fun clearUTMParameters() {
|
|
305
|
+
utmSource = null
|
|
306
|
+
utmMedium = null
|
|
307
|
+
utmCampaign = null
|
|
308
|
+
utmTerm = null
|
|
309
|
+
utmContent = null
|
|
310
|
+
|
|
311
|
+
storage.remove(KEY_UTM_SOURCE)
|
|
312
|
+
storage.remove(KEY_UTM_MEDIUM)
|
|
313
|
+
storage.remove(KEY_UTM_CAMPAIGN)
|
|
314
|
+
storage.remove(KEY_UTM_TERM)
|
|
315
|
+
storage.remove(KEY_UTM_CONTENT)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// MARK: - Timestamp Utilities
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get current timestamp in ISO 8601 format with milliseconds
|
|
322
|
+
*
|
|
323
|
+
* @return ISO 8601 timestamp string (e.g., "2025-01-15T10:30:00.000Z")
|
|
324
|
+
*/
|
|
325
|
+
fun getCurrentTimestamp(): String {
|
|
326
|
+
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
327
|
+
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
328
|
+
return dateFormat.format(Date())
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.bigcrunch.ads.internal
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.provider.Settings
|
|
6
|
+
import android.telephony.TelephonyManager
|
|
7
|
+
import android.util.DisplayMetrics
|
|
8
|
+
import com.bigcrunch.ads.models.DeviceData
|
|
9
|
+
import java.util.*
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper class to get device information
|
|
13
|
+
*/
|
|
14
|
+
internal object DeviceHelper {
|
|
15
|
+
|
|
16
|
+
fun getDeviceData(context: Context): DeviceData {
|
|
17
|
+
val displayMetrics = context.resources.displayMetrics
|
|
18
|
+
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
19
|
+
|
|
20
|
+
return DeviceData(
|
|
21
|
+
deviceId = getDeviceId(context),
|
|
22
|
+
deviceModel = "${Build.MANUFACTURER} ${Build.MODEL}",
|
|
23
|
+
osVersion = Build.VERSION.RELEASE,
|
|
24
|
+
appVersion = getAppVersion(context),
|
|
25
|
+
screenWidth = displayMetrics.widthPixels,
|
|
26
|
+
screenHeight = displayMetrics.heightPixels,
|
|
27
|
+
language = Locale.getDefault().language,
|
|
28
|
+
country = Locale.getDefault().country,
|
|
29
|
+
carrier = telephonyManager?.networkOperatorName,
|
|
30
|
+
networkType = getNetworkType(context),
|
|
31
|
+
isTablet = isTablet(context)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun getDeviceId(context: Context): String {
|
|
36
|
+
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "unknown"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private fun getAppVersion(context: Context): String {
|
|
40
|
+
return try {
|
|
41
|
+
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
|
42
|
+
packageInfo.versionName ?: "unknown"
|
|
43
|
+
} catch (e: Exception) {
|
|
44
|
+
"unknown"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun getNetworkType(context: Context): String? {
|
|
49
|
+
// TODO: Implement network type detection
|
|
50
|
+
return "unknown"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun isTablet(context: Context): Boolean {
|
|
54
|
+
val displayMetrics = context.resources.displayMetrics
|
|
55
|
+
val widthDp = displayMetrics.widthPixels / displayMetrics.density
|
|
56
|
+
val heightDp = displayMetrics.heightPixels / displayMetrics.density
|
|
57
|
+
val screenSize = Math.sqrt((widthDp * widthDp + heightDp * heightDp).toDouble())
|
|
58
|
+
return screenSize >= 600
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package com.bigcrunch.ads.internal
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.Dispatchers
|
|
4
|
+
import kotlinx.coroutines.withContext
|
|
5
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
6
|
+
import okhttp3.OkHttpClient
|
|
7
|
+
import okhttp3.Request
|
|
8
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
9
|
+
import java.util.concurrent.TimeUnit
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTTP client for making network requests
|
|
13
|
+
*
|
|
14
|
+
* Uses OkHttp for reliable networking with automatic retries and connection pooling.
|
|
15
|
+
* All methods are suspend functions that run on the IO dispatcher.
|
|
16
|
+
*/
|
|
17
|
+
internal class HttpClient {
|
|
18
|
+
|
|
19
|
+
private val client = OkHttpClient.Builder()
|
|
20
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
21
|
+
.readTimeout(10, TimeUnit.SECONDS)
|
|
22
|
+
.writeTimeout(10, TimeUnit.SECONDS)
|
|
23
|
+
.build()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Perform a GET request
|
|
27
|
+
*
|
|
28
|
+
* @param url The URL to fetch
|
|
29
|
+
* @param headers Optional HTTP headers
|
|
30
|
+
* @return Result containing response body or error
|
|
31
|
+
*/
|
|
32
|
+
suspend fun get(
|
|
33
|
+
url: String,
|
|
34
|
+
headers: Map<String, String> = emptyMap()
|
|
35
|
+
): Result<String> {
|
|
36
|
+
return withContext(Dispatchers.IO) {
|
|
37
|
+
try {
|
|
38
|
+
val requestBuilder = Request.Builder().url(url)
|
|
39
|
+
|
|
40
|
+
headers.forEach { (key, value) ->
|
|
41
|
+
requestBuilder.addHeader(key, value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val response = client.newCall(requestBuilder.build()).execute()
|
|
45
|
+
|
|
46
|
+
if (response.isSuccessful) {
|
|
47
|
+
val body = response.body?.string() ?: ""
|
|
48
|
+
BCLogger.v("HttpClient", "GET success: $url (${body.length} bytes)")
|
|
49
|
+
Result.success(body)
|
|
50
|
+
} else {
|
|
51
|
+
val error = "HTTP ${response.code}: ${response.message}"
|
|
52
|
+
BCLogger.w("HttpClient", "GET failed: $url - $error")
|
|
53
|
+
Result.failure(Exception(error))
|
|
54
|
+
}
|
|
55
|
+
} catch (e: Exception) {
|
|
56
|
+
BCLogger.e("HttpClient", "GET request failed: $url", e)
|
|
57
|
+
Result.failure(e)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Perform a POST request
|
|
64
|
+
*
|
|
65
|
+
* @param url The URL to post to
|
|
66
|
+
* @param body Request body (JSON string)
|
|
67
|
+
* @param headers Optional HTTP headers
|
|
68
|
+
* @return Result containing response body or error
|
|
69
|
+
*/
|
|
70
|
+
suspend fun post(
|
|
71
|
+
url: String,
|
|
72
|
+
body: String,
|
|
73
|
+
headers: Map<String, String> = emptyMap()
|
|
74
|
+
): Result<String> {
|
|
75
|
+
return withContext(Dispatchers.IO) {
|
|
76
|
+
try {
|
|
77
|
+
// Log the request body for debugging
|
|
78
|
+
BCLogger.d("HttpClient", "POST to: $url")
|
|
79
|
+
BCLogger.d("HttpClient", "Request body: $body")
|
|
80
|
+
|
|
81
|
+
val mediaType = "application/json".toMediaType()
|
|
82
|
+
val requestBody = body.toRequestBody(mediaType)
|
|
83
|
+
|
|
84
|
+
val requestBuilder = Request.Builder()
|
|
85
|
+
.url(url)
|
|
86
|
+
.post(requestBody)
|
|
87
|
+
|
|
88
|
+
headers.forEach { (key, value) ->
|
|
89
|
+
requestBuilder.addHeader(key, value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
val response = client.newCall(requestBuilder.build()).execute()
|
|
93
|
+
|
|
94
|
+
if (response.isSuccessful) {
|
|
95
|
+
val responseBody = response.body?.string() ?: ""
|
|
96
|
+
BCLogger.v("HttpClient", "POST success: $url")
|
|
97
|
+
Result.success(responseBody)
|
|
98
|
+
} else {
|
|
99
|
+
// Log response body for 4xx errors to see what the server says
|
|
100
|
+
val errorBody = response.body?.string() ?: ""
|
|
101
|
+
val error = "HTTP ${response.code}: ${response.message}"
|
|
102
|
+
BCLogger.w("HttpClient", "POST failed: $url - $error")
|
|
103
|
+
if (errorBody.isNotEmpty()) {
|
|
104
|
+
BCLogger.w("HttpClient", "Error response body: $errorBody")
|
|
105
|
+
}
|
|
106
|
+
Result.failure(Exception(error))
|
|
107
|
+
}
|
|
108
|
+
} catch (e: Exception) {
|
|
109
|
+
BCLogger.e("HttpClient", "POST request failed: $url", e)
|
|
110
|
+
Result.failure(e)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.bigcrunch.ads.internal
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal logger for BigCrunch Ads SDK
|
|
7
|
+
*
|
|
8
|
+
* All logs are prefixed with "BCrunch:" for easy filtering.
|
|
9
|
+
* Logging can be disabled in production by setting isEnabled = false.
|
|
10
|
+
* Error logs are always shown regardless of isEnabled flag.
|
|
11
|
+
*/
|
|
12
|
+
internal object BCLogger {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enable/disable debug logging
|
|
16
|
+
* Defaults to false for production builds
|
|
17
|
+
*/
|
|
18
|
+
var isEnabled = false
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verbose log - detailed debugging information
|
|
22
|
+
*/
|
|
23
|
+
fun v(tag: String, msg: String) {
|
|
24
|
+
if (isEnabled) {
|
|
25
|
+
Log.v("BCrunch:$tag", msg)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Debug log - general debugging information
|
|
31
|
+
*/
|
|
32
|
+
fun d(tag: String, msg: String) {
|
|
33
|
+
if (isEnabled) {
|
|
34
|
+
Log.d("BCrunch:$tag", msg)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Info log - informational messages
|
|
40
|
+
*/
|
|
41
|
+
fun i(tag: String, msg: String) {
|
|
42
|
+
if (isEnabled) {
|
|
43
|
+
Log.i("BCrunch:$tag", msg)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Warning log - potential issues
|
|
49
|
+
*/
|
|
50
|
+
fun w(tag: String, msg: String, throwable: Throwable? = null) {
|
|
51
|
+
if (isEnabled) {
|
|
52
|
+
if (throwable != null) {
|
|
53
|
+
Log.w("BCrunch:$tag", msg, throwable)
|
|
54
|
+
} else {
|
|
55
|
+
Log.w("BCrunch:$tag", msg)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Error log - critical errors
|
|
62
|
+
* Always logs regardless of isEnabled flag
|
|
63
|
+
*/
|
|
64
|
+
fun e(tag: String, msg: String, throwable: Throwable? = null) {
|
|
65
|
+
if (throwable != null) {
|
|
66
|
+
Log.e("BCrunch:$tag", msg, throwable)
|
|
67
|
+
} else {
|
|
68
|
+
Log.e("BCrunch:$tag", msg)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package com.bigcrunch.ads.internal
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.preference.PreferenceManager
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Centralized privacy signal store
|
|
8
|
+
*
|
|
9
|
+
* Manages GDPR, CCPA, and COPPA privacy signals for the SDK.
|
|
10
|
+
* Reads IAB TCF/USP standard keys from SharedPreferences when available,
|
|
11
|
+
* and provides structured objects for bid request construction.
|
|
12
|
+
*/
|
|
13
|
+
internal class PrivacyStore(private val context: Context) {
|
|
14
|
+
|
|
15
|
+
private val lock = Any()
|
|
16
|
+
|
|
17
|
+
// SDK-set values (from public API)
|
|
18
|
+
@Volatile private var _gdprConsentString: String? = null
|
|
19
|
+
@Volatile private var _gdprApplies: Boolean? = null
|
|
20
|
+
@Volatile private var _ccpaString: String? = null
|
|
21
|
+
@Volatile private var _coppaApplies: Boolean = false
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
// IAB standard SharedPreferences keys
|
|
25
|
+
private const val IAB_TCF_CONSENT_KEY = "IABTCF_TCString"
|
|
26
|
+
private const val IAB_TCF_GDPR_APPLIES_KEY = "IABTCF_gdprApplies"
|
|
27
|
+
private const val IAB_USP_STRING_KEY = "IABUSPrivacy_String"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - Setters (called from public API)
|
|
31
|
+
|
|
32
|
+
fun setGdprConsent(consent: String) {
|
|
33
|
+
synchronized(lock) {
|
|
34
|
+
_gdprApplies = true
|
|
35
|
+
_gdprConsentString = consent
|
|
36
|
+
}
|
|
37
|
+
BCLogger.d("PrivacyStore", "GDPR consent set")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun setCcpaString(ccpa: String) {
|
|
41
|
+
synchronized(lock) {
|
|
42
|
+
_ccpaString = ccpa
|
|
43
|
+
}
|
|
44
|
+
// Also write to IAB standard key for CMP interop
|
|
45
|
+
PreferenceManager.getDefaultSharedPreferences(context)
|
|
46
|
+
.edit()
|
|
47
|
+
.putString(IAB_USP_STRING_KEY, ccpa)
|
|
48
|
+
.apply()
|
|
49
|
+
BCLogger.d("PrivacyStore", "CCPA string set")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun setCoppaApplies(applies: Boolean) {
|
|
53
|
+
synchronized(lock) {
|
|
54
|
+
_coppaApplies = applies
|
|
55
|
+
}
|
|
56
|
+
BCLogger.d("PrivacyStore", "COPPA set to $applies")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// MARK: - Getters
|
|
60
|
+
|
|
61
|
+
/** Effective GDPR consent string (SDK-set takes priority, then IAB TCF) */
|
|
62
|
+
val gdprConsentString: String?
|
|
63
|
+
get() = synchronized(lock) {
|
|
64
|
+
_gdprConsentString
|
|
65
|
+
} ?: iabPrefs.getString(IAB_TCF_CONSENT_KEY, null)
|
|
66
|
+
|
|
67
|
+
/** Whether GDPR applies (SDK-set takes priority, then IAB TCF) */
|
|
68
|
+
val gdprApplies: Boolean?
|
|
69
|
+
get() {
|
|
70
|
+
synchronized(lock) {
|
|
71
|
+
_gdprApplies?.let { return it }
|
|
72
|
+
}
|
|
73
|
+
val prefs = iabPrefs
|
|
74
|
+
return if (prefs.contains(IAB_TCF_GDPR_APPLIES_KEY)) {
|
|
75
|
+
prefs.getInt(IAB_TCF_GDPR_APPLIES_KEY, 0) == 1
|
|
76
|
+
} else {
|
|
77
|
+
null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Effective CCPA/USP string (SDK-set takes priority, then IAB USP) */
|
|
82
|
+
val ccpaString: String?
|
|
83
|
+
get() = synchronized(lock) {
|
|
84
|
+
_ccpaString
|
|
85
|
+
} ?: iabPrefs.getString(IAB_USP_STRING_KEY, null)
|
|
86
|
+
|
|
87
|
+
val coppaApplies: Boolean
|
|
88
|
+
get() = synchronized(lock) { _coppaApplies }
|
|
89
|
+
|
|
90
|
+
private val iabPrefs
|
|
91
|
+
get() = PreferenceManager.getDefaultSharedPreferences(context)
|
|
92
|
+
|
|
93
|
+
// MARK: - Bid Request Helpers
|
|
94
|
+
|
|
95
|
+
/** Build the `regs` object for an OpenRTB bid request */
|
|
96
|
+
fun buildRegsMap(): Map<String, Any> {
|
|
97
|
+
val regs = mutableMapOf<String, Any>()
|
|
98
|
+
val ext = mutableMapOf<String, Any>()
|
|
99
|
+
|
|
100
|
+
regs["coppa"] = if (coppaApplies) 1 else 0
|
|
101
|
+
|
|
102
|
+
gdprApplies?.let { ext["gdpr"] = if (it) 1 else 0 }
|
|
103
|
+
ccpaString?.let { ext["us_privacy"] = it }
|
|
104
|
+
|
|
105
|
+
if (ext.isNotEmpty()) {
|
|
106
|
+
regs["ext"] = ext
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return regs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Build the `user` object for an OpenRTB bid request */
|
|
113
|
+
fun buildUserMap(): Map<String, Any> {
|
|
114
|
+
val user = mutableMapOf<String, Any>()
|
|
115
|
+
val ext = mutableMapOf<String, Any>()
|
|
116
|
+
|
|
117
|
+
gdprConsentString?.let { ext["consent"] = it }
|
|
118
|
+
|
|
119
|
+
if (ext.isNotEmpty()) {
|
|
120
|
+
user["ext"] = ext
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return user
|
|
124
|
+
}
|
|
125
|
+
}
|