package com.bigcrunch.ads import android.content.Context import com.bigcrunch.ads.core.AnalyticsClient import com.bigcrunch.ads.core.BidRequestClient import com.bigcrunch.ads.core.ConfigManager import com.bigcrunch.ads.core.SessionManager import com.bigcrunch.ads.internal.BCLogger import com.bigcrunch.ads.internal.DeviceHelper import com.bigcrunch.ads.internal.HttpClient import com.bigcrunch.ads.internal.PrivacyStore import com.bigcrunch.ads.internal.SharedPreferencesStore import com.bigcrunch.ads.models.AppConfig import com.bigcrunch.ads.models.DeviceData import com.bigcrunch.ads.models.ScreenViewOptions import com.bigcrunch.ads.models.SessionInfo import com.google.android.gms.ads.MobileAds import com.google.android.gms.ads.RequestConfiguration import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.CompletableDeferred /** * BigCrunch Mobile Ads SDK - Main entry point * * Initialize the SDK before using any ad components: * ``` * BigCrunchAds.initialize( * context = applicationContext, * propertyId = "your-property-id" * ) * ``` */ object BigCrunchAds { private const val TAG = "BigCrunchAds" enum class Environment { Prod, Staging } /** * Safely execute a block of code, catching any exceptions to prevent SDK errors * from crashing the host app. Used for public API methods. */ private inline fun runSafely(default: T, block: () -> T): T { return try { block() } catch (e: Exception) { BCLogger.e(TAG, "SDK exception caught - will not propagate to prevent app crash", e) default } } /** * Safely execute a Unit-returning block, catching any exceptions. */ private inline fun runSafely(block: () -> Unit) { try { block() } catch (e: Exception) { BCLogger.e(TAG, "SDK exception caught - will not propagate to prevent app crash", e) } } private var initialized = false private var configReady = false private val configDeferred = CompletableDeferred() private lateinit var appContext: Context private lateinit var configManager: ConfigManager private lateinit var analyticsClient: AnalyticsClient private val initScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private var debugMode = false private val testDeviceIds = mutableSetOf() internal lateinit var privacyStore: PrivacyStore private set internal var bidRequestClient: BidRequestClient? = null private set internal var propertyId: String = "" private set internal var environment: Environment = Environment.Prod private set /** * Callback interface for SDK initialization completion */ interface InitializationCallback { /** * Called when SDK initialization is complete and config is ready */ fun onInitialized() /** * Called if SDK initialization fails * @param error Error message describing the failure */ fun onInitializationFailed(error: String) } /** * Initialize the BigCrunch Ads SDK * * @param context Application context * @param propertyId Your BigCrunch property ID * @param env Environment (Prod or Staging) * @param useMockConfig If true, uses hardcoded mock config for testing (default: false) * @param useTestAds If true, uses Google's test ad units instead of production units (default: false) * @param callback Optional callback to be notified when initialization is complete */ fun initialize( context: Context, propertyId: String, env: Environment = Environment.Prod, useMockConfig: Boolean = false, useTestAds: Boolean = false, callback: InitializationCallback? = null ) { if (initialized) { BCLogger.w("BigCrunchAds", "SDK already initialized, ignoring duplicate call") return } BCLogger.i("BigCrunchAds", "BigCrunch Ads SDK v${com.bigcrunch.ads.core.DeviceContext.SDK_VERSION} initializing...") BCLogger.d("BigCrunchAds", "Property ID: $propertyId, Environment: $env") // Store initialization parameters this.appContext = context.applicationContext this.propertyId = propertyId this.environment = env // Initialize Google Mobile Ads SDK BCLogger.d("BigCrunchAds", "Initializing Google Mobile Ads SDK...") MobileAds.initialize(appContext) { initStatus -> BCLogger.d("BigCrunchAds", "Google Mobile Ads initialized: ${initStatus.adapterStatusMap}") } // Initialize device context FIRST - must be done synchronously before anything else BCLogger.d("BigCrunchAds", "Initializing device context...") com.bigcrunch.ads.core.DeviceContext.initialize(appContext) // Enable verbose logging in staging if (env == Environment.Staging) { BCLogger.isEnabled = true } // Create internal components val httpClient = HttpClient() val storage = SharedPreferencesStore(appContext) val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() // Initialize session manager BCLogger.d("BigCrunchAds", "Initializing session manager...") SessionManager.initialize(storage) // Analytics always uses pipeline.bigcrunch.com for both prod and staging val baseUrl = "https://pipeline.bigcrunch.com" configManager = ConfigManager(httpClient, storage, moshi) if (useTestAds) { configManager.useTestAdsOverride = true } analyticsClient = AnalyticsClient(appContext, httpClient, moshi, baseUrl) privacyStore = PrivacyStore(appContext) // Mark as initialized (but not config ready yet) initialized = true // Kick off async config fetch BCLogger.d("BigCrunchAds", "Fetching app configuration...") initScope.launch { val result = configManager.loadConfig( propertyId = propertyId, isProd = env == Environment.Prod, useMockConfig = useMockConfig ) when { result.isSuccess -> { val config = result.getOrNull() if (config == null) { BCLogger.e(TAG, "Config was null despite success result") configDeferred.complete(false) callback?.onInitializationFailed("Internal error: config was null") return@launch } BCLogger.d(TAG, "Configuration loaded successfully") BCLogger.v(TAG, "Placements: ${config.placements.size}") // Create shared BidRequestClient for S2S demand if (config.s2s.enabled) { bidRequestClient = BidRequestClient( httpClient = httpClient, configManager = configManager, privacyStore = privacyStore, s2sConfig = config.s2s ) BCLogger.d(TAG, "BidRequestClient created: ${config.s2s.serverUrl}") } else { BCLogger.d(TAG, "S2S is disabled in config") } // Mark config as ready and complete the deferred configReady = true configDeferred.complete(true) // Notify callback of successful initialization callback?.onInitialized() } result.isFailure -> { val error = result.exceptionOrNull()?.message ?: "Unknown error" BCLogger.e(TAG, "Failed to load configuration", result.exceptionOrNull()) // Complete the deferred even on failure so waiters don't hang configDeferred.complete(false) // Notify callback of initialization failure callback?.onInitializationFailed(error) } } } BCLogger.d("BigCrunchAds", "BigCrunch Ads SDK initialization complete") } /** * Track screen view for analytics * * @param screenName Name of the screen being viewed * @param options Optional overrides for page URL, content metadata, and custom dimensions */ fun trackScreen(screenName: String, options: ScreenViewOptions? = null) = runSafely { if (!initialized) { BCLogger.w(TAG, "trackScreen called before initialization, ignoring") return@runSafely } analyticsClient.trackScreenView(screenName, options) SessionManager.incrementScreenViewCount() } /** * Track screen view for analytics (alias for React Native compatibility) * * @param screenName Name of the screen being viewed * @param options Optional overrides for page URL, content metadata, and custom dimensions */ fun trackScreenView(screenName: String, options: ScreenViewOptions? = null) = runSafely { trackScreen(screenName, options) } /** * Check if the SDK has been initialized * * @return true if initialized, false otherwise */ fun isInitialized(): Boolean = initialized /** * Check if configuration has been loaded * * @return true if configuration has been successfully loaded */ fun isConfigReady(): Boolean = configReady /** * Wait for configuration to be loaded * * This suspends until the configuration is loaded or fails to load. * Returns true if config loaded successfully, false otherwise. * * @return true if configuration loaded successfully */ suspend fun waitForConfig(): Boolean { return configDeferred.await() } /** * Get the current app configuration * * @return The app configuration, or null if not yet loaded */ fun getAppConfig(): AppConfig? { return if (initialized) { configManager.getCachedConfig() } else { null } } /** * Refresh the app configuration from the server */ suspend fun refreshConfig() { if (!initialized) { BCLogger.w("BigCrunchAds", "refreshConfig called before initialization") return } configManager.loadConfig( propertyId = propertyId, isProd = environment == Environment.Prod, useMockConfig = false ) } /** * Get current session information * * @return Session info with tracking counts */ fun getSessionInfo(): SessionInfo { val sessionManager = SessionManager.getInstance() return SessionInfo( sessionId = SessionManager.getSessionId(), userId = sessionManager.userId, startTime = SessionManager.getSessionStartTime(), isNewUser = sessionManager.isNewUser, sessionDepth = SessionManager.getSessionDepth(), screenViewCount = SessionManager.getScreenViewCount(), adRequestCount = SessionManager.getAdRequestCount(), adImpressionCount = SessionManager.getAdImpressionCount(), totalRevenueMicros = SessionManager.getTotalRevenueMicros() ) } /** * Set UTM attribution parameters for the current and future sessions * * Parameters are persisted and included in all analytics events until cleared. * Typically set from deep link parameters. */ fun setUTMParameters( source: String? = null, medium: String? = null, campaign: String? = null, term: String? = null, content: String? = null ) { SessionManager.getInstance().setUTMParameters(source, medium, campaign, term, content) } /** * Clear all stored UTM attribution parameters */ fun clearUTMParameters() { SessionManager.getInstance().clearUTMParameters() } /** * Start a new session (resets all counters) */ fun startNewSession() { SessionManager.startNewSession() } /** * Get device data * * @return Device data with device information */ fun getDeviceData(): DeviceData? = runSafely(null) { requireInitialized() DeviceHelper.getDeviceData(appContext) } /** * Set the account type for the current user * * Included in all analytics events. Defaults to "guest" if not set. * Valid values: "guest", "logged_in", "paid", "subscriber", "free" * * @param accountType The user's account type */ fun setAccountType(accountType: String) = runSafely { requireInitialized() analyticsClient.setAccountType(accountType) BCLogger.d(TAG, "Account type set to: $accountType") } /** * Set GDPR consent string for privacy compliance * * @param consent GDPR consent string */ fun setGdprConsent(consent: String) = runSafely { privacyStore.setGdprConsent(consent) BCLogger.d(TAG, "GDPR consent set") } /** * Set CCPA string for privacy compliance * * @param ccpaString CCPA consent string */ fun setCcpaString(ccpaString: String) = runSafely { privacyStore.setCcpaString(ccpaString) BCLogger.d(TAG, "CCPA string set: $ccpaString") } /** * Set COPPA compliance flag * * @param isCompliant true if the app should comply with COPPA */ fun setCoppaCompliant(isCompliant: Boolean) = runSafely { privacyStore.setCoppaApplies(isCompliant) val config = RequestConfiguration.Builder() .setTagForChildDirectedTreatment( if (isCompliant) RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_TRUE else RequestConfiguration.TAG_FOR_CHILD_DIRECTED_TREATMENT_FALSE ) .build() MobileAds.setRequestConfiguration(config) BCLogger.d(TAG, "COPPA compliance set to: $isCompliant") } /** * Enable or disable debug mode * * @param enabled true to enable debug logging */ fun setDebugMode(enabled: Boolean) = runSafely { debugMode = enabled BCLogger.isEnabled = enabled BCLogger.d(TAG, "Debug mode set to: $enabled") } /** * Add a test device ID for Google Ads testing * * @param deviceId Test device ID from Google Ads logs */ fun addTestDevice(deviceId: String) = runSafely { testDeviceIds.add(deviceId) updateTestDeviceConfiguration() BCLogger.d(TAG, "Added test device: $deviceId") } /** * Remove a test device ID * * @param deviceId Test device ID to remove */ fun removeTestDevice(deviceId: String) = runSafely { testDeviceIds.remove(deviceId) updateTestDeviceConfiguration() BCLogger.d(TAG, "Removed test device: $deviceId") } /** * Get list of test device IDs * * @return List of currently configured test device IDs */ fun getTestDevices(): List = testDeviceIds.toList() private fun updateTestDeviceConfiguration() { val config = RequestConfiguration.Builder() .setTestDeviceIds(testDeviceIds.toList()) .build() MobileAds.setRequestConfiguration(config) } internal fun requireInitialized() { check(initialized) { "BigCrunchAds SDK not initialized. Call BigCrunchAds.initialize() first." } } internal fun getConfigManager(): ConfigManager { requireInitialized() return configManager } internal fun getAnalyticsClient(): AnalyticsClient { requireInitialized() return analyticsClient } }