diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java index c9bf17c610cedb..adb039d7ffee5d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerImpl.java @@ -52,6 +52,8 @@ import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.devsupport.DevServerHelper; import com.facebook.react.devsupport.DevSupportManager; +import com.facebook.react.devsupport.DevSupportManagerImpl; +import com.facebook.react.devsupport.DisabledDevSupportManager; import com.facebook.react.devsupport.ReactInstanceDevCommandsHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; @@ -252,15 +254,15 @@ public T get() throws Exception { mJSMainModuleName = jsMainModuleName; mPackages = packages; mUseDeveloperSupport = useDeveloperSupport; - // We need to instantiate DevSupportManager regardless to the useDeveloperSupport option, - // although will prevent dev support manager from displaying any options or dialogs by - // checking useDeveloperSupport option before calling setDevSupportEnabled on this manager - // TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false - mDevSupportManager = new DevSupportManager( - applicationContext, - mDevInterface, - mJSMainModuleName, - useDeveloperSupport); + if (mUseDeveloperSupport) { + mDevSupportManager = new DevSupportManagerImpl( + applicationContext, + mDevInterface, + mJSMainModuleName, + useDeveloperSupport); + } else { + mDevSupportManager = new DisabledDevSupportManager(); + } mBridgeIdleDebugListener = bridgeIdleDebugListener; mLifecycleState = initialLifecycleState; mUIImplementationProvider = uiImplementationProvider; diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/DefaultNativeModuleCallExceptionHandler.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/DefaultNativeModuleCallExceptionHandler.java new file mode 100644 index 00000000000000..4d47156c747956 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/DefaultNativeModuleCallExceptionHandler.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +/** + * Crashy crashy exception handler. + */ +public class DefaultNativeModuleCallExceptionHandler implements NativeModuleCallExceptionHandler { + + @Override + public void handleException(Exception e) { + if (e instanceof RuntimeException) { + // Because we are rethrowing the original exception, the original stacktrace will be + // preserved. + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index cf86bd3b35474d..4f114ded6099a5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -9,698 +9,34 @@ package com.facebook.react.devsupport; -import javax.annotation.Nullable; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import android.app.AlertDialog; -import android.app.ProgressDialog; -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.os.Debug; -import android.os.Environment; -import android.view.WindowManager; -import android.widget.Toast; - -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.R; -import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.JavaJSExecutor; import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.bridge.WebsocketJavaScriptExecutor; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.ShakeDetector; -import com.facebook.react.common.futures.SimpleSettableFuture; -import com.facebook.react.devsupport.StackTraceHelper.StackFrame; import com.facebook.react.modules.debug.DeveloperSettings; /** - * Interface for accessing and interacting with development features. Following features - * are supported through this manager class: - * 1) Displaying JS errors (aka RedBox) - * 2) Displaying developers menu (Reload JS, Debug JS) - * 3) Communication with developer server in order to download updated JS bundle - * 4) Starting/stopping broadcast receiver for js reload signals - * 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may - * trigger developers menu. - * 6) Launching developers settings view - * - * This class automatically monitors the state of registered views and activities to which they are - * bound to make sure that we don't display overlay or that we we don't listen for sensor events - * when app is backgrounded. - * - * {@link ReactInstanceDevCommandsHandler} implementation is responsible for instantiating this - * instance and for populating with an instance of {@link CatalystInstance} whenever instance - * manager recreates it (through {@link #onNewCatalystContextCreated}). Also, instance manager is - * responsible for enabling/disabling dev support in case when app is backgrounded or when all the - * views has been detached from the instance (through {@link #setDevSupportEnabled} method). - * - * IMPORTANT: In order for developer support to work correctly it is required that the - * manifest of your application contain the following entries: - * {@code } - * {@code } + * Interface for accessing and interacting with development features. + * In dev mode, use the implementation {@link DevSupportManagerImpl}. + * In production mode, use the dummy implementation {@link DisabledDevSupportManager}. */ -public class DevSupportManager implements NativeModuleCallExceptionHandler { - - private static final int JAVA_ERROR_COOKIE = -1; - private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; - - private static final String EXOPACKAGE_LOCATION_FORMAT - = "/data/local/tmp/exopackage/%s//secondary-dex"; - - private static final int JAVA_SAMPLING_PROFILE_MEMORY_BYTES = 8 * 1024 * 1024; - private static final int JAVA_SAMPLING_PROFILE_DELTA_US = 100; - - private final Context mApplicationContext; - private final ShakeDetector mShakeDetector; - private final BroadcastReceiver mReloadAppBroadcastReceiver; - private final DevServerHelper mDevServerHelper; - private final LinkedHashMap mCustomDevOptions = - new LinkedHashMap<>(); - private final ReactInstanceDevCommandsHandler mReactInstanceCommandsHandler; - private final @Nullable String mJSAppBundleName; - private final File mJSBundleTempFile; - - private @Nullable RedBoxDialog mRedBoxDialog; - private @Nullable AlertDialog mDevOptionsDialog; - private @Nullable DebugOverlayController mDebugOverlayController; - private @Nullable ReactContext mCurrentContext; - private DevInternalSettings mDevSettings; - private boolean mIsUsingJSProxy = false; - private boolean mIsReceiverRegistered = false; - private boolean mIsShakeDetectorStarted = false; - private boolean mIsDevSupportEnabled = false; - private boolean mIsCurrentlyProfiling = false; - private int mProfileIndex = 0; - - public DevSupportManager( - Context applicationContext, - ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, - @Nullable String packagerPathForJSBundleName, - boolean enableOnCreate) { - mReactInstanceCommandsHandler = reactInstanceCommandsHandler; - mApplicationContext = applicationContext; - mJSAppBundleName = packagerPathForJSBundleName; - mDevSettings = new DevInternalSettings(applicationContext, this); - mDevServerHelper = new DevServerHelper(mDevSettings); - - // Prepare shake gesture detector (will be started/stopped from #reload) - mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { - @Override - public void onShake() { - showDevOptionsDialog(); - } - }); - - // 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 (DevServerHelper.getReloadAppAction(context).equals(action)) { - if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { - mIsUsingJSProxy = true; - mDevServerHelper.launchChromeDevtools(); - } else { - mIsUsingJSProxy = 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); - - setDevSupportEnabled(enableOnCreate); - } - - @Override - public void handleException(Exception e) { - if (mIsDevSupportEnabled) { - FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); - showNewJavaError(e.getMessage(), e); - } else { - if (e instanceof RuntimeException) { - // Because we are rethrowing the original exception, the original stacktrace will be - // preserved - throw (RuntimeException) e; - } else { - throw new RuntimeException(e); - } - } - } - - public void showNewJavaError(String message, Throwable e) { - showNewError(message, StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE); - } - - /** - * 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. - */ - public void addCustomDevOption( - String optionName, - DevOptionHandler optionHandler) { - mCustomDevOptions.put(optionName, optionHandler); - } - - public void showNewJSError(String message, ReadableArray details, int errorCookie) { - showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie); - } - - 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 != mRedBoxDialog.getErrorCookie()) { - return; - } - mRedBoxDialog.setExceptionDetails( - message, - StackTraceHelper.convertJsStackTrace(details)); - mRedBoxDialog.show(); - } - }); - } - - private void showNewError( - final String message, - final StackFrame[] stack, - final int errorCookie) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (mRedBoxDialog == null) { - mRedBoxDialog = new RedBoxDialog(mApplicationContext, DevSupportManager.this); - mRedBoxDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - } - 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; - } - mRedBoxDialog.setExceptionDetails(message, stack); - mRedBoxDialog.setErrorCookie(errorCookie); - mRedBoxDialog.show(); - } - }); - } - - public void showDevOptionsDialog() { - if (mDevOptionsDialog != null || !mIsDevSupportEnabled) { - return; - } - LinkedHashMap options = new LinkedHashMap<>(); - /* register standard options */ - options.put( - mApplicationContext.getString(R.string.catalyst_reloadjs), new DevOptionHandler() { - @Override - public void onOptionSelected() { - handleReloadJS(); - } - }); - options.put( - mIsUsingJSProxy ? - mApplicationContext.getString(R.string.catalyst_debugjs_off) : - mApplicationContext.getString(R.string.catalyst_debugjs), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mIsUsingJSProxy = !mIsUsingJSProxy; - handleReloadJS(); - } - }); - options.put( - mDevSettings.isHotModuleReplacementEnabled() - ? mApplicationContext.getString(R.string.catalyst_hot_module_replacement_off) - : mApplicationContext.getString(R.string.catalyst_hot_module_replacement), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setHotModuleReplacementEnabled(!mDevSettings.isHotModuleReplacementEnabled()); - handleReloadJS(); - } - }); - options.put( - mDevSettings.isReloadOnJSChangeEnabled() - ? mApplicationContext.getString(R.string.catalyst_live_reload_off) - : mApplicationContext.getString(R.string.catalyst_live_reload), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setReloadOnJSChangeEnabled(!mDevSettings.isReloadOnJSChangeEnabled()); - } - }); - options.put( - mDevSettings.isElementInspectorEnabled() - ? mApplicationContext.getString(R.string.catalyst_element_inspector_off) - : mApplicationContext.getString(R.string.catalyst_element_inspector), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); - mReactInstanceCommandsHandler.toggleElementInspector(); - } - }); - options.put( - mDevSettings.isFpsDebugEnabled() - ? mApplicationContext.getString(R.string.catalyst_perf_monitor_off) - : mApplicationContext.getString(R.string.catalyst_perf_monitor), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); - } - }); - if (mCurrentContext != null && - mCurrentContext.getCatalystInstance() != null && - !mCurrentContext.getCatalystInstance().isDestroyed() && - mCurrentContext.getCatalystInstance().supportsProfiling()) { - options.put( - mApplicationContext.getString( - mIsCurrentlyProfiling ? R.string.catalyst_stop_profile : - R.string.catalyst_start_profile), - new DevOptionHandler() { - @Override - public void onOptionSelected() { - if (mCurrentContext != null && mCurrentContext.hasActiveCatalystInstance()) { - String profileName = (Environment.getExternalStorageDirectory().getPath() + - "/profile_" + mProfileIndex + ".json"); - if (mIsCurrentlyProfiling) { - mIsCurrentlyProfiling = false; - mProfileIndex++; - Debug.stopMethodTracing(); - mCurrentContext.getCatalystInstance() - .stopProfiler("profile", profileName); - Toast.makeText( - mCurrentContext, - "Profile output to " + profileName, - Toast.LENGTH_LONG).show(); - } else { - mIsCurrentlyProfiling = true; - mCurrentContext.getCatalystInstance().startProfiler("profile"); - Debug.startMethodTracingSampling( - profileName, - JAVA_SAMPLING_PROFILE_MEMORY_BYTES, - JAVA_SAMPLING_PROFILE_DELTA_US); - } - } - } - }); - } - 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]); - - mDevOptionsDialog = - new AlertDialog.Builder(mApplicationContext) - .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.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - mDevOptionsDialog.show(); - } - - /** - * {@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). - */ - public void setDevSupportEnabled(boolean isDevSupportEnabled) { - mIsDevSupportEnabled = isDevSupportEnabled; - reload(); - } - - public boolean getDevSupportEnabled() { - return mIsDevSupportEnabled; - } - - public DeveloperSettings getDevSettings() { - return mDevSettings; - } - - public void onNewReactContextCreated(ReactContext reactContext) { - resetCurrentContext(reactContext); - } - - 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); - } - } - - public String getSourceMapUrl() { - if (mJSAppBundleName == null) { - return ""; - } - - return mDevServerHelper.getSourceMapUrl(Assertions.assertNotNull(mJSAppBundleName)); - } - - public String getSourceUrl() { - if (mJSAppBundleName == null) { - return ""; - } - - return mDevServerHelper.getSourceUrl(Assertions.assertNotNull(mJSAppBundleName)); - } - - public String getJSBundleURLForRemoteDebugging() { - return mDevServerHelper.getJSBundleURLForRemoteDebugging( - Assertions.assertNotNull(mJSAppBundleName)); - } - - public String getDownloadedJSBundleFile() { - return mJSBundleTempFile.getAbsolutePath(); - } - - /** - * @return {@code true} if {@link 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. - */ - 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 donwloaded 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 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; - } - - // if currently profiling stop and write the profile file - if (mIsCurrentlyProfiling) { - mIsCurrentlyProfiling = false; - String profileName = (Environment.getExternalStorageDirectory().getPath() + - "/profile_" + mProfileIndex + ".json"); - mProfileIndex++; - Debug.stopMethodTracing(); - mCurrentContext.getCatalystInstance().stopProfiler("profile", profileName); - } - - mCurrentContext = reactContext; - - // Recreate debug overlay controller with new CatalystInstance object - if (mDebugOverlayController != null) { - mDebugOverlayController.setFpsDebugViewVisible(false); - } - if (reactContext != null) { - mDebugOverlayController = new DebugOverlayController(reactContext); - } - - reloadSettings(); - } - - /* package */ void reloadSettings() { - reload(); - } - - public void handleReloadJS() { - UiThreadUtil.assertOnUiThread(); - - // dismiss redbox if exists - if (mRedBoxDialog != null) { - mRedBoxDialog.dismiss(); - } - - ProgressDialog progressDialog = new ProgressDialog(mApplicationContext); - progressDialog.setTitle(R.string.catalyst_jsload_title); - progressDialog.setMessage(mApplicationContext.getString( - mIsUsingJSProxy ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message)); - progressDialog.setIndeterminate(true); - progressDialog.setCancelable(false); - progressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - progressDialog.show(); - - if (mIsUsingJSProxy) { - reloadJSInProxyMode(progressDialog); - } else { - reloadJSFromServer(progressDialog); - } - } - - public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) { - mDevServerHelper.isPackagerRunning(callback); - } - - private void reloadJSInProxyMode(final ProgressDialog progressDialog) { - // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that - // anyway - mDevServerHelper.launchChromeDevtools(); - - 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(progressDialog, 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); - } - } - }; - mReactInstanceCommandsHandler.onReloadWithJSDebugger(factory); - } - - private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback( - final ProgressDialog progressDialog, - final SimpleSettableFuture future) { - return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { - @Override - public void onSuccess() { - future.set(true); - progressDialog.dismiss(); - } - - @Override - public void onFailure(final Throwable cause) { - progressDialog.dismiss(); - FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause); - future.setException( - new IOException( - mApplicationContext.getString(R.string.catalyst_remotedbg_error), cause)); - } - }; - } - - private void reloadJSFromServer(final ProgressDialog progressDialog) { - mDevServerHelper.downloadBundleFromURL( - new DevServerHelper.BundleDownloadCallback() { - @Override - public void onSuccess() { - progressDialog.dismiss(); - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - mReactInstanceCommandsHandler.onJSBundleLoadedFromServer(); - } - }); - } - - @Override - public void onFailure(final Exception cause) { - progressDialog.dismiss(); - 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.description, cause); - } else { - showNewJavaError( - mApplicationContext.getString(R.string.catalyst_jsload_error), - cause); - } - } - }); - } - }, - Assertions.assertNotNull(mJSAppBundleName), - mJSBundleTempFile); - progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - mDevServerHelper.cancelDownloadBundleFromURL(); - } - }); - progressDialog.setCancelable(true); - } - - private void reload() { - // 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(DevServerHelper.getReloadAppAction(mApplicationContext)); - mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); - mIsReceiverRegistered = true; - } - - 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 - if (mRedBoxDialog != null) { - mRedBoxDialog.dismiss(); - } - - // hide dev options dialog - if (mDevOptionsDialog != null) { - mDevOptionsDialog.dismiss(); - } - - mDevServerHelper.stopPollingOnChangeEndpoint(); - } - } +public interface DevSupportManager extends NativeModuleCallExceptionHandler { + + void showNewJavaError(String message, Throwable e); + void addCustomDevOption(String optionName, DevOptionHandler optionHandler); + void showNewJSError(String message, ReadableArray details, int errorCookie); + void updateJSError(final String message, final ReadableArray details, final int errorCookie); + void showDevOptionsDialog(); + void setDevSupportEnabled(boolean isDevSupportEnabled); + boolean getDevSupportEnabled(); + DeveloperSettings getDevSettings(); + void onNewReactContextCreated(ReactContext reactContext); + void onReactInstanceDestroyed(ReactContext reactContext); + String getSourceMapUrl(); + String getSourceUrl(); + String getJSBundleURLForRemoteDebugging(); + String getDownloadedJSBundleFile(); + boolean hasUpToDateJSBundleInCache(); + void reloadSettings(); + void handleReloadJS(); + void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java new file mode 100644 index 00000000000000..7bdeea2d7ce56d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -0,0 +1,722 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import javax.annotation.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +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.os.Debug; +import android.os.Environment; +import android.view.WindowManager; +import android.widget.Toast; + +import com.facebook.common.logging.FLog; +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.NativeModuleCallExceptionHandler; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WebsocketJavaScriptExecutor; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.common.ShakeDetector; +import com.facebook.react.common.futures.SimpleSettableFuture; +import com.facebook.react.devsupport.StackTraceHelper.StackFrame; +import com.facebook.react.modules.debug.DeveloperSettings; + +/** + * Interface for accessing and interacting with development features. Following features + * are supported through this manager class: + * 1) Displaying JS errors (aka RedBox) + * 2) Displaying developers menu (Reload JS, Debug JS) + * 3) Communication with developer server in order to download updated JS bundle + * 4) Starting/stopping broadcast receiver for js reload signals + * 5) Starting/stopping motion sensor listener that recognize shake gestures which in turn may + * trigger developers menu. + * 6) Launching developers settings view + * + * This class automatically monitors the state of registered views and activities to which they are + * bound to make sure that we don't display overlay or that we we don't listen for sensor events + * when app is backgrounded. + * + * {@link ReactInstanceDevCommandsHandler} implementation is responsible for instantiating this + * instance and for populating with an instance of {@link CatalystInstance} whenever instance + * manager recreates it (through {@link #onNewCatalystContextCreated}). Also, instance manager is + * responsible for enabling/disabling dev support in case when app is backgrounded or when all the + * views has been detached from the instance (through {@link #setDevSupportEnabled} method). + * + * IMPORTANT: In order for developer support to work correctly it is required that the + * manifest of your application contain the following entries: + * {@code } + * {@code } + */ +public class DevSupportManagerImpl implements DevSupportManager { + + private static final int JAVA_ERROR_COOKIE = -1; + private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js"; + + private static final String EXOPACKAGE_LOCATION_FORMAT + = "/data/local/tmp/exopackage/%s//secondary-dex"; + + private static final int JAVA_SAMPLING_PROFILE_MEMORY_BYTES = 8 * 1024 * 1024; + private static final int JAVA_SAMPLING_PROFILE_DELTA_US = 100; + + private final Context mApplicationContext; + private final ShakeDetector mShakeDetector; + private final BroadcastReceiver mReloadAppBroadcastReceiver; + private final DevServerHelper mDevServerHelper; + private final LinkedHashMap mCustomDevOptions = + new LinkedHashMap<>(); + private final ReactInstanceDevCommandsHandler mReactInstanceCommandsHandler; + private final @Nullable String mJSAppBundleName; + private final File mJSBundleTempFile; + private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler; + + private @Nullable RedBoxDialog mRedBoxDialog; + private @Nullable AlertDialog mDevOptionsDialog; + private @Nullable DebugOverlayController mDebugOverlayController; + private @Nullable ReactContext mCurrentContext; + private DevInternalSettings mDevSettings; + private boolean mIsUsingJSProxy = false; + private boolean mIsReceiverRegistered = false; + private boolean mIsShakeDetectorStarted = false; + private boolean mIsDevSupportEnabled = false; + private boolean mIsCurrentlyProfiling = false; + private int mProfileIndex = 0; + + public DevSupportManagerImpl( + Context applicationContext, + ReactInstanceDevCommandsHandler reactInstanceCommandsHandler, + @Nullable String packagerPathForJSBundleName, + boolean enableOnCreate) { + mReactInstanceCommandsHandler = reactInstanceCommandsHandler; + mApplicationContext = applicationContext; + mJSAppBundleName = packagerPathForJSBundleName; + mDevSettings = new DevInternalSettings(applicationContext, this); + mDevServerHelper = new DevServerHelper(mDevSettings); + + // Prepare shake gesture detector (will be started/stopped from #reload) + mShakeDetector = new ShakeDetector(new ShakeDetector.ShakeListener() { + @Override + public void onShake() { + showDevOptionsDialog(); + } + }); + + // 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 (DevServerHelper.getReloadAppAction(context).equals(action)) { + if (intent.getBooleanExtra(DevServerHelper.RELOAD_APP_EXTRA_JS_PROXY, false)) { + mIsUsingJSProxy = true; + mDevServerHelper.launchChromeDevtools(); + } else { + mIsUsingJSProxy = 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); + } + + @Override + public void handleException(Exception e) { + if (mIsDevSupportEnabled) { + FLog.e(ReactConstants.TAG, "Exception in native call from JS", e); + showNewJavaError(e.getMessage(), e); + } else { + mDefaultNativeModuleCallExceptionHandler.handleException(e); + } + } + + @Override + public void showNewJavaError(String message, Throwable e) { + showNewError(message, StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE); + } + + /** + * 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); + } + + @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 != mRedBoxDialog.getErrorCookie()) { + return; + } + mRedBoxDialog.setExceptionDetails( + message, + StackTraceHelper.convertJsStackTrace(details)); + mRedBoxDialog.show(); + } + }); + } + + private void showNewError( + final String message, + final StackFrame[] stack, + final int errorCookie) { + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (mRedBoxDialog == null) { + mRedBoxDialog = new RedBoxDialog(mApplicationContext, DevSupportManagerImpl.this); + mRedBoxDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + } + 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; + } + mRedBoxDialog.setExceptionDetails(message, stack); + mRedBoxDialog.setErrorCookie(errorCookie); + mRedBoxDialog.show(); + } + }); + } + + @Override + public void showDevOptionsDialog() { + if (mDevOptionsDialog != null || !mIsDevSupportEnabled) { + return; + } + LinkedHashMap options = new LinkedHashMap<>(); + /* register standard options */ + options.put( + mApplicationContext.getString(R.string.catalyst_reloadjs), new DevOptionHandler() { + @Override + public void onOptionSelected() { + handleReloadJS(); + } + }); + options.put( + mIsUsingJSProxy ? + mApplicationContext.getString(R.string.catalyst_debugjs_off) : + mApplicationContext.getString(R.string.catalyst_debugjs), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mIsUsingJSProxy = !mIsUsingJSProxy; + handleReloadJS(); + } + }); + options.put( + mDevSettings.isHotModuleReplacementEnabled() + ? mApplicationContext.getString(R.string.catalyst_hot_module_replacement_off) + : mApplicationContext.getString(R.string.catalyst_hot_module_replacement), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setHotModuleReplacementEnabled(!mDevSettings.isHotModuleReplacementEnabled()); + handleReloadJS(); + } + }); + options.put( + mDevSettings.isReloadOnJSChangeEnabled() + ? mApplicationContext.getString(R.string.catalyst_live_reload_off) + : mApplicationContext.getString(R.string.catalyst_live_reload), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setReloadOnJSChangeEnabled(!mDevSettings.isReloadOnJSChangeEnabled()); + } + }); + options.put( + mDevSettings.isElementInspectorEnabled() + ? mApplicationContext.getString(R.string.catalyst_element_inspector_off) + : mApplicationContext.getString(R.string.catalyst_element_inspector), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setElementInspectorEnabled(!mDevSettings.isElementInspectorEnabled()); + mReactInstanceCommandsHandler.toggleElementInspector(); + } + }); + options.put( + mDevSettings.isFpsDebugEnabled() + ? mApplicationContext.getString(R.string.catalyst_perf_monitor_off) + : mApplicationContext.getString(R.string.catalyst_perf_monitor), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + mDevSettings.setFpsDebugEnabled(!mDevSettings.isFpsDebugEnabled()); + } + }); + if (mCurrentContext != null && + mCurrentContext.getCatalystInstance() != null && + !mCurrentContext.getCatalystInstance().isDestroyed() && + mCurrentContext.getCatalystInstance().supportsProfiling()) { + options.put( + mApplicationContext.getString( + mIsCurrentlyProfiling ? R.string.catalyst_stop_profile : + R.string.catalyst_start_profile), + new DevOptionHandler() { + @Override + public void onOptionSelected() { + if (mCurrentContext != null && mCurrentContext.hasActiveCatalystInstance()) { + String profileName = (Environment.getExternalStorageDirectory().getPath() + + "/profile_" + mProfileIndex + ".json"); + if (mIsCurrentlyProfiling) { + mIsCurrentlyProfiling = false; + mProfileIndex++; + Debug.stopMethodTracing(); + mCurrentContext.getCatalystInstance() + .stopProfiler("profile", profileName); + Toast.makeText( + mCurrentContext, + "Profile output to " + profileName, + Toast.LENGTH_LONG).show(); + } else { + mIsCurrentlyProfiling = true; + mCurrentContext.getCatalystInstance().startProfiler("profile"); + Debug.startMethodTracingSampling( + profileName, + JAVA_SAMPLING_PROFILE_MEMORY_BYTES, + JAVA_SAMPLING_PROFILE_DELTA_US); + } + } + } + }); + } + 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]); + + mDevOptionsDialog = + new AlertDialog.Builder(mApplicationContext) + .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.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + mDevOptionsDialog.show(); + } + + /** + * {@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; + reload(); + } + + @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 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 donwloaded 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 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; + } + + // if currently profiling stop and write the profile file + if (mIsCurrentlyProfiling) { + mIsCurrentlyProfiling = false; + String profileName = (Environment.getExternalStorageDirectory().getPath() + + "/profile_" + mProfileIndex + ".json"); + mProfileIndex++; + Debug.stopMethodTracing(); + mCurrentContext.getCatalystInstance().stopProfiler("profile", profileName); + } + + mCurrentContext = reactContext; + + // Recreate debug overlay controller with new CatalystInstance object + if (mDebugOverlayController != null) { + mDebugOverlayController.setFpsDebugViewVisible(false); + } + if (reactContext != null) { + mDebugOverlayController = new DebugOverlayController(reactContext); + } + + reloadSettings(); + } + + @Override + public void reloadSettings() { + reload(); + } + + @Override + public void handleReloadJS() { + UiThreadUtil.assertOnUiThread(); + + // dismiss redbox if exists + if (mRedBoxDialog != null) { + mRedBoxDialog.dismiss(); + } + + ProgressDialog progressDialog = new ProgressDialog(mApplicationContext); + progressDialog.setTitle(R.string.catalyst_jsload_title); + progressDialog.setMessage(mApplicationContext.getString( + mIsUsingJSProxy ? R.string.catalyst_remotedbg_message : R.string.catalyst_jsload_message)); + progressDialog.setIndeterminate(true); + progressDialog.setCancelable(false); + progressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + progressDialog.show(); + + if (mIsUsingJSProxy) { + reloadJSInProxyMode(progressDialog); + } else { + reloadJSFromServer(progressDialog); + } + } + + @Override + public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) { + mDevServerHelper.isPackagerRunning(callback); + } + + private void reloadJSInProxyMode(final ProgressDialog progressDialog) { + // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that + // anyway + mDevServerHelper.launchChromeDevtools(); + + 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(progressDialog, 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); + } + } + }; + mReactInstanceCommandsHandler.onReloadWithJSDebugger(factory); + } + + private WebsocketJavaScriptExecutor.JSExecutorConnectCallback getExecutorConnectCallback( + final ProgressDialog progressDialog, + final SimpleSettableFuture future) { + return new WebsocketJavaScriptExecutor.JSExecutorConnectCallback() { + @Override + public void onSuccess() { + future.set(true); + progressDialog.dismiss(); + } + + @Override + public void onFailure(final Throwable cause) { + progressDialog.dismiss(); + FLog.e(ReactConstants.TAG, "Unable to connect to remote debugger", cause); + future.setException( + new IOException( + mApplicationContext.getString(R.string.catalyst_remotedbg_error), cause)); + } + }; + } + + private void reloadJSFromServer(final ProgressDialog progressDialog) { + mDevServerHelper.downloadBundleFromURL( + new DevServerHelper.BundleDownloadCallback() { + @Override + public void onSuccess() { + progressDialog.dismiss(); + UiThreadUtil.runOnUiThread( + new Runnable() { + @Override + public void run() { + mReactInstanceCommandsHandler.onJSBundleLoadedFromServer(); + } + }); + } + + @Override + public void onFailure(final Exception cause) { + progressDialog.dismiss(); + 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.description, cause); + } else { + showNewJavaError( + mApplicationContext.getString(R.string.catalyst_jsload_error), + cause); + } + } + }); + } + }, + Assertions.assertNotNull(mJSAppBundleName), + mJSBundleTempFile); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mDevServerHelper.cancelDownloadBundleFromURL(); + } + }); + progressDialog.setCancelable(true); + } + + private void reload() { + // 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(DevServerHelper.getReloadAppAction(mApplicationContext)); + mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); + mIsReceiverRegistered = true; + } + + 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 + if (mRedBoxDialog != null) { + mRedBoxDialog.dismiss(); + } + + // hide dev options dialog + if (mDevOptionsDialog != null) { + mDevOptionsDialog.dismiss(); + } + + mDevServerHelper.stopPollingOnChangeEndpoint(); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java new file mode 100644 index 00000000000000..44edbc8866ecd9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.devsupport; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.modules.debug.DeveloperSettings; + +/** + * A dummy implementation of {@link DevSupportManager} to be used in production mode where + * development features aren't needed. + */ +public class DisabledDevSupportManager implements DevSupportManager { + + @Override + public void showNewJavaError(String message, Throwable e) { + + } + + @Override + public void addCustomDevOption(String optionName, DevOptionHandler optionHandler) { + + } + + @Override + public void showNewJSError(String message, ReadableArray details, int errorCookie) { + + } + + @Override + public void updateJSError(String message, ReadableArray details, int errorCookie) { + + } + + @Override + public void showDevOptionsDialog() { + + } + + @Override + public void setDevSupportEnabled(boolean isDevSupportEnabled) { + + } + + @Override + public boolean getDevSupportEnabled() { + return false; + } + + @Override + public DeveloperSettings getDevSettings() { + return null; + } + + @Override + public void onNewReactContextCreated(ReactContext reactContext) { + + } + + @Override + public void onReactInstanceDestroyed(ReactContext reactContext) { + + } + + @Override + public String getSourceMapUrl() { + return null; + } + + @Override + public String getSourceUrl() { + return null; + } + + @Override + public String getJSBundleURLForRemoteDebugging() { + return null; + } + + @Override + public String getDownloadedJSBundleFile() { + return null; + } + + @Override + public boolean hasUpToDateJSBundleInCache() { + return false; + } + + @Override + public void reloadSettings() { + + } + + @Override + public void handleReloadJS() { + + } + + @Override + public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) { + + } + + @Override + public void handleException(Exception e) { + + } +}