Don't create DevSupportManager when not in Dev mode

Summary:
public

1. fixes I/O on UI Thread diffusion/FA/browse/master/java/com/facebook/catalyst/js/react-native-github/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java;e6819b923967f9380dd6c10bfa8f1f40be558e2f$149 in prod builds
2. Less calls to Choreographer (i.e. https://fburl.com/188102408 in AutoProfiler) ==> better newsfeed scroll perf
3. Lower Memory footprint in prod

Reviewed By: astreet

Differential Revision: D2759498

fb-gh-sync-id: 4f593ba9219febb7045f4e470a14995e995ebbb1
This commit is contained in:
Aaron Chiu 2016-01-08 00:47:44 -08:00 committed by facebook-github-bot-5
parent 47d0e3c288
commit 648364594c
5 changed files with 898 additions and 695 deletions

View File

@ -52,6 +52,8 @@ import com.facebook.react.common.ReactConstants;
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 @@ import com.facebook.systrace.Systrace;
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(
if (mUseDeveloperSupport) {
mDevSupportManager = new DevSupportManagerImpl(
applicationContext,
mDevInterface,
mJSMainModuleName,
useDeveloperSupport);
} else {
mDevSupportManager = new DisabledDevSupportManager();
}
mBridgeIdleDebugListener = bridgeIdleDebugListener;
mLifecycleState = initialLifecycleState;
mUIImplementationProvider = uiImplementationProvider;

View File

@ -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);
}
}
}

View File

@ -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 <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>}
* {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>}
* 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 {
public interface DevSupportManager extends 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<String, DevOptionHandler> 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<String, DevOptionHandler> 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<Boolean> 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<Boolean> 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();
}
}
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);
}

View File

@ -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 <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>}
* {@code <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>}
*/
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<String, DevOptionHandler> 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<String, DevOptionHandler> 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<Boolean> 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<Boolean> 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();
}
}
}

View File

@ -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) {
}
}