Add RedBoxSurfaceDelegate to DevSupportManagerBase to abstract surface logic to show RedBox

Summary:
This diff adds `RedBoxSurfaceDelegate` to replace existing logic in `DevSupportManagerBase` to abstract how we show/hide the RedBox surface. The delegate will wrap a RedBoxDialog instance, which is used to show/hide the dialog for default behavior (when there is no surface delegate for redbox got provided).

I also updated the interface for delegate to accomodate new use cases:
- Add `isShowing` for the `SurfaceDelegate`
- Add a list of getters for `DevSupportManager` for data access in the delegate
- (Update 2/7) Separate Dialog from `RedBoxDialog`, and re-named it to `RedBoxContentView`. This is to make it clear that the delegate is responsible to provide actual surface implementation (Dialog). The content view is meant to be shared.

Changelog:
[Android][Internal]

Reviewed By: javache

Differential Revision: D33987835

fbshipit-source-id: 57c20648e7f2ec8238963feca27ccd5518e7931d
This commit is contained in:
Xin Chen
2022-02-09 15:16:22 -08:00
committed by Facebook GitHub Bot
parent 5341ad8962
commit fc7eb91f56
8 changed files with 232 additions and 114 deletions
@@ -37,4 +37,7 @@ public interface SurfaceDelegate {
/** Hide the surface containing the React content view */
void hide();
/** Check if the surface is currently showing */
boolean isShowing();
}
@@ -97,7 +97,7 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler;
private final DevLoadingViewController mDevLoadingViewController;
private @Nullable RedBoxDialog mRedBoxDialog;
private @Nullable SurfaceDelegate mRedBoxSurfaceDelegate;
private @Nullable AlertDialog mDevOptionsDialog;
private @Nullable DebugOverlayController mDebugOverlayController;
private boolean mDevLoadingViewVisible = false;
@@ -273,7 +273,8 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
mErrorCustomizers.add(errorCustomizer);
}
private Pair<String, StackFrame[]> processErrorCustomizers(Pair<String, StackFrame[]> errorInfo) {
@Override
public Pair<String, StackFrame[]> processErrorCustomizers(Pair<String, StackFrame[]> errorInfo) {
if (mErrorCustomizers == null) {
return errorInfo;
} else {
@@ -297,33 +298,25 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
// Since we only show the first JS error in a succession of JS errors, make sure we only
// update the error message for that error message. This assumes that updateJSError
// belongs to the most recent showNewJSError
if (mRedBoxDialog == null
|| !mRedBoxDialog.isShowing()
|| errorCookie != mLastErrorCookie) {
if (!mRedBoxSurfaceDelegate.isShowing() || errorCookie != mLastErrorCookie) {
return;
}
StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details);
Pair<String, StackFrame[]> errorInfo =
processErrorCustomizers(Pair.create(message, stack));
mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second);
updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS);
// JS errors are reported here after source mapping.
if (mRedBoxHandler != null) {
mRedBoxHandler.handleRedbox(message, stack, ErrorType.JS);
mRedBoxDialog.resetReporting();
}
mRedBoxDialog.show();
// The RedBox surface delegate will always show the latest error
updateLastErrorInfo(
message, StackTraceHelper.convertJsStackTrace(details), errorCookie, ErrorType.JS);
mRedBoxSurfaceDelegate.show();
}
});
}
@Override
public void hideRedboxDialog() {
// dismiss redbox if exists
if (mRedBoxDialog != null) {
mRedBoxDialog.dismiss();
mRedBoxDialog = null;
if (mRedBoxSurfaceDelegate == null) {
return;
}
mRedBoxSurfaceDelegate.hide();
}
public @Nullable View createRootView(String appKey) {
@@ -350,41 +343,27 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
new Runnable() {
@Override
public void run() {
Activity context = mReactInstanceDevHelper.getCurrentActivity();
if (context != null && !context.isFinishing() && currentActivity != context) {
currentActivity = context;
// Create a new RedBox when currentActivity get updated
mRedBoxDialog =
new RedBoxDialog(currentActivity, DevSupportManagerBase.this, mRedBoxHandler);
if (mRedBoxSurfaceDelegate == null) {
@Nullable SurfaceDelegate redBoxSurfaceDelegate = createSurfaceDelegate("RedBox");
if (redBoxSurfaceDelegate != null) {
mRedBoxSurfaceDelegate = redBoxSurfaceDelegate;
} else {
mRedBoxSurfaceDelegate =
new RedBoxDialogSurfaceDelegate(DevSupportManagerBase.this);
}
mRedBoxSurfaceDelegate.createContentView("RedBox");
}
if (currentActivity == null || currentActivity.isFinishing()) {
FLog.e(
ReactConstants.TAG,
"Unable to launch redbox because react activity "
+ "is not available, here is the error that redbox would've displayed: "
+ message);
return;
}
if (mRedBoxDialog == null) {
mRedBoxDialog =
new RedBoxDialog(currentActivity, DevSupportManagerBase.this, mRedBoxHandler);
}
if (mRedBoxDialog.isShowing()) {
if (mRedBoxSurfaceDelegate.isShowing()) {
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
// show the first and most actionable one.
return;
}
Pair<String, StackFrame[]> errorInfo =
processErrorCustomizers(Pair.create(message, stack));
mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second);
// The RedBox surface delegate will always show the latest error
updateLastErrorInfo(message, stack, errorCookie, errorType);
// Only report native errors here. JS errors are reported
// inside {@link #updateJSError} after source mapping.
if (mRedBoxHandler != null && errorType == ErrorType.NATIVE) {
mRedBoxHandler.handleRedbox(message, stack, ErrorType.NATIVE);
}
mRedBoxDialog.resetReporting();
mRedBoxDialog.show();
mRedBoxSurfaceDelegate.show();
}
});
}
@@ -623,6 +602,11 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
return mDevSettings;
}
@Override
public RedBoxHandler getRedBoxHandler() {
return mRedBoxHandler;
}
@Override
public void onNewReactContextCreated(ReactContext reactContext) {
resetCurrentContext(reactContext);
@@ -888,6 +872,11 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
return mLastErrorStack;
}
@Override
public int getLastErrorCookie() {
return mLastErrorCookie;
}
@Override
public @Nullable ErrorType getLastErrorType() {
return mLastErrorType;
@@ -8,6 +8,7 @@
package com.facebook.react.devsupport;
import android.app.Activity;
import android.util.Pair;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
@@ -21,6 +22,7 @@ import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.ErrorCustomizer;
import com.facebook.react.devsupport.interfaces.ErrorType;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
import com.facebook.react.devsupport.interfaces.StackFrame;
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
import java.io.File;
@@ -94,6 +96,11 @@ public class DisabledDevSupportManager implements DevSupportManager {
return null;
}
@Override
public RedBoxHandler getRedBoxHandler() {
return null;
}
@Override
public void onNewReactContextCreated(ReactContext reactContext) {}
@@ -166,9 +173,19 @@ public class DisabledDevSupportManager implements DevSupportManager {
return null;
}
@Override
public int getLastErrorCookie() {
return 0;
}
@Override
public void registerErrorCustomizer(ErrorCustomizer errorCustomizer) {}
@Override
public Pair<String, StackFrame[]> processErrorCustomizers(Pair<String, StackFrame[]> errorInfo) {
return errorInfo;
}
@Override
public void setPackagerLocationCustomizer(
DevSupportManager.PackagerLocationCustomizer packagerLocationCustomizer) {}
@@ -14,7 +14,7 @@ import android.widget.EditText;
/**
* A class allows recognizing double key tap of "R", used to reload JS in {@link
* AbstractReactActivity}, {@link RedBoxDialog} and {@link ReactActivity}.
* AbstractReactActivity}, {@link RedBoxDialogSurfaceDelegate} and {@link ReactActivity}.
*/
public class DoubleTapReloadRecognizer {
private boolean mDoRefresh = false;
@@ -55,7 +55,7 @@ public class LogBoxDialogSurfaceDelegate implements SurfaceDelegate {
@Override
public void show() {
if (isSurfaceVisible() || !isContentViewReady()) {
if (isShowing() || !isContentViewReady()) {
return;
}
@@ -74,7 +74,7 @@ public class LogBoxDialogSurfaceDelegate implements SurfaceDelegate {
@Override
public void hide() {
if (!isSurfaceVisible()) {
if (!isShowing()) {
return;
}
@@ -86,7 +86,8 @@ public class LogBoxDialogSurfaceDelegate implements SurfaceDelegate {
mDialog = null;
}
private boolean isSurfaceVisible() {
return mDialog != null;
@Override
public boolean isShowing() {
return mDialog != null && mDialog.isShowing();
}
}
@@ -7,21 +7,20 @@
package com.facebook.react.devsupport;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.SpannedString;
import android.text.method.LinkMovementMethod;
import android.view.KeyEvent;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -32,6 +31,7 @@ import com.facebook.react.R;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.ErrorType;
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
import com.facebook.react.devsupport.interfaces.RedBoxHandler.ReportCompletedListener;
import com.facebook.react.devsupport.interfaces.StackFrame;
@@ -42,15 +42,10 @@ import okhttp3.RequestBody;
import org.json.JSONObject;
/** Dialog for displaying JS errors in an eye-catching form (red box). */
/* package */ class RedBoxDialog implements AdapterView.OnItemClickListener {
public class RedBoxContentView extends LinearLayout implements AdapterView.OnItemClickListener {
private final DevSupportManager mDevSupportManager;
private final DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
private final @Nullable RedBoxHandler mRedBoxHandler;
private final View mContentView;
private final Context mContext;
private @Nullable Dialog mDialog;
private @Nullable RedBoxHandler mRedBoxHandler;
private DevSupportManager mDevSupportManager;
private ListView mStackView;
private Button mReloadJsButton;
private Button mDismissButton;
@@ -239,42 +234,50 @@ import org.json.JSONObject;
}
}
protected RedBoxDialog(
Context context, DevSupportManager devSupportManager, @Nullable RedBoxHandler redBoxHandler) {
mContext = context;
mContentView = (View) LayoutInflater.from(context).inflate(R.layout.redbox_view, null);
public RedBoxContentView(Context context) {
super(context);
}
public RedBoxContentView setDevSupportManager(DevSupportManager devSupportManager) {
mDevSupportManager = devSupportManager;
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
mRedBoxHandler = redBoxHandler;
return this;
}
mStackView = (ListView) mContentView.findViewById(R.id.rn_redbox_stack);
public RedBoxContentView setRedBoxHandler(@Nullable RedBoxHandler redBoxHandler) {
mRedBoxHandler = redBoxHandler;
return this;
}
public void init() {
LayoutInflater.from(getContext()).inflate(R.layout.redbox_view, this);
mStackView = (ListView) findViewById(R.id.rn_redbox_stack);
mStackView.setOnItemClickListener(this);
mReloadJsButton = (Button) mContentView.findViewById(R.id.rn_redbox_reload_button);
mReloadJsButton = (Button) findViewById(R.id.rn_redbox_reload_button);
mReloadJsButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
mDevSupportManager.handleReloadJS();
Assertions.assertNotNull(mDevSupportManager).handleReloadJS();
}
});
mDismissButton = (Button) mContentView.findViewById(R.id.rn_redbox_dismiss_button);
mDismissButton = (Button) findViewById(R.id.rn_redbox_dismiss_button);
mDismissButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
Assertions.assertNotNull(mDevSupportManager).hideRedboxDialog();
}
});
if (mRedBoxHandler != null && mRedBoxHandler.isReportEnabled()) {
mLoadingIndicator = (ProgressBar) mContentView.findViewById(R.id.rn_redbox_loading_indicator);
mLineSeparator = (View) mContentView.findViewById(R.id.rn_redbox_line_separator);
mReportTextView = (TextView) mContentView.findViewById(R.id.rn_redbox_report_label);
mLoadingIndicator = (ProgressBar) findViewById(R.id.rn_redbox_loading_indicator);
mLineSeparator = (View) findViewById(R.id.rn_redbox_line_separator);
mReportTextView = (TextView) findViewById(R.id.rn_redbox_report_label);
mReportTextView.setMovementMethod(LinkMovementMethod.getInstance());
mReportTextView.setHighlightColor(Color.TRANSPARENT);
mReportButton = (Button) mContentView.findViewById(R.id.rn_redbox_report_button);
mReportButton = (Button) findViewById(R.id.rn_redbox_report_button);
mReportButton.setOnClickListener(mReportButtonOnClickListener);
}
}
@@ -298,44 +301,25 @@ import org.json.JSONObject;
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
new OpenStackFrameTask(mDevSupportManager)
new OpenStackFrameTask(Assertions.assertNotNull(mDevSupportManager))
.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR, (StackFrame) mStackView.getAdapter().getItem(position));
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
mDevSupportManager.showDevOptionsDialog();
return true;
/** Refresh the content view with latest errors from dev support manager */
public void refreshContentView() {
@Nullable String message = mDevSupportManager.getLastErrorTitle();
@Nullable StackFrame[] stack = mDevSupportManager.getLastErrorStack();
@Nullable ErrorType errorType = mDevSupportManager.getLastErrorType();
Pair<String, StackFrame[]> errorInfo =
mDevSupportManager.processErrorCustomizers(Pair.create(message, stack));
setExceptionDetails(errorInfo.first, errorInfo.second);
// JS errors are reported here after source mapping.
RedBoxHandler redBoxHandler = mDevSupportManager.getRedBoxHandler();
if (redBoxHandler != null) {
redBoxHandler.handleRedbox(message, stack, errorType);
resetReporting();
}
if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, mDialog.getCurrentFocus())) {
mDevSupportManager.handleReloadJS();
}
return mDialog.onKeyUp(keyCode, event);
}
public Context getContext() {
return mContext;
}
public View getContentView() {
return mContentView;
}
public boolean isShowing() {
return mDialog.isShowing();
}
public void show() {
if (mDialog == null) {
mDialog = new Dialog(mContext, R.style.Theme_Catalyst_RedBox);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
}
mDialog.setContentView(mContentView);
mDialog.show();
}
public void dismiss() {
mDialog.dismiss();
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport;
import android.app.Activity;
import android.app.Dialog;
import android.view.KeyEvent;
import android.view.Window;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.R;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.RedBoxHandler;
/**
* The implementation of SurfaceDelegate with {@link Activity}. This is the default SurfaceDelegate
* for Mobile.
*/
public class RedBoxDialogSurfaceDelegate implements SurfaceDelegate {
private final DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
private final DevSupportManager mDevSupportManager;
private @Nullable Dialog mDialog;
private @Nullable RedBoxContentView mRedBoxContentView;
public RedBoxDialogSurfaceDelegate(DevSupportManager devSupportManager) {
mDevSupportManager = devSupportManager;
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
@Override
public void createContentView(String appKey) {
// The content view is created in android instead of using react app. Hence the appKey is not
// used here.
RedBoxHandler redBoxHandler = mDevSupportManager.getRedBoxHandler();
Activity context = mDevSupportManager.getCurrentActivity();
// Create a new RedBox when currentActivity get updated
mRedBoxContentView = new RedBoxContentView(context);
mRedBoxContentView
.setDevSupportManager(mDevSupportManager)
.setRedBoxHandler(redBoxHandler)
.init();
}
@Override
public boolean isContentViewReady() {
return mRedBoxContentView != null;
}
@Override
public void destroyContentView() {
mRedBoxContentView = null;
}
@Override
public void show() {
@Nullable String message = mDevSupportManager.getLastErrorTitle();
Activity context = mDevSupportManager.getCurrentActivity();
if (context == null || context.isFinishing()) {
FLog.e(
ReactConstants.TAG,
"Unable to launch redbox because react activity "
+ "is not available, here is the error that redbox would've displayed: "
+ (message != null ? message : "N/A"));
return;
}
if (mRedBoxContentView == null || mRedBoxContentView.getContext() != context) {
// Create a new RedBox when currentActivity get updated
createContentView("RedBox");
}
mRedBoxContentView.refreshContentView();
if (mDialog == null) {
mDialog =
new Dialog(context, R.style.Theme_Catalyst_RedBox) {
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
mDevSupportManager.showDevOptionsDialog();
return true;
}
if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) {
mDevSupportManager.handleReloadJS();
}
return super.onKeyUp(keyCode, event);
}
};
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setContentView(mRedBoxContentView);
}
mDialog.show();
}
@Override
public void hide() {
// dismiss redbox if exists
if (mDialog != null) {
mDialog.dismiss();
destroyContentView();
mDialog = null;
}
}
@Override
public boolean isShowing() {
return mDialog != null && mDialog.isShowing();
}
}
@@ -8,6 +8,7 @@
package com.facebook.react.devsupport.interfaces;
import android.app.Activity;
import android.util.Pair;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
@@ -51,6 +52,8 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
DeveloperSettings getDevSettings();
RedBoxHandler getRedBoxHandler();
void onNewReactContextCreated(ReactContext reactContext);
void onReactInstanceDestroyed(ReactContext reactContext);
@@ -97,8 +100,12 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
@Nullable
ErrorType getLastErrorType();
int getLastErrorCookie();
void registerErrorCustomizer(ErrorCustomizer errorCustomizer);
Pair<String, StackFrame[]> processErrorCustomizers(Pair<String, StackFrame[]> errorInfo);
/**
* The PackagerLocationCustomizer allows you to have a dynamic packager location that is
* determined right before loading the packager. Your customizer must call |callback|, as loading