diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 4169e98ff69..6bb0a7db41c 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 (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; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt index e24c4fd2c69..1cc34b3e77c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgeDevSupportManager.kt @@ -56,7 +56,7 @@ public class BridgeDevSupportManager( redBoxHandler: RedBoxHandler?, devBundleDownloadListener: DevBundleDownloadListener?, minNumShakes: Int, - customPackagerCommandHandlers: Map?, + customPackagerCommandHandlers: Map?, 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( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt index fd747969635..f8027afae5d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt @@ -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()") } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java deleted file mode 100644 index 734ad9b1281..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java +++ /dev/null @@ -1,1144 +0,0 @@ -/* - * 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.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.PackageInfo; -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.Nullable; -import androidx.annotation.UiThread; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.R; -import com.facebook.react.bridge.DefaultJSExceptionHandler; -import com.facebook.react.bridge.JSBundleLoader; -import com.facebook.react.bridge.JavaScriptExecutorFactory; -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.DevServerHelper.PackagerCommandListener; -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.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.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -@Nullsafe(Nullsafe.Mode.LOCAL) -public abstract class DevSupportManagerBase implements DevSupportManager { - - public interface CallbackWithBundleLoader { - void onSuccess(JSBundleLoader bundleLoader); - - void onError(String url, Throwable cause); - } - - private static final int JAVA_ERROR_COOKIE = -1; - private static final int JSEXCEPTION_ERROR_COOKIE = -1; - private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; - private static final String EXOPACKAGE_LOCATION_FORMAT = - "/data/local/tmp/exopackage/%s//secondary-dex"; - - private final Context mApplicationContext; - private final ShakeDetector mShakeDetector; - private final BroadcastReceiver mReloadAppBroadcastReceiver; - private final DevServerHelper mDevServerHelper; - private final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); - protected final ReactInstanceDevHelper mReactInstanceDevHelper; - private final @Nullable String mJSAppBundleName; - private final File mJSBundleDownloadedFile; - private final File mJSSplitBundlesDir; - private final DefaultJSExceptionHandler mDefaultJSExceptionHandler; - private final DevLoadingViewManager mDevLoadingViewManager; - private final PausedInDebuggerOverlayManager mPausedInDebuggerOverlayManager; - - private @Nullable SurfaceDelegate mRedBoxSurfaceDelegate; - private @Nullable AlertDialog mDevOptionsDialog; - private @Nullable DebugOverlayController mDebugOverlayController; - private boolean mDevLoadingViewVisible = false; - private int mPendingJSSplitBundleRequests = 0; - private @Nullable ReactContext mCurrentReactContext; - private final DeveloperSettings mDevSettings; - private boolean mIsReceiverRegistered = false; - private boolean mIsShakeDetectorStarted = false; - private boolean mIsDevSupportEnabled = false; - private boolean mIsPackagerConnected; - private @Nullable final RedBoxHandler mRedBoxHandler; - private @Nullable String mLastErrorTitle; - private @Nullable StackFrame[] mLastErrorStack; - private @Nullable ErrorType mLastErrorType; - private int mLastErrorCookie = 0; - private @Nullable final DevBundleDownloadListener mBundleDownloadListener; - private @Nullable List mErrorCustomizers; - private @Nullable PackagerLocationCustomizer mPackagerLocationCustomizer; - - private @Nullable final Map mCustomPackagerCommandHandlers; - - private @Nullable final SurfaceDelegateFactory mSurfaceDelegateFactory; - - public DevSupportManagerBase( - Context applicationContext, - ReactInstanceDevHelper reactInstanceDevHelper, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate, - @Nullable RedBoxHandler redBoxHandler, - @Nullable DevBundleDownloadListener devBundleDownloadListener, - int minNumShakes, - @Nullable Map customPackagerCommandHandlers, - @Nullable SurfaceDelegateFactory surfaceDelegateFactory, - @Nullable DevLoadingViewManager devLoadingViewManager, - @Nullable PausedInDebuggerOverlayManager pausedInDebuggerOverlayManager) { - mReactInstanceDevHelper = reactInstanceDevHelper; - mApplicationContext = applicationContext; - mJSAppBundleName = packagerPathForJSBundleName; - mDevSettings = new DevInternalSettings(applicationContext, this::reloadSettings); - mDevServerHelper = - new DevServerHelper( - mDevSettings, mApplicationContext, mDevSettings.getPackagerConnectionSettings()); - mBundleDownloadListener = devBundleDownloadListener; - - // Prepare shake gesture detector (will be started/stopped from #reload) - mShakeDetector = new ShakeDetector(this::showDevOptionsDialog, minNumShakes); - - mCustomPackagerCommandHandlers = customPackagerCommandHandlers; - - // Prepare reload APP broadcast receiver (will be registered/unregistered from #reload) - mReloadAppBroadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (getReloadAppAction(context).equals(action)) { - handleReloadJS(); - } - } - }; - - // 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 - final String subclassTag = getUniqueTag(); - final String bundleFile = subclassTag + "ReactNativeDevBundle.js"; - mJSBundleDownloadedFile = new File(applicationContext.getFilesDir(), bundleFile); - - final String splitBundlesDir = subclassTag.toLowerCase(Locale.ROOT) + "_dev_js_split_bundles"; - mJSSplitBundlesDir = mApplicationContext.getDir(splitBundlesDir, Context.MODE_PRIVATE); - - mDefaultJSExceptionHandler = new DefaultJSExceptionHandler(); - - setDevSupportEnabled(enableOnCreate); - - mRedBoxHandler = redBoxHandler; - mDevLoadingViewManager = - devLoadingViewManager != null - ? devLoadingViewManager - : new DefaultDevLoadingViewImplementation(reactInstanceDevHelper); - mSurfaceDelegateFactory = surfaceDelegateFactory; - mPausedInDebuggerOverlayManager = - pausedInDebuggerOverlayManager != null - ? pausedInDebuggerOverlayManager - : new PausedInDebuggerOverlayDialogManager( - () -> { - Activity context = mReactInstanceDevHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - return null; - } - return context; - }); - } - ; - - protected abstract String getUniqueTag(); - - @Override - public void handleException(Exception e) { - if (mIsDevSupportEnabled) { - logJSException(e); - } else { - mDefaultJSExceptionHandler.handleException(e); - } - } - - private void logJSException(Exception e) { - StringBuilder message = - new StringBuilder( - e.getMessage() == null ? "Exception in native call from JS" : e.getMessage()); - Throwable cause = e.getCause(); - while (cause != null) { - message.append("\n\n").append(cause.getMessage()); - cause = cause.getCause(); - } - - if (e instanceof JavascriptException) { - FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); - showNewError(e.getMessage(), new StackFrame[] {}, JSEXCEPTION_ERROR_COOKIE, ErrorType.JS); - } else { - showNewJavaError(message.toString(), e); - } - } - - @Override - public void showNewJavaError(@Nullable String message, Throwable e) { - FLog.e(ReactConstants.TAG, "Exception in native call", e); - showNewError( - message, StackTraceHelper.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 - public void addCustomDevOption(String optionName, DevOptionHandler optionHandler) { - mCustomDevOptions.put(optionName, optionHandler); - } - - @Override - public void showNewJSError( - @Nullable String message, @Nullable ReadableArray details, int errorCookie) { - showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS); - } - - @Override - public void registerErrorCustomizer(ErrorCustomizer errorCustomizer) { - if (mErrorCustomizers == null) { - mErrorCustomizers = new ArrayList<>(); - } - mErrorCustomizers.add(errorCustomizer); - } - - @Override - public Pair processErrorCustomizers(Pair errorInfo) { - if (mErrorCustomizers != null) { - for (ErrorCustomizer errorCustomizer : mErrorCustomizers) { - Pair result = errorCustomizer.customizeErrorInfo(errorInfo); - if (result != null) { - errorInfo = result; - } - } - } - return errorInfo; - } - - @Override - public void hideRedboxDialog() { - if (mRedBoxSurfaceDelegate == null) { - return; - } - - mRedBoxSurfaceDelegate.hide(); - } - - public @Nullable View createRootView(String appKey) { - return mReactInstanceDevHelper.createRootView(appKey); - } - - public void destroyRootView(@Nullable View rootView) { - if (rootView != null) { - mReactInstanceDevHelper.destroyRootView(rootView); - } - } - - private void hideDevOptionsDialog() { - if (mDevOptionsDialog != null) { - mDevOptionsDialog.dismiss(); - mDevOptionsDialog = null; - } - } - - private void showNewError( - @Nullable final String message, - final StackFrame[] stack, - final int errorCookie, - final ErrorType errorType) { - UiThreadUtil.runOnUiThread( - () -> { - // Keep a copy of the latest error to be shown by the RedBoxSurface - updateLastErrorInfo(message, stack, errorCookie, errorType); - - if (mRedBoxSurfaceDelegate == null) { - @Nullable SurfaceDelegate redBoxSurfaceDelegate = createSurfaceDelegate("RedBox"); - if (redBoxSurfaceDelegate != null) { - mRedBoxSurfaceDelegate = redBoxSurfaceDelegate; - } else { - mRedBoxSurfaceDelegate = new RedBoxDialogSurfaceDelegate(DevSupportManagerBase.this); - } - - mRedBoxSurfaceDelegate.createContentView("RedBox"); - } - - if (mRedBoxSurfaceDelegate.isShowing()) { - // Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only - // show the first and most actionable one. - return; - } - - mRedBoxSurfaceDelegate.show(); - }); - } - - @Override - public void showDevOptionsDialog() { - if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { - return; - } - LinkedHashMap options = new LinkedHashMap<>(); - Set disabledItemKeys = new HashSet<>(); - /* register standard options */ - options.put( - mApplicationContext.getString(R.string.catalyst_reload), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isJSDevModeEnabled() - && mDevSettings.isHotModuleReplacementEnabled()) { - Toast.makeText( - mApplicationContext, - mApplicationContext.getString(R.string.catalyst_hot_reloading_auto_disable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setHotModuleReplacementEnabled(false); - } - handleReloadJS(); - } - }); - - if (mDevSettings.isDeviceDebugEnabled()) { - // On-device JS debugging (CDP). Render action to open debugger frontend. - boolean isConnected = mIsPackagerConnected; - String debuggerItemString = - mApplicationContext.getString( - isConnected ? R.string.catalyst_debug_open : R.string.catalyst_debug_open_disabled); - if (!isConnected) { - disabledItemKeys.add(debuggerItemString); - } - options.put(debuggerItemString, () -> openDebugger()); - } - - options.put( - mApplicationContext.getString(R.string.catalyst_change_bundle_location), - () -> { - Activity context = mReactInstanceDevHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e( - ReactConstants.TAG, - "Unable to launch change bundle location because react activity is not available"); - return; - } - - final EditText input = new EditText(context); - input.setHint("localhost:8081"); - - AlertDialog bundleLocationDialog = - new AlertDialog.Builder(context) - .setTitle(mApplicationContext.getString(R.string.catalyst_change_bundle_location)) - .setView(input) - .setPositiveButton( - android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String host = input.getText().toString(); - mDevSettings.getPackagerConnectionSettings().setDebugServerHost(host); - handleReloadJS(); - } - }) - .create(); - bundleLocationDialog.show(); - }); - - options.put( - mApplicationContext.getString(R.string.catalyst_inspector_toggle), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceDevHelper.toggleElementInspector(); - } - }); - - options.put( - mDevSettings.isHotModuleReplacementEnabled() - ? mApplicationContext.getString(R.string.catalyst_hot_reloading_stop) - : mApplicationContext.getString(R.string.catalyst_hot_reloading), - () -> { - boolean nextEnabled = !mDevSettings.isHotModuleReplacementEnabled(); - mDevSettings.setHotModuleReplacementEnabled(nextEnabled); - if (mCurrentReactContext != null) { - if (nextEnabled) { - mCurrentReactContext.getJSModule(HMRClient.class).enable(); - } else { - mCurrentReactContext.getJSModule(HMRClient.class).disable(); - } - } - if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { - Toast.makeText( - mApplicationContext, - mApplicationContext.getString(R.string.catalyst_hot_reloading_auto_enable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setJSDevModeEnabled(true); - handleReloadJS(); - } - }); - - options.put( - mDevSettings.isFpsDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_perf_monitor_stop) - : mApplicationContext.getString(R.string.catalyst_perf_monitor), - () -> { - if (!mDevSettings.isFpsDebugEnabled()) { - // Request overlay permission if needed when "Show Perf Monitor" option is selected - Context context = mReactInstanceDevHelper.getCurrentActivity(); - if (context == null) { - FLog.e(ReactConstants.TAG, "Unable to get reference to react activity"); - } else { - DebugOverlayController.requestPermission(context); - } - } - mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); - }); - options.put( - mApplicationContext.getString(R.string.catalyst_settings), - () -> { - Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mApplicationContext.startActivity(intent); - }); - - if (mCustomDevOptions.size() > 0) { - options.putAll(mCustomDevOptions); - } - - final DevOptionHandler[] optionHandlers = options.values().toArray(new DevOptionHandler[0]); - - Activity context = mReactInstanceDevHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e( - ReactConstants.TAG, - "Unable to launch dev options menu because react activity " + "isn't available"); - return; - } - - final LinearLayout header = new LinearLayout(context); - header.setOrientation(LinearLayout.VERTICAL); - - final TextView title = new TextView(context); - title.setText(context.getString(R.string.catalyst_dev_menu_header, getUniqueTag())); - title.setPadding(0, 50, 0, 0); - title.setGravity(Gravity.CENTER); - title.setTextSize(16); - title.setTypeface(title.getTypeface(), Typeface.BOLD); - header.addView(title); - - String jsExecutorDescription = getJSExecutorDescription(); - - if (jsExecutorDescription != null) { - final TextView jsExecutorLabel = new TextView(context); - jsExecutorLabel.setText( - context.getString(R.string.catalyst_dev_menu_sub_header, jsExecutorDescription)); - jsExecutorLabel.setPadding(0, 20, 0, 0); - jsExecutorLabel.setGravity(Gravity.CENTER); - jsExecutorLabel.setTextSize(14); - header.addView(jsExecutorLabel); - } - - ListAdapter adapter = - new ArrayAdapter( - context, android.R.layout.simple_list_item_1, options.keySet().toArray(new String[0])) { - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(int position) { - return !disabledItemKeys.contains(getItem(position)); - } - - @Override - public View getView(int position, @Nullable View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setEnabled(isEnabled(position)); - - return view; - } - }; - - mDevOptionsDialog = - new AlertDialog.Builder(context) - .setCustomTitle(header) - .setAdapter( - adapter, - (dialog, which) -> { - optionHandlers[which].onOptionSelected(); - mDevOptionsDialog = null; - }) - .setOnCancelListener(dialog -> mDevOptionsDialog = null) - .create(); - mDevOptionsDialog.show(); - if (mCurrentReactContext != null) { - mCurrentReactContext - .getJSModule(RCTNativeAppEventEmitter.class) - .emit("RCTDevMenuShown", null); - } - } - - private @Nullable String getJSExecutorDescription() { - try { - JavaScriptExecutorFactory factory = - getReactInstanceDevHelper().getJavaScriptExecutorFactory(); - if (factory != null) { - return factory.toString(); - } else { - return null; - } - } catch (IllegalStateException e) { - return null; - } - } - - /** - * {@link 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). - */ - @Override - public void setDevSupportEnabled(boolean isDevSupportEnabled) { - mIsDevSupportEnabled = isDevSupportEnabled; - reloadSettings(); - } - - @Override - public boolean getDevSupportEnabled() { - return mIsDevSupportEnabled; - } - - @Override - public DeveloperSettings getDevSettings() { - return mDevSettings; - } - - @Override - public @Nullable RedBoxHandler getRedBoxHandler() { - return mRedBoxHandler; - } - - @Override - public void onNewReactContextCreated(ReactContext reactContext) { - resetCurrentContext(reactContext); - } - - @Override - public void onReactInstanceDestroyed(ReactContext reactContext) { - if (reactContext == mCurrentReactContext) { - // 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(); - } - - @Override - public String getSourceMapUrl() { - if (mJSAppBundleName == null) { - return ""; - } - - return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); - } - - @Override - public String getSourceUrl() { - if (mJSAppBundleName == null) { - return ""; - } - - return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); - } - - @Override - public String getDownloadedJSBundleFile() { - return mJSBundleDownloadedFile.getAbsolutePath(); - } - - /** - * @return {@code true} if {@link 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 - public boolean hasUpToDateJSBundleInCache() { - if (mIsDevSupportEnabled && mJSBundleDownloadedFile.exists()) { - try { - String packageName = mApplicationContext.getPackageName(); - PackageManager packageManager = mApplicationContext.getPackageManager(); - if (packageManager != null) { - PackageInfo thisPackage = packageManager.getPackageInfo(packageName, 0); - if (mJSBundleDownloadedFile.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. - File exopackageDir = - new File(String.format(Locale.US, EXOPACKAGE_LOCATION_FORMAT, packageName)); - if (exopackageDir.exists()) { - return mJSBundleDownloadedFile.lastModified() > exopackageDir.lastModified(); - } - return true; - } - } - } catch (PackageManager.NameNotFoundException e) { - // 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 void resetCurrentContext(@Nullable ReactContext reactContext) { - if (mCurrentReactContext == reactContext) { - // new context is the same as the old one - do nothing - return; - } - - mCurrentReactContext = reactContext; - - // Recreate debug overlay controller with new CatalystInstance object - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); - } - if (reactContext != null) { - mDebugOverlayController = new DebugOverlayController(reactContext); - } - - if (mCurrentReactContext != null) { - try { - URL sourceUrl = new URL(getSourceUrl()); - String path = sourceUrl.getPath(); - if (path != null) { - path = path.substring(1); // strip initial slash in path - } - String host = sourceUrl.getHost(); - String scheme = sourceUrl.getProtocol(); - int port = sourceUrl.getPort() != -1 ? sourceUrl.getPort() : sourceUrl.getDefaultPort(); - mCurrentReactContext - .getJSModule(HMRClient.class) - .setup( - "android", path, host, port, mDevSettings.isHotModuleReplacementEnabled(), scheme); - } catch (MalformedURLException e) { - showNewJavaError(e.getMessage(), e); - } - } - - reloadSettings(); - } - - @Override - public void reloadSettings() { - if (UiThreadUtil.isOnUiThread()) { - reload(); - } else { - UiThreadUtil.runOnUiThread(this::reload); - } - } - - public @Nullable ReactContext getCurrentReactContext() { - return mCurrentReactContext; - } - - public @Nullable String getJSAppBundleName() { - return mJSAppBundleName; - } - - protected Context getApplicationContext() { - return mApplicationContext; - } - - public DevServerHelper getDevServerHelper() { - return mDevServerHelper; - } - - public DevLoadingViewManager getDevLoadingViewManager() { - return mDevLoadingViewManager; - } - - public ReactInstanceDevHelper getReactInstanceDevHelper() { - return mReactInstanceDevHelper; - } - - @UiThread - private void showDevLoadingViewForUrl(String bundleUrl) { - if (mApplicationContext == null) { - return; - } - - URL parsedURL; - - try { - parsedURL = new URL(bundleUrl); - } catch (MalformedURLException e) { - FLog.e(ReactConstants.TAG, "Bundle url format is invalid. \n\n" + e.toString()); - return; - } - - int port = parsedURL.getPort() != -1 ? parsedURL.getPort() : parsedURL.getDefaultPort(); - mDevLoadingViewManager.showMessage( - mApplicationContext.getString( - R.string.catalyst_loading_from_url, parsedURL.getHost() + ":" + port)); - mDevLoadingViewVisible = true; - } - - @UiThread - protected void showDevLoadingViewForRemoteJSEnabled() { - if (mApplicationContext == null) { - return; - } - - mDevLoadingViewManager.showMessage( - mApplicationContext.getString(R.string.catalyst_debug_connecting)); - mDevLoadingViewVisible = true; - } - - @UiThread - protected void hideDevLoadingView() { - mDevLoadingViewManager.hide(); - mDevLoadingViewVisible = false; - } - - public void fetchSplitBundleAndCreateBundleLoader( - String bundlePath, final CallbackWithBundleLoader callback) { - final String bundleUrl = mDevServerHelper.getDevServerSplitBundleURL(bundlePath); - // The bundle path may contain the '/' character, which is not allowed in file names. - final File bundleFile = - new File(mJSSplitBundlesDir, bundlePath.replaceAll("/", "_") + ".jsbundle"); - UiThreadUtil.runOnUiThread( - () -> { - showSplitBundleDevLoadingView(bundleUrl); - mDevServerHelper.downloadBundleFromURL( - new DevBundleDownloadListener() { - @Override - public void onSuccess() { - UiThreadUtil.runOnUiThread(() -> hideSplitBundleDevLoadingView()); - - @Nullable ReactContext context = mCurrentReactContext; - if (context == null || !context.hasActiveReactInstance()) { - return; - } - - JSBundleLoader bundleLoader = - JSBundleLoader.createCachedSplitBundleFromNetworkLoader( - bundleUrl, bundleFile.getAbsolutePath()); - - callback.onSuccess(bundleLoader); - } - - @Override - public void onProgress( - @Nullable String status, @Nullable Integer done, @Nullable Integer total) { - mDevLoadingViewManager.updateProgress(status, done, total); - } - - @Override - public void onFailure(Exception cause) { - UiThreadUtil.runOnUiThread( - DevSupportManagerBase.this::hideSplitBundleDevLoadingView); - callback.onError(bundleUrl, cause); - } - }, - bundleFile, - bundleUrl, - null); - }); - } - - @UiThread - private void showSplitBundleDevLoadingView(String bundleUrl) { - showDevLoadingViewForUrl(bundleUrl); - mPendingJSSplitBundleRequests++; - } - - @UiThread - private void hideSplitBundleDevLoadingView() { - if (--mPendingJSSplitBundleRequests == 0) { - hideDevLoadingView(); - } - } - - @Override - public void isPackagerRunning(final PackagerStatusCallback callback) { - Runnable checkPackagerRunning = () -> mDevServerHelper.isPackagerRunning(callback); - if (mPackagerLocationCustomizer != null) { - mPackagerLocationCustomizer.run(checkPackagerRunning); - } else { - checkPackagerRunning.run(); - } - } - - @Override - public @Nullable File downloadBundleResourceFromUrlSync( - final String resourceURL, final File outputFile) { - return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); - } - - @Override - public @Nullable String getLastErrorTitle() { - return mLastErrorTitle; - } - - @Override - public @Nullable StackFrame[] getLastErrorStack() { - return mLastErrorStack; - } - - @Override - public int getLastErrorCookie() { - return mLastErrorCookie; - } - - @Override - public @Nullable ErrorType getLastErrorType() { - return mLastErrorType; - } - - private void updateLastErrorInfo( - @Nullable final String message, - final StackFrame[] stack, - final int errorCookie, - final ErrorType errorType) { - mLastErrorTitle = message; - mLastErrorStack = stack; - mLastErrorCookie = errorCookie; - mLastErrorType = errorType; - } - - public void reloadJSFromServer(final String bundleURL, final BundleLoadCallback callback) { - ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START); - - showDevLoadingViewForUrl(bundleURL); - - final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo(); - - mDevServerHelper.downloadBundleFromURL( - new DevBundleDownloadListener() { - @Override - public void onSuccess() { - hideDevLoadingView(); - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onSuccess(); - } - ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); - callback.onSuccess(); - } - - @Override - public void onProgress( - @Nullable final String status, - @Nullable final Integer done, - @Nullable final Integer total) { - mDevLoadingViewManager.updateProgress(status, done, total); - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onProgress(status, done, total); - } - } - - @Override - public void onFailure(final Exception cause) { - hideDevLoadingView(); - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onFailure(cause); - } - FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); - reportBundleLoadingFailure(cause); - callback.onError(cause); - } - }, - mJSBundleDownloadedFile, - bundleURL, - bundleInfo); - } - - private void reportBundleLoadingFailure(final Exception cause) { - UiThreadUtil.runOnUiThread( - () -> { - if (cause instanceof DebugServerException) { - DebugServerException debugServerException = (DebugServerException) cause; - showNewJavaError(debugServerException.getMessage(), cause); - } else { - showNewJavaError(mApplicationContext.getString(R.string.catalyst_reload_error), cause); - } - }); - } - - @Override - public void startInspector() { - if (mIsDevSupportEnabled) { - mDevServerHelper.openInspectorConnection(); - } - } - - @Override - public void stopInspector() { - mDevServerHelper.closeInspectorConnection(); - } - - @Override - public void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mDevSettings.setHotModuleReplacementEnabled(isHotModuleReplacementEnabled); - handleReloadJS(); - }); - } - - @Override - public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - - UiThreadUtil.runOnUiThread(() -> mDevSettings.setFpsDebugEnabled(isFpsDebugEnabled)); - } - - @Override - public void toggleElementInspector() { - if (!mIsDevSupportEnabled) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceDevHelper.toggleElementInspector(); - }); - } - - private void reload() { - UiThreadUtil.assertOnUiThread(); - - // reload settings, show/hide debug overlay if required & start/stop shake detector - if (mIsDevSupportEnabled) { - // update visibility of FPS debug overlay depending on the settings - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(mDevSettings.isFpsDebugEnabled()); - } - - // start shake gesture detector - if (!mIsShakeDetectorStarted) { - SensorManager sensorManager = - (SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE); - if (sensorManager != null) { - mShakeDetector.start(sensorManager); - mIsShakeDetectorStarted = true; - } else { - FLog.w( - ReactConstants.TAG, - "Couldn't register shake gesture detector, sensor service is null"); - } - } - - // register reload app broadcast receiver - if (!mIsReceiverRegistered) { - IntentFilter filter = new IntentFilter(); - filter.addAction(getReloadAppAction(mApplicationContext)); - compatRegisterReceiver(mApplicationContext, mReloadAppBroadcastReceiver, filter, true); - mIsReceiverRegistered = true; - } - - // show the dev loading if it should be - if (mDevLoadingViewVisible) { - mDevLoadingViewManager.showMessage("Reloading..."); - } - - mDevServerHelper.openPackagerConnection( - this.getClass().getSimpleName(), - new PackagerCommandListener() { - @Override - public void onPackagerConnected() { - mIsPackagerConnected = true; - } - - @Override - public void onPackagerDisconnected() { - mIsPackagerConnected = false; - } - - @Override - public void onPackagerReloadCommand() { - if (!InspectorFlags.getFuseboxEnabled()) { - // Disable debugger to resume the JsVM & avoid thread locks while reloading - mDevServerHelper.disableDebugger(); - } - UiThreadUtil.runOnUiThread(() -> handleReloadJS()); - } - - @Override - public void onPackagerDevMenuCommand() { - UiThreadUtil.runOnUiThread(() -> showDevOptionsDialog()); - } - - @Override - public @Nullable Map customCommandHandlers() { - return mCustomPackagerCommandHandlers; - } - }); - } else { - // hide FPS debug overlay - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); - } - - // stop shake gesture detector - if (mIsShakeDetectorStarted) { - mShakeDetector.stop(); - mIsShakeDetectorStarted = false; - } - - // unregister app reload broadcast receiver - if (mIsReceiverRegistered) { - mApplicationContext.unregisterReceiver(mReloadAppBroadcastReceiver); - mIsReceiverRegistered = false; - } - - // hide redbox dialog - hideRedboxDialog(); - - // hide dev options dialog - hideDevOptionsDialog(); - - // hide loading view - mDevLoadingViewManager.hide(); - mDevServerHelper.closePackagerConnection(); - } - } - - /** Intent action for reloading the JS */ - private static String getReloadAppAction(Context context) { - return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; - } - - @Override - public void setPackagerLocationCustomizer( - DevSupportManager.PackagerLocationCustomizer packagerLocationCustomizer) { - mPackagerLocationCustomizer = packagerLocationCustomizer; - } - - @Override - public @Nullable Activity getCurrentActivity() { - return mReactInstanceDevHelper.getCurrentActivity(); - } - - @Override - public @Nullable SurfaceDelegate createSurfaceDelegate(String moduleName) { - if (mSurfaceDelegateFactory == null) { - return null; - } - - return mSurfaceDelegateFactory.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 void compatRegisterReceiver( - Context context, BroadcastReceiver receiver, IntentFilter filter, boolean exported) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE - && context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - context.registerReceiver( - receiver, filter, exported ? Context.RECEIVER_EXPORTED : Context.RECEIVER_NOT_EXPORTED); - } else { - context.registerReceiver(receiver, filter); - } - } - - @Override - public void openDebugger() { - mDevServerHelper.openDebugger( - mCurrentReactContext, mApplicationContext.getString(R.string.catalyst_open_debugger_error)); - } - - @Override - public void showPausedInDebuggerOverlay( - String message, PausedInDebuggerOverlayCommandListener listener) { - mPausedInDebuggerOverlayManager.showPausedInDebuggerOverlay(message, listener); - } - - @Override - public void hidePausedInDebuggerOverlay() { - mPausedInDebuggerOverlayManager.hidePausedInDebuggerOverlay(); - } - - @Override - public void setAdditionalOptionForPackager(String name, String value) { - mDevSettings.getPackagerConnectionSettings().setAdditionalOptionForPackager(name, value); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt new file mode 100644 index 00000000000..f6920bc038d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.kt @@ -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?, + 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? = 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() + 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 = 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> + ): Pair> { + 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, + 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() + val disabledItemKeys: MutableSet = 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() + + 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( + context, android.R.layout.simple_list_item_1, options.keys.toTypedArray()) { + 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, + 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? { + 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 + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.kt index bb033704309..d66020b767e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.kt @@ -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 { + public fun convertJsStackTrace(stack: ReadableArray?): Array { val size = stack?.size() ?: 0 - val result = arrayOfNulls(size) if (stack == null) { - return result + return arrayOf() } - for (i in 0.. 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 { - val size = stack?.length() ?: 0 - val result = arrayOfNulls(size) + public fun convertJsStackTrace(stack: JSONArray?): Array { if (stack == null) { - return result + return arrayOf() } + val size = stack.length() try { - for (i in 0.. 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 { + public fun convertJsStackTrace(stack: String): Array { val stackTrace = stack.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val result = arrayOfNulls(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 { + public fun convertJavaStackTrace(exception: Throwable): Array { val stackTrace = exception.stackTrace - val result = arrayOfNulls(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). */