Migrate to Kotlin - DevSupportManagerBase.kt (#50559)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50559

This diff migrates the following file to Kotlin - DevSupportManagerBase.kt
as part of our ongoing effort of migrating the codebase to Kotlin

This file was 1000LOC+ so its migration is quite involved.
I had to look into nullability of various parameters. Most notably the `Array<StackFrame?>` -> `Array<StackFrame>`.

This also is the last file left in the devsupport package that needs to be migrated to Kotlin.

Changelog:
[Android] [Breaking] - DevSupportManagerBase is now converted to Kotlin. If you're subclassing this class, you will have to adjust some of the parameters as types have changed during the migration.

Reviewed By: javache, mdvacca

Differential Revision: D72637098

fbshipit-source-id: b87d5f17e30d69fe9c09ec9b49f96818df34ea3b
This commit is contained in:
Nicola Corti
2025-04-09 03:39:19 -07:00
committed by Facebook GitHub Bot
parent dd0cd07405
commit 9da485b54c
6 changed files with 996 additions and 1232 deletions
@@ -1996,35 +1996,35 @@ public abstract interface class com/facebook/react/devsupport/DevServerHelper$Pa
}
public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/facebook/react/devsupport/interfaces/DevSupportManager {
protected final field mReactInstanceDevHelper Lcom/facebook/react/devsupport/ReactInstanceDevHelper;
public static final field Companion Lcom/facebook/react/devsupport/DevSupportManagerBase$Companion;
public fun <init> (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)V
public fun addCustomDevOption (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/DevOptionHandler;)V
public fun createRootView (Ljava/lang/String;)Landroid/view/View;
public fun createSurfaceDelegate (Ljava/lang/String;)Lcom/facebook/react/common/SurfaceDelegate;
public fun destroyRootView (Landroid/view/View;)V
public fun downloadBundleResourceFromUrlSync (Ljava/lang/String;Ljava/io/File;)Ljava/io/File;
public fun fetchSplitBundleAndCreateBundleLoader (Ljava/lang/String;Lcom/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader;)V
protected fun getApplicationContext ()Landroid/content/Context;
public final fun fetchSplitBundleAndCreateBundleLoader (Ljava/lang/String;Lcom/facebook/react/devsupport/DevSupportManagerBase$CallbackWithBundleLoader;)V
protected final fun getApplicationContext ()Landroid/content/Context;
public fun getCurrentActivity ()Landroid/app/Activity;
public fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext;
public fun getDevLoadingViewManager ()Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;
public fun getDevServerHelper ()Lcom/facebook/react/devsupport/DevServerHelper;
public fun getDevSettings ()Lcom/facebook/react/modules/debug/interfaces/DeveloperSettings;
public fun getDevSupportEnabled ()Z
public final fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext;
public final fun getDevLoadingViewManager ()Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;
public final fun getDevServerHelper ()Lcom/facebook/react/devsupport/DevServerHelper;
public final fun getDevSettings ()Lcom/facebook/react/modules/debug/interfaces/DeveloperSettings;
public final fun getDevSupportEnabled ()Z
public fun getDownloadedJSBundleFile ()Ljava/lang/String;
public fun getJSAppBundleName ()Ljava/lang/String;
public fun getLastErrorCookie ()I
public fun getLastErrorStack ()[Lcom/facebook/react/devsupport/interfaces/StackFrame;
public fun getLastErrorTitle ()Ljava/lang/String;
public fun getLastErrorType ()Lcom/facebook/react/devsupport/interfaces/ErrorType;
public fun getReactInstanceDevHelper ()Lcom/facebook/react/devsupport/ReactInstanceDevHelper;
public final fun getJSAppBundleName ()Ljava/lang/String;
public final fun getLastErrorCookie ()I
public final fun getLastErrorStack ()[Lcom/facebook/react/devsupport/interfaces/StackFrame;
public final fun getLastErrorTitle ()Ljava/lang/String;
public final fun getLastErrorType ()Lcom/facebook/react/devsupport/interfaces/ErrorType;
public final fun getReactInstanceDevHelper ()Lcom/facebook/react/devsupport/ReactInstanceDevHelper;
public fun getRedBoxHandler ()Lcom/facebook/react/devsupport/interfaces/RedBoxHandler;
public fun getSourceMapUrl ()Ljava/lang/String;
public fun getSourceUrl ()Ljava/lang/String;
protected abstract fun getUniqueTag ()Ljava/lang/String;
public fun handleException (Ljava/lang/Exception;)V
public fun hasUpToDateJSBundleInCache ()Z
protected fun hideDevLoadingView ()V
protected final fun hideDevLoadingView ()V
public fun hidePausedInDebuggerOverlay ()V
public fun hideRedboxDialog ()V
public fun isPackagerRunning (Lcom/facebook/react/devsupport/interfaces/PackagerStatusCallback;)V
@@ -2036,11 +2036,16 @@ public abstract class com/facebook/react/devsupport/DevSupportManagerBase : com/
public fun reloadJSFromServer (Ljava/lang/String;Lcom/facebook/react/devsupport/interfaces/BundleLoadCallback;)V
public fun reloadSettings ()V
public fun setAdditionalOptionForPackager (Ljava/lang/String;Ljava/lang/String;)V
public fun setDevSupportEnabled (Z)V
public final fun setDevLoadingViewManager (Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;)V
public final fun setDevSupportEnabled (Z)V
public fun setFpsDebugEnabled (Z)V
public fun setHotModuleReplacementEnabled (Z)V
public final fun setLastErrorCookie (I)V
public final fun setLastErrorStack ([Lcom/facebook/react/devsupport/interfaces/StackFrame;)V
public final fun setLastErrorTitle (Ljava/lang/String;)V
public final fun setLastErrorType (Lcom/facebook/react/devsupport/interfaces/ErrorType;)V
public fun setPackagerLocationCustomizer (Lcom/facebook/react/devsupport/interfaces/DevSupportManager$PackagerLocationCustomizer;)V
protected fun showDevLoadingViewForRemoteJSEnabled ()V
protected final fun showDevLoadingViewForRemoteJSEnabled ()V
public fun showDevOptionsDialog ()V
public fun showNewJSError (Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;I)V
public fun showNewJavaError (Ljava/lang/String;Ljava/lang/Throwable;)V
@@ -2055,6 +2060,9 @@ public abstract interface class com/facebook/react/devsupport/DevSupportManagerB
public abstract fun onSuccess (Lcom/facebook/react/bridge/JSBundleLoader;)V
}
public final class com/facebook/react/devsupport/DevSupportManagerBase$Companion {
}
public abstract interface class com/facebook/react/devsupport/DevSupportManagerFactory {
public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;)Lcom/facebook/react/devsupport/interfaces/DevSupportManager;
public abstract fun create (Landroid/content/Context;Lcom/facebook/react/devsupport/ReactInstanceDevHelper;Ljava/lang/String;ZLcom/facebook/react/devsupport/interfaces/RedBoxHandler;Lcom/facebook/react/devsupport/interfaces/DevBundleDownloadListener;ILjava/util/Map;Lcom/facebook/react/common/SurfaceDelegateFactory;Lcom/facebook/react/devsupport/interfaces/DevLoadingViewManager;Lcom/facebook/react/devsupport/interfaces/PausedInDebuggerOverlayManager;Z)Lcom/facebook/react/devsupport/interfaces/DevSupportManager;
@@ -56,7 +56,7 @@ public class BridgeDevSupportManager(
redBoxHandler: RedBoxHandler?,
devBundleDownloadListener: DevBundleDownloadListener?,
minNumShakes: Int,
customPackagerCommandHandlers: Map<String?, RequestHandler?>?,
customPackagerCommandHandlers: Map<String, RequestHandler>?,
surfaceDelegateFactory: SurfaceDelegateFactory?,
devLoadingViewManager: DevLoadingViewManager?,
pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?
@@ -74,7 +74,8 @@ public class BridgeDevSupportManager(
devLoadingViewManager,
pausedInDebuggerOverlayManager) {
override fun getUniqueTag(): String = "Bridge"
override val uniqueTag: String
get() = "Bridge"
override fun loadSplitBundleFromServer(bundlePath: String, callback: DevSplitBundleCallback) {
fetchSplitBundleAndCreateBundleLoader(
@@ -71,7 +71,8 @@ internal class BridgelessDevSupportManager(
devLoadingViewManager = null,
pausedInDebuggerOverlayManager = null)
override fun getUniqueTag(): String = "Bridgeless"
override val uniqueTag: String
get() = "Bridgeless"
override fun loadSplitBundleFromServer(bundlePath: String, callback: DevSplitBundleCallback) {
fetchSplitBundleAndCreateBundleLoader(
@@ -79,9 +80,9 @@ internal class BridgelessDevSupportManager(
object : CallbackWithBundleLoader {
override fun onSuccess(bundleLoader: JSBundleLoader) {
try {
mReactInstanceDevHelper.loadBundle(bundleLoader).waitForCompletion()
reactInstanceDevHelper.loadBundle(bundleLoader).waitForCompletion()
val bundleURL = devServerHelper.getDevServerSplitBundleURL(bundlePath)
val reactContext = mReactInstanceDevHelper.currentReactContext
val reactContext = reactInstanceDevHelper.currentReactContext
reactContext?.getJSModule(HMRClient::class.java)?.registerBundle(bundleURL)
callback.onSuccess()
} catch (e: InterruptedException) {
@@ -99,6 +100,6 @@ internal class BridgelessDevSupportManager(
UiThreadUtil.assertOnUiThread()
// dismiss redbox if exists
hideRedboxDialog()
mReactInstanceDevHelper.reload("BridgelessDevSupportManager.handleReloadJS()")
reactInstanceDevHelper.reload("BridgelessDevSupportManager.handleReloadJS()")
}
}
@@ -0,0 +1,913 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.hardware.SensorManager
import android.os.Build
import android.util.Pair
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ListAdapter
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.UiThread
import androidx.core.util.Supplier
import com.facebook.common.logging.FLog
import com.facebook.react.R
import com.facebook.react.bridge.DefaultJSExceptionHandler
import com.facebook.react.bridge.JSBundleLoader
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactMarker
import com.facebook.react.bridge.ReactMarkerConstants
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.common.DebugServerException
import com.facebook.react.common.JavascriptException
import com.facebook.react.common.ReactConstants
import com.facebook.react.common.ShakeDetector
import com.facebook.react.common.SurfaceDelegate
import com.facebook.react.common.SurfaceDelegateFactory
import com.facebook.react.devsupport.DebugOverlayController.Companion.requestPermission
import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener
import com.facebook.react.devsupport.InspectorFlags.getFuseboxEnabled
import com.facebook.react.devsupport.StackTraceHelper.convertJavaStackTrace
import com.facebook.react.devsupport.StackTraceHelper.convertJsStackTrace
import com.facebook.react.devsupport.interfaces.BundleLoadCallback
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
import com.facebook.react.devsupport.interfaces.DevOptionHandler
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.devsupport.interfaces.DevSupportManager.PackagerLocationCustomizer
import com.facebook.react.devsupport.interfaces.DevSupportManager.PausedInDebuggerOverlayCommandListener
import com.facebook.react.devsupport.interfaces.ErrorCustomizer
import com.facebook.react.devsupport.interfaces.ErrorType
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
import com.facebook.react.devsupport.interfaces.RedBoxHandler
import com.facebook.react.devsupport.interfaces.StackFrame
import com.facebook.react.modules.core.RCTNativeAppEventEmitter
import com.facebook.react.modules.debug.interfaces.DeveloperSettings
import com.facebook.react.packagerconnection.RequestHandler
import java.io.File
import java.net.MalformedURLException
import java.net.URL
import java.util.Locale
public abstract class DevSupportManagerBase(
protected val applicationContext: Context,
public val reactInstanceDevHelper: ReactInstanceDevHelper,
@get:JvmName("getJSAppBundleName") public val jsAppBundleName: String?,
enableOnCreate: Boolean,
public override val redBoxHandler: RedBoxHandler?,
private val devBundleDownloadListener: DevBundleDownloadListener?,
minNumShakes: Int,
private val customPackagerCommandHandlers: Map<String, RequestHandler>?,
private val surfaceDelegateFactory: SurfaceDelegateFactory?,
public var devLoadingViewManager: DevLoadingViewManager?,
private var pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?
) : DevSupportManager {
public interface CallbackWithBundleLoader {
public fun onSuccess(bundleLoader: JSBundleLoader)
public fun onError(url: String, cause: Throwable)
}
protected abstract val uniqueTag: String
public final override var currentReactContext: ReactContext? = null
private set
public final override val devSettings: DeveloperSettings =
DevInternalSettings(
applicationContext,
object : DevInternalSettings.Listener {
override fun onInternalSettingsChanged() {
this@DevSupportManagerBase.reloadSettings()
}
})
override val currentActivity: Activity?
get() = reactInstanceDevHelper.currentActivity
/**
* [com.facebook.react.ReactInstanceManager] is responsible for enabling/disabling dev support
* when a React view is attached/detached or when application state changes (e.g. the application
* is backgrounded).
*/
final override var devSupportEnabled: Boolean
get() = isDevSupportEnabled
set(isDevSupportEnabled) {
this.isDevSupportEnabled = isDevSupportEnabled
reloadSettings()
}
override val sourceMapUrl: String
get() = jsAppBundleName?.let { devServerHelper.getSourceMapUrl(it) } ?: ""
override val sourceUrl: String
get() = jsAppBundleName?.let { devServerHelper.getSourceUrl(it) } ?: ""
override val downloadedJSBundleFile: String
get() = jsBundleDownloadedFile.absolutePath
public val devServerHelper: DevServerHelper =
DevServerHelper(devSettings, applicationContext, devSettings.packagerConnectionSettings)
public final override var lastErrorTitle: String? = null
public final override var lastErrorStack: Array<StackFrame>? = null
public final override var lastErrorType: ErrorType? = null
public final override var lastErrorCookie: Int = 0
// Prepare shake gesture detector (will be started/stopped from #reload)
private val shakeDetector: ShakeDetector =
ShakeDetector({ this.showDevOptionsDialog() }, minNumShakes)
// Prepare reload APP broadcast receiver (will be registered/unregistered from #reload)
private val reloadAppBroadcastReceiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (getReloadAppAction(context) == action) {
handleReloadJS()
}
}
}
private val customDevOptions = LinkedHashMap<String, DevOptionHandler>()
private val jsBundleDownloadedFile: File
private val jsSplitBundlesDir: File
private val defaultJSExceptionHandler: DefaultJSExceptionHandler = DefaultJSExceptionHandler()
private var redBoxSurfaceDelegate: SurfaceDelegate? = null
private var devOptionsDialog: AlertDialog? = null
private var debugOverlayController: DebugOverlayController? = null
private var devLoadingViewVisible = false
private var pendingJSSplitBundleRequests = 0
private var isReceiverRegistered = false
private var isShakeDetectorStarted = false
private var isDevSupportEnabled = false
private var isPackagerConnected = false
private val errorCustomizers: MutableList<ErrorCustomizer> = mutableListOf()
private var packagerLocationCustomizer: PackagerLocationCustomizer? = null
private val jSExecutorDescription: String?
get() =
try {
reactInstanceDevHelper.javaScriptExecutorFactory.toString()
} catch (e: IllegalStateException) {
null
}
init {
// We store JS bundle loaded from dev server in a single destination in app's data dir.
// In case when someone schedule 2 subsequent reloads it may happen that JS thread will
// start reading first reload output while the second reload starts writing to the same
// file. As this should only be the case in dev mode we leave it as it is.
// TODO(6418010): Fix readers-writers problem in debug reload from HTTP server
val subclassTag = uniqueTag
val bundleFile = subclassTag + "ReactNativeDevBundle.js"
jsBundleDownloadedFile = File(applicationContext.filesDir, bundleFile)
val splitBundlesDir = subclassTag.lowercase() + "_dev_js_split_bundles"
jsSplitBundlesDir = applicationContext.getDir(splitBundlesDir, Context.MODE_PRIVATE)
devSupportEnabled = enableOnCreate
if (devLoadingViewManager == null) {
devLoadingViewManager = DefaultDevLoadingViewImplementation(reactInstanceDevHelper)
}
if (pausedInDebuggerOverlayManager == null) {
pausedInDebuggerOverlayManager =
PausedInDebuggerOverlayDialogManager(
Supplier {
val context = reactInstanceDevHelper.currentActivity
if (context == null || context.isFinishing) {
return@Supplier null
}
context
})
}
}
override fun handleException(e: Exception) {
if (isDevSupportEnabled) {
logJSException(e)
} else {
defaultJSExceptionHandler.handleException(e)
}
}
override fun showNewJavaError(message: String?, e: Throwable) {
FLog.e(ReactConstants.TAG, "Exception in native call", e)
showNewError(message, convertJavaStackTrace(e), JAVA_ERROR_COOKIE, ErrorType.NATIVE)
}
/**
* Add option item to dev settings dialog displayed by this manager. In the case user select given
* option from that dialog, the appropriate handler passed as {@param optionHandler} will be
* called.
*/
override fun addCustomDevOption(optionName: String, optionHandler: DevOptionHandler) {
customDevOptions[optionName] = optionHandler
}
override fun showNewJSError(message: String?, details: ReadableArray?, errorCookie: Int) {
showNewError(message, convertJsStackTrace(details), errorCookie, ErrorType.JS)
}
override fun registerErrorCustomizer(errorCustomizer: ErrorCustomizer) {
errorCustomizers.add(errorCustomizer)
}
override fun processErrorCustomizers(
errorInfo: Pair<String, Array<StackFrame>>
): Pair<String, Array<StackFrame>> {
var errorInfoLocal = errorInfo
for (errorCustomizer in errorCustomizers) {
errorInfoLocal = errorCustomizer.customizeErrorInfo(errorInfo)
}
return errorInfoLocal
}
override fun hideRedboxDialog() {
redBoxSurfaceDelegate?.hide()
}
override fun createRootView(appKey: String): View? = reactInstanceDevHelper.createRootView(appKey)
override fun destroyRootView(rootView: View?) {
rootView?.let { reactInstanceDevHelper.destroyRootView(it) }
}
private fun logJSException(e: Exception) {
val message = StringBuilder(e.message ?: "Exception in native call from JS")
var cause = e.cause
while (cause != null) {
message.append("\n\n").append(cause.message)
cause = cause.cause
}
if (e is JavascriptException) {
FLog.e(ReactConstants.TAG, "Exception in native call from JS", e)
showNewError(e.message, arrayOf(), JSEXCEPTION_ERROR_COOKIE, ErrorType.JS)
} else {
showNewJavaError(message.toString(), e)
}
}
private fun hideDevOptionsDialog() {
devOptionsDialog?.dismiss()
devOptionsDialog = null
}
private fun showNewError(
message: String?,
stack: Array<StackFrame>,
errorCookie: Int,
errorType: ErrorType
) {
UiThreadUtil.runOnUiThread {
// Keep a copy of the latest error to be shown by the RedBoxSurface
updateLastErrorInfo(message, stack, errorCookie, errorType)
if (redBoxSurfaceDelegate == null) {
this.redBoxSurfaceDelegate =
createSurfaceDelegate("RedBox")
?: RedBoxDialogSurfaceDelegate(this@DevSupportManagerBase).apply {
createContentView("RedBox")
}
}
if (redBoxSurfaceDelegate?.isShowing() == true) {
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
// show the first and most actionable one.
return@runOnUiThread
}
redBoxSurfaceDelegate?.show()
}
}
override fun showDevOptionsDialog() {
if (devOptionsDialog != null || !isDevSupportEnabled || ActivityManager.isUserAMonkey()) {
return
}
val options = LinkedHashMap<String, DevOptionHandler>()
val disabledItemKeys: MutableSet<String?> = HashSet() /* register standard options */
options[applicationContext.getString(R.string.catalyst_reload)] = DevOptionHandler {
if (!devSettings.isJSDevModeEnabled && devSettings.isHotModuleReplacementEnabled) {
Toast.makeText(
applicationContext,
applicationContext.getString(R.string.catalyst_hot_reloading_auto_disable),
Toast.LENGTH_LONG)
.show()
devSettings.isHotModuleReplacementEnabled = false
}
handleReloadJS()
}
if (devSettings.isDeviceDebugEnabled) {
// On-device JS debugging (CDP). Render action to open debugger frontend.
val isConnected = isPackagerConnected
val debuggerItemString =
applicationContext.getString(
if (isConnected) R.string.catalyst_debug_open
else R.string.catalyst_debug_open_disabled)
if (!isConnected) {
disabledItemKeys.add(debuggerItemString)
}
options[debuggerItemString] = DevOptionHandler { this.openDebugger() }
}
options[applicationContext.getString(R.string.catalyst_change_bundle_location)] =
DevOptionHandler {
val context = reactInstanceDevHelper.currentActivity
if (context == null || context.isFinishing) {
FLog.e(
ReactConstants.TAG,
"Unable to launch change bundle location because react activity is not available")
return@DevOptionHandler
}
val input = EditText(context)
input.hint = "localhost:8081"
val bundleLocationDialog =
AlertDialog.Builder(context)
.setTitle(applicationContext.getString(R.string.catalyst_change_bundle_location))
.setView(input)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val host = input.text.toString()
devSettings.packagerConnectionSettings.debugServerHost = host
handleReloadJS()
}
.create()
bundleLocationDialog.show()
}
options[applicationContext.getString(R.string.catalyst_inspector_toggle)] = DevOptionHandler {
devSettings.isElementInspectorEnabled = !devSettings.isElementInspectorEnabled
reactInstanceDevHelper.toggleElementInspector()
}
val hotReloadLabel =
if (devSettings.isHotModuleReplacementEnabled) {
applicationContext.getString(R.string.catalyst_hot_reloading_stop)
} else {
applicationContext.getString(R.string.catalyst_hot_reloading)
}
options[hotReloadLabel] = DevOptionHandler {
val nextEnabled = !devSettings.isHotModuleReplacementEnabled
devSettings.isHotModuleReplacementEnabled = nextEnabled
val reactContext = currentReactContext
if (reactContext != null) {
if (nextEnabled) {
reactContext.getJSModule(HMRClient::class.java)?.enable()
} else {
reactContext.getJSModule(HMRClient::class.java)?.disable()
}
}
if (nextEnabled && !devSettings.isJSDevModeEnabled) {
Toast.makeText(
applicationContext,
applicationContext.getString(R.string.catalyst_hot_reloading_auto_enable),
Toast.LENGTH_LONG)
.show()
devSettings.isJSDevModeEnabled = true
handleReloadJS()
}
}
val fpsDebugLabel =
if (devSettings.isFpsDebugEnabled)
applicationContext.getString(R.string.catalyst_perf_monitor_stop)
else applicationContext.getString(R.string.catalyst_perf_monitor)
options[fpsDebugLabel] = DevOptionHandler {
if (!devSettings.isFpsDebugEnabled) {
// Request overlay permission if needed when "Show Perf Monitor" option is selected
val context: Context? = reactInstanceDevHelper.currentActivity
if (context == null) {
FLog.e(ReactConstants.TAG, "Unable to get reference to react activity")
} else {
requestPermission(context)
}
}
devSettings.isFpsDebugEnabled = !devSettings.isFpsDebugEnabled
}
options[applicationContext.getString(R.string.catalyst_settings)] = DevOptionHandler {
val intent = Intent(applicationContext, DevSettingsActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
applicationContext.startActivity(intent)
}
if (customDevOptions.isNotEmpty()) {
options.putAll(customDevOptions)
}
val optionHandlers = options.values.toTypedArray<DevOptionHandler>()
val context = reactInstanceDevHelper.currentActivity
if (context == null || context.isFinishing) {
FLog.e(
ReactConstants.TAG,
"Unable to launch dev options menu because react activity " + "isn't available")
return
}
val header = LinearLayout(context)
header.orientation = LinearLayout.VERTICAL
TextView(context).apply {
text = context.getString(R.string.catalyst_dev_menu_header, uniqueTag)
setPadding(0, 50, 0, 0)
gravity = Gravity.CENTER
textSize = 16f
setTypeface(typeface, Typeface.BOLD)
header.addView(this)
}
val jsExecutorDescription = jSExecutorDescription
if (jsExecutorDescription != null) {
TextView(context).apply {
text = context.getString(R.string.catalyst_dev_menu_sub_header, jsExecutorDescription)
setPadding(0, 20, 0, 0)
gravity = Gravity.CENTER
textSize = 14f
header.addView(this)
}
}
val adapter: ListAdapter =
object :
ArrayAdapter<String?>(
context, android.R.layout.simple_list_item_1, options.keys.toTypedArray<String>()) {
override fun areAllItemsEnabled(): Boolean = false
override fun isEnabled(position: Int): Boolean =
!disabledItemKeys.contains(getItem(position))
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View =
super.getView(position, convertView, parent).apply { isEnabled = isEnabled(position) }
}
devOptionsDialog =
AlertDialog.Builder(context)
.setCustomTitle(header)
.setAdapter(adapter) { _: DialogInterface?, which: Int ->
optionHandlers[which].onOptionSelected()
devOptionsDialog = null
}
.setOnCancelListener { devOptionsDialog = null }
.create()
devOptionsDialog?.show()
val reactContext = currentReactContext
if (reactContext != null) {
reactContext.getJSModule(RCTNativeAppEventEmitter::class.java).emit("RCTDevMenuShown", null)
}
}
override fun onNewReactContextCreated(reactContext: ReactContext) {
resetCurrentContext(reactContext)
}
override fun onReactInstanceDestroyed(reactContext: ReactContext) {
if (reactContext === currentReactContext) {
// only call reset context when the destroyed context matches the one that is currently set
// for this manager
resetCurrentContext(null)
}
// If some JNI types (e.g. jni::HybridClass) are used in JSI (e.g. jsi::HostObject), they might
// not be immediately deleted on an app refresh as both Java and JavaScript are
// garbage-collected languages and the memory might float around for a while. For C++
// developers, this will be hard to debug as destructors might be called at a later point, so in
// this case we trigger a Java GC to maybe eagerly collect such objects when the app
// reloads.
System.gc()
}
/**
* @return `true` if [com.facebook.react.ReactInstanceManager] should use downloaded JS bundle
* file instead of using JS file from assets. This may happen when app has not been updated
* since the last time we fetched the bundle.
*/
override fun hasUpToDateJSBundleInCache(): Boolean {
if (isDevSupportEnabled && jsBundleDownloadedFile.exists()) {
try {
val packageName = applicationContext.packageName
val packageManager = applicationContext.packageManager
if (packageManager != null) {
val thisPackage = packageManager.getPackageInfo(packageName, 0)
if (jsBundleDownloadedFile.lastModified() > thisPackage.lastUpdateTime) {
// Base APK has not been updated since we downloaded JS, but if app is using exopackage
// it may only be a single dex that has been updated. We check for exopackage dir update
// time in that case.
val exopackageDir =
File(String.format(Locale.US, EXOPACKAGE_LOCATION_FORMAT, packageName))
if (exopackageDir.exists()) {
return jsBundleDownloadedFile.lastModified() > exopackageDir.lastModified()
}
return true
}
}
} catch (e: PackageManager.NameNotFoundException) {
// Ignore this error and just fallback to loading JS from assets
FLog.e(ReactConstants.TAG, "DevSupport is unable to get current app info")
}
}
return false
}
private fun resetCurrentContext(reactContext: ReactContext?) {
if (currentReactContext === reactContext) {
// new context is the same as the old one - do nothing
return
}
currentReactContext = reactContext
// Recreate debug overlay controller with new CatalystInstance object
debugOverlayController?.setFpsDebugViewVisible(false)
reactContext?.let { debugOverlayController = DebugOverlayController(it) }
if (reactContext != null) {
try {
val sourceUrl = URL(sourceUrl)
var path = sourceUrl.path
if (path != null) {
path = path.substring(1) // strip initial slash in path
}
val host = sourceUrl.host
val scheme = sourceUrl.protocol
val port = if (sourceUrl.port != -1) sourceUrl.port else sourceUrl.defaultPort
reactContext
.getJSModule(HMRClient::class.java)
.setup("android", path, host, port, devSettings.isHotModuleReplacementEnabled, scheme)
} catch (e: MalformedURLException) {
showNewJavaError(e.message, e)
}
}
reloadSettings()
}
override fun reloadSettings() {
if (UiThreadUtil.isOnUiThread()) {
reload()
} else {
UiThreadUtil.runOnUiThread { this.reload() }
}
}
@UiThread
private fun showDevLoadingViewForUrl(bundleUrl: String) {
val parsedURL: URL
try {
parsedURL = URL(bundleUrl)
} catch (e: MalformedURLException) {
FLog.e(ReactConstants.TAG, "Bundle url format is invalid. \n\n$e")
return
}
val port = if (parsedURL.port != -1) parsedURL.port else parsedURL.defaultPort
devLoadingViewManager?.showMessage(
applicationContext.getString(
R.string.catalyst_loading_from_url, parsedURL.host + ":" + port))
devLoadingViewVisible = true
}
@UiThread
protected fun showDevLoadingViewForRemoteJSEnabled() {
devLoadingViewManager?.showMessage(
applicationContext.getString(R.string.catalyst_debug_connecting))
devLoadingViewVisible = true
}
@UiThread
protected fun hideDevLoadingView() {
devLoadingViewManager?.hide()
devLoadingViewVisible = false
}
public fun fetchSplitBundleAndCreateBundleLoader(
bundlePath: String,
callback: CallbackWithBundleLoader
) {
val bundleUrl = devServerHelper.getDevServerSplitBundleURL(bundlePath)
// The bundle path may contain the '/' character, which is not allowed in file names.
val bundleFile = File(jsSplitBundlesDir, bundlePath.replace("/".toRegex(), "_") + ".jsbundle")
UiThreadUtil.runOnUiThread {
showSplitBundleDevLoadingView(bundleUrl)
devServerHelper.downloadBundleFromURL(
object : DevBundleDownloadListener {
override fun onSuccess() {
UiThreadUtil.runOnUiThread { hideSplitBundleDevLoadingView() }
val context: ReactContext? = this@DevSupportManagerBase.currentReactContext
if (context == null || !context.hasActiveReactInstance()) {
return
}
val bundleLoader =
JSBundleLoader.createCachedSplitBundleFromNetworkLoader(
bundleUrl, bundleFile.absolutePath)
callback.onSuccess(bundleLoader)
}
override fun onProgress(status: String?, done: Int?, total: Int?) {
devLoadingViewManager?.updateProgress(status, done, total)
}
override fun onFailure(cause: Exception) {
UiThreadUtil.runOnUiThread {
this@DevSupportManagerBase.hideSplitBundleDevLoadingView()
}
callback.onError(bundleUrl, cause)
}
},
bundleFile,
bundleUrl,
null)
}
}
@UiThread
private fun showSplitBundleDevLoadingView(bundleUrl: String) {
showDevLoadingViewForUrl(bundleUrl)
pendingJSSplitBundleRequests++
}
@UiThread
private fun hideSplitBundleDevLoadingView() {
if (--pendingJSSplitBundleRequests == 0) {
hideDevLoadingView()
}
}
override fun isPackagerRunning(callback: PackagerStatusCallback) {
val checkPackagerRunning = Runnable { devServerHelper.isPackagerRunning(callback) }
packagerLocationCustomizer?.run(checkPackagerRunning) ?: checkPackagerRunning.run()
}
override fun downloadBundleResourceFromUrlSync(resourceURL: String, outputFile: File): File? =
devServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile)
private fun updateLastErrorInfo(
message: String?,
stack: Array<StackFrame>,
errorCookie: Int,
errorType: ErrorType
) {
lastErrorTitle = message
lastErrorStack = stack
lastErrorCookie = errorCookie
lastErrorType = errorType
}
override fun reloadJSFromServer(bundleURL: String, callback: BundleLoadCallback) {
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START)
showDevLoadingViewForUrl(bundleURL)
val bundleInfo = BundleDownloader.BundleInfo()
devServerHelper.downloadBundleFromURL(
object : DevBundleDownloadListener {
override fun onSuccess() {
hideDevLoadingView()
devBundleDownloadListener?.onSuccess()
ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString())
callback.onSuccess()
}
override fun onProgress(status: String?, done: Int?, total: Int?) {
devLoadingViewManager?.updateProgress(status, done, total)
devBundleDownloadListener?.onProgress(status, done, total)
}
override fun onFailure(cause: Exception) {
hideDevLoadingView()
devBundleDownloadListener?.onFailure(cause)
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause)
reportBundleLoadingFailure(cause)
callback.onError(cause)
}
},
jsBundleDownloadedFile,
bundleURL,
bundleInfo)
}
private fun reportBundleLoadingFailure(cause: Exception) {
UiThreadUtil.runOnUiThread {
if (cause is DebugServerException) {
showNewJavaError(cause.message, cause)
} else {
showNewJavaError(applicationContext.getString(R.string.catalyst_reload_error), cause)
}
}
}
override fun startInspector() {
if (isDevSupportEnabled) {
devServerHelper.openInspectorConnection()
}
}
override fun stopInspector() {
devServerHelper.closeInspectorConnection()
}
override fun setHotModuleReplacementEnabled(isHotModuleReplacementEnabled: Boolean) {
if (!isDevSupportEnabled) {
return
}
UiThreadUtil.runOnUiThread {
devSettings.isHotModuleReplacementEnabled = isHotModuleReplacementEnabled
handleReloadJS()
}
}
override fun setFpsDebugEnabled(isFpsDebugEnabled: Boolean) {
if (!isDevSupportEnabled) {
return
}
UiThreadUtil.runOnUiThread { devSettings.isFpsDebugEnabled = isFpsDebugEnabled }
}
override fun toggleElementInspector() {
if (!isDevSupportEnabled) {
return
}
UiThreadUtil.runOnUiThread {
devSettings.isElementInspectorEnabled = !devSettings.isElementInspectorEnabled
reactInstanceDevHelper.toggleElementInspector()
}
}
private fun reload() {
UiThreadUtil.assertOnUiThread()
// reload settings, show/hide debug overlay if required & start/stop shake detector
if (isDevSupportEnabled) {
// update visibility of FPS debug overlay depending on the settings
debugOverlayController?.setFpsDebugViewVisible(devSettings.isFpsDebugEnabled)
// start shake gesture detector
if (!isShakeDetectorStarted) {
val sensorManager =
applicationContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
shakeDetector.start(sensorManager)
isShakeDetectorStarted = true
}
// register reload app broadcast receiver
if (!isReceiverRegistered) {
val filter = IntentFilter()
filter.addAction(getReloadAppAction(applicationContext))
compatRegisterReceiver(applicationContext, reloadAppBroadcastReceiver, filter, true)
isReceiverRegistered = true
}
// show the dev loading if it should be
if (devLoadingViewVisible) {
devLoadingViewManager?.showMessage("Reloading...")
}
devServerHelper.openPackagerConnection(
javaClass.simpleName,
object : PackagerCommandListener {
override fun onPackagerConnected() {
isPackagerConnected = true
}
override fun onPackagerDisconnected() {
isPackagerConnected = false
}
override fun onPackagerReloadCommand() {
if (!getFuseboxEnabled()) {
// Disable debugger to resume the JsVM & avoid thread locks while reloading
devServerHelper.disableDebugger()
}
UiThreadUtil.runOnUiThread { handleReloadJS() }
}
override fun onPackagerDevMenuCommand() {
UiThreadUtil.runOnUiThread { showDevOptionsDialog() }
}
override fun customCommandHandlers(): Map<String, RequestHandler>? {
return customPackagerCommandHandlers
}
})
} else {
// hide FPS debug overlay
debugOverlayController?.setFpsDebugViewVisible(false)
// stop shake gesture detector
if (isShakeDetectorStarted) {
shakeDetector.stop()
isShakeDetectorStarted = false
}
// unregister app reload broadcast receiver
if (isReceiverRegistered) {
applicationContext.unregisterReceiver(reloadAppBroadcastReceiver)
isReceiverRegistered = false
}
// hide redbox dialog
hideRedboxDialog()
// hide dev options dialog
hideDevOptionsDialog()
// hide loading view
devLoadingViewManager?.hide()
devServerHelper.closePackagerConnection()
}
}
override fun setPackagerLocationCustomizer(
packagerLocationCustomizer: PackagerLocationCustomizer
) {
this.packagerLocationCustomizer = packagerLocationCustomizer
}
override fun createSurfaceDelegate(moduleName: String): SurfaceDelegate? =
surfaceDelegateFactory?.createSurfaceDelegate(moduleName)
/**
* Starting with Android 14, apps and services that target Android 14 and use context-registered
* receivers are required to specify a flag to indicate whether or not the receiver should be
* exported to all other apps on the device: either RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED
*
* https://developer.android.com/about/versions/14/behavior-changes-14#runtime-receivers-exported
*/
private fun compatRegisterReceiver(
context: Context,
receiver: BroadcastReceiver,
filter: IntentFilter,
exported: Boolean
) {
@SuppressLint("UnspecifiedRegisterReceiverFlag")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context.registerReceiver(
receiver,
filter,
if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(receiver, filter)
}
}
override fun openDebugger() {
devServerHelper.openDebugger(
currentReactContext, applicationContext.getString(R.string.catalyst_open_debugger_error))
}
override fun showPausedInDebuggerOverlay(
message: String,
listener: PausedInDebuggerOverlayCommandListener
) {
pausedInDebuggerOverlayManager?.showPausedInDebuggerOverlay(message, listener)
}
override fun hidePausedInDebuggerOverlay() {
pausedInDebuggerOverlayManager?.hidePausedInDebuggerOverlay()
}
override fun setAdditionalOptionForPackager(name: String, value: String) {
devSettings.packagerConnectionSettings.setAdditionalOptionForPackager(name, value)
}
public companion object {
private const val JAVA_ERROR_COOKIE = -1
private const val JSEXCEPTION_ERROR_COOKIE = -1
private const val RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"
private const val EXOPACKAGE_LOCATION_FORMAT = "/data/local/tmp/exopackage/%s//secondary-dex"
/** Intent action for reloading the JS */
private fun getReloadAppAction(context: Context): String =
context.packageName + RELOAD_APP_ACTION_SUFFIX
}
}
@@ -12,7 +12,6 @@ import com.facebook.react.bridge.ReadableType
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.devsupport.interfaces.StackFrame
import java.io.File
import java.util.regex.Matcher
import java.util.regex.Pattern
import org.json.JSONArray
import org.json.JSONException
@@ -43,13 +42,12 @@ public object StackTraceHelper {
/** Convert a JavaScript stack trace (see `parseErrorStack` JS module) to an array of [ ]s. */
@JvmStatic
public fun convertJsStackTrace(stack: ReadableArray?): Array<StackFrame?> {
public fun convertJsStackTrace(stack: ReadableArray?): Array<StackFrame> {
val size = stack?.size() ?: 0
val result = arrayOfNulls<StackFrame>(size)
if (stack == null) {
return result
return arrayOf()
}
for (i in 0..<size) {
return Array(size) { i ->
val type = stack.getType(i)
if (type == ReadableType.Map) {
val frame = requireNotNull(stack.getMap(i))
@@ -65,34 +63,31 @@ public object StackTraceHelper {
if (frame.hasKey(COLUMN_KEY) && !frame.isNull(COLUMN_KEY)) {
columnNumber = frame.getInt(COLUMN_KEY)
}
result[i] =
StackFrameImpl(
file = file,
method = method,
line = lineNumber,
column = columnNumber,
isCollapsed = collapse)
StackFrameImpl(
file = file,
method = method,
line = lineNumber,
column = columnNumber,
isCollapsed = collapse)
} else if (type == ReadableType.String) {
val stackFrame = stack.getString(i)
if (stackFrame != null) {
result[i] = StackFrameImpl(file = null, method = stackFrame, line = -1, column = -1)
}
StackFrameImpl(
file = null, method = checkNotNull(stack.getString(i)), line = -1, column = -1)
} else {
error("Cannot parse the stackframe for $stack")
}
}
return result
}
/**
* Convert a JavaScript stack trace (see `parseErrorStack` JS module) to an array of [StackFrame].
*/
public fun convertJsStackTrace(stack: JSONArray?): Array<StackFrame?> {
val size = stack?.length() ?: 0
val result = arrayOfNulls<StackFrame>(size)
public fun convertJsStackTrace(stack: JSONArray?): Array<StackFrame> {
if (stack == null) {
return result
return arrayOf()
}
val size = stack.length()
try {
for (i in 0..<size) {
return Array(size) { i ->
val frame = stack.getJSONObject(i)
val method = frame.getString("methodName")
val file = frame.getString("file")
@@ -106,68 +101,58 @@ public object StackTraceHelper {
}
val collapse =
frame.has("collapse") && !frame.isNull("collapse") && frame.getBoolean("collapse")
result[i] =
StackFrameImpl(
file = file,
method = method,
line = lineNumber,
column = columnNumber,
isCollapsed = collapse)
StackFrameImpl(
file = file,
method = method,
line = lineNumber,
column = columnNumber,
isCollapsed = collapse)
}
} catch (exception: JSONException) {
throw RuntimeException(exception)
}
return result
}
/** Convert a JavaScript stack trace to an array of [StackFrame]s. */
public fun convertJsStackTrace(stack: String): Array<StackFrame?> {
public fun convertJsStackTrace(stack: String): Array<StackFrame> {
val stackTrace = stack.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val result = arrayOfNulls<StackFrame>(stackTrace.size)
for (i in stackTrace.indices) {
return Array(stackTrace.size) { i ->
val matcher1 = STACK_FRAME_PATTERN1.matcher(stackTrace[i])
val matcher2 = STACK_FRAME_PATTERN2.matcher(stackTrace[i])
val matcher: Matcher
if (matcher2.find()) {
matcher = matcher2
} else if (matcher1.find()) {
matcher = matcher1
val matcher =
when {
matcher2.find() -> matcher2
matcher1.find() -> matcher1
else -> null
}
if (matcher != null) {
val file = matcher.group(2)
val method = matcher.group(1) ?: "(unknown)"
val lineString = matcher.group(3)
val columnString = matcher.group(4)
StackFrameImpl(
file = file,
method = method,
line = checkNotNull(lineString).toInt(),
column = checkNotNull(columnString).toInt())
} else {
result[i] = StackFrameImpl(file = null, method = stackTrace[i], line = -1, column = -1)
continue
StackFrameImpl(file = null, method = stackTrace[i], line = -1, column = -1)
}
val file = matcher.group(2)
val method = matcher.group(1) ?: "(unknown)"
val lineString = matcher.group(3)
val columnString = matcher.group(4)
if (file == null || lineString == null || columnString == null) {
continue
}
result[i] =
StackFrameImpl(
file = file,
method = method,
line = lineString.toInt(),
column = columnString.toInt())
}
return result
}
/** Convert a [Throwable] to an array of [StackFrame]s. */
@JvmStatic
public fun convertJavaStackTrace(exception: Throwable): Array<StackFrame?> {
public fun convertJavaStackTrace(exception: Throwable): Array<StackFrame> {
val stackTrace = exception.stackTrace
val result = arrayOfNulls<StackFrame>(stackTrace.size)
for (i in stackTrace.indices) {
result[i] =
StackFrameImpl(
file = stackTrace[i].className,
fileName = stackTrace[i].fileName,
method = stackTrace[i].methodName,
line = stackTrace[i].lineNumber,
column = -1)
return Array(stackTrace.size) { i ->
StackFrameImpl(
file = stackTrace[i].className,
fileName = stackTrace[i].fileName,
method = stackTrace[i].methodName,
line = stackTrace[i].lineNumber,
column = -1)
}
return result
}
/** Format a [StackFrame] to a String (method name is not included). */