package com.microsoft.reacttestapp.react import android.app.Activity import android.app.Application import android.content.Context import android.content.Intent import androidx.core.net.toUri import com.facebook.hermes.reactexecutor.HermesExecutorFactory import com.facebook.react.PackageList import com.facebook.react.ReactInstanceEventListener import com.facebook.react.ReactPackage import com.facebook.react.bridge.JavaScriptExecutorFactory import com.facebook.react.bridge.ReactContext import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.packagerconnection.PackagerConnectionSettings import com.microsoft.reacttestapp.BuildConfig import com.microsoft.reacttestapp.MainActivity import com.microsoft.reacttestapp.R import com.microsoft.reacttestapp.TestApp import java.util.Collections.synchronizedList import java.util.concurrent.CountDownLatch @Suppress("TYPEALIAS_EXPANSION_DEPRECATION") class MainReactNativeHost( application: Application, private val reactBundleNameProvider: ReactBundleNameProvider ) : com.microsoft.reacttestapp.compat.ReactNativeHostCompat(application) { val jsExecutorName: String get() = javaScriptExecutorFactory.toString() var source: BundleSource = if (reactBundleNameProvider.bundleName == null || isPackagerRunning(application)) { BundleSource.Server } else { BundleSource.Disk } var onBundleSourceChanged: ((newSource: BundleSource) -> Unit)? = null private var currentActivityTracker = CurrentActivityTracker() private val reactInstanceEventListeners = synchronizedList(arrayListOf()) fun init(beforeReactNativeInit: () -> Unit, afterReactNativeInit: () -> Unit) { if (BuildConfig.DEBUG && hasInstance()) { error("init() can only be called once on startup") } application.registerActivityLifecycleCallbacks(currentActivityTracker) // When we reference `reactInstanceManager` below, `ReactNativeHost` // will start creating a `ReactInstanceManager` instance. beforeReactNativeInit() if (BuildConfig.REACTAPP_USE_BRIDGELESS) { val reactHost = (application as TestApp).reactHost reactHost.addReactInstanceEventListener(object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { // proactively removing the listener to avoid leaking memory // and to avoid dupe calls to afterReactNativeInit() reactHost.removeReactInstanceEventListener(this) reactHost.devSupportManager?.let { addCustomDevOptions(it) } afterReactNativeInit() synchronized(reactInstanceEventListeners) { val i = reactInstanceEventListeners.iterator() while (i.hasNext()) { i.next().onReactContextInitialized(context) } } } }) } else { reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { // proactively removing the listener to avoid leaking memory // and to avoid dupe calls to afterReactNativeInit() reactInstanceManager.removeReactInstanceEventListener(this) addCustomDevOptions(reactInstanceManager.devSupportManager) afterReactNativeInit() synchronized(reactInstanceEventListeners) { val i = reactInstanceEventListeners.iterator() while (i.hasNext()) { i.next().onReactContextInitialized(context) } } } }) } } fun addReactInstanceEventListener(listener: ReactInstanceEventListener) { reactInstanceEventListeners.add(listener) reactInstanceManager.addReactInstanceEventListener(listener) } fun reload(activity: Activity, newSource: BundleSource) { val action = source.moveTo(newSource) source = newSource when (action) { BundleSource.Action.RELOAD -> { reactInstanceManager.devSupportManager.handleReloadJS() } BundleSource.Action.RESTART -> { if (activity !is MainActivity) { activity.navigateUpTo(Intent(application, MainActivity.Companion::class.java)) } clear() } } onBundleSourceChanged?.invoke(newSource) } fun reloadJSFromServer(activity: Activity, bundleURL: String) { val uri = bundleURL.toUri() PackagerConnectionSettings(activity).debugServerHost = if (uri.port > 0) { "${uri.host}:${uri.port}" } else { uri.host ?: "localhost" } reload(activity, BundleSource.Server) } override fun getJavaScriptExecutorFactory(): JavaScriptExecutorFactory = HermesExecutorFactory() override fun getJSMainModuleName() = "index" // We may not always have (or need) a JS bundle, but // `ReactNativeHost.createReactInstanceManager` asserts it so we need to // return something. override fun getBundleAssetName() = reactBundleNameProvider.bundleName ?: "main.android.bundle" override fun getUseDeveloperSupport() = source == BundleSource.Server override fun getPackages(): List = PackageList(application).packages private fun addCustomDevOptions(devSupportManager: DevSupportManager) { val bundleOption = application.resources.getString( if (source == BundleSource.Disk) { R.string.load_from_dev_server } else { R.string.load_embedded_js_bundle } ) devSupportManager.addCustomDevOption(bundleOption) { currentActivityTracker.get()?.let { when (source) { BundleSource.Disk -> reload(it, BundleSource.Server) BundleSource.Server -> reload(it, BundleSource.Disk) } } } } private fun isPackagerRunning(context: Context): Boolean { if (!hasInstance()) { // Return early otherwise we will get in an initialization loop. // `source` may be initialized by calling this function. Without // this check, the `getReactInstanceManager()` call below will // instantiate `ReactInstanceManager`, which in turn will try to // access `source`. return BuildConfig.DEBUG } val latch = CountDownLatch(1) var packagerIsRunning = false reactInstanceManager.devSupportManager.devSettings?.let { devSettings -> createDevServerHelper(context, devSettings).isPackagerRunning { packagerIsRunning = it latch.countDown() } } latch.await() return packagerIsRunning } }