package com.nitrochucker import android.content.Context import android.util.Log import com.chuckerteam.chucker.api.ChuckerInterceptor import com.facebook.react.modules.network.OkHttpClientFactory import com.facebook.react.modules.network.OkHttpClientProvider import com.margelo.nitro.NitroModules import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference /** * Shared Android capture state. Registered once at process startup (via * [ChuckerStartupInitializer]); the JS API mutates [enabled] and reads * NitroModules.applicationContext for launching the Chucker UI. */ object ChuckerCapture { val enabled = AtomicBoolean(true) private val registered = AtomicBoolean(false) /** Application context captured at registration; used to build the Chucker delegate. */ private val appContext = AtomicReference(null) /** Hosts skipped entirely, mirroring the iOS ignored-hosts list. */ private val ignoredHosts = AtomicReference>(emptyList()) /** * Bumped whenever capture configuration changes. Chucker applies its options at * builder time, so a cached interceptor has to be rebuilt for a config change to * take effect rather than being silently ignored. */ private val configGeneration = AtomicInteger(0) /** Replaces the skipped-host list; takes effect on the next intercepted request. */ fun setIgnoredHosts(hosts: List) { val cleaned = hosts.map { it.trim() }.filter { it.isNotEmpty() } ignoredHosts.set(cleaned) configGeneration.incrementAndGet() Log.i("NitroChucker", "setIgnoredHosts($cleaned)") } internal fun generation(): Int = configGeneration.get() /** * Builds an interceptor for the current configuration. Chucker's own defaults already * bound memory (bodies truncate at 250 KB and transactions persist to a database * rather than RAM), so only the skip list needs applying. */ internal fun buildInterceptor(context: Context): ChuckerInterceptor { val hosts = ignoredHosts.get() val builder = ChuckerInterceptor.Builder(context) if (hosts.isNotEmpty()) builder.skipDomains(*hosts.toTypedArray()) return builder.build() } /** * Routes RN's OkHttp traffic through a gated Chucker interceptor. Idempotent. * * [context] may be null (e.g. the [HybridNitroChucker] defense-in-depth call before * Nitro's application context is ready); the delegate falls back to * NitroModules.applicationContext at first intercept if so. */ fun register(context: Context? = null) { // Record the application context even on a repeat call so a later non-null // caller can supply it if the first registration had none. context?.applicationContext?.let { appContext.compareAndSet(null, it) } if (!registered.compareAndSet(false, true)) return OkHttpClientProvider.setOkHttpClientFactory(object : OkHttpClientFactory { override fun createNewNetworkModuleClient(): OkHttpClient = OkHttpClientProvider.createClientBuilder() .addInterceptor(GatedChuckerInterceptor()) .build() }) Log.i("NitroChucker", "Chucker interceptor registered on RN OkHttp stack") } internal fun resolveContext(): Context? = appContext.get() ?: NitroModules.applicationContext } /** * Delegates to a real [ChuckerInterceptor] only while capture is enabled, so * [ChuckerCapture.enabled] can pause/resume recording at runtime. The delegate is built * on first use (once an application Context exists) and rebuilt whenever capture * configuration changes, since Chucker bakes its options in at builder time. */ private class GatedChuckerInterceptor : Interceptor { /** Cached delegate paired with the config generation it was built from. */ private val cached = AtomicReference?>(null) private fun delegate(): ChuckerInterceptor? { val generation = ChuckerCapture.generation() cached.get()?.let { (builtFrom, interceptor) -> if (builtFrom == generation) return interceptor } val context = ChuckerCapture.resolveContext() ?: return null // A concurrent rebuild is harmless: both interceptors are valid and last write wins. return ChuckerCapture.buildInterceptor(context).also { cached.set(generation to it) } } override fun intercept(chain: Interceptor.Chain): Response { val d = if (ChuckerCapture.enabled.get()) delegate() else null return d?.intercept(chain) ?: chain.proceed(chain.request()) } }