From 704c4a4d5dcdbeed8ad1eb19ffef7361193540ed Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Thu, 7 Nov 2019 11:11:28 -0800 Subject: [PATCH 1/5] [android] Always reload from manifest https://github.com/expo/expo/pull/6134 --- .../com/facebook/react/ReactDelegate.java | 2 +- .../react/devsupport/DevInternalSettings.java | 320 +-- .../devsupport/DevSupportManagerImpl.java | 1944 ++++++++--------- .../devsupport/DisabledDevSupportManager.java | 4 + .../react/devsupport/RedBoxDialog.java | 4 +- .../interfaces/DevSupportManager.java | 2 + 6 files changed, 1081 insertions(+), 1195 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java index 72013128388704..2cd1dab1e732fa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java @@ -128,7 +128,7 @@ public boolean shouldShowDevMenuOrReload(int keyCode, KeyEvent event) { Assertions.assertNotNull(mDoubleTapReloadRecognizer) .didDoubleTapR(keyCode, mActivity.getCurrentFocus()); if (didDoubleTapR) { - getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS(); + getReactNativeHost().getReactInstanceManager().getDevSupportManager().reloadExpoApp(); return true; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java index c4a48a4281b309..ae5a138fad6051 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java @@ -21,159 +21,169 @@ * this class implements an external interface {@link DeveloperSettings}. */ @VisibleForTesting -public class DevInternalSettings - implements DeveloperSettings, SharedPreferences.OnSharedPreferenceChangeListener { - - private static final String PREFS_FPS_DEBUG_KEY = "fps_debug"; - private static final String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug"; - private static final String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug"; - private static final String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas"; - private static final String PREFS_JS_BUNDLE_DELTAS_CPP_KEY = "js_bundle_deltas_cpp"; - private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug"; - // This option is no longer exposed in the dev menu UI. - // It was renamed in D15958697 so it doesn't get stuck with no way to turn it off: - private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change_LEGACY"; - private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug"; - private static final String PREFS_HOT_MODULE_REPLACEMENT_KEY = "hot_module_replacement"; - private static final String PREFS_REMOTE_JS_DEBUG_KEY = "remote_js_debug"; - private static final String PREFS_START_SAMPLING_PROFILER_ON_INIT = - "start_sampling_profiler_on_init"; - - private final SharedPreferences mPreferences; - private final Listener mListener; - private final PackagerConnectionSettings mPackagerConnectionSettings; - private final boolean mSupportsNativeDeltaClients; - - public static DevInternalSettings withoutNativeDeltaClient( - Context applicationContext, Listener listener) { - return new DevInternalSettings(applicationContext, listener, false); - } - - public DevInternalSettings(Context applicationContext, Listener listener) { - this(applicationContext, listener, true); - } - - private DevInternalSettings( - Context applicationContext, Listener listener, boolean supportsNativeDeltaClients) { - mListener = listener; - mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); - mPreferences.registerOnSharedPreferenceChangeListener(this); - mPackagerConnectionSettings = new PackagerConnectionSettings(applicationContext); - mSupportsNativeDeltaClients = supportsNativeDeltaClients; - } - - public PackagerConnectionSettings getPackagerConnectionSettings() { - return mPackagerConnectionSettings; - } - - @Override - public boolean isFpsDebugEnabled() { - return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false); - } - - public void setFpsDebugEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_FPS_DEBUG_KEY, enabled).apply(); - } - - @Override - public boolean isAnimationFpsDebugEnabled() { - return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false); - } - - @Override - public boolean isJSDevModeEnabled() { - return mPreferences.getBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, true); - } - - public void setJSDevModeEnabled(boolean value) { - mPreferences.edit().putBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, value).apply(); - } - - @Override - public boolean isJSMinifyEnabled() { - return mPreferences.getBoolean(PREFS_JS_MINIFY_DEBUG_KEY, false); - } - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (mListener != null) { - if (PREFS_FPS_DEBUG_KEY.equals(key) - || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) - || PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) - || PREFS_JS_BUNDLE_DELTAS_KEY.equals(key) - || PREFS_JS_BUNDLE_DELTAS_CPP_KEY.equals(key) - || PREFS_START_SAMPLING_PROFILER_ON_INIT.equals(key) - || PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) { - mListener.onInternalSettingsChanged(); - } - } - } - - public boolean isHotModuleReplacementEnabled() { - return mPreferences.getBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, true); - } - - public void setHotModuleReplacementEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, enabled).apply(); - } - - public boolean isReloadOnJSChangeEnabled() { - return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, false); - } - - public void setReloadOnJSChangeEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); - } - - public boolean isElementInspectorEnabled() { - return mPreferences.getBoolean(PREFS_INSPECTOR_DEBUG_KEY, false); - } - - public void setElementInspectorEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply(); - } - - @SuppressLint("SharedPreferencesUse") - public boolean isBundleDeltasEnabled() { - return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false); - } - - @SuppressLint("SharedPreferencesUse") - public void setBundleDeltasEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply(); - } - - @SuppressLint("SharedPreferencesUse") - public boolean isBundleDeltasCppEnabled() { - return mSupportsNativeDeltaClients - && mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, false); - } - - @SuppressLint("SharedPreferencesUse") - public void setBundleDeltasCppEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, enabled).apply(); - } - - @Override - public boolean isNuclideJSDebugEnabled() { - return ReactBuildConfig.IS_INTERNAL_BUILD && ReactBuildConfig.DEBUG; - } - - @Override - public boolean isRemoteJSDebugEnabled() { - return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false); - } - - @Override - public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { - mPreferences.edit().putBoolean(PREFS_REMOTE_JS_DEBUG_KEY, remoteJSDebugEnabled).apply(); - } - - @Override - public boolean isStartSamplingProfilerOnInit() { - return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false); - } - - public interface Listener { - void onInternalSettingsChanged(); - } +public class DevInternalSettings implements DeveloperSettings, SharedPreferences.OnSharedPreferenceChangeListener { + + public static String PREFS_FPS_DEBUG_KEY = "fps_debug"; + + public static String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug"; + + public static String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug"; + + public static String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas"; + + public static String PREFS_JS_BUNDLE_DELTAS_CPP_KEY = "js_bundle_deltas_cpp"; + + public static String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug"; + + // This option is no longer exposed in the dev menu UI. + // It was renamed in D15958697 so it doesn't get stuck with no way to turn it off: + public static String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change_LEGACY"; + + public static String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug"; + + public static String PREFS_HOT_MODULE_REPLACEMENT_KEY = "hot_module_replacement"; + + public static String PREFS_REMOTE_JS_DEBUG_KEY = "remote_js_debug"; + + public static String PREFS_START_SAMPLING_PROFILER_ON_INIT = "start_sampling_profiler_on_init"; + + public final SharedPreferences mPreferences; + + public final Listener mListener; + + public final PackagerConnectionSettings mPackagerConnectionSettings; + + public final boolean mSupportsNativeDeltaClients; + + public static DevInternalSettings withoutNativeDeltaClient(Context applicationContext, Listener listener) { + return new DevInternalSettings(applicationContext, listener, false); + } + + public DevInternalSettings(Context applicationContext, Listener listener) { + this(applicationContext, listener, true); + } + + private DevInternalSettings(Context applicationContext, Listener listener, boolean supportsNativeDeltaClients) { + mListener = listener; + mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); + mPreferences.registerOnSharedPreferenceChangeListener(this); + mPackagerConnectionSettings = new PackagerConnectionSettings(applicationContext); + mSupportsNativeDeltaClients = supportsNativeDeltaClients; + } + + public PackagerConnectionSettings getPackagerConnectionSettings() { + return mPackagerConnectionSettings; + } + + @Override + public boolean isFpsDebugEnabled() { + return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false); + } + + public void setFpsDebugEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_FPS_DEBUG_KEY, enabled).apply(); + } + + @Override + public boolean isAnimationFpsDebugEnabled() { + return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false); + } + + @Override + public boolean isJSDevModeEnabled() { + return mPreferences.getBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, true); + } + + public void setJSDevModeEnabled(boolean value) { + mPreferences.edit().putBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, value).apply(); + } + + @Override + public boolean isJSMinifyEnabled() { + return mPreferences.getBoolean(PREFS_JS_MINIFY_DEBUG_KEY, false); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (mListener != null) { + if (PREFS_FPS_DEBUG_KEY.equals(key) || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) || PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) || PREFS_JS_BUNDLE_DELTAS_KEY.equals(key) || PREFS_JS_BUNDLE_DELTAS_CPP_KEY.equals(key) || PREFS_START_SAMPLING_PROFILER_ON_INIT.equals(key) || PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) { + mListener.onInternalSettingsChanged(); + } + } + } + + public boolean isHotModuleReplacementEnabled() { + return mPreferences.getBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, true); + } + + public void setHotModuleReplacementEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, enabled).apply(); + } + + public boolean isReloadOnJSChangeEnabled() { + // NOTE(brentvatne): This is not possible to enable/disable so we should always disable it for + // now. I managed to get into a state where fast refresh wouldn't work because live reload + // would kick in every time and there was no way to turn it off from the dev menu. + return false; + // return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, true); + } + + public void setReloadOnJSChangeEnabled(boolean enabled) { + // NOTE(brentvatne): We don't need to do anything here because this option is always false + // mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); + } + + public boolean isElementInspectorEnabled() { + return mPreferences.getBoolean(PREFS_INSPECTOR_DEBUG_KEY, false); + } + + public void setElementInspectorEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply(); + } + + @SuppressLint("SharedPreferencesUse") + public boolean isBundleDeltasEnabled() { + return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false); + } + + @SuppressLint("SharedPreferencesUse") + public void setBundleDeltasEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply(); + } + + @SuppressLint("SharedPreferencesUse") + public boolean isBundleDeltasCppEnabled() { + return mSupportsNativeDeltaClients && mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, false); + } + + @SuppressLint("SharedPreferencesUse") + public void setBundleDeltasCppEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, enabled).apply(); + } + + @Override + public boolean isNuclideJSDebugEnabled() { + return ReactBuildConfig.IS_INTERNAL_BUILD && ReactBuildConfig.DEBUG; + } + + @Override + public boolean isRemoteJSDebugEnabled() { + return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false); + } + + @Override + public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { + mPreferences.edit().putBoolean(PREFS_REMOTE_JS_DEBUG_KEY, remoteJSDebugEnabled).apply(); + } + + @Override + public boolean isStartSamplingProfilerOnInit() { + return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false); + } + + public interface Listener { + + void onInternalSettingsChanged(); + } + +public int exponentActivityId = -1; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 1bf470d1478ff3..a8b5f819227b64 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -89,1172 +89,1042 @@ * {@code } * {@code } */ -public class DevSupportManagerImpl - implements DevSupportManager, PackagerCommandListener, DevInternalSettings.Listener { - - private static final int JAVA_ERROR_COOKIE = -1; - private static final int JSEXCEPTION_ERROR_COOKIE = -1; - private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; - private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; - private boolean mIsSamplingProfilerEnabled = false; - - private enum ErrorType { - JS, - NATIVE - } - - private static final String EXOPACKAGE_LOCATION_FORMAT = - "/data/local/tmp/exopackage/%s//secondary-dex"; - - public static final String EMOJI_HUNDRED_POINTS_SYMBOL = " \uD83D\uDCAF"; - public static final String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " \uD83D\uDE45"; - - private final List mExceptionLoggers = new ArrayList<>(); - - private final Context mApplicationContext; - private final ShakeDetector mShakeDetector; - private final BroadcastReceiver mReloadAppBroadcastReceiver; - private final DevServerHelper mDevServerHelper; - private final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); - private final ReactInstanceManagerDevHelper mReactInstanceManagerHelper; - private final @Nullable String mJSAppBundleName; - private final File mJSBundleTempFile; - private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; - private final DevLoadingViewController mDevLoadingViewController; - - private @Nullable RedBoxDialog mRedBoxDialog; - private @Nullable AlertDialog mDevOptionsDialog; - private @Nullable DebugOverlayController mDebugOverlayController; - private boolean mDevLoadingViewVisible = false; - private @Nullable ReactContext mCurrentContext; - private DevInternalSettings mDevSettings; - private boolean mIsReceiverRegistered = false; - private boolean mIsShakeDetectorStarted = false; - private boolean mIsDevSupportEnabled = false; - private @Nullable RedBoxHandler mRedBoxHandler; - private @Nullable String mLastErrorTitle; - private @Nullable StackFrame[] mLastErrorStack; - private int mLastErrorCookie = 0; - private @Nullable ErrorType mLastErrorType; - private @Nullable DevBundleDownloadListener mBundleDownloadListener; - private @Nullable List mErrorCustomizers; - - private InspectorPackagerConnection.BundleStatus mBundleStatus; - - private @Nullable Map mCustomPackagerCommandHandlers; - - public DevSupportManagerImpl( - Context applicationContext, - ReactInstanceManagerDevHelper reactInstanceManagerHelper, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate, - int minNumShakes) { - - this( - applicationContext, - reactInstanceManagerHelper, - packagerPathForJSBundleName, - enableOnCreate, - null, - null, - minNumShakes, - null); - } - - public DevSupportManagerImpl( - Context applicationContext, - ReactInstanceManagerDevHelper reactInstanceManagerHelper, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate, - @Nullable RedBoxHandler redBoxHandler, - @Nullable DevBundleDownloadListener devBundleDownloadListener, - int minNumShakes, - @Nullable Map customPackagerCommandHandlers) { - mReactInstanceManagerHelper = reactInstanceManagerHelper; - mApplicationContext = applicationContext; - mJSAppBundleName = packagerPathForJSBundleName; - mDevSettings = new DevInternalSettings(applicationContext, this); - mBundleStatus = new InspectorPackagerConnection.BundleStatus(); - mDevServerHelper = - new DevServerHelper( - mDevSettings, - mApplicationContext.getPackageName(), - new InspectorPackagerConnection.BundleStatusProvider() { - @Override - public InspectorPackagerConnection.BundleStatus getBundleStatus() { - return mBundleStatus; - } - }); - mBundleDownloadListener = devBundleDownloadListener; - - // Prepare shake gesture detector (will be started/stopped from #reload) - mShakeDetector = - new ShakeDetector( - new ShakeDetector.ShakeListener() { - @Override - public void onShake() { - 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)) { - if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { - mDevSettings.setRemoteJSDebugEnabled(true); - mDevServerHelper.launchJSDevtools(); - } else { - mDevSettings.setRemoteJSDebugEnabled(false); - } - handleReloadJS(); - } - } - }; +public class DevSupportManagerImpl implements DevSupportManager, PackagerCommandListener, DevInternalSettings.Listener { - // 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 - mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME); - - mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); - - setDevSupportEnabled(enableOnCreate); - - mRedBoxHandler = redBoxHandler; - mDevLoadingViewController = - new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); - - mExceptionLoggers.add(new JSExceptionLogger()); - - if (mDevSettings.isStartSamplingProfilerOnInit()) { - // Only start the profiler. If its already running, there is an error - if (!mIsSamplingProfilerEnabled) { - toggleJSSamplingProfiler(); - } else { - Toast.makeText( - mApplicationContext, - "JS Sampling Profiler was already running, so did not start the sampling profiler", - Toast.LENGTH_LONG) - .show(); - } + public static int JAVA_ERROR_COOKIE = -1; + + public static int JSEXCEPTION_ERROR_COOKIE = -1; + + public static String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + + public static String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; + + public boolean mIsSamplingProfilerEnabled = false; + + private enum ErrorType { + + JS, NATIVE } - } - @Override - public void handleException(Exception e) { - if (mIsDevSupportEnabled) { + public static String EXOPACKAGE_LOCATION_FORMAT = "/data/local/tmp/exopackage/%s//secondary-dex"; - for (ExceptionLogger logger : mExceptionLoggers) { - logger.log(e); - } + public static String EMOJI_HUNDRED_POINTS_SYMBOL = " 💯"; + + public static String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " 🙅"; + + public final List mExceptionLoggers = new ArrayList<>(); + + public final Context mApplicationContext; + + public final ShakeDetector mShakeDetector; + + public final BroadcastReceiver mReloadAppBroadcastReceiver; + + public final DevServerHelper mDevServerHelper; + + public final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); + + public final ReactInstanceManagerDevHelper mReactInstanceManagerHelper; + + @Nullable + public final String mJSAppBundleName; - } else { - mDefaultNativeModuleCallExceptionHandler.handleException(e); + public final File mJSBundleTempFile; + + public final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; + + public final DevLoadingViewController mDevLoadingViewController; + + @Nullable + public RedBoxDialog mRedBoxDialog; + + @Nullable + public AlertDialog mDevOptionsDialog; + + @Nullable + public DebugOverlayController mDebugOverlayController; + + public boolean mDevLoadingViewVisible = false; + + @Nullable + public ReactContext mCurrentContext; + + public DevInternalSettings mDevSettings; + + public boolean mIsReceiverRegistered = false; + + public boolean mIsShakeDetectorStarted = false; + + public boolean mIsDevSupportEnabled = false; + + @Nullable + public RedBoxHandler mRedBoxHandler; + + @Nullable + public String mLastErrorTitle; + + @Nullable + public StackFrame[] mLastErrorStack; + + public int mLastErrorCookie = 0; + + @Nullable + public ErrorType mLastErrorType; + + @Nullable + public DevBundleDownloadListener mBundleDownloadListener; + + @Nullable + public List mErrorCustomizers; + + public InspectorPackagerConnection.BundleStatus mBundleStatus; + + @Nullable + public Map mCustomPackagerCommandHandlers; + + public DevSupportManagerImpl(Context applicationContext, ReactInstanceManagerDevHelper reactInstanceManagerHelper, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, int minNumShakes) { + this(applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, enableOnCreate, null, null, minNumShakes, null); } - } - private interface ExceptionLogger { - void log(Exception ex); - } + public DevSupportManagerImpl(Context applicationContext, ReactInstanceManagerDevHelper reactInstanceManagerHelper, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, @Nullable RedBoxHandler redBoxHandler, @Nullable DevBundleDownloadListener devBundleDownloadListener, int minNumShakes, @Nullable Map customPackagerCommandHandlers) { + mReactInstanceManagerHelper = reactInstanceManagerHelper; + mApplicationContext = applicationContext; + mJSAppBundleName = packagerPathForJSBundleName; + mDevSettings = new DevInternalSettings(applicationContext, this); + mBundleStatus = new InspectorPackagerConnection.BundleStatus(); + mDevServerHelper = new DevServerHelper(mDevSettings, mApplicationContext.getPackageName(), new InspectorPackagerConnection.BundleStatusProvider() { + + @Override + public InspectorPackagerConnection.BundleStatus getBundleStatus() { + return mBundleStatus; + } + }); + mBundleDownloadListener = devBundleDownloadListener; + // Prepare shake gesture detector (will be started/stopped from #reload) + mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { - private class JSExceptionLogger implements ExceptionLogger { + @Override + public void onShake() { + 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)) { + if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { + mDevSettings.setRemoteJSDebugEnabled(true); + mDevServerHelper.launchJSDevtools(); + } else { + mDevSettings.setRemoteJSDebugEnabled(false); + } + 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 + mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME); + mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); + setDevSupportEnabled(enableOnCreate); + mRedBoxHandler = redBoxHandler; + mDevLoadingViewController = new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); + mExceptionLoggers.add(new JSExceptionLogger()); + if (mDevSettings.isStartSamplingProfilerOnInit()) { + // Only start the profiler. If its already running, there is an error + if (!mIsSamplingProfilerEnabled) { + toggleJSSamplingProfiler(); + } else { + Toast.makeText(mApplicationContext, "JS Sampling Profiler was already running, so did not start the sampling profiler", Toast.LENGTH_LONG).show(); + } + } + } @Override - public void log(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(); - } + public void handleException(Exception e) { + try { + if (mIsDevSupportEnabled) { + for (ExceptionLogger logger : mExceptionLoggers) { + logger.log(e); + } + } else { + mDefaultNativeModuleCallExceptionHandler.handleException(e); + } + } catch (RuntimeException expoException) { + try { + Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("handleReactNativeError", String.class, Object.class, Integer.class, Boolean.class).invoke(null, expoException.getMessage(), null, -1, true); + } catch (Exception expoHandleErrorException) { + expoHandleErrorException.printStackTrace(); + } + } + } - if (e instanceof JSException) { - FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); - String stack = ((JSException) e).getStack(); - message.append("\n\n").append(stack); + private interface ExceptionLogger { - // TODO #11638796: convert the stack into something useful - showNewError( - message.toString(), new StackFrame[] {}, JSEXCEPTION_ERROR_COOKIE, ErrorType.JS); - } else { - showNewJavaError(message.toString(), e); - } + void log(Exception ex); } - } - @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); - } + private class JSExceptionLogger implements ExceptionLogger { + + @Override + public void log(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 JSException) { + FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); + String stack = ((JSException) e).getStack(); + message.append("\n\n").append(stack); + // TODO #11638796: convert the stack into something useful + showNewError(message.toString(), 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(String message, ReadableArray details, int errorCookie) { - showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS); - } - - @Override - public void registerErrorCustomizer(ErrorCustomizer errorCustomizer) { - if (mErrorCustomizers == null) { - mErrorCustomizers = new ArrayList<>(); + @Override + public void addCustomDevOption(String optionName, DevOptionHandler optionHandler) { + mCustomDevOptions.put(optionName, optionHandler); + } + + @Override + public void showNewJSError(String message, ReadableArray details, int errorCookie) { + showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS); } - mErrorCustomizers.add(errorCustomizer); - } - - private Pair processErrorCustomizers(Pair errorInfo) { - if (mErrorCustomizers == null) { - return errorInfo; - } else { - for (ErrorCustomizer errorCustomizer : mErrorCustomizers) { - Pair result = errorCustomizer.customizeErrorInfo(errorInfo); - if (result != null) { - errorInfo = result; + + @Override + public void registerErrorCustomizer(ErrorCustomizer errorCustomizer) { + if (mErrorCustomizers == null) { + mErrorCustomizers = new ArrayList<>(); } - } - return errorInfo; + mErrorCustomizers.add(errorCustomizer); } - } - - @Override - public void updateJSError( - final String message, final ReadableArray details, final int errorCookie) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - // Since we only show the first JS error in a succession of JS errors, make sure we only - // update the error message for that error message. This assumes that updateJSError - // belongs to the most recent showNewJSError - if (mRedBoxDialog == null - || !mRedBoxDialog.isShowing() - || errorCookie != mLastErrorCookie) { - return; + + private Pair processErrorCustomizers(Pair errorInfo) { + if (mErrorCustomizers == null) { + return errorInfo; + } else { + for (ErrorCustomizer errorCustomizer : mErrorCustomizers) { + Pair result = errorCustomizer.customizeErrorInfo(errorInfo); + if (result != null) { + errorInfo = result; + } } - StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); - Pair errorInfo = - processErrorCustomizers(Pair.create(message, stack)); - mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); - updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS); - // JS errors are reported here after source mapping. - if (mRedBoxHandler != null) { - mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); - mRedBoxDialog.resetReporting(); + return errorInfo; + } + } + + @Override + public void updateJSError(final String message, final ReadableArray details, final int errorCookie) { + UiThreadUtil.runOnUiThread(new Runnable() { + + @Override + public void run() { + // belongs to the most recent showNewJSError + if (mRedBoxDialog == null || !mRedBoxDialog.isShowing() || errorCookie != mLastErrorCookie) { + return; + } + StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); + Pair errorInfo = processErrorCustomizers(Pair.create(message, stack)); + mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); + updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS); + // JS errors are reported here after source mapping. + if (mRedBoxHandler != null) { + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); + mRedBoxDialog.resetReporting(); + } + mRedBoxDialog.show(); } - mRedBoxDialog.show(); - } }); - } - - @Override - public void hideRedboxDialog() { - // dismiss redbox if exists - if (mRedBoxDialog != null) { - mRedBoxDialog.dismiss(); - mRedBoxDialog = null; } - } - private void hideDevOptionsDialog() { - if (mDevOptionsDialog != null) { - mDevOptionsDialog.dismiss(); - mDevOptionsDialog = null; + @Override + public void hideRedboxDialog() { + // dismiss redbox if exists + if (mRedBoxDialog != null) { + mRedBoxDialog.dismiss(); + mRedBoxDialog = null; + } } - } - - private void showNewError( - @Nullable final String message, - final StackFrame[] stack, - final int errorCookie, - final ErrorType errorType) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (mRedBoxDialog == null) { - Activity context = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e( - ReactConstants.TAG, - "Unable to launch redbox because react activity " - + "is not available, here is the error that redbox would've displayed: " - + message); - return; - } - mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler); - } - if (mRedBoxDialog.isShowing()) { - // Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only - // show the first and most actionable one. - return; - } - Pair errorInfo = - processErrorCustomizers(Pair.create(message, stack)); - mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); - updateLastErrorInfo(message, stack, errorCookie, errorType); - // Only report native errors here. JS errors are reported - // inside {@link #updateJSError} after source mapping. - if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) { - mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.NATIVE); + + 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(new Runnable() { + + @Override + public void run() { + if (mRedBoxDialog == null) { + Activity context = mReactInstanceManagerHelper.getCurrentActivity(); + if (context == null || context.isFinishing()) { + FLog.e(ReactConstants.TAG, "Unable to launch redbox because react activity " + "is not available, here is the error that redbox would've displayed: " + message); + return; + } + mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler); + } + if (mRedBoxDialog.isShowing()) { + // show the first and most actionable one. + return; + } + Pair errorInfo = processErrorCustomizers(Pair.create(message, stack)); + mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); + updateLastErrorInfo(message, stack, errorCookie, errorType); + // inside {@link #updateJSError} after source mapping. + if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) { + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.NATIVE); + } + mRedBoxDialog.resetReporting(); + mRedBoxDialog.show(); } - mRedBoxDialog.resetReporting(); - mRedBoxDialog.show(); - } }); - } + } - @Override - public void showDevOptionsDialog() { - if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { - return; + @Override + public void reloadExpoApp() { + try { + int activityId = mDevServerHelper.mSettings.exponentActivityId; + Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("reloadFromManifest", int.class).invoke(null, activityId); + } catch (Exception expoHandleErrorException) { + expoHandleErrorException.printStackTrace(); + } } - LinkedHashMap options = new LinkedHashMap<>(); - /* register standard options */ - options.put( - mApplicationContext.getString(R.string.reactandroid_catalyst_reload), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isJSDevModeEnabled() - && mDevSettings.isHotModuleReplacementEnabled()) { - Toast.makeText( - mApplicationContext, - mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_disable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setHotModuleReplacementEnabled(false); + + + @Override + public void showDevOptionsDialog() { + if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { + return; + } + LinkedHashMap options = new LinkedHashMap<>(); + /* register standard options */ + options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_reload), new DevOptionHandler() { + + @Override + public void onOptionSelected() { + if (!mDevSettings.isJSDevModeEnabled() && mDevSettings.isHotModuleReplacementEnabled()) { + Toast.makeText(mApplicationContext, mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_disable), Toast.LENGTH_LONG).show(); + mDevSettings.setHotModuleReplacementEnabled(false); + } + + // NOTE(brentvatne): rather than reload just JS we need to reload the entire project from manifest + // handleReloadJS(); + reloadExpoApp(); } - handleReloadJS(); - } }); - options.put( - mDevSettings.isNuclideJSDebugEnabled() - ? mDevSettings.isRemoteJSDebugEnabled() - ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome_stop) - : mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome) - : mDevSettings.isRemoteJSDebugEnabled() - ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_stop) - : mApplicationContext.getString(R.string.reactandroid_catalyst_debug), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); - handleReloadJS(); - } - }); - if (mDevSettings.isNuclideJSDebugEnabled()) { - options.put( - mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), - new DevOptionHandler() { + +// if (mDevSettings.isNuclideJSDebugEnabled()) { +// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), new DevOptionHandler() { +// +// @Override +// public void onOptionSelected() { +// mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); +// } +// }); +// } + + // NOTE(brentvatne): This option does not make sense for Expo +// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), new DevOptionHandler() { +// +// @Override +// public void onOptionSelected() { +// Activity context = mReactInstanceManagerHelper.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.reactandroid_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(); +// } +// }); + + // "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh". + // While native code for "Live reload" is still there, please don't add the option back. + // See D15958697 for more context. + options.put(mDevSettings.isHotModuleReplacementEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading), new DevOptionHandler() { + @Override public void onOptionSelected() { - mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); - } - }); - } - options.put( - mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - Activity context = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e( - ReactConstants.TAG, - "Unable to launch change bundle location because react activity is not available"); - return; + boolean nextEnabled = !mDevSettings.isHotModuleReplacementEnabled(); + mDevSettings.setHotModuleReplacementEnabled(nextEnabled); + if (mCurrentContext != null) { + if (nextEnabled) { + mCurrentContext.getJSModule(HMRClient.class).enable(); + } else { + mCurrentContext.getJSModule(HMRClient.class).disable(); + } + } +// if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { +// Toast.makeText(mApplicationContext, mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), Toast.LENGTH_LONG).show(); +// mDevSettings.setJSDevModeEnabled(true); +// handleReloadJS(); +// } } - - final EditText input = new EditText(context); - input.setHint("localhost:8081"); - - AlertDialog bundleLocationDialog = - new AlertDialog.Builder(context) - .setTitle( - mApplicationContext.getString(R.string.reactandroid_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( - // NOTE: `isElementInspectorEnabled` is not guaranteed to be accurate. - mApplicationContext.getString(R.string.reactandroid_catalyst_inspector), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } }); - // "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh". - // While native code for "Live reload" is still there, please don't add the option back. - // See D15958697 for more context. - options.put( - mDevSettings.isHotModuleReplacementEnabled() - ? mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_stop) - : mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - boolean nextEnabled = !mDevSettings.isHotModuleReplacementEnabled(); - mDevSettings.setHotModuleReplacementEnabled(nextEnabled); - if (mCurrentContext != null) { - if (nextEnabled) { - mCurrentContext.getJSModule(HMRClient.class).enable(); - } else { - mCurrentContext.getJSModule(HMRClient.class).disable(); - } - } - if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { - Toast.makeText( - mApplicationContext, - mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setJSDevModeEnabled(true); - handleReloadJS(); + + + options.put(mDevSettings.isNuclideJSDebugEnabled() ? mDevSettings.isRemoteJSDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome) : mDevSettings.isRemoteJSDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_debug), new DevOptionHandler() { + + @Override + public void onOptionSelected() { + mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); + handleReloadJS(); + } + }); + + + +// options.put(mIsSamplingProfilerEnabled ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), new DevOptionHandler() { +// +// @Override +// public void onOptionSelected() { +// toggleJSSamplingProfiler(); +// } +// }); + options.put(mDevSettings.isFpsDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor), new DevOptionHandler() { + + @Override + public void onOptionSelected() { + if (!mDevSettings.isFpsDebugEnabled()) { + // Request overlay permission if needed when "Show Perf Monitor" option is selected + Context context = mReactInstanceManagerHelper.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( - mIsSamplingProfilerEnabled - ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) - : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - toggleJSSamplingProfiler(); - } - }); + options.put(// NOTE: `isElementInspectorEnabled` is not guaranteed to be accurate. + mApplicationContext.getString(R.string.reactandroid_catalyst_inspector), new DevOptionHandler() { - options.put( - mDevSettings.isFpsDebugEnabled() - ? mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor_stop) - : mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isFpsDebugEnabled()) { - // Request overlay permission if needed when "Show Perf Monitor" option is selected - Context context = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null) { - FLog.e(ReactConstants.TAG, "Unable to get reference to react activity"); - } else { - DebugOverlayController.requestPermission(context); - } + @Override + public void onOptionSelected() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); } - mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); - } - }); - options.put( - mApplicationContext.getString(R.string.reactandroid_catalyst_settings), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - 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]); +// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_settings), new DevOptionHandler() { +// +// @Override +// public void onOptionSelected() { +// Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class); +// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// mApplicationContext.startActivity(intent); +// } +// }); - Activity context = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e( - ReactConstants.TAG, - "Unable to launch dev options menu because react activity " + "isn't available"); - return; + if (mCustomDevOptions.size() > 0) { + options.putAll(mCustomDevOptions); + } + final DevOptionHandler[] optionHandlers = options.values().toArray(new DevOptionHandler[0]); + Activity context = mReactInstanceManagerHelper.getCurrentActivity(); + if (context == null || context.isFinishing()) { + FLog.e(ReactConstants.TAG, "Unable to launch dev options menu because react activity " + "isn't available"); + return; + } + mDevOptionsDialog = new AlertDialog.Builder(context).setItems(options.keySet().toArray(new String[0]), new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + optionHandlers[which].onOptionSelected(); + mDevOptionsDialog = null; + } + }).setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + mDevOptionsDialog = null; + } + }).create(); + mDevOptionsDialog.show(); } - mDevOptionsDialog = - new AlertDialog.Builder(context) - .setItems( - options.keySet().toArray(new String[0]), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - optionHandlers[which].onOptionSelected(); - mDevOptionsDialog = null; - } - }) - .setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - mDevOptionsDialog = null; - } - }) - .create(); - mDevOptionsDialog.show(); - } - - /** Starts of stops the sampling profiler */ - private void toggleJSSamplingProfiler() { - JavaScriptExecutorFactory javaScriptExecutorFactory = - mReactInstanceManagerHelper.getJavaScriptExecutorFactory(); - if (!mIsSamplingProfilerEnabled) { - try { - javaScriptExecutorFactory.startSamplingProfiler(); - Toast.makeText(mApplicationContext, "Starting Sampling Profiler", Toast.LENGTH_SHORT) - .show(); - } catch (UnsupportedOperationException e) { - Toast.makeText( - mApplicationContext, - javaScriptExecutorFactory.toString() + " does not support Sampling Profiler", - Toast.LENGTH_LONG) - .show(); - } finally { - mIsSamplingProfilerEnabled = true; - } - } else { - try { - final String outputPath = - File.createTempFile( - "sampling-profiler-trace", ".cpuprofile", mApplicationContext.getCacheDir()) - .getPath(); - javaScriptExecutorFactory.stopSamplingProfiler(outputPath); - Toast.makeText( - mApplicationContext, - "Saved results from Profiler to " + outputPath, - Toast.LENGTH_LONG) - .show(); - } catch (IOException e) { - FLog.e( - ReactConstants.TAG, - "Could not create temporary file for saving results from Sampling Profiler"); - } catch (UnsupportedOperationException e) { - Toast.makeText( - mApplicationContext, - javaScriptExecutorFactory.toString() + "does not support Sampling Profiler", - Toast.LENGTH_LONG) - .show(); - } finally { - mIsSamplingProfilerEnabled = false; - } + + /** Starts of stops the sampling profiler */ + private void toggleJSSamplingProfiler() { + JavaScriptExecutorFactory javaScriptExecutorFactory = mReactInstanceManagerHelper.getJavaScriptExecutorFactory(); + if (!mIsSamplingProfilerEnabled) { + try { + javaScriptExecutorFactory.startSamplingProfiler(); + Toast.makeText(mApplicationContext, "Starting Sampling Profiler", Toast.LENGTH_SHORT).show(); + } catch (UnsupportedOperationException e) { + Toast.makeText(mApplicationContext, javaScriptExecutorFactory.toString() + " does not support Sampling Profiler", Toast.LENGTH_LONG).show(); + } finally { + mIsSamplingProfilerEnabled = true; + } + } else { + try { + final String outputPath = File.createTempFile("sampling-profiler-trace", ".cpuprofile", mApplicationContext.getCacheDir()).getPath(); + javaScriptExecutorFactory.stopSamplingProfiler(outputPath); + Toast.makeText(mApplicationContext, "Saved results from Profiler to " + outputPath, Toast.LENGTH_LONG).show(); + } catch (IOException e) { + FLog.e(ReactConstants.TAG, "Could not create temporary file for saving results from Sampling Profiler"); + } catch (UnsupportedOperationException e) { + Toast.makeText(mApplicationContext, javaScriptExecutorFactory.toString() + "does not support Sampling Profiler", Toast.LENGTH_LONG).show(); + } finally { + mIsSamplingProfilerEnabled = false; + } + } } - } - /** + /** * {@link ReactInstanceDevCommandsHandler} 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 void onNewReactContextCreated(ReactContext reactContext) { - resetCurrentContext(reactContext); - } - - @Override - public void onReactInstanceDestroyed(ReactContext reactContext) { - if (reactContext == mCurrentContext) { - // only call reset context when the destroyed context matches the one that is currently set - // for this manager - resetCurrentContext(null); + @Override + public void setDevSupportEnabled(boolean isDevSupportEnabled) { + mIsDevSupportEnabled = isDevSupportEnabled; + reloadSettings(); + } + + @Override + public boolean getDevSupportEnabled() { + return mIsDevSupportEnabled; } - } - @Override - public String getSourceMapUrl() { - if (mJSAppBundleName == null) { - return ""; + @Override + public DeveloperSettings getDevSettings() { + return mDevSettings; } - return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); - } + @Override + public void onNewReactContextCreated(ReactContext reactContext) { + resetCurrentContext(reactContext); + } - @Override - public String getSourceUrl() { - if (mJSAppBundleName == null) { - return ""; + @Override + public void onReactInstanceDestroyed(ReactContext reactContext) { + if (reactContext == mCurrentContext) { + // only call reset context when the destroyed context matches the one that is currently set + // for this manager + resetCurrentContext(null); + } } - return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); - } + @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 getJSBundleURLForRemoteDebugging() { - return mDevServerHelper.getJSBundleURLForRemoteDebugging( - Assertions.assertNotNull(mJSAppBundleName)); - } + @Override + public String getJSBundleURLForRemoteDebugging() { + return mDevServerHelper.getJSBundleURLForRemoteDebugging(Assertions.assertNotNull(mJSAppBundleName)); + } - @Override - public String getDownloadedJSBundleFile() { - return mJSBundleTempFile.getAbsolutePath(); - } + @Override + public String getDownloadedJSBundleFile() { + return mJSBundleTempFile.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 && mJSBundleTempFile.exists()) { - try { - String packageName = mApplicationContext.getPackageName(); - PackageInfo thisPackage = - mApplicationContext.getPackageManager().getPackageInfo(packageName, 0); - if (mJSBundleTempFile.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 mJSBundleTempFile.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"); - } + @Override + public boolean hasUpToDateJSBundleInCache() { + return false; } - return false; - } - /** + /** * @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case {@link * com.facebook.react.ReactInstanceManager} should use that file from assets instead of * downloading bundle from dev server */ - public boolean hasBundleInAssets(String bundleAssetName) { - try { - String[] assets = mApplicationContext.getAssets().list(""); - for (int i = 0; i < assets.length; i++) { - if (assets[i].equals(bundleAssetName)) { - return true; + public boolean hasBundleInAssets(String bundleAssetName) { + try { + String[] assets = mApplicationContext.getAssets().list(""); + for (int i = 0; i < assets.length; i++) { + if (assets[i].equals(bundleAssetName)) { + return true; + } + } + } catch (IOException e) { + // Ignore this error and just fallback to downloading JS from devserver + FLog.e(ReactConstants.TAG, "Error while loading assets list"); } - } - } catch (IOException e) { - // Ignore this error and just fallback to downloading JS from devserver - FLog.e(ReactConstants.TAG, "Error while loading assets list"); + return false; } - return false; - } - private void resetCurrentContext(@Nullable ReactContext reactContext) { - if (mCurrentContext == reactContext) { - // new context is the same as the old one - do nothing - return; + private void resetCurrentContext(@Nullable ReactContext reactContext) { + if (mCurrentContext == reactContext) { + // new context is the same as the old one - do nothing + return; + } + mCurrentContext = reactContext; + // Recreate debug overlay controller with new CatalystInstance object + if (mDebugOverlayController != null) { + mDebugOverlayController.setFpsDebugViewVisible(false); + } + if (reactContext != null) { + mDebugOverlayController = new DebugOverlayController(reactContext); + } + if (mCurrentContext != null) { + try { + URL sourceUrl = new URL(getSourceUrl()); + // strip initial slash in path + String path = sourceUrl.getPath().substring(1); + String host = sourceUrl.getHost(); + int port = sourceUrl.getPort(); + mCurrentContext.getJSModule(HMRClient.class).setup("android", path, host, port, mDevSettings.isHotModuleReplacementEnabled()); + } catch (MalformedURLException e) { + showNewJavaError(e.getMessage(), e); + } + } + reloadSettings(); } - mCurrentContext = reactContext; + @Override + public void reloadSettings() { + if (UiThreadUtil.isOnUiThread()) { + reload(); + } else { + UiThreadUtil.runOnUiThread(new Runnable() { + + @Override + public void run() { + reload(); + } + }); + } + } - // Recreate debug overlay controller with new CatalystInstance object - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); + public void onInternalSettingsChanged() { + reloadSettings(); } - if (reactContext != null) { - mDebugOverlayController = new DebugOverlayController(reactContext); + + // NOTE(brentvatne): this is confusingly called the first time the app loads! + @Override + public void handleReloadJS() { + UiThreadUtil.assertOnUiThread(); + + ReactMarker.logMarker(ReactMarkerConstants.RELOAD, mDevSettings.getPackagerConnectionSettings().getDebugServerHost()); + // dismiss redbox if exists + hideRedboxDialog(); + if (mDevSettings.isRemoteJSDebugEnabled()) { + PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy"); + mDevLoadingViewController.showForRemoteJSEnabled(); + mDevLoadingViewVisible = true; + reloadJSInProxyMode(); + } else { + PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server"); + String bundleURL = mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName)); + reloadJSFromServer(bundleURL); + } } - if (mCurrentContext != null) { - try { - URL sourceUrl = new URL(getSourceUrl()); - String path = sourceUrl.getPath().substring(1); // strip initial slash in path - String host = sourceUrl.getHost(); - int port = sourceUrl.getPort(); - mCurrentContext - .getJSModule(HMRClient.class) - .setup("android", path, host, port, mDevSettings.isHotModuleReplacementEnabled()); - } catch (MalformedURLException e) { - showNewJavaError(e.getMessage(), e); - } + @Override + public void isPackagerRunning(PackagerStatusCallback callback) { + mDevServerHelper.isPackagerRunning(callback); } - reloadSettings(); - } + @Override + @Nullable + public File downloadBundleResourceFromUrlSync(final String resourceURL, final File outputFile) { + return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); + } - @Override - public void reloadSettings() { - if (UiThreadUtil.isOnUiThread()) { - reload(); - } else { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - reload(); - } - }); + @Override + @Nullable + public String getLastErrorTitle() { + return mLastErrorTitle; } - } - - public void onInternalSettingsChanged() { - reloadSettings(); - } - - @Override - public void handleReloadJS() { - - UiThreadUtil.assertOnUiThread(); - - ReactMarker.logMarker( - ReactMarkerConstants.RELOAD, - mDevSettings.getPackagerConnectionSettings().getDebugServerHost()); - - // dismiss redbox if exists - hideRedboxDialog(); - - if (mDevSettings.isRemoteJSDebugEnabled()) { - PrinterHolder.getPrinter() - .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy"); - mDevLoadingViewController.showForRemoteJSEnabled(); - mDevLoadingViewVisible = true; - reloadJSInProxyMode(); - } else { - PrinterHolder.getPrinter() - .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server"); - String bundleURL = - mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName)); - reloadJSFromServer(bundleURL); + + @Override + @Nullable + public StackFrame[] getLastErrorStack() { + return mLastErrorStack; } - } - - @Override - public void isPackagerRunning(PackagerStatusCallback callback) { - mDevServerHelper.isPackagerRunning(callback); - } - - @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 void onPackagerConnected() { + + @Override + public void onPackagerConnected() { // No-op - } + } - @Override - public void onPackagerDisconnected() { + @Override + public void onPackagerDisconnected() { // No-op - } - - @Override - public void onPackagerReloadCommand() { - // Disable debugger to resume the JsVM & avoid thread locks while reloading - mDevServerHelper.disableDebugger(); - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - handleReloadJS(); - } - }); - } - - @Override - public void onPackagerDevMenuCommand() { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - showDevOptionsDialog(); - } - }); - } - - @Override - public void onCaptureHeapCommand(final Responder responder) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - handleCaptureHeap(responder); - } - }); - } + } - @Override - public @Nullable Map customCommandHandlers() { - return mCustomPackagerCommandHandlers; - } + @Override + public void onPackagerReloadCommand() { + // Disable debugger to resume the JsVM & avoid thread locks while reloading + mDevServerHelper.disableDebugger(); + UiThreadUtil.runOnUiThread(new Runnable() { - private void handleCaptureHeap(final Responder responder) { - if (mCurrentContext == null) { - return; - } - JSCHeapCapture heapCapture = mCurrentContext.getNativeModule(JSCHeapCapture.class); - heapCapture.captureHeap( - mApplicationContext.getCacheDir().getPath(), - new JSCHeapCapture.CaptureCallback() { - @Override - public void onSuccess(File capture) { - responder.respond(capture.toString()); - } - - @Override - public void onFailure(JSCHeapCapture.CaptureException error) { - responder.error(error.toString()); - } + @Override + public void run() { + handleReloadJS(); + } }); - } - - private void updateLastErrorInfo( - @Nullable final String message, - final StackFrame[] stack, - final int errorCookie, - final ErrorType errorType) { - mLastErrorTitle = message; - mLastErrorStack = stack; - mLastErrorCookie = errorCookie; - mLastErrorType = errorType; - } - - private void reloadJSInProxyMode() { - // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that - // anyway - mDevServerHelper.launchJSDevtools(); - - JavaJSExecutor.Factory factory = - new JavaJSExecutor.Factory() { - @Override - public JavaJSExecutor create() throws Exception { - WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor(); - SimpleSettableFuture future = new SimpleSettableFuture<>(); - executor.connect( - mDevServerHelper.getWebsocketProxyURL(), getExecutorConnectCallback(future)); - // TODO(t9349129) Don't use timeout - try { - future.get(90, TimeUnit.SECONDS); - return executor; - } catch (ExecutionException e) { - throw (Exception) e.getCause(); - } catch (InterruptedException | TimeoutException e) { - throw new RuntimeException(e); + } + + @Override + public void onPackagerDevMenuCommand() { + UiThreadUtil.runOnUiThread(new Runnable() { + + @Override + public void run() { + showDevOptionsDialog(); } - } - }; - mReactInstanceManagerHelper.onReloadWithJSDebugger(factory); - } - - private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback( - final SimpleSettableFuture future) { - return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { - @Override - public void onSuccess() { - future.set(true); - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - } + }); + } - @Override - public void onFailure(final Throwable cause) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - FLog.e(ReactConstants.TAG, "Failed to connect to debugger!", cause); - future.setException( - new IOException(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_error), cause)); - } - }; - } + @Override + public void onCaptureHeapCommand(final Responder responder) { + UiThreadUtil.runOnUiThread(new Runnable() { - public void reloadJSFromServer(final String bundleURL) { - ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START); + @Override + public void run() { + handleCaptureHeap(responder); + } + }); + } - mDevLoadingViewController.showForUrl(bundleURL); - mDevLoadingViewVisible = true; + @Override + @Nullable + public Map customCommandHandlers() { + return mCustomPackagerCommandHandlers; + } - final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo(); + private void handleCaptureHeap(final Responder responder) { + if (mCurrentContext == null) { + return; + } + JSCHeapCapture heapCapture = mCurrentContext.getNativeModule(JSCHeapCapture.class); + heapCapture.captureHeap(mApplicationContext.getCacheDir().getPath(), new JSCHeapCapture.CaptureCallback() { - mDevServerHelper.downloadBundleFromURL( - new DevBundleDownloadListener() { - @Override - public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - synchronized (DevSupportManagerImpl.this) { - mBundleStatus.isLastDownloadSucess = true; - mBundleStatus.updateTimestamp = System.currentTimeMillis(); - } - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onSuccess(nativeDeltaClient); - } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - ReactMarker.logMarker( - ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); - mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient); - } - }); - } - - @Override - public void onProgress( - @Nullable final String status, - @Nullable final Integer done, - @Nullable final Integer total) { - mDevLoadingViewController.updateProgress(status, done, total); - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onProgress(status, done, total); + @Override + public void onSuccess(File capture) { + responder.respond(capture.toString()); } - } - @Override - public void onFailure(final Exception cause) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - synchronized (DevSupportManagerImpl.this) { - mBundleStatus.isLastDownloadSucess = false; - } - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onFailure(cause); + @Override + public void onFailure(JSCHeapCapture.CaptureException error) { + responder.error(error.toString()); } - FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (cause instanceof DebugServerException) { - DebugServerException debugServerException = (DebugServerException) cause; - showNewJavaError(debugServerException.getMessage(), cause); - } else { - showNewJavaError( - mApplicationContext.getString(R.string.reactandroid_catalyst_reload_error), cause); - } - } - }); - } - }, - mJSBundleTempFile, - bundleURL, - bundleInfo); - } - - @Override - public void startInspector() { - if (mIsDevSupportEnabled) { - mDevServerHelper.openInspectorConnection(); + }); } - } - - @Override - public void stopInspector() { - mDevServerHelper.closeInspectorConnection(); - } - @Override - public void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled) { - if (!mIsDevSupportEnabled) { - return; + private void updateLastErrorInfo(@Nullable final String message, final StackFrame[] stack, final int errorCookie, final ErrorType errorType) { + mLastErrorTitle = message; + mLastErrorStack = stack; + mLastErrorCookie = errorCookie; + mLastErrorType = errorType; } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mDevSettings.setHotModuleReplacementEnabled(isHotModuleReplacementEnabled); - handleReloadJS(); - } - }); - } + private void reloadJSInProxyMode() { + // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that + // anyway + mDevServerHelper.launchJSDevtools(); + JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() { - @Override - public void setRemoteJSDebugEnabled(final boolean isRemoteJSDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; + @Override + public JavaJSExecutor create() throws Exception { + WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor(); + SimpleSettableFuture future = new SimpleSettableFuture<>(); + executor.connect(mDevServerHelper.getWebsocketProxyURL(), getExecutorConnectCallback(future)); + // TODO(t9349129) Don't use timeout + try { + future.get(90, TimeUnit.SECONDS); + return executor; + } catch (ExecutionException e) { + throw (Exception) e.getCause(); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); + } + } + }; + mReactInstanceManagerHelper.onReloadWithJSDebugger(factory); } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); - handleReloadJS(); - } - }); - } + private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback(final SimpleSettableFuture future) { + return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { + + @Override + public void onSuccess() { + future.set(true); + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + } - @Override - public void setReloadOnJSChangeEnabled(final boolean isReloadOnJSChangeEnabled) { - if (!mIsDevSupportEnabled) { - return; + @Override + public void onFailure(final Throwable cause) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + FLog.e(ReactConstants.TAG, "Failed to connect to debugger!", cause); + future.setException(new IOException(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_error), cause)); + } + }; } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mDevSettings.setReloadOnJSChangeEnabled(isReloadOnJSChangeEnabled); - handleReloadJS(); - } - }); - } + public void reloadJSFromServer(final String bundleURL) { + ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START); + mDevLoadingViewController.showForUrl(bundleURL); + mDevLoadingViewVisible = true; + final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo(); + mDevServerHelper.downloadBundleFromURL(new DevBundleDownloadListener() { - @Override - public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; - } + @Override + public void onSuccess(@Nullable final NativeDeltaClient nativeDeltaClient) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + synchronized (DevSupportManagerImpl.this) { + mBundleStatus.isLastDownloadSucess = true; + mBundleStatus.updateTimestamp = System.currentTimeMillis(); + } + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onSuccess(nativeDeltaClient); + } + UiThreadUtil.runOnUiThread(new Runnable() { + + @Override + public void run() { + ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); + mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient); + } + }); + } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mDevSettings.setFpsDebugEnabled(isFpsDebugEnabled); - } - }); - } + @Override + public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) { + mDevLoadingViewController.updateProgress(status, done, total); + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onProgress(status, done, total); + } + } - @Override - public void toggleElementInspector() { - if (!mIsDevSupportEnabled) { - return; + @Override + public void onFailure(final Exception cause) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + synchronized (DevSupportManagerImpl.this) { + mBundleStatus.isLastDownloadSucess = false; + } + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onFailure(cause); + } + FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); + UiThreadUtil.runOnUiThread(new Runnable() { + + @Override + public void run() { + if (cause instanceof DebugServerException) { + DebugServerException debugServerException = (DebugServerException) cause; + showNewJavaError(debugServerException.getMessage(), cause); + } else { + showNewJavaError(mApplicationContext.getString(R.string.reactandroid_catalyst_reload_error), cause); + } + } + }); + } + }, mJSBundleTempFile, bundleURL, bundleInfo); } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } - }); - } + @Override + public void startInspector() { + if (mIsDevSupportEnabled) { + mDevServerHelper.openInspectorConnection(); + } + } - private void reload() { - UiThreadUtil.assertOnUiThread(); + @Override + public void stopInspector() { + mDevServerHelper.closeInspectorConnection(); + } - // 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()); - } + @Override + public void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled) { + if (!mIsDevSupportEnabled) { + return; + } + UiThreadUtil.runOnUiThread(new Runnable() { - // start shake gesture detector - if (!mIsShakeDetectorStarted) { - mShakeDetector.start( - (SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE)); - mIsShakeDetectorStarted = true; - } + @Override + public void run() { + mDevSettings.setHotModuleReplacementEnabled(isHotModuleReplacementEnabled); + handleReloadJS(); + } + }); + } - // register reload app broadcast receiver - if (!mIsReceiverRegistered) { - IntentFilter filter = new IntentFilter(); - filter.addAction(getReloadAppAction(mApplicationContext)); - mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); - mIsReceiverRegistered = true; - } + @Override + public void setRemoteJSDebugEnabled(final boolean isRemoteJSDebugEnabled) { + if (!mIsDevSupportEnabled) { + return; + } + UiThreadUtil.runOnUiThread(new Runnable() { - // show the dev loading if it should be - if (mDevLoadingViewVisible) { - mDevLoadingViewController.showMessage("Reloading..."); - } + @Override + public void run() { + mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); + handleReloadJS(); + } + }); + } + + @Override + public void setReloadOnJSChangeEnabled(final boolean isReloadOnJSChangeEnabled) { + if (!mIsDevSupportEnabled) { + return; + } + UiThreadUtil.runOnUiThread(new Runnable() { - mDevServerHelper.openPackagerConnection(this.getClass().getSimpleName(), this); - if (mDevSettings.isReloadOnJSChangeEnabled()) { - mDevServerHelper.startPollingOnChangeEndpoint( - new DevServerHelper.OnServerContentChangeListener() { - @Override - public void onServerContentChanged() { + @Override + public void run() { + mDevSettings.setReloadOnJSChangeEnabled(isReloadOnJSChangeEnabled); handleReloadJS(); - } - }); - } else { - mDevServerHelper.stopPollingOnChangeEndpoint(); - } - } else { - // hide FPS debug overlay - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); - } + } + }); + } - // stop shake gesture detector - if (mIsShakeDetectorStarted) { - mShakeDetector.stop(); - mIsShakeDetectorStarted = false; - } + @Override + public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { + if (!mIsDevSupportEnabled) { + return; + } + UiThreadUtil.runOnUiThread(new Runnable() { - // unregister app reload broadcast receiver - if (mIsReceiverRegistered) { - mApplicationContext.unregisterReceiver(mReloadAppBroadcastReceiver); - mIsReceiverRegistered = false; - } + @Override + public void run() { + mDevSettings.setFpsDebugEnabled(isFpsDebugEnabled); + } + }); + } - // hide redbox dialog - hideRedboxDialog(); + @Override + public void toggleElementInspector() { + if (!mIsDevSupportEnabled) { + return; + } + UiThreadUtil.runOnUiThread(new Runnable() { - // hide dev options dialog - hideDevOptionsDialog(); + @Override + public void run() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); + } + }); + } + + // NOTE(brentvatne): this is confusingly called the first time the app loads! + 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) { + mShakeDetector.start((SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE)); + mIsShakeDetectorStarted = true; + } + // register reload app broadcast receiver + if (!mIsReceiverRegistered) { + IntentFilter filter = new IntentFilter(); + filter.addAction(getReloadAppAction(mApplicationContext)); + mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); + mIsReceiverRegistered = true; + } + // show the dev loading if it should be + if (mDevLoadingViewVisible) { + mDevLoadingViewController.showMessage("Reloading..."); + } + mDevServerHelper.openPackagerConnection(this.getClass().getSimpleName(), this); + if (mDevSettings.isReloadOnJSChangeEnabled()) { + mDevServerHelper.startPollingOnChangeEndpoint(new DevServerHelper.OnServerContentChangeListener() { - // hide loading view - mDevLoadingViewController.hide(); - mDevServerHelper.closePackagerConnection(); - mDevServerHelper.stopPollingOnChangeEndpoint(); + @Override + public void onServerContentChanged() { + handleReloadJS(); + } + }); + } else { + mDevServerHelper.stopPollingOnChangeEndpoint(); + } + } 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 + mDevLoadingViewController.hide(); + mDevServerHelper.closePackagerConnection(); + mDevServerHelper.stopPollingOnChangeEndpoint(); + } } - } - /** Intent action for reloading the JS */ - private static String getReloadAppAction(Context context) { - return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; - } + /** Intent action for reloading the JS */ + private static String getReloadAppAction(Context context) { + return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 09de892ecdf8f5..b2f6a704d67212 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -119,6 +119,10 @@ public void reloadSettings() {} @Override public void handleReloadJS() {} + + @Override + public void reloadExpoApp() {} + @Override public void reloadJSFromServer(String bundleURL) {} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java index d44574972b5253..5cba5343fdd582 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -249,7 +249,7 @@ protected RedBoxDialog( new View.OnClickListener() { @Override public void onClick(View v) { - mDevSupportManager.handleReloadJS(); + mDevSupportManager.reloadExpoApp(); } }); mDismissButton = (Button) findViewById(R.id.rn_redbox_dismiss_button); @@ -303,7 +303,7 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { return true; } if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) { - mDevSupportManager.handleReloadJS(); + mDevSupportManager.reloadExpoApp(); } return super.onKeyUp(keyCode, event); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java index 360b76c04e7e01..b5d3295966ab18 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java @@ -60,6 +60,8 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { void handleReloadJS(); + void reloadExpoApp(); + void reloadJSFromServer(final String bundleURL); void isPackagerRunning(PackagerStatusCallback callback); From d483619c068ae0521d8950a3355465b260140d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 14 Nov 2019 09:07:46 +0100 Subject: [PATCH 2/5] [android] Reformat changes from 2a2663ff --- .../react/devsupport/DevInternalSettings.java | 327 ++- .../devsupport/DevSupportManagerImpl.java | 1967 +++++++++-------- 2 files changed, 1223 insertions(+), 1071 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java index ae5a138fad6051..14fe73a6524fb2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java @@ -21,169 +21,166 @@ * this class implements an external interface {@link DeveloperSettings}. */ @VisibleForTesting -public class DevInternalSettings implements DeveloperSettings, SharedPreferences.OnSharedPreferenceChangeListener { - - public static String PREFS_FPS_DEBUG_KEY = "fps_debug"; - - public static String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug"; - - public static String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug"; - - public static String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas"; - - public static String PREFS_JS_BUNDLE_DELTAS_CPP_KEY = "js_bundle_deltas_cpp"; - - public static String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug"; - - // This option is no longer exposed in the dev menu UI. - // It was renamed in D15958697 so it doesn't get stuck with no way to turn it off: - public static String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change_LEGACY"; - - public static String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug"; - - public static String PREFS_HOT_MODULE_REPLACEMENT_KEY = "hot_module_replacement"; - - public static String PREFS_REMOTE_JS_DEBUG_KEY = "remote_js_debug"; - - public static String PREFS_START_SAMPLING_PROFILER_ON_INIT = "start_sampling_profiler_on_init"; - - public final SharedPreferences mPreferences; - - public final Listener mListener; - - public final PackagerConnectionSettings mPackagerConnectionSettings; - - public final boolean mSupportsNativeDeltaClients; - - public static DevInternalSettings withoutNativeDeltaClient(Context applicationContext, Listener listener) { - return new DevInternalSettings(applicationContext, listener, false); - } - - public DevInternalSettings(Context applicationContext, Listener listener) { - this(applicationContext, listener, true); - } - - private DevInternalSettings(Context applicationContext, Listener listener, boolean supportsNativeDeltaClients) { - mListener = listener; - mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); - mPreferences.registerOnSharedPreferenceChangeListener(this); - mPackagerConnectionSettings = new PackagerConnectionSettings(applicationContext); - mSupportsNativeDeltaClients = supportsNativeDeltaClients; - } - - public PackagerConnectionSettings getPackagerConnectionSettings() { - return mPackagerConnectionSettings; - } - - @Override - public boolean isFpsDebugEnabled() { - return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false); - } - - public void setFpsDebugEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_FPS_DEBUG_KEY, enabled).apply(); - } - - @Override - public boolean isAnimationFpsDebugEnabled() { - return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false); - } - - @Override - public boolean isJSDevModeEnabled() { - return mPreferences.getBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, true); - } - - public void setJSDevModeEnabled(boolean value) { - mPreferences.edit().putBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, value).apply(); - } - - @Override - public boolean isJSMinifyEnabled() { - return mPreferences.getBoolean(PREFS_JS_MINIFY_DEBUG_KEY, false); - } - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (mListener != null) { - if (PREFS_FPS_DEBUG_KEY.equals(key) || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) || PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) || PREFS_JS_BUNDLE_DELTAS_KEY.equals(key) || PREFS_JS_BUNDLE_DELTAS_CPP_KEY.equals(key) || PREFS_START_SAMPLING_PROFILER_ON_INIT.equals(key) || PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) { - mListener.onInternalSettingsChanged(); - } - } - } - - public boolean isHotModuleReplacementEnabled() { - return mPreferences.getBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, true); - } - - public void setHotModuleReplacementEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, enabled).apply(); - } - - public boolean isReloadOnJSChangeEnabled() { - // NOTE(brentvatne): This is not possible to enable/disable so we should always disable it for - // now. I managed to get into a state where fast refresh wouldn't work because live reload - // would kick in every time and there was no way to turn it off from the dev menu. - return false; - // return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, true); - } - - public void setReloadOnJSChangeEnabled(boolean enabled) { - // NOTE(brentvatne): We don't need to do anything here because this option is always false - // mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); - } - - public boolean isElementInspectorEnabled() { - return mPreferences.getBoolean(PREFS_INSPECTOR_DEBUG_KEY, false); - } - - public void setElementInspectorEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply(); - } - - @SuppressLint("SharedPreferencesUse") - public boolean isBundleDeltasEnabled() { - return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false); - } - - @SuppressLint("SharedPreferencesUse") - public void setBundleDeltasEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply(); - } - - @SuppressLint("SharedPreferencesUse") - public boolean isBundleDeltasCppEnabled() { - return mSupportsNativeDeltaClients && mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, false); - } - - @SuppressLint("SharedPreferencesUse") - public void setBundleDeltasCppEnabled(boolean enabled) { - mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, enabled).apply(); - } - - @Override - public boolean isNuclideJSDebugEnabled() { - return ReactBuildConfig.IS_INTERNAL_BUILD && ReactBuildConfig.DEBUG; - } - - @Override - public boolean isRemoteJSDebugEnabled() { - return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false); - } - - @Override - public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { - mPreferences.edit().putBoolean(PREFS_REMOTE_JS_DEBUG_KEY, remoteJSDebugEnabled).apply(); - } - - @Override - public boolean isStartSamplingProfilerOnInit() { - return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false); - } - - public interface Listener { - - void onInternalSettingsChanged(); - } - -public int exponentActivityId = -1; +public class DevInternalSettings + implements DeveloperSettings, SharedPreferences.OnSharedPreferenceChangeListener { + + private static final String PREFS_FPS_DEBUG_KEY = "fps_debug"; + private static final String PREFS_JS_DEV_MODE_DEBUG_KEY = "js_dev_mode_debug"; + private static final String PREFS_JS_MINIFY_DEBUG_KEY = "js_minify_debug"; + private static final String PREFS_JS_BUNDLE_DELTAS_KEY = "js_bundle_deltas"; + private static final String PREFS_JS_BUNDLE_DELTAS_CPP_KEY = "js_bundle_deltas_cpp"; + private static final String PREFS_ANIMATIONS_DEBUG_KEY = "animations_debug"; + // This option is no longer exposed in the dev menu UI. + // It was renamed in D15958697 so it doesn't get stuck with no way to turn it off: + private static final String PREFS_RELOAD_ON_JS_CHANGE_KEY = "reload_on_js_change_LEGACY"; + private static final String PREFS_INSPECTOR_DEBUG_KEY = "inspector_debug"; + private static final String PREFS_HOT_MODULE_REPLACEMENT_KEY = "hot_module_replacement"; + private static final String PREFS_REMOTE_JS_DEBUG_KEY = "remote_js_debug"; + private static final String PREFS_START_SAMPLING_PROFILER_ON_INIT = + "start_sampling_profiler_on_init"; + + private final SharedPreferences mPreferences; + private final Listener mListener; + private final PackagerConnectionSettings mPackagerConnectionSettings; + private final boolean mSupportsNativeDeltaClients; + + public static DevInternalSettings withoutNativeDeltaClient( + Context applicationContext, Listener listener) { + return new DevInternalSettings(applicationContext, listener, false); + } + + public DevInternalSettings(Context applicationContext, Listener listener) { + this(applicationContext, listener, true); + } + + private DevInternalSettings( + Context applicationContext, Listener listener, boolean supportsNativeDeltaClients) { + mListener = listener; + mPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); + mPreferences.registerOnSharedPreferenceChangeListener(this); + mPackagerConnectionSettings = new PackagerConnectionSettings(applicationContext); + mSupportsNativeDeltaClients = supportsNativeDeltaClients; + } + + public PackagerConnectionSettings getPackagerConnectionSettings() { + return mPackagerConnectionSettings; + } + + @Override + public boolean isFpsDebugEnabled() { + return mPreferences.getBoolean(PREFS_FPS_DEBUG_KEY, false); + } + + public void setFpsDebugEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_FPS_DEBUG_KEY, enabled).apply(); + } + + @Override + public boolean isAnimationFpsDebugEnabled() { + return mPreferences.getBoolean(PREFS_ANIMATIONS_DEBUG_KEY, false); + } + + @Override + public boolean isJSDevModeEnabled() { + return mPreferences.getBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, true); + } + + public void setJSDevModeEnabled(boolean value) { + mPreferences.edit().putBoolean(PREFS_JS_DEV_MODE_DEBUG_KEY, value).apply(); + } + + @Override + public boolean isJSMinifyEnabled() { + return mPreferences.getBoolean(PREFS_JS_MINIFY_DEBUG_KEY, false); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (mListener != null) { + if (PREFS_FPS_DEBUG_KEY.equals(key) + || PREFS_RELOAD_ON_JS_CHANGE_KEY.equals(key) + || PREFS_JS_DEV_MODE_DEBUG_KEY.equals(key) + || PREFS_JS_BUNDLE_DELTAS_KEY.equals(key) + || PREFS_JS_BUNDLE_DELTAS_CPP_KEY.equals(key) + || PREFS_START_SAMPLING_PROFILER_ON_INIT.equals(key) + || PREFS_JS_MINIFY_DEBUG_KEY.equals(key)) { + mListener.onInternalSettingsChanged(); + } + } + } + + public boolean isHotModuleReplacementEnabled() { + return mPreferences.getBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, true); + } + + public void setHotModuleReplacementEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_HOT_MODULE_REPLACEMENT_KEY, enabled).apply(); + } + + public boolean isReloadOnJSChangeEnabled() { + // NOTE(brentvatne): This is not possible to enable/disable so we should always disable it for + // now. I managed to get into a state where fast refresh wouldn't work because live reload + // would kick in every time and there was no way to turn it off from the dev menu. + return false; + // return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, true); + } + + public void setReloadOnJSChangeEnabled(boolean enabled) { + // NOTE(brentvatne): We don't need to do anything here because this option is always false + // mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); + } + + public boolean isElementInspectorEnabled() { + return mPreferences.getBoolean(PREFS_INSPECTOR_DEBUG_KEY, false); + } + + public void setElementInspectorEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_INSPECTOR_DEBUG_KEY, enabled).apply(); + } + + @SuppressLint("SharedPreferencesUse") + public boolean isBundleDeltasEnabled() { + return mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, false); + } + + @SuppressLint("SharedPreferencesUse") + public void setBundleDeltasEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_KEY, enabled).apply(); + } + + @SuppressLint("SharedPreferencesUse") + public boolean isBundleDeltasCppEnabled() { + return mSupportsNativeDeltaClients + && mPreferences.getBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, false); + } + + @SuppressLint("SharedPreferencesUse") + public void setBundleDeltasCppEnabled(boolean enabled) { + mPreferences.edit().putBoolean(PREFS_JS_BUNDLE_DELTAS_CPP_KEY, enabled).apply(); + } + + @Override + public boolean isNuclideJSDebugEnabled() { + return ReactBuildConfig.IS_INTERNAL_BUILD && ReactBuildConfig.DEBUG; + } + + @Override + public boolean isRemoteJSDebugEnabled() { + return mPreferences.getBoolean(PREFS_REMOTE_JS_DEBUG_KEY, false); + } + + @Override + public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) { + mPreferences.edit().putBoolean(PREFS_REMOTE_JS_DEBUG_KEY, remoteJSDebugEnabled).apply(); + } + + @Override + public boolean isStartSamplingProfilerOnInit() { + return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false); + } + + public interface Listener { + void onInternalSettingsChanged(); + } + + public int exponentActivityId = -1; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index a8b5f819227b64..c6efc06068b5b9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -89,1042 +89,1197 @@ * {@code } * {@code } */ -public class DevSupportManagerImpl implements DevSupportManager, PackagerCommandListener, DevInternalSettings.Listener { - - public static int JAVA_ERROR_COOKIE = -1; - - public static int JSEXCEPTION_ERROR_COOKIE = -1; - - public static String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; - - public static String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; - - public boolean mIsSamplingProfilerEnabled = false; - - private enum ErrorType { - - JS, NATIVE - } - - public static String EXOPACKAGE_LOCATION_FORMAT = "/data/local/tmp/exopackage/%s//secondary-dex"; - - public static String EMOJI_HUNDRED_POINTS_SYMBOL = " 💯"; - - public static String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " 🙅"; - - public final List mExceptionLoggers = new ArrayList<>(); - - public final Context mApplicationContext; - - public final ShakeDetector mShakeDetector; - - public final BroadcastReceiver mReloadAppBroadcastReceiver; - - public final DevServerHelper mDevServerHelper; - - public final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); - - public final ReactInstanceManagerDevHelper mReactInstanceManagerHelper; - - @Nullable - public final String mJSAppBundleName; - - public final File mJSBundleTempFile; - - public final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; - - public final DevLoadingViewController mDevLoadingViewController; - - @Nullable - public RedBoxDialog mRedBoxDialog; - - @Nullable - public AlertDialog mDevOptionsDialog; - - @Nullable - public DebugOverlayController mDebugOverlayController; - - public boolean mDevLoadingViewVisible = false; - - @Nullable - public ReactContext mCurrentContext; - - public DevInternalSettings mDevSettings; - - public boolean mIsReceiverRegistered = false; - - public boolean mIsShakeDetectorStarted = false; - - public boolean mIsDevSupportEnabled = false; - - @Nullable - public RedBoxHandler mRedBoxHandler; - - @Nullable - public String mLastErrorTitle; - - @Nullable - public StackFrame[] mLastErrorStack; - - public int mLastErrorCookie = 0; - - @Nullable - public ErrorType mLastErrorType; - - @Nullable - public DevBundleDownloadListener mBundleDownloadListener; - - @Nullable - public List mErrorCustomizers; - - public InspectorPackagerConnection.BundleStatus mBundleStatus; - - @Nullable - public Map mCustomPackagerCommandHandlers; - - public DevSupportManagerImpl(Context applicationContext, ReactInstanceManagerDevHelper reactInstanceManagerHelper, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, int minNumShakes) { - this(applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, enableOnCreate, null, null, minNumShakes, null); - } - - public DevSupportManagerImpl(Context applicationContext, ReactInstanceManagerDevHelper reactInstanceManagerHelper, @Nullable String packagerPathForJSBundleName, boolean enableOnCreate, @Nullable RedBoxHandler redBoxHandler, @Nullable DevBundleDownloadListener devBundleDownloadListener, int minNumShakes, @Nullable Map customPackagerCommandHandlers) { - mReactInstanceManagerHelper = reactInstanceManagerHelper; - mApplicationContext = applicationContext; - mJSAppBundleName = packagerPathForJSBundleName; - mDevSettings = new DevInternalSettings(applicationContext, this); - mBundleStatus = new InspectorPackagerConnection.BundleStatus(); - mDevServerHelper = new DevServerHelper(mDevSettings, mApplicationContext.getPackageName(), new InspectorPackagerConnection.BundleStatusProvider() { - - @Override - public InspectorPackagerConnection.BundleStatus getBundleStatus() { +public class DevSupportManagerImpl + implements DevSupportManager, PackagerCommandListener, DevInternalSettings.Listener { + + private static final int JAVA_ERROR_COOKIE = -1; + private static final int JSEXCEPTION_ERROR_COOKIE = -1; + private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION"; + private boolean mIsSamplingProfilerEnabled = false; + + private enum ErrorType { + JS, + NATIVE + } + + private static final String EXOPACKAGE_LOCATION_FORMAT = + "/data/local/tmp/exopackage/%s//secondary-dex"; + + public static String EMOJI_HUNDRED_POINTS_SYMBOL = " 💯"; + public static String EMOJI_FACE_WITH_NO_GOOD_GESTURE = " 🙅"; + + private final List mExceptionLoggers = new ArrayList<>(); + + private final Context mApplicationContext; + private final ShakeDetector mShakeDetector; + private final BroadcastReceiver mReloadAppBroadcastReceiver; + private final DevServerHelper mDevServerHelper; + private final LinkedHashMap mCustomDevOptions = new LinkedHashMap<>(); + private final ReactInstanceManagerDevHelper mReactInstanceManagerHelper; + private final @Nullable String mJSAppBundleName; + private final File mJSBundleTempFile; + private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; + private final DevLoadingViewController mDevLoadingViewController; + + private @Nullable RedBoxDialog mRedBoxDialog; + private @Nullable AlertDialog mDevOptionsDialog; + private @Nullable DebugOverlayController mDebugOverlayController; + private boolean mDevLoadingViewVisible = false; + private @Nullable ReactContext mCurrentContext; + private DevInternalSettings mDevSettings; + private boolean mIsReceiverRegistered = false; + private boolean mIsShakeDetectorStarted = false; + private boolean mIsDevSupportEnabled = false; + private @Nullable RedBoxHandler mRedBoxHandler; + private @Nullable String mLastErrorTitle; + private @Nullable StackFrame[] mLastErrorStack; + private int mLastErrorCookie = 0; + private @Nullable ErrorType mLastErrorType; + private @Nullable DevBundleDownloadListener mBundleDownloadListener; + private @Nullable List mErrorCustomizers; + + private InspectorPackagerConnection.BundleStatus mBundleStatus; + + private @Nullable Map mCustomPackagerCommandHandlers; + + public DevSupportManagerImpl( + Context applicationContext, + ReactInstanceManagerDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + int minNumShakes) { + + this( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + null, + null, + minNumShakes, + null); + } + + public DevSupportManagerImpl( + Context applicationContext, + ReactInstanceManagerDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + @Nullable RedBoxHandler redBoxHandler, + @Nullable DevBundleDownloadListener devBundleDownloadListener, + int minNumShakes, + @Nullable Map customPackagerCommandHandlers) { + mReactInstanceManagerHelper = reactInstanceManagerHelper; + mApplicationContext = applicationContext; + mJSAppBundleName = packagerPathForJSBundleName; + mDevSettings = new DevInternalSettings(applicationContext, this); + mBundleStatus = new InspectorPackagerConnection.BundleStatus(); + mDevServerHelper = + new DevServerHelper( + mDevSettings, + mApplicationContext.getPackageName(), + new InspectorPackagerConnection.BundleStatusProvider() { + @Override + public InspectorPackagerConnection.BundleStatus getBundleStatus() { return mBundleStatus; - } - }); - mBundleDownloadListener = devBundleDownloadListener; - // Prepare shake gesture detector (will be started/stopped from #reload) - mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { - - @Override - public void onShake() { + } + }); + mBundleDownloadListener = devBundleDownloadListener; + + // Prepare shake gesture detector (will be started/stopped from #reload) + mShakeDetector = + new ShakeDetector( + new ShakeDetector.ShakeListener() { + @Override + public void onShake() { 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)) { + if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { + mDevSettings.setRemoteJSDebugEnabled(true); + mDevServerHelper.launchJSDevtools(); + } else { + mDevSettings.setRemoteJSDebugEnabled(false); + } + handleReloadJS(); } - }, 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)) { - if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { - mDevSettings.setRemoteJSDebugEnabled(true); - mDevServerHelper.launchJSDevtools(); - } else { - mDevSettings.setRemoteJSDebugEnabled(false); - } - 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 - mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME); - mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); - setDevSupportEnabled(enableOnCreate); - mRedBoxHandler = redBoxHandler; - mDevLoadingViewController = new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); - mExceptionLoggers.add(new JSExceptionLogger()); - if (mDevSettings.isStartSamplingProfilerOnInit()) { - // Only start the profiler. If its already running, there is an error - if (!mIsSamplingProfilerEnabled) { - toggleJSSamplingProfiler(); - } else { - Toast.makeText(mApplicationContext, "JS Sampling Profiler was already running, so did not start the sampling profiler", Toast.LENGTH_LONG).show(); - } - } - } - @Override - public void handleException(Exception e) { - try { - if (mIsDevSupportEnabled) { - for (ExceptionLogger logger : mExceptionLoggers) { - logger.log(e); - } - } else { - mDefaultNativeModuleCallExceptionHandler.handleException(e); - } - } catch (RuntimeException expoException) { - try { - Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("handleReactNativeError", String.class, Object.class, Integer.class, Boolean.class).invoke(null, expoException.getMessage(), null, -1, true); - } catch (Exception expoHandleErrorException) { - expoHandleErrorException.printStackTrace(); - } - } + // 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 + mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME); + + mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler(); + + setDevSupportEnabled(enableOnCreate); + + mRedBoxHandler = redBoxHandler; + mDevLoadingViewController = + new DevLoadingViewController(applicationContext, reactInstanceManagerHelper); + + mExceptionLoggers.add(new JSExceptionLogger()); + + if (mDevSettings.isStartSamplingProfilerOnInit()) { + // Only start the profiler. If its already running, there is an error + if (!mIsSamplingProfilerEnabled) { + toggleJSSamplingProfiler(); + } else { + Toast.makeText( + mApplicationContext, + "JS Sampling Profiler was already running, so did not start the sampling profiler", + Toast.LENGTH_LONG) + .show(); + } } + } - private interface ExceptionLogger { + @Override + public void handleException(Exception e) { + try { + if (mIsDevSupportEnabled) { - void log(Exception ex); + for (ExceptionLogger logger : mExceptionLoggers) { + logger.log(e); + } + + } else { + mDefaultNativeModuleCallExceptionHandler.handleException(e); + } + } catch (RuntimeException expoException) { + try { + Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("handleReactNativeError", String.class, Object.class, Integer.class, Boolean.class).invoke(null, expoException.getMessage(), null, -1, true); + } catch (Exception expoHandleErrorException) { + expoHandleErrorException.printStackTrace(); + } } + } - private class JSExceptionLogger implements ExceptionLogger { + private interface ExceptionLogger { + void log(Exception ex); + } - @Override - public void log(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 JSException) { - FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); - String stack = ((JSException) e).getStack(); - message.append("\n\n").append(stack); - // TODO #11638796: convert the stack into something useful - showNewError(message.toString(), new StackFrame[] {}, JSEXCEPTION_ERROR_COOKIE, ErrorType.JS); - } else { - showNewJavaError(message.toString(), e); - } - } - } + private class JSExceptionLogger implements ExceptionLogger { @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); + public void log(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 JSException) { + FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); + String stack = ((JSException) e).getStack(); + message.append("\n\n").append(stack); + + // TODO #11638796: convert the stack into something useful + showNewError( + message.toString(), 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 addCustomDevOption(String optionName, DevOptionHandler optionHandler) { + mCustomDevOptions.put(optionName, optionHandler); + } + + @Override + public void showNewJSError(String message, ReadableArray details, int errorCookie) { + showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS); + } + + @Override + public void registerErrorCustomizer(ErrorCustomizer errorCustomizer) { + if (mErrorCustomizers == null) { + mErrorCustomizers = new ArrayList<>(); } - - @Override - public void showNewJSError(String message, 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); + } + + private Pair processErrorCustomizers(Pair errorInfo) { + if (mErrorCustomizers == null) { + return errorInfo; + } else { + for (ErrorCustomizer errorCustomizer : mErrorCustomizers) { + Pair result = errorCustomizer.customizeErrorInfo(errorInfo); + if (result != null) { + errorInfo = result; } - mErrorCustomizers.add(errorCustomizer); + } + return errorInfo; } - - private Pair processErrorCustomizers(Pair errorInfo) { - if (mErrorCustomizers == null) { - return errorInfo; - } else { - for (ErrorCustomizer errorCustomizer : mErrorCustomizers) { - Pair result = errorCustomizer.customizeErrorInfo(errorInfo); - if (result != null) { - errorInfo = result; - } + } + + @Override + public void updateJSError( + final String message, final ReadableArray details, final int errorCookie) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + // Since we only show the first JS error in a succession of JS errors, make sure we only + // update the error message for that error message. This assumes that updateJSError + // belongs to the most recent showNewJSError + if (mRedBoxDialog == null + || !mRedBoxDialog.isShowing() + || errorCookie != mLastErrorCookie) { + return; } - return errorInfo; - } - } - - @Override - public void updateJSError(final String message, final ReadableArray details, final int errorCookie) { - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - // belongs to the most recent showNewJSError - if (mRedBoxDialog == null || !mRedBoxDialog.isShowing() || errorCookie != mLastErrorCookie) { - return; - } - StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); - Pair errorInfo = processErrorCustomizers(Pair.create(message, stack)); - mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); - updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS); - // JS errors are reported here after source mapping. - if (mRedBoxHandler != null) { - mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); - mRedBoxDialog.resetReporting(); - } - mRedBoxDialog.show(); + StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); + Pair errorInfo = + processErrorCustomizers(Pair.create(message, stack)); + mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); + updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS); + // JS errors are reported here after source mapping. + if (mRedBoxHandler != null) { + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); + mRedBoxDialog.resetReporting(); } + mRedBoxDialog.show(); + } }); + } + + @Override + public void hideRedboxDialog() { + // dismiss redbox if exists + if (mRedBoxDialog != null) { + mRedBoxDialog.dismiss(); + mRedBoxDialog = null; } + } - @Override - public void hideRedboxDialog() { - // dismiss redbox if exists - if (mRedBoxDialog != null) { - mRedBoxDialog.dismiss(); - mRedBoxDialog = null; - } - } - - private void hideDevOptionsDialog() { - if (mDevOptionsDialog != null) { - mDevOptionsDialog.dismiss(); - mDevOptionsDialog = null; - } + 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(new Runnable() { - - @Override - public void run() { - if (mRedBoxDialog == null) { - Activity context = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e(ReactConstants.TAG, "Unable to launch redbox because react activity " + "is not available, here is the error that redbox would've displayed: " + message); - return; - } - mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler); - } - if (mRedBoxDialog.isShowing()) { - // show the first and most actionable one. - return; - } - Pair errorInfo = processErrorCustomizers(Pair.create(message, stack)); - mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); - updateLastErrorInfo(message, stack, errorCookie, errorType); - // inside {@link #updateJSError} after source mapping. - if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) { - mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.NATIVE); - } - mRedBoxDialog.resetReporting(); - mRedBoxDialog.show(); + } + + private void showNewError( + @Nullable final String message, + final StackFrame[] stack, + final int errorCookie, + final ErrorType errorType) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (mRedBoxDialog == null) { + Activity context = mReactInstanceManagerHelper.getCurrentActivity(); + if (context == null || context.isFinishing()) { + FLog.e( + ReactConstants.TAG, + "Unable to launch redbox because react activity " + + "is not available, here is the error that redbox would've displayed: " + + message); + return; + } + mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler); } + if (mRedBoxDialog.isShowing()) { + // Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only + // show the first and most actionable one. + return; + } + Pair errorInfo = + processErrorCustomizers(Pair.create(message, stack)); + mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second); + updateLastErrorInfo(message, stack, errorCookie, errorType); + // Only report native errors here. JS errors are reported + // inside {@link #updateJSError} after source mapping. + if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) { + mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.NATIVE); + } + mRedBoxDialog.resetReporting(); + mRedBoxDialog.show(); + } }); + } + + @Override + public void reloadExpoApp() { + try { + int activityId = mDevServerHelper.mSettings.exponentActivityId; + Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("reloadFromManifest", int.class).invoke(null, activityId); + } catch (Exception expoHandleErrorException) { + expoHandleErrorException.printStackTrace(); } + } - @Override - public void reloadExpoApp() { - try { - int activityId = mDevServerHelper.mSettings.exponentActivityId; - Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("reloadFromManifest", int.class).invoke(null, activityId); - } catch (Exception expoHandleErrorException) { - expoHandleErrorException.printStackTrace(); - } - } - - - @Override - public void showDevOptionsDialog() { - if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { - return; - } - LinkedHashMap options = new LinkedHashMap<>(); - /* register standard options */ - options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_reload), new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isJSDevModeEnabled() && mDevSettings.isHotModuleReplacementEnabled()) { - Toast.makeText(mApplicationContext, mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_disable), Toast.LENGTH_LONG).show(); - mDevSettings.setHotModuleReplacementEnabled(false); - } - - // NOTE(brentvatne): rather than reload just JS we need to reload the entire project from manifest - // handleReloadJS(); - reloadExpoApp(); + @Override + public void showDevOptionsDialog() { + if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { + return; + } + LinkedHashMap options = new LinkedHashMap<>(); + /* register standard options */ + options.put( + mApplicationContext.getString(R.string.reactandroid_catalyst_reload), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + if (!mDevSettings.isJSDevModeEnabled() + && mDevSettings.isHotModuleReplacementEnabled()) { + Toast.makeText( + mApplicationContext, + mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_disable), + Toast.LENGTH_LONG) + .show(); + mDevSettings.setHotModuleReplacementEnabled(false); } - }); - -// if (mDevSettings.isNuclideJSDebugEnabled()) { -// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), new DevOptionHandler() { -// -// @Override -// public void onOptionSelected() { -// mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); -// } -// }); -// } - // NOTE(brentvatne): This option does not make sense for Expo -// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), new DevOptionHandler() { -// + // NOTE(brentvatne): rather than reload just JS we need to reload the entire project from manifest + // handleReloadJS(); + reloadExpoApp(); + } + }); + options.put( + mDevSettings.isNuclideJSDebugEnabled() + ? mDevSettings.isRemoteJSDebugEnabled() + ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome_stop) + : mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome) + : mDevSettings.isRemoteJSDebugEnabled() + ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_stop) + : mApplicationContext.getString(R.string.reactandroid_catalyst_debug), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); + handleReloadJS(); + } + }); +// if (mDevSettings.isNuclideJSDebugEnabled()) { +// options.put( +// mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), +// new DevOptionHandler() { // @Override // public void onOptionSelected() { -// Activity context = mReactInstanceManagerHelper.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.reactandroid_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(); +// mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); // } -// }); - - // "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh". - // While native code for "Live reload" is still there, please don't add the option back. - // See D15958697 for more context. - options.put(mDevSettings.isHotModuleReplacementEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading), new DevOptionHandler() { - - @Override - public void onOptionSelected() { - boolean nextEnabled = !mDevSettings.isHotModuleReplacementEnabled(); - mDevSettings.setHotModuleReplacementEnabled(nextEnabled); - if (mCurrentContext != null) { - if (nextEnabled) { - mCurrentContext.getJSModule(HMRClient.class).enable(); - } else { - mCurrentContext.getJSModule(HMRClient.class).disable(); - } - } -// if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { -// Toast.makeText(mApplicationContext, mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), Toast.LENGTH_LONG).show(); -// mDevSettings.setJSDevModeEnabled(true); -// handleReloadJS(); -// } +// }); +// } +// NOTE(brentvatne): This option does not make sense for Expo +// options.put( +// mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), +// new DevOptionHandler() { +// @Override +// public void onOptionSelected() { +// Activity context = mReactInstanceManagerHelper.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.reactandroid_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( + // NOTE: `isElementInspectorEnabled` is not guaranteed to be accurate. + mApplicationContext.getString(R.string.reactandroid_catalyst_inspector), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); + } + }); + // "Live reload" which refreshes on every edit was removed in favor of "Fast Refresh". + // While native code for "Live reload" is still there, please don't add the option back. + // See D15958697 for more context. + options.put( + mDevSettings.isHotModuleReplacementEnabled() + ? mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_stop) + : mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + boolean nextEnabled = !mDevSettings.isHotModuleReplacementEnabled(); + mDevSettings.setHotModuleReplacementEnabled(nextEnabled); + if (mCurrentContext != null) { + if (nextEnabled) { + mCurrentContext.getJSModule(HMRClient.class).enable(); + } else { + mCurrentContext.getJSModule(HMRClient.class).disable(); + } } +// if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { +// Toast.makeText( +// mApplicationContext, +// mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), +// Toast.LENGTH_LONG) +// .show(); +// mDevSettings.setJSDevModeEnabled(true); +// handleReloadJS(); +// } + } }); - - options.put(mDevSettings.isNuclideJSDebugEnabled() ? mDevSettings.isRemoteJSDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_debug_chrome) : mDevSettings.isRemoteJSDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_debug_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_debug), new DevOptionHandler() { - - @Override - public void onOptionSelected() { - mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); - handleReloadJS(); - } - }); - - - -// options.put(mIsSamplingProfilerEnabled ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), new DevOptionHandler() { -// -// @Override -// public void onOptionSelected() { -// toggleJSSamplingProfiler(); -// } +// options.put( +// mIsSamplingProfilerEnabled +// ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) +// : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), +// new DevOptionHandler() { +// @Override +// public void onOptionSelected() { +// toggleJSSamplingProfiler(); +// } // }); - options.put(mDevSettings.isFpsDebugEnabled() ? mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor_stop) : mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor), new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isFpsDebugEnabled()) { - // Request overlay permission if needed when "Show Perf Monitor" option is selected - Context context = mReactInstanceManagerHelper.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( + mDevSettings.isFpsDebugEnabled() + ? mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor_stop) + : mApplicationContext.getString(R.string.reactandroid_catalyst_perf_monitor), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + if (!mDevSettings.isFpsDebugEnabled()) { + // Request overlay permission if needed when "Show Perf Monitor" option is selected + Context context = mReactInstanceManagerHelper.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(// NOTE: `isElementInspectorEnabled` is not guaranteed to be accurate. - mApplicationContext.getString(R.string.reactandroid_catalyst_inspector), new DevOptionHandler() { - - @Override - public void onOptionSelected() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } - }); - - -// options.put(mApplicationContext.getString(R.string.reactandroid_catalyst_settings), new DevOptionHandler() { -// -// @Override -// public void onOptionSelected() { -// Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class); -// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// mApplicationContext.startActivity(intent); -// } +// options.put( +// mApplicationContext.getString(R.string.reactandroid_catalyst_settings), +// new DevOptionHandler() { +// @Override +// public void onOptionSelected() { +// 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 = mReactInstanceManagerHelper.getCurrentActivity(); - if (context == null || context.isFinishing()) { - FLog.e(ReactConstants.TAG, "Unable to launch dev options menu because react activity " + "isn't available"); - return; - } - mDevOptionsDialog = new AlertDialog.Builder(context).setItems(options.keySet().toArray(new String[0]), new DialogInterface.OnClickListener() { + if (mCustomDevOptions.size() > 0) { + options.putAll(mCustomDevOptions); + } - @Override - public void onClick(DialogInterface dialog, int which) { - optionHandlers[which].onOptionSelected(); - mDevOptionsDialog = null; - } - }).setOnCancelListener(new DialogInterface.OnCancelListener() { + final DevOptionHandler[] optionHandlers = options.values().toArray(new DevOptionHandler[0]); - @Override - public void onCancel(DialogInterface dialog) { - mDevOptionsDialog = null; - } - }).create(); - mDevOptionsDialog.show(); + Activity context = mReactInstanceManagerHelper.getCurrentActivity(); + if (context == null || context.isFinishing()) { + FLog.e( + ReactConstants.TAG, + "Unable to launch dev options menu because react activity " + "isn't available"); + return; } - - /** Starts of stops the sampling profiler */ - private void toggleJSSamplingProfiler() { - JavaScriptExecutorFactory javaScriptExecutorFactory = mReactInstanceManagerHelper.getJavaScriptExecutorFactory(); - if (!mIsSamplingProfilerEnabled) { - try { - javaScriptExecutorFactory.startSamplingProfiler(); - Toast.makeText(mApplicationContext, "Starting Sampling Profiler", Toast.LENGTH_SHORT).show(); - } catch (UnsupportedOperationException e) { - Toast.makeText(mApplicationContext, javaScriptExecutorFactory.toString() + " does not support Sampling Profiler", Toast.LENGTH_LONG).show(); - } finally { - mIsSamplingProfilerEnabled = true; - } - } else { - try { - final String outputPath = File.createTempFile("sampling-profiler-trace", ".cpuprofile", mApplicationContext.getCacheDir()).getPath(); - javaScriptExecutorFactory.stopSamplingProfiler(outputPath); - Toast.makeText(mApplicationContext, "Saved results from Profiler to " + outputPath, Toast.LENGTH_LONG).show(); - } catch (IOException e) { - FLog.e(ReactConstants.TAG, "Could not create temporary file for saving results from Sampling Profiler"); - } catch (UnsupportedOperationException e) { - Toast.makeText(mApplicationContext, javaScriptExecutorFactory.toString() + "does not support Sampling Profiler", Toast.LENGTH_LONG).show(); - } finally { - mIsSamplingProfilerEnabled = false; - } - } + mDevOptionsDialog = + new AlertDialog.Builder(context) + .setItems( + options.keySet().toArray(new String[0]), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + optionHandlers[which].onOptionSelected(); + mDevOptionsDialog = null; + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mDevOptionsDialog = null; + } + }) + .create(); + mDevOptionsDialog.show(); + } + + /** Starts of stops the sampling profiler */ + private void toggleJSSamplingProfiler() { + JavaScriptExecutorFactory javaScriptExecutorFactory = + mReactInstanceManagerHelper.getJavaScriptExecutorFactory(); + if (!mIsSamplingProfilerEnabled) { + try { + javaScriptExecutorFactory.startSamplingProfiler(); + Toast.makeText(mApplicationContext, "Starting Sampling Profiler", Toast.LENGTH_SHORT) + .show(); + } catch (UnsupportedOperationException e) { + Toast.makeText( + mApplicationContext, + javaScriptExecutorFactory.toString() + " does not support Sampling Profiler", + Toast.LENGTH_LONG) + .show(); + } finally { + mIsSamplingProfilerEnabled = true; + } + } else { + try { + final String outputPath = + File.createTempFile( + "sampling-profiler-trace", ".cpuprofile", mApplicationContext.getCacheDir()) + .getPath(); + javaScriptExecutorFactory.stopSamplingProfiler(outputPath); + Toast.makeText( + mApplicationContext, + "Saved results from Profiler to " + outputPath, + Toast.LENGTH_LONG) + .show(); + } catch (IOException e) { + FLog.e( + ReactConstants.TAG, + "Could not create temporary file for saving results from Sampling Profiler"); + } catch (UnsupportedOperationException e) { + Toast.makeText( + mApplicationContext, + javaScriptExecutorFactory.toString() + "does not support Sampling Profiler", + Toast.LENGTH_LONG) + .show(); + } finally { + mIsSamplingProfilerEnabled = false; + } } + } - /** + /** * {@link ReactInstanceDevCommandsHandler} 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 void setDevSupportEnabled(boolean isDevSupportEnabled) { + mIsDevSupportEnabled = isDevSupportEnabled; + reloadSettings(); + } + + @Override + public boolean getDevSupportEnabled() { + return mIsDevSupportEnabled; + } + + @Override + public DeveloperSettings getDevSettings() { + return mDevSettings; + } + + @Override + public void onNewReactContextCreated(ReactContext reactContext) { + resetCurrentContext(reactContext); + } + + @Override + public void onReactInstanceDestroyed(ReactContext reactContext) { + if (reactContext == mCurrentContext) { + // only call reset context when the destroyed context matches the one that is currently set + // for this manager + resetCurrentContext(null); } + } - @Override - public void onNewReactContextCreated(ReactContext reactContext) { - resetCurrentContext(reactContext); + @Override + public String getSourceMapUrl() { + if (mJSAppBundleName == null) { + return ""; } - @Override - public void onReactInstanceDestroyed(ReactContext reactContext) { - if (reactContext == mCurrentContext) { - // only call reset context when the destroyed context matches the one that is currently set - // for this manager - resetCurrentContext(null); - } - } + return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); + } - @Override - public String getSourceMapUrl() { - if (mJSAppBundleName == null) { - return ""; - } - return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); + @Override + public String getSourceUrl() { + if (mJSAppBundleName == null) { + return ""; } - @Override - public String getSourceUrl() { - if (mJSAppBundleName == null) { - return ""; - } - return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); - } + return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); + } - @Override - public String getJSBundleURLForRemoteDebugging() { - return mDevServerHelper.getJSBundleURLForRemoteDebugging(Assertions.assertNotNull(mJSAppBundleName)); - } + @Override + public String getJSBundleURLForRemoteDebugging() { + return mDevServerHelper.getJSBundleURLForRemoteDebugging( + Assertions.assertNotNull(mJSAppBundleName)); + } - @Override - public String getDownloadedJSBundleFile() { - return mJSBundleTempFile.getAbsolutePath(); - } + @Override + public String getDownloadedJSBundleFile() { + return mJSBundleTempFile.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() { - return false; - } - - /** + @Override + public boolean hasUpToDateJSBundleInCache() { +// if (mIsDevSupportEnabled && mJSBundleTempFile.exists()) { +// try { +// String packageName = mApplicationContext.getPackageName(); +// PackageInfo thisPackage = +// mApplicationContext.getPackageManager().getPackageInfo(packageName, 0); +// if (mJSBundleTempFile.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 mJSBundleTempFile.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; + } + + /** * @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case {@link * com.facebook.react.ReactInstanceManager} should use that file from assets instead of * downloading bundle from dev server */ - public boolean hasBundleInAssets(String bundleAssetName) { - try { - String[] assets = mApplicationContext.getAssets().list(""); - for (int i = 0; i < assets.length; i++) { - if (assets[i].equals(bundleAssetName)) { - return true; - } - } - } catch (IOException e) { - // Ignore this error and just fallback to downloading JS from devserver - FLog.e(ReactConstants.TAG, "Error while loading assets list"); + public boolean hasBundleInAssets(String bundleAssetName) { + try { + String[] assets = mApplicationContext.getAssets().list(""); + for (int i = 0; i < assets.length; i++) { + if (assets[i].equals(bundleAssetName)) { + return true; } - return false; + } + } catch (IOException e) { + // Ignore this error and just fallback to downloading JS from devserver + FLog.e(ReactConstants.TAG, "Error while loading assets list"); } + return false; + } - private void resetCurrentContext(@Nullable ReactContext reactContext) { - if (mCurrentContext == reactContext) { - // new context is the same as the old one - do nothing - return; - } - mCurrentContext = reactContext; - // Recreate debug overlay controller with new CatalystInstance object - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); - } - if (reactContext != null) { - mDebugOverlayController = new DebugOverlayController(reactContext); - } - if (mCurrentContext != null) { - try { - URL sourceUrl = new URL(getSourceUrl()); - // strip initial slash in path - String path = sourceUrl.getPath().substring(1); - String host = sourceUrl.getHost(); - int port = sourceUrl.getPort(); - mCurrentContext.getJSModule(HMRClient.class).setup("android", path, host, port, mDevSettings.isHotModuleReplacementEnabled()); - } catch (MalformedURLException e) { - showNewJavaError(e.getMessage(), e); - } - } - reloadSettings(); + private void resetCurrentContext(@Nullable ReactContext reactContext) { + if (mCurrentContext == reactContext) { + // new context is the same as the old one - do nothing + return; } - @Override - public void reloadSettings() { - if (UiThreadUtil.isOnUiThread()) { - reload(); - } else { - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - reload(); - } - }); - } - } + mCurrentContext = reactContext; - public void onInternalSettingsChanged() { - reloadSettings(); + // Recreate debug overlay controller with new CatalystInstance object + if (mDebugOverlayController != null) { + mDebugOverlayController.setFpsDebugViewVisible(false); } - - // NOTE(brentvatne): this is confusingly called the first time the app loads! - @Override - public void handleReloadJS() { - UiThreadUtil.assertOnUiThread(); - - ReactMarker.logMarker(ReactMarkerConstants.RELOAD, mDevSettings.getPackagerConnectionSettings().getDebugServerHost()); - // dismiss redbox if exists - hideRedboxDialog(); - if (mDevSettings.isRemoteJSDebugEnabled()) { - PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy"); - mDevLoadingViewController.showForRemoteJSEnabled(); - mDevLoadingViewVisible = true; - reloadJSInProxyMode(); - } else { - PrinterHolder.getPrinter().logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server"); - String bundleURL = mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName)); - reloadJSFromServer(bundleURL); - } + if (reactContext != null) { + mDebugOverlayController = new DebugOverlayController(reactContext); } - @Override - public void isPackagerRunning(PackagerStatusCallback callback) { - mDevServerHelper.isPackagerRunning(callback); + if (mCurrentContext != null) { + try { + URL sourceUrl = new URL(getSourceUrl()); + String path = sourceUrl.getPath().substring(1); // strip initial slash in path + String host = sourceUrl.getHost(); + int port = sourceUrl.getPort(); + mCurrentContext + .getJSModule(HMRClient.class) + .setup("android", path, host, port, mDevSettings.isHotModuleReplacementEnabled()); + } catch (MalformedURLException e) { + showNewJavaError(e.getMessage(), e); + } } - @Override - @Nullable - public File downloadBundleResourceFromUrlSync(final String resourceURL, final File outputFile) { - return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); - } + reloadSettings(); + } - @Override - @Nullable - public String getLastErrorTitle() { - return mLastErrorTitle; + @Override + public void reloadSettings() { + if (UiThreadUtil.isOnUiThread()) { + reload(); + } else { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + reload(); + } + }); } - - @Override - @Nullable - public StackFrame[] getLastErrorStack() { - return mLastErrorStack; + } + + public void onInternalSettingsChanged() { + reloadSettings(); + } + + // NOTE(brentvatne): this is confusingly called the first time the app loads! + @Override + public void handleReloadJS() { + + UiThreadUtil.assertOnUiThread(); + + ReactMarker.logMarker( + ReactMarkerConstants.RELOAD, + mDevSettings.getPackagerConnectionSettings().getDebugServerHost()); + + // dismiss redbox if exists + hideRedboxDialog(); + + if (mDevSettings.isRemoteJSDebugEnabled()) { + PrinterHolder.getPrinter() + .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy"); + mDevLoadingViewController.showForRemoteJSEnabled(); + mDevLoadingViewVisible = true; + reloadJSInProxyMode(); + } else { + PrinterHolder.getPrinter() + .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server"); + String bundleURL = + mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName)); + reloadJSFromServer(bundleURL); } - - @Override - public void onPackagerConnected() { + } + + @Override + public void isPackagerRunning(PackagerStatusCallback callback) { + mDevServerHelper.isPackagerRunning(callback); + } + + @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 void onPackagerConnected() { // No-op - } + } - @Override - public void onPackagerDisconnected() { + @Override + public void onPackagerDisconnected() { // No-op - } - - @Override - public void onPackagerReloadCommand() { - // Disable debugger to resume the JsVM & avoid thread locks while reloading - mDevServerHelper.disableDebugger(); - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - handleReloadJS(); - } + } + + @Override + public void onPackagerReloadCommand() { + // Disable debugger to resume the JsVM & avoid thread locks while reloading + mDevServerHelper.disableDebugger(); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + handleReloadJS(); + } }); - } - - @Override - public void onPackagerDevMenuCommand() { - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - showDevOptionsDialog(); - } + } + + @Override + public void onPackagerDevMenuCommand() { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + showDevOptionsDialog(); + } }); - } - - @Override - public void onCaptureHeapCommand(final Responder responder) { - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - handleCaptureHeap(responder); - } + } + + @Override + public void onCaptureHeapCommand(final Responder responder) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + handleCaptureHeap(responder); + } }); - } - - @Override - @Nullable - public Map customCommandHandlers() { - return mCustomPackagerCommandHandlers; - } + } - private void handleCaptureHeap(final Responder responder) { - if (mCurrentContext == null) { - return; - } - JSCHeapCapture heapCapture = mCurrentContext.getNativeModule(JSCHeapCapture.class); - heapCapture.captureHeap(mApplicationContext.getCacheDir().getPath(), new JSCHeapCapture.CaptureCallback() { - - @Override - public void onSuccess(File capture) { - responder.respond(capture.toString()); - } + @Override + public @Nullable Map customCommandHandlers() { + return mCustomPackagerCommandHandlers; + } - @Override - public void onFailure(JSCHeapCapture.CaptureException error) { - responder.error(error.toString()); - } - }); - } - - private void updateLastErrorInfo(@Nullable final String message, final StackFrame[] stack, final int errorCookie, final ErrorType errorType) { - mLastErrorTitle = message; - mLastErrorStack = stack; - mLastErrorCookie = errorCookie; - mLastErrorType = errorType; + private void handleCaptureHeap(final Responder responder) { + if (mCurrentContext == null) { + return; } - - private void reloadJSInProxyMode() { - // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that - // anyway - mDevServerHelper.launchJSDevtools(); - JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() { - - @Override - public JavaJSExecutor create() throws Exception { - WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor(); - SimpleSettableFuture future = new SimpleSettableFuture<>(); - executor.connect(mDevServerHelper.getWebsocketProxyURL(), getExecutorConnectCallback(future)); - // TODO(t9349129) Don't use timeout - try { - future.get(90, TimeUnit.SECONDS); - return executor; - } catch (ExecutionException e) { - throw (Exception) e.getCause(); - } catch (InterruptedException | TimeoutException e) { - throw new RuntimeException(e); - } + JSCHeapCapture heapCapture = mCurrentContext.getNativeModule(JSCHeapCapture.class); + heapCapture.captureHeap( + mApplicationContext.getCacheDir().getPath(), + new JSCHeapCapture.CaptureCallback() { + @Override + public void onSuccess(File capture) { + responder.respond(capture.toString()); + } + + @Override + public void onFailure(JSCHeapCapture.CaptureException error) { + responder.error(error.toString()); + } + }); + } + + private void updateLastErrorInfo( + @Nullable final String message, + final StackFrame[] stack, + final int errorCookie, + final ErrorType errorType) { + mLastErrorTitle = message; + mLastErrorStack = stack; + mLastErrorCookie = errorCookie; + mLastErrorType = errorType; + } + + private void reloadJSInProxyMode() { + // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that + // anyway + mDevServerHelper.launchJSDevtools(); + + JavaJSExecutor.Factory factory = + new JavaJSExecutor.Factory() { + @Override + public JavaJSExecutor create() throws Exception { + WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor(); + SimpleSettableFuture future = new SimpleSettableFuture<>(); + executor.connect( + mDevServerHelper.getWebsocketProxyURL(), getExecutorConnectCallback(future)); + // TODO(t9349129) Don't use timeout + try { + future.get(90, TimeUnit.SECONDS); + return executor; + } catch (ExecutionException e) { + throw (Exception) e.getCause(); + } catch (InterruptedException | TimeoutException e) { + throw new RuntimeException(e); } + } }; - mReactInstanceManagerHelper.onReloadWithJSDebugger(factory); - } + mReactInstanceManagerHelper.onReloadWithJSDebugger(factory); + } + + private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback( + final SimpleSettableFuture future) { + return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { + @Override + public void onSuccess() { + future.set(true); + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + } - private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback(final SimpleSettableFuture future) { - return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { + @Override + public void onFailure(final Throwable cause) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + FLog.e(ReactConstants.TAG, "Failed to connect to debugger!", cause); + future.setException( + new IOException(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_error), cause)); + } + }; + } - @Override - public void onSuccess() { - future.set(true); - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - } + public void reloadJSFromServer(final String bundleURL) { + ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START); - @Override - public void onFailure(final Throwable cause) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - FLog.e(ReactConstants.TAG, "Failed to connect to debugger!", cause); - future.setException(new IOException(mApplicationContext.getString(R.string.reactandroid_catalyst_debug_error), cause)); - } - }; - } + mDevLoadingViewController.showForUrl(bundleURL); + mDevLoadingViewVisible = true; - public void reloadJSFromServer(final String bundleURL) { - ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START); - mDevLoadingViewController.showForUrl(bundleURL); - mDevLoadingViewVisible = true; - final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo(); - mDevServerHelper.downloadBundleFromURL(new DevBundleDownloadListener() { + final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo(); - @Override - public void onSuccess(@Nullable final NativeDeltaClient nativeDeltaClient) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - synchronized (DevSupportManagerImpl.this) { - mBundleStatus.isLastDownloadSucess = true; - mBundleStatus.updateTimestamp = System.currentTimeMillis(); - } - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onSuccess(nativeDeltaClient); - } - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); - mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient); - } + mDevServerHelper.downloadBundleFromURL( + new DevBundleDownloadListener() { + @Override + public void onSuccess(final @Nullable NativeDeltaClient nativeDeltaClient) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + synchronized (DevSupportManagerImpl.this) { + mBundleStatus.isLastDownloadSucess = true; + mBundleStatus.updateTimestamp = System.currentTimeMillis(); + } + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onSuccess(nativeDeltaClient); + } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + ReactMarker.logMarker( + ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); + mReactInstanceManagerHelper.onJSBundleLoadedFromServer(nativeDeltaClient); + } }); + } + + @Override + public void onProgress( + @Nullable final String status, + @Nullable final Integer done, + @Nullable final Integer total) { + mDevLoadingViewController.updateProgress(status, done, total); + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onProgress(status, done, total); } + } - @Override - public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) { - mDevLoadingViewController.updateProgress(status, done, total); - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onProgress(status, done, total); - } + @Override + public void onFailure(final Exception cause) { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + synchronized (DevSupportManagerImpl.this) { + mBundleStatus.isLastDownloadSucess = false; } - - @Override - public void onFailure(final Exception cause) { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - synchronized (DevSupportManagerImpl.this) { - mBundleStatus.isLastDownloadSucess = false; - } - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onFailure(cause); - } - FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - if (cause instanceof DebugServerException) { - DebugServerException debugServerException = (DebugServerException) cause; - showNewJavaError(debugServerException.getMessage(), cause); - } else { - showNewJavaError(mApplicationContext.getString(R.string.reactandroid_catalyst_reload_error), cause); - } + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onFailure(cause); + } + FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (cause instanceof DebugServerException) { + DebugServerException debugServerException = (DebugServerException) cause; + showNewJavaError(debugServerException.getMessage(), cause); + } else { + showNewJavaError( + mApplicationContext.getString(R.string.reactandroid_catalyst_reload_error), cause); } + } }); - } - }, mJSBundleTempFile, bundleURL, bundleInfo); + } + }, + mJSBundleTempFile, + bundleURL, + bundleInfo); + } + + @Override + public void startInspector() { + if (mIsDevSupportEnabled) { + mDevServerHelper.openInspectorConnection(); } + } - @Override - public void startInspector() { - if (mIsDevSupportEnabled) { - mDevServerHelper.openInspectorConnection(); - } - } + @Override + public void stopInspector() { + mDevServerHelper.closeInspectorConnection(); + } - @Override - public void stopInspector() { - mDevServerHelper.closeInspectorConnection(); + @Override + public void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled) { + if (!mIsDevSupportEnabled) { + return; } - @Override - public void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - UiThreadUtil.runOnUiThread(new Runnable() { - - @Override - public void run() { - mDevSettings.setHotModuleReplacementEnabled(isHotModuleReplacementEnabled); - handleReloadJS(); - } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mDevSettings.setHotModuleReplacementEnabled(isHotModuleReplacementEnabled); + handleReloadJS(); + } }); - } + } - @Override - public void setRemoteJSDebugEnabled(final boolean isRemoteJSDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void setRemoteJSDebugEnabled(final boolean isRemoteJSDebugEnabled) { + if (!mIsDevSupportEnabled) { + return; + } - @Override - public void run() { - mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); - handleReloadJS(); - } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); + handleReloadJS(); + } }); - } + } - @Override - public void setReloadOnJSChangeEnabled(final boolean isReloadOnJSChangeEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void setReloadOnJSChangeEnabled(final boolean isReloadOnJSChangeEnabled) { + if (!mIsDevSupportEnabled) { + return; + } - @Override - public void run() { - mDevSettings.setReloadOnJSChangeEnabled(isReloadOnJSChangeEnabled); - handleReloadJS(); - } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mDevSettings.setReloadOnJSChangeEnabled(isReloadOnJSChangeEnabled); + handleReloadJS(); + } }); - } + } - @Override - public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { + if (!mIsDevSupportEnabled) { + return; + } - @Override - public void run() { - mDevSettings.setFpsDebugEnabled(isFpsDebugEnabled); - } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mDevSettings.setFpsDebugEnabled(isFpsDebugEnabled); + } }); - } + } - @Override - public void toggleElementInspector() { - if (!mIsDevSupportEnabled) { - return; - } - UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void toggleElementInspector() { + if (!mIsDevSupportEnabled) { + return; + } - @Override - public void run() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); + } }); - } + } - // NOTE(brentvatne): this is confusingly called the first time the app loads! - 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) { - mShakeDetector.start((SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE)); - mIsShakeDetectorStarted = true; - } - // register reload app broadcast receiver - if (!mIsReceiverRegistered) { - IntentFilter filter = new IntentFilter(); - filter.addAction(getReloadAppAction(mApplicationContext)); - mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); - mIsReceiverRegistered = true; - } - // show the dev loading if it should be - if (mDevLoadingViewVisible) { - mDevLoadingViewController.showMessage("Reloading..."); - } - mDevServerHelper.openPackagerConnection(this.getClass().getSimpleName(), this); - if (mDevSettings.isReloadOnJSChangeEnabled()) { - mDevServerHelper.startPollingOnChangeEndpoint(new DevServerHelper.OnServerContentChangeListener() { + // NOTE(brentvatne): this is confusingly called the first time the app loads! + private void reload() { + UiThreadUtil.assertOnUiThread(); - @Override - public void onServerContentChanged() { - handleReloadJS(); - } - }); - } else { - mDevServerHelper.stopPollingOnChangeEndpoint(); - } - } 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 - mDevLoadingViewController.hide(); - mDevServerHelper.closePackagerConnection(); - mDevServerHelper.stopPollingOnChangeEndpoint(); - } - } + // 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) { + mShakeDetector.start( + (SensorManager) mApplicationContext.getSystemService(Context.SENSOR_SERVICE)); + mIsShakeDetectorStarted = true; + } + + // register reload app broadcast receiver + if (!mIsReceiverRegistered) { + IntentFilter filter = new IntentFilter(); + filter.addAction(getReloadAppAction(mApplicationContext)); + mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); + mIsReceiverRegistered = true; + } - /** Intent action for reloading the JS */ - private static String getReloadAppAction(Context context) { - return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; + // show the dev loading if it should be + if (mDevLoadingViewVisible) { + mDevLoadingViewController.showMessage("Reloading..."); + } + + mDevServerHelper.openPackagerConnection(this.getClass().getSimpleName(), this); + if (mDevSettings.isReloadOnJSChangeEnabled()) { + mDevServerHelper.startPollingOnChangeEndpoint( + new DevServerHelper.OnServerContentChangeListener() { + @Override + public void onServerContentChanged() { + handleReloadJS(); + } + }); + } else { + mDevServerHelper.stopPollingOnChangeEndpoint(); + } + } 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 + mDevLoadingViewController.hide(); + mDevServerHelper.closePackagerConnection(); + mDevServerHelper.stopPollingOnChangeEndpoint(); } + } + + /** Intent action for reloading the JS */ + private static String getReloadAppAction(Context context) { + return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; + } } From 6f62ba22249393b7b0dbb090df1b6770c091eb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 14 Nov 2019 11:26:23 +0100 Subject: [PATCH 3/5] [android] Revert changes that will be done by :tools:execute --- .../react/devsupport/DevInternalSettings.java | 11 +--- .../devsupport/DevSupportManagerImpl.java | 50 ++++++++----------- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java index 14fe73a6524fb2..c4a48a4281b309 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java @@ -117,16 +117,11 @@ public void setHotModuleReplacementEnabled(boolean enabled) { } public boolean isReloadOnJSChangeEnabled() { - // NOTE(brentvatne): This is not possible to enable/disable so we should always disable it for - // now. I managed to get into a state where fast refresh wouldn't work because live reload - // would kick in every time and there was no way to turn it off from the dev menu. - return false; - // return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, true); + return mPreferences.getBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, false); } public void setReloadOnJSChangeEnabled(boolean enabled) { - // NOTE(brentvatne): We don't need to do anything here because this option is always false - // mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); + mPreferences.edit().putBoolean(PREFS_RELOAD_ON_JS_CHANGE_KEY, enabled).apply(); } public boolean isElementInspectorEnabled() { @@ -181,6 +176,4 @@ public boolean isStartSamplingProfilerOnInit() { public interface Listener { void onInternalSettingsChanged(); } - - public int exponentActivityId = -1; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index c6efc06068b5b9..3cb0dfb0b8afc3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -251,7 +251,6 @@ public void onReceive(Context context, Intent intent) { @Override public void handleException(Exception e) { - try { if (mIsDevSupportEnabled) { for (ExceptionLogger logger : mExceptionLoggers) { @@ -261,13 +260,6 @@ public void handleException(Exception e) { } else { mDefaultNativeModuleCallExceptionHandler.handleException(e); } - } catch (RuntimeException expoException) { - try { - Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("handleReactNativeError", String.class, Object.class, Integer.class, Boolean.class).invoke(null, expoException.getMessage(), null, -1, true); - } catch (Exception expoHandleErrorException) { - expoHandleErrorException.printStackTrace(); - } - } } private interface ExceptionLogger { @@ -765,27 +757,27 @@ public String getDownloadedJSBundleFile() { */ @Override public boolean hasUpToDateJSBundleInCache() { -// if (mIsDevSupportEnabled && mJSBundleTempFile.exists()) { -// try { -// String packageName = mApplicationContext.getPackageName(); -// PackageInfo thisPackage = -// mApplicationContext.getPackageManager().getPackageInfo(packageName, 0); -// if (mJSBundleTempFile.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 mJSBundleTempFile.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"); -// } -// } + if (mIsDevSupportEnabled && mJSBundleTempFile.exists()) { + try { + String packageName = mApplicationContext.getPackageName(); + PackageInfo thisPackage = + mApplicationContext.getPackageManager().getPackageInfo(packageName, 0); + if (mJSBundleTempFile.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 mJSBundleTempFile.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; } From 3ba4045211fd1923a237e519b28dace3d737cd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Thu, 14 Nov 2019 11:26:53 +0100 Subject: [PATCH 4/5] [android] Prepare ReactAndroid for bare and :tools:execute --- .../react/devsupport/DevSupportManagerImpl.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 3cb0dfb0b8afc3..6fa6ded142be01 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -424,13 +424,22 @@ public void run() { }); } + private int getExponentActivityId() { + return -1; + } + @Override public void reloadExpoApp() { try { - int activityId = mDevServerHelper.mSettings.exponentActivityId; - Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("reloadFromManifest", int.class).invoke(null, activityId); + Class.forName("host.exp.exponent.ReactNativeStaticHelpers").getMethod("reloadFromManifest", int.class).invoke(null, getExponentActivityId()); } catch (Exception expoHandleErrorException) { expoHandleErrorException.printStackTrace(); + + // reloadExpoApp replaces handleReloadJS in some places + // where in Expo we would like to reload from manifest. + // If so, if anything goes wrong here, we can fall back + // to plain JS reload. + handleReloadJS(); } } From 4c65ae2dcdd1b7356e9d8e5621fe12f6b08c6612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Fri, 15 Nov 2019 12:05:56 +0100 Subject: [PATCH 5/5] Label code instead of commenting out --- .../devsupport/DevSupportManagerImpl.java | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 6fa6ded142be01..e05977894d6ff1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -486,52 +486,52 @@ public void onOptionSelected() { handleReloadJS(); } }); -// if (mDevSettings.isNuclideJSDebugEnabled()) { -// options.put( -// mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), -// new DevOptionHandler() { -// @Override -// public void onOptionSelected() { -// mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); -// } -// }); -// } -// NOTE(brentvatne): This option does not make sense for Expo -// options.put( -// mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), -// new DevOptionHandler() { -// @Override -// public void onOptionSelected() { -// Activity context = mReactInstanceManagerHelper.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.reactandroid_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(); -// } -// }); + expo_transformer_remove: if (mDevSettings.isNuclideJSDebugEnabled()) { + options.put( + mApplicationContext.getString(R.string.reactandroid_catalyst_debug_nuclide), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); + } + }); + } + // NOTE(brentvatne): This option does not make sense for Expo + expo_transformer_remove: options.put( + mApplicationContext.getString(R.string.reactandroid_catalyst_change_bundle_location), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + Activity context = mReactInstanceManagerHelper.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.reactandroid_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( // NOTE: `isElementInspectorEnabled` is not guaranteed to be accurate. mApplicationContext.getString(R.string.reactandroid_catalyst_inspector), @@ -561,28 +561,28 @@ public void onOptionSelected() { mCurrentContext.getJSModule(HMRClient.class).disable(); } } -// if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { -// Toast.makeText( -// mApplicationContext, -// mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), -// Toast.LENGTH_LONG) -// .show(); -// mDevSettings.setJSDevModeEnabled(true); -// handleReloadJS(); -// } + expo_transformer_remove: if (nextEnabled && !mDevSettings.isJSDevModeEnabled()) { + Toast.makeText( + mApplicationContext, + mApplicationContext.getString(R.string.reactandroid_catalyst_hot_reloading_auto_enable), + Toast.LENGTH_LONG) + .show(); + mDevSettings.setJSDevModeEnabled(true); + handleReloadJS(); + } } }); -// options.put( -// mIsSamplingProfilerEnabled -// ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) -// : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), -// new DevOptionHandler() { -// @Override -// public void onOptionSelected() { -// toggleJSSamplingProfiler(); -// } -// }); + expo_transformer_remove: options.put( + mIsSamplingProfilerEnabled + ? mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_disable) + : mApplicationContext.getString(R.string.reactandroid_catalyst_sample_profiler_enable), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + toggleJSSamplingProfiler(); + } + }); options.put( mDevSettings.isFpsDebugEnabled() @@ -603,16 +603,16 @@ public void onOptionSelected() { mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); } }); -// options.put( -// mApplicationContext.getString(R.string.reactandroid_catalyst_settings), -// new DevOptionHandler() { -// @Override -// public void onOptionSelected() { -// Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class); -// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// mApplicationContext.startActivity(intent); -// } -// }); + expo_transformer_remove: options.put( + mApplicationContext.getString(R.string.reactandroid_catalyst_settings), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + Intent intent = new Intent(mApplicationContext, DevSettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mApplicationContext.startActivity(intent); + } + }); if (mCustomDevOptions.size() > 0) { options.putAll(mCustomDevOptions);