mirror of
https://github.com/status-im/react-native.git
synced 2025-01-15 03:56:03 +00:00
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:
parent
47d0e3c288
commit
648364594c
@ -52,6 +52,8 @@ import com.facebook.react.common.ReactConstants;
|
|||||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||||
import com.facebook.react.devsupport.DevServerHelper;
|
import com.facebook.react.devsupport.DevServerHelper;
|
||||||
import com.facebook.react.devsupport.DevSupportManager;
|
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.devsupport.ReactInstanceDevCommandsHandler;
|
||||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
@ -252,15 +254,15 @@ import com.facebook.systrace.Systrace;
|
|||||||
mJSMainModuleName = jsMainModuleName;
|
mJSMainModuleName = jsMainModuleName;
|
||||||
mPackages = packages;
|
mPackages = packages;
|
||||||
mUseDeveloperSupport = useDeveloperSupport;
|
mUseDeveloperSupport = useDeveloperSupport;
|
||||||
// We need to instantiate DevSupportManager regardless to the useDeveloperSupport option,
|
if (mUseDeveloperSupport) {
|
||||||
// although will prevent dev support manager from displaying any options or dialogs by
|
mDevSupportManager = new DevSupportManagerImpl(
|
||||||
// checking useDeveloperSupport option before calling setDevSupportEnabled on this manager
|
applicationContext,
|
||||||
// TODO(6803830): Don't instantiate devsupport manager when useDeveloperSupport is false
|
mDevInterface,
|
||||||
mDevSupportManager = new DevSupportManager(
|
mJSMainModuleName,
|
||||||
applicationContext,
|
useDeveloperSupport);
|
||||||
mDevInterface,
|
} else {
|
||||||
mJSMainModuleName,
|
mDevSupportManager = new DisabledDevSupportManager();
|
||||||
useDeveloperSupport);
|
}
|
||||||
mBridgeIdleDebugListener = bridgeIdleDebugListener;
|
mBridgeIdleDebugListener = bridgeIdleDebugListener;
|
||||||
mLifecycleState = initialLifecycleState;
|
mLifecycleState = initialLifecycleState;
|
||||||
mUIImplementationProvider = uiImplementationProvider;
|
mUIImplementationProvider = uiImplementationProvider;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,698 +9,34 @@
|
|||||||
|
|
||||||
package com.facebook.react.devsupport;
|
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.NativeModuleCallExceptionHandler;
|
||||||
import com.facebook.react.bridge.ReactContext;
|
import com.facebook.react.bridge.ReactContext;
|
||||||
import com.facebook.react.bridge.ReadableArray;
|
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;
|
import com.facebook.react.modules.debug.DeveloperSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for accessing and interacting with development features. Following features
|
* Interface for accessing and interacting with development features.
|
||||||
* are supported through this manager class:
|
* In dev mode, use the implementation {@link DevSupportManagerImpl}.
|
||||||
* 1) Displaying JS errors (aka RedBox)
|
* In production mode, use the dummy implementation {@link DisabledDevSupportManager}.
|
||||||
* 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 DevSupportManager implements NativeModuleCallExceptionHandler {
|
public interface DevSupportManager extends NativeModuleCallExceptionHandler {
|
||||||
|
|
||||||
private static final int JAVA_ERROR_COOKIE = -1;
|
void showNewJavaError(String message, Throwable e);
|
||||||
private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js";
|
void addCustomDevOption(String optionName, DevOptionHandler optionHandler);
|
||||||
|
void showNewJSError(String message, ReadableArray details, int errorCookie);
|
||||||
private static final String EXOPACKAGE_LOCATION_FORMAT
|
void updateJSError(final String message, final ReadableArray details, final int errorCookie);
|
||||||
= "/data/local/tmp/exopackage/%s//secondary-dex";
|
void showDevOptionsDialog();
|
||||||
|
void setDevSupportEnabled(boolean isDevSupportEnabled);
|
||||||
private static final int JAVA_SAMPLING_PROFILE_MEMORY_BYTES = 8 * 1024 * 1024;
|
boolean getDevSupportEnabled();
|
||||||
private static final int JAVA_SAMPLING_PROFILE_DELTA_US = 100;
|
DeveloperSettings getDevSettings();
|
||||||
|
void onNewReactContextCreated(ReactContext reactContext);
|
||||||
private final Context mApplicationContext;
|
void onReactInstanceDestroyed(ReactContext reactContext);
|
||||||
private final ShakeDetector mShakeDetector;
|
String getSourceMapUrl();
|
||||||
private final BroadcastReceiver mReloadAppBroadcastReceiver;
|
String getSourceUrl();
|
||||||
private final DevServerHelper mDevServerHelper;
|
String getJSBundleURLForRemoteDebugging();
|
||||||
private final LinkedHashMap<String, DevOptionHandler> mCustomDevOptions =
|
String getDownloadedJSBundleFile();
|
||||||
new LinkedHashMap<>();
|
boolean hasUpToDateJSBundleInCache();
|
||||||
private final ReactInstanceDevCommandsHandler mReactInstanceCommandsHandler;
|
void reloadSettings();
|
||||||
private final @Nullable String mJSAppBundleName;
|
void handleReloadJS();
|
||||||
private final File mJSBundleTempFile;
|
void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user