package expo.modules.kotlin import android.app.Activity import android.content.Context import android.content.Intent import android.os.Handler import android.os.HandlerThread import android.view.View import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.modules.network.OkHttpClientProvider import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.UIManagerModule import com.facebook.react.uimanager.common.UIManagerType import expo.modules.adapters.react.NativeModulesProxy import expo.modules.core.errors.ContextDestroyedException import expo.modules.core.interfaces.ActivityProvider import expo.modules.interfaces.permissions.Permissions import expo.modules.kotlin.activityresult.ActivityResultsManager import expo.modules.kotlin.activityresult.DefaultAppContextActivityResultCaller import expo.modules.kotlin.defaultmodules.ErrorManagerModule import expo.modules.kotlin.defaultmodules.JSLoggerModule import expo.modules.kotlin.defaultmodules.NativeModulesProxyModule import expo.modules.kotlin.events.EventEmitter import expo.modules.kotlin.events.EventName import expo.modules.kotlin.events.KEventEmitterWrapper import expo.modules.kotlin.events.KModuleEventEmitterWrapper import expo.modules.kotlin.events.OnActivityResultPayload import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.modules.Module import expo.modules.kotlin.providers.CurrentActivityProvider import expo.modules.kotlin.runtime.MainRuntime import expo.modules.kotlin.runtime.WorkletRuntime import expo.modules.kotlin.services.AppDirectoriesService import expo.modules.kotlin.services.FilePermissionService import expo.modules.kotlin.services.Service import expo.modules.kotlin.services.ServicesRegistry import expo.modules.kotlin.tracing.trace import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancel import okhttp3.OkHttpClient import java.io.File import java.lang.ref.WeakReference class AppContext( modulesProvider: ModulesProvider, val legacyModuleRegistry: expo.modules.core.ModuleRegistry, reactContextHolder: WeakReference ) : CurrentActivityProvider { // The main context used in the app. // Modules attached to this context will be available on the main js context. @Deprecated("Use AppContext.runtimeContext instead", ReplaceWith("runtime")) val hostingRuntimeContext = MainRuntime(this, reactContextHolder) val runtime: MainRuntime get() = hostingRuntimeContext private val uiRuntimeHolder = lazy { WorkletRuntime(this, reactContextHolder) } val uiRuntime get() = uiRuntimeHolder.value private val reactLifecycleDelegate = ReactLifecycleDelegate(this) private var hostWasDestroyed = false private val modulesQueueDispatcher = HandlerThread("expo.modules.AsyncFunctionQueue") .apply { start() } .looper.let { Handler(it) } .asCoroutineDispatcher() /** * A scope used to dispatch all background work. */ val backgroundCoroutineScope = CoroutineScope( Dispatchers.IO + SupervisorJob() + CoroutineName("expo.modules.BackgroundCoroutineScope") ) /** * A queue used to dispatch all async methods that are called via JSI. */ val modulesQueue = CoroutineScope( modulesQueueDispatcher + SupervisorJob() + CoroutineName("expo.modules.AsyncFunctionQueue") ) val mainQueue = CoroutineScope( Dispatchers.Main + SupervisorJob() + CoroutineName("expo.modules.MainQueue") ) val registry = ModuleRegistry(this.weak()) val services = ServicesRegistry(this.weak()) internal var legacyModulesProxyHolder: WeakReference? = null private val activityResultsManager = ActivityResultsManager(this) internal val appContextActivityResultCaller = DefaultAppContextActivityResultCaller( activityResultsManager ) init { requireNotNull(reactContextHolder.get()) { "The app context should be created with valid react context." }.apply { legacyModuleRegistry.appContext = this@AppContext addLifecycleEventListener(reactLifecycleDelegate) addActivityEventListener(reactLifecycleDelegate) services.register() services.register() services.register(modulesProvider.getServices()) // Registering modules has to happen at the very end of `AppContext` creation. Some modules need to access // `AppContext` during their initialisation, so we need to ensure all `AppContext`'s // properties are initialized first. Not having that would trigger NPE. registry.register(NativeModulesProxyModule(), null) registry.register(JSLoggerModule(), null) registry.register(modulesProvider) registerInlineModulesList() logger.info("✅ AppContext was initialized") } } fun onCreate() = trace("AppContext.onCreate") { registry.postOnCreate() } private fun registerInlineModulesList() { try { val inlineModulesList = Class.forName("inline.modules.ExpoInlineModulesList").getConstructor() .newInstance() as ModulesProvider registry.register(inlineModulesList) } catch (_: ClassNotFoundException) { } } /** * Initializes a JSI part of the module registry. * It will be a NOOP if the remote debugging was activated. */ fun installJSIInterop() { runtime.install() } /** * Returns a legacy module implementing given interface. */ inline fun legacyModule(): Module? { return try { val module = legacyModuleRegistry.getModule(Module::class.java) return module } catch (_: Exception) { null } } /** * Returns a service implementing given interface. */ inline fun service(): T? = services.service() /** * Returns a service implementing given interface. * It's compatible with Java callers. */ @Suppress("UNCHECKED_CAST") fun service(serviceClass: Class): T? = services.registry[serviceClass] as? T /** * Provides access to the file system service */ val filePermission: FilePermissionService get() = service() ?: throw IllegalStateException( "FilePermissionService is not registered in the ServicesRegistry." ) /** * Provides access to the scoped directories from the legacy module registry. */ private val appDirectories: AppDirectoriesService get() = service() ?: throw IllegalStateException( "AppDirectoriesService is not registered in the ServicesRegistry." ) /** * A directory for storing user documents and other permanent files. */ val persistentFilesDirectory: File get() = appDirectories.persistentFilesDirectory /** * A directory for storing temporary files that can be removed at any time by the device's operating system. */ val cacheDirectory: File get() = appDirectories.cacheDirectory /** * Provides access to the permissions manager from the legacy module registry */ val permissions: Permissions? get() = legacyModule() /** * Provides access to the activity provider from the legacy module registry */ val activityProvider: ActivityProvider? get() = legacyModule() /** * Provides access to the react application context */ val reactContext: Context? get() = runtime.reactContext /** * @return true if there is an non-null, alive react native instance */ val hasActiveReactInstance: Boolean get() = runtime.reactContext?.hasActiveReactInstance() == true /** * @returns an OkHttpClient instance that can be used to perform network requests * with the same configuration as React Native's networking module. See [com.facebook.react.modules.network.OkHttpClientProvider]. * This client will share the same cookie jar and has no timeouts by default. */ val okHttpClient: OkHttpClient get() = OkHttpClientProvider.getOkHttpClient() fun createOkHttpClientBuilder(): OkHttpClient.Builder = OkHttpClientProvider.createClientBuilder() /** * Provides access to the event emitter */ fun eventEmitter(module: Module): EventEmitter? { val legacyEventEmitter = legacyModule() ?: return null return KModuleEventEmitterWrapper( requireNotNull(registry.getModuleHolder(module)) { val availableModulesNames = registry.registry.keys.joinToString(", ") "Cannot create an event emitter for module ${module.javaClass} that isn't present in the module registry. Available modules: [$availableModulesNames]." }, legacyEventEmitter, runtime.reactContextHolder ) } internal val callbackInvoker: EventEmitter? get() { val legacyEventEmitter = legacyModule() ?: return null return KEventEmitterWrapper(legacyEventEmitter, runtime.reactContextHolder) } @Deprecated("Use AppContext.jsLogger instead") val errorManager: ErrorManagerModule? by lazy { registry.getModule() } val jsLogger by lazy { registry.getModule()?.logger } internal fun onDestroy() = trace("AppContext.onDestroy") { runtime.reactContext?.run { removeLifecycleEventListener(reactLifecycleDelegate) removeActivityEventListener(reactLifecycleDelegate) } with(registry) { post(EventName.MODULE_DESTROY) cleanUp() } modulesQueue.cancel(ContextDestroyedException()) mainQueue.cancel(ContextDestroyedException()) backgroundCoroutineScope.cancel(ContextDestroyedException()) runtime.deallocate() if (uiRuntimeHolder.isInitialized()) { uiRuntime.deallocate() } logger.info("✅ AppContext was destroyed") } internal fun onHostResume() { // If the current activity is null, it means that the current React context was destroyed. // We can just return here. val activity = currentActivity ?: return check(activity is AppCompatActivity) { "Current Activity is of incorrect class, expected AppCompatActivity, received ${currentActivity?.localClassName}" } // We need to re-register activity contracts when reusing AppContext with new Activity after host destruction. if (hostWasDestroyed) { hostWasDestroyed = false registry.registerActivityContracts() } activityResultsManager.onHostResume(activity) registry.post(EventName.ACTIVITY_ENTERS_FOREGROUND) } internal fun onHostPause() { registry.post(EventName.ACTIVITY_ENTERS_BACKGROUND) } internal fun onUserLeaveHint() { registry.post(EventName.ON_USER_LEAVES_ACTIVITY) } internal fun onHostDestroy() { currentActivity?.let { check(it is AppCompatActivity) { "Current Activity is of incorrect class, expected AppCompatActivity, received ${currentActivity?.localClassName}" } activityResultsManager.onHostDestroy(it) } registry.post(EventName.ACTIVITY_DESTROYS) // The host (Activity) was destroyed, but it doesn't mean that modules will be destroyed too. // So we save that information, and we will re-register activity contracts when the host will be resumed with new Activity. hostWasDestroyed = true } internal fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent? ) { activityResultsManager.onActivityResult(requestCode, resultCode, data) registry.post( EventName.ON_ACTIVITY_RESULT, activity, OnActivityResultPayload( requestCode, resultCode, data ) ) } internal fun onNewIntent(intent: Intent?) { registry.post( EventName.ON_NEW_INTENT, intent ) } @Suppress("UNCHECKED_CAST") @UiThread fun findView(viewTag: Int): T? { val reactContext = runtime.reactContext ?: return null return UIManagerHelper .getUIManagerForReactTag(reactContext, viewTag) ?.resolveView(viewTag) as? T } internal fun dispatchOnMainUsingUIManager(block: () -> Unit) { val reactContext = runtime.reactContext ?: throw Exceptions.ReactContextLost() val uiManager = UIManagerHelper.getUIManagerForReactTag( reactContext, UIManagerType.DEFAULT ) as UIManagerModule uiManager.addUIBlock { block() } } internal fun assertMainThread() { Utils.assertMainThread() } /** * Runs a code block on the JavaScript thread. */ @Deprecated("Use RuntimeContext.schedule instead", ReplaceWith("runtime.schedule(runnable)")) fun executeOnJavaScriptThread(runnable: Runnable) { runtime.reactContext?.runOnJSQueueThread(runnable) } // region CurrentActivityProvider override val currentActivity: Activity? get() { return activityProvider?.currentActivity ?: (reactContext as? ReactApplicationContext)?.currentActivity } val throwingActivity: Activity get() { val current = activityProvider?.currentActivity ?: (reactContext as? ReactApplicationContext)?.currentActivity return current ?: throw Exceptions.MissingActivity() } // endregion }