diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java new file mode 100644 index 00000000000..3da833ab4f8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java @@ -0,0 +1,1233 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.devsupport; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.hardware.SensorManager; +import android.util.Pair; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.debug.holder.PrinterHolder; +import com.facebook.debug.tags.ReactDebugOverlayTags; +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.R; +import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; +import com.facebook.react.bridge.JavaJSExecutor; +import com.facebook.react.bridge.JavaScriptExecutorFactory; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactMarker; +import com.facebook.react.bridge.ReactMarkerConstants; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.DebugServerException; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.ShakeDetector; +import com.facebook.react.common.futures.SimpleSettableFuture; +import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener; +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; +import com.facebook.react.devsupport.interfaces.DevOptionHandler; +import com.facebook.react.devsupport.interfaces.DevSupportManager; +import com.facebook.react.devsupport.interfaces.ErrorCustomizer; +import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; +import com.facebook.react.devsupport.interfaces.StackFrame; +import com.facebook.react.modules.core.RCTNativeAppEventEmitter; +import com.facebook.react.modules.debug.interfaces.DeveloperSettings; +import com.facebook.react.packagerconnection.RequestHandler; +import com.facebook.react.packagerconnection.Responder; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public abstract class DevSupportManagerBase + 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 @Nullable PackagerLocationCustomizer mPackagerLocationCustomizer; + + private InspectorPackagerConnection.BundleStatus mBundleStatus; + + private @Nullable Map mCustomPackagerCommandHandlers; + + public DevSupportManagerBase( + Context applicationContext, + ReactInstanceManagerDevHelper reactInstanceManagerHelper, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate, + int minNumShakes) { + + this( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + null, + null, + minNumShakes, + null); + } + + public DevSupportManagerBase( + 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(); + } + } + }; + + // 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) { + if (mIsDevSupportEnabled) { + + for (ExceptionLogger logger : mExceptionLoggers) { + logger.log(e); + } + + } else { + mDefaultNativeModuleCallExceptionHandler.handleException(e); + } + } + + private interface ExceptionLogger { + void log(Exception ex); + } + + 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<>(); + } + 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; + } + } + return errorInfo; + } + } + + @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; + } + 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; + } + } + + public @Nullable View createRootView(String appKey) { + return mReactInstanceManagerHelper.createRootView(appKey); + } + + public void destroyRootView(View rootView) { + mReactInstanceManagerHelper.destroyRootView(rootView); + } + + private void hideDevOptionsDialog() { + if (mDevOptionsDialog != null) { + mDevOptionsDialog.dismiss(); + mDevOptionsDialog = null; + } + } + + private void showNewError( + @Nullable final String message, + final StackFrame[] stack, + final int errorCookie, + final ErrorType errorType) { + UiThreadUtil.runOnUiThread( + 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, DevSupportManagerBase.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 showDevOptionsDialog() { + if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { + return; + } + LinkedHashMap options = new LinkedHashMap<>(); + /* register standard options */ + options.put( + mApplicationContext.getString(R.string.catalyst_reload), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + if (!mDevSettings.isJSDevModeEnabled() + && mDevSettings.isHotModuleReplacementEnabled()) { + Toast.makeText( + mApplicationContext, + mApplicationContext.getString(R.string.catalyst_hot_reloading_auto_disable), + Toast.LENGTH_LONG) + .show(); + mDevSettings.setHotModuleReplacementEnabled(false); + } + handleReloadJS(); + } + }); + options.put( + mDevSettings.isNuclideJSDebugEnabled() + ? mDevSettings.isRemoteJSDebugEnabled() + ? mApplicationContext.getString(R.string.catalyst_debug_chrome_stop) + : mApplicationContext.getString(R.string.catalyst_debug_chrome) + : mDevSettings.isRemoteJSDebugEnabled() + ? mApplicationContext.getString(R.string.catalyst_debug_stop) + : mApplicationContext.getString(R.string.catalyst_debug), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); + handleReloadJS(); + } + }); + if (mDevSettings.isNuclideJSDebugEnabled()) { + options.put( + mApplicationContext.getString(R.string.catalyst_debug_nuclide), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); + } + }); + } + options.put( + mApplicationContext.getString(R.string.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.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.catalyst_inspector), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); + } + }); + + options.put( + mDevSettings.isHotModuleReplacementEnabled() + ? mApplicationContext.getString(R.string.catalyst_hot_reloading_stop) + : mApplicationContext.getString(R.string.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.catalyst_hot_reloading_auto_enable), + Toast.LENGTH_LONG) + .show(); + mDevSettings.setJSDevModeEnabled(true); + handleReloadJS(); + } + } + }); + + options.put( + mIsSamplingProfilerEnabled + ? mApplicationContext.getString(R.string.catalyst_sample_profiler_disable) + : mApplicationContext.getString(R.string.catalyst_sample_profiler_enable), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + toggleJSSamplingProfiler(); + } + }); + + options.put( + mDevSettings.isFpsDebugEnabled() + ? mApplicationContext.getString(R.string.catalyst_perf_monitor_stop) + : mApplicationContext.getString(R.string.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( + mApplicationContext.getString(R.string.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() { + @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(); + if (mCurrentContext != null) { + mCurrentContext.getJSModule(RCTNativeAppEventEmitter.class).emit("RCTDevMenuShown", null); + } + } + + /** 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 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 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"); + } + } + 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"); + } + 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()); + 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); + } + } + + reloadSettings(); + } + + @Override + public void reloadSettings() { + if (UiThreadUtil.isOnUiThread()) { + reload(); + } else { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + reload(); + } + }); + } + } + + 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 + public void isPackagerRunning(final PackagerStatusCallback callback) { + Runnable checkPackagerRunning = + new Runnable() { + @Override + public void run() { + mDevServerHelper.isPackagerRunning(callback); + } + }; + if (mPackagerLocationCustomizer != null) { + mPackagerLocationCustomizer.run(checkPackagerRunning); + } else { + checkPackagerRunning.run(); + } + } + + @Override + public @Nullable File downloadBundleResourceFromUrlSync( + final String resourceURL, final File outputFile) { + return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); + } + + @Override + public @Nullable String getLastErrorTitle() { + return mLastErrorTitle; + } + + @Override + public @Nullable StackFrame[] getLastErrorStack() { + return mLastErrorStack; + } + + @Override + public void onPackagerConnected() { + // No-op + } + + @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; + } + + 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()); + } + }); + } + + 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); + } + + 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.catalyst_debug_error), cause)); + } + }; + } + + 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 onSuccess() { + mDevLoadingViewController.hide(); + mDevLoadingViewVisible = false; + synchronized (DevSupportManagerBase.this) { + mBundleStatus.isLastDownloadSucess = true; + mBundleStatus.updateTimestamp = System.currentTimeMillis(); + } + if (mBundleDownloadListener != null) { + mBundleDownloadListener.onSuccess(); + } + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + ReactMarker.logMarker( + ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); + mReactInstanceManagerHelper.onJSBundleLoadedFromServer(); + } + }); + } + + @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 (DevSupportManagerBase.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.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; + } + + 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 run() { + mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); + handleReloadJS(); + } + }); + } + + @Override + public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { + if (!mIsDevSupportEnabled) { + return; + } + + 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 run() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceManagerHelper.toggleElementInspector(); + } + }); + } + + private void reload() { + UiThreadUtil.assertOnUiThread(); + + // reload settings, show/hide debug overlay if required & start/stop shake detector + if (mIsDevSupportEnabled) { + // update visibility of FPS debug overlay depending on the settings + if (mDebugOverlayController != null) { + mDebugOverlayController.setFpsDebugViewVisible(mDevSettings.isFpsDebugEnabled()); + } + + // start shake gesture detector + if (!mIsShakeDetectorStarted) { + 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); + } 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(); + } + } + + /** Intent action for reloading the JS */ + private static String getReloadAppAction(Context context) { + return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; + } + + @Override + public void setPackagerLocationCustomizer(PackagerLocationCustomizer packagerLocationCustomizer) { + mPackagerLocationCustomizer = packagerLocationCustomizer; + } +} 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 0d37ec5982e..8c0beeafe5b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -7,63 +7,12 @@ package com.facebook.react.devsupport; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.hardware.SensorManager; -import android.util.Pair; -import android.view.View; -import android.widget.EditText; -import android.widget.Toast; import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.debug.holder.PrinterHolder; -import com.facebook.debug.tags.ReactDebugOverlayTags; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.R; import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; -import com.facebook.react.bridge.JavaJSExecutor; -import com.facebook.react.bridge.JavaScriptExecutorFactory; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactMarker; -import com.facebook.react.bridge.ReactMarkerConstants; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.DebugServerException; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.ShakeDetector; -import com.facebook.react.common.futures.SimpleSettableFuture; -import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener; import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener; -import com.facebook.react.devsupport.interfaces.DevOptionHandler; -import com.facebook.react.devsupport.interfaces.DevSupportManager; -import com.facebook.react.devsupport.interfaces.ErrorCustomizer; -import com.facebook.react.devsupport.interfaces.PackagerStatusCallback; -import com.facebook.react.devsupport.interfaces.StackFrame; -import com.facebook.react.modules.core.RCTNativeAppEventEmitter; -import com.facebook.react.modules.debug.interfaces.DeveloperSettings; import com.facebook.react.packagerconnection.RequestHandler; -import com.facebook.react.packagerconnection.Responder; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; /** * Interface for accessing and interacting with development features. Following features @@ -91,60 +40,7 @@ import java.util.concurrent.TimeoutException; * {@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 @Nullable PackagerLocationCustomizer mPackagerLocationCustomizer; - - private InspectorPackagerConnection.BundleStatus mBundleStatus; - - private @Nullable Map mCustomPackagerCommandHandlers; +public final class DevSupportManagerImpl extends DevSupportManagerBase { public DevSupportManagerImpl( Context applicationContext, @@ -153,7 +49,7 @@ public class DevSupportManagerImpl boolean enableOnCreate, int minNumShakes) { - this( + super( applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, @@ -173,1088 +69,14 @@ public class DevSupportManagerImpl @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(); - } - } - }; - - // 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) { - if (mIsDevSupportEnabled) { - - for (ExceptionLogger logger : mExceptionLoggers) { - logger.log(e); - } - - } else { - mDefaultNativeModuleCallExceptionHandler.handleException(e); - } - } - - private interface ExceptionLogger { - void log(Exception ex); - } - - 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<>(); - } - 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; - } - } - return errorInfo; - } - } - - @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; - } - 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; - } - } - - public @Nullable View createRootView(String appKey) { - return mReactInstanceManagerHelper.createRootView(appKey); - } - - public void destroyRootView(View rootView) { - mReactInstanceManagerHelper.destroyRootView(rootView); - } - - private void hideDevOptionsDialog() { - if (mDevOptionsDialog != null) { - mDevOptionsDialog.dismiss(); - mDevOptionsDialog = null; - } - } - - private void showNewError( - @Nullable final String message, - final StackFrame[] stack, - final int errorCookie, - final ErrorType errorType) { - UiThreadUtil.runOnUiThread( - 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 showDevOptionsDialog() { - if (mDevOptionsDialog != null || !mIsDevSupportEnabled || ActivityManager.isUserAMonkey()) { - return; - } - LinkedHashMap options = new LinkedHashMap<>(); - /* register standard options */ - options.put( - mApplicationContext.getString(R.string.catalyst_reload), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (!mDevSettings.isJSDevModeEnabled() - && mDevSettings.isHotModuleReplacementEnabled()) { - Toast.makeText( - mApplicationContext, - mApplicationContext.getString(R.string.catalyst_hot_reloading_auto_disable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setHotModuleReplacementEnabled(false); - } - handleReloadJS(); - } - }); - options.put( - mDevSettings.isNuclideJSDebugEnabled() - ? mDevSettings.isRemoteJSDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_debug_chrome_stop) - : mApplicationContext.getString(R.string.catalyst_debug_chrome) - : mDevSettings.isRemoteJSDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_debug_stop) - : mApplicationContext.getString(R.string.catalyst_debug), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled()); - handleReloadJS(); - } - }); - if (mDevSettings.isNuclideJSDebugEnabled()) { - options.put( - mApplicationContext.getString(R.string.catalyst_debug_nuclide), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevServerHelper.attachDebugger(mApplicationContext, "ReactNative"); - } - }); - } - options.put( - mApplicationContext.getString(R.string.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.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.catalyst_inspector), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } - }); - - options.put( - mDevSettings.isHotModuleReplacementEnabled() - ? mApplicationContext.getString(R.string.catalyst_hot_reloading_stop) - : mApplicationContext.getString(R.string.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.catalyst_hot_reloading_auto_enable), - Toast.LENGTH_LONG) - .show(); - mDevSettings.setJSDevModeEnabled(true); - handleReloadJS(); - } - } - }); - - options.put( - mIsSamplingProfilerEnabled - ? mApplicationContext.getString(R.string.catalyst_sample_profiler_disable) - : mApplicationContext.getString(R.string.catalyst_sample_profiler_enable), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - toggleJSSamplingProfiler(); - } - }); - - options.put( - mDevSettings.isFpsDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_perf_monitor_stop) - : mApplicationContext.getString(R.string.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( - mApplicationContext.getString(R.string.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() { - @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(); - if (mCurrentContext != null) { - mCurrentContext.getJSModule(RCTNativeAppEventEmitter.class).emit("RCTDevMenuShown", null); - } - } - - /** 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 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 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"); - } - } - 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"); - } - 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()); - 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); - } - } - - reloadSettings(); - } - - @Override - public void reloadSettings() { - if (UiThreadUtil.isOnUiThread()) { - reload(); - } else { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - reload(); - } - }); - } - } - - 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 - public void isPackagerRunning(final PackagerStatusCallback callback) { - Runnable checkPackagerRunning = - new Runnable() { - @Override - public void run() { - mDevServerHelper.isPackagerRunning(callback); - } - }; - if (mPackagerLocationCustomizer != null) { - mPackagerLocationCustomizer.run(checkPackagerRunning); - } else { - checkPackagerRunning.run(); - } - } - - @Override - public @Nullable File downloadBundleResourceFromUrlSync( - final String resourceURL, final File outputFile) { - return mDevServerHelper.downloadBundleResourceFromUrlSync(resourceURL, outputFile); - } - - @Override - public @Nullable String getLastErrorTitle() { - return mLastErrorTitle; - } - - @Override - public @Nullable StackFrame[] getLastErrorStack() { - return mLastErrorStack; - } - - @Override - public void onPackagerConnected() { - // No-op - } - - @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; - } - - 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()); - } - }); - } - - 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); - } - - 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.catalyst_debug_error), cause)); - } - }; - } - - 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 onSuccess() { - mDevLoadingViewController.hide(); - mDevLoadingViewVisible = false; - synchronized (DevSupportManagerImpl.this) { - mBundleStatus.isLastDownloadSucess = true; - mBundleStatus.updateTimestamp = System.currentTimeMillis(); - } - if (mBundleDownloadListener != null) { - mBundleDownloadListener.onSuccess(); - } - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - ReactMarker.logMarker( - ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString()); - mReactInstanceManagerHelper.onJSBundleLoadedFromServer(); - } - }); - } - - @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; - } - 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.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; - } - - 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 run() { - mDevSettings.setRemoteJSDebugEnabled(isRemoteJSDebugEnabled); - handleReloadJS(); - } - }); - } - - @Override - public void setFpsDebugEnabled(final boolean isFpsDebugEnabled) { - if (!mIsDevSupportEnabled) { - return; - } - - 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 run() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceManagerHelper.toggleElementInspector(); - } - }); - } - - private void reload() { - UiThreadUtil.assertOnUiThread(); - - // reload settings, show/hide debug overlay if required & start/stop shake detector - if (mIsDevSupportEnabled) { - // update visibility of FPS debug overlay depending on the settings - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(mDevSettings.isFpsDebugEnabled()); - } - - // start shake gesture detector - if (!mIsShakeDetectorStarted) { - 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); - } 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(); - } - } - - /** Intent action for reloading the JS */ - private static String getReloadAppAction(Context context) { - return context.getPackageName() + RELOAD_APP_ACTION_SUFFIX; - } - - @Override - public void setPackagerLocationCustomizer(PackagerLocationCustomizer packagerLocationCustomizer) { - mPackagerLocationCustomizer = packagerLocationCustomizer; + super( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers); } }