diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java
index e20d67380f..1fb477b13d 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java
@@ -43,6 +43,7 @@ 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.devsupport.StackTraceHelper.StackFrame;
import com.facebook.react.modules.debug.DeveloperSettings;
/**
@@ -154,8 +155,7 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
public void handleException(Exception e) {
if (mIsDevSupportEnabled) {
FLog.e(ReactConstants.TAG, "Exception in native call from JS", e);
- CharSequence details = ExceptionFormatterHelper.javaStackTraceToHtml(e.getStackTrace());
- showNewError(e.getMessage(), details, JAVA_ERROR_COOKIE);
+ showNewError(e.getMessage(), StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE);
} else {
if (e instanceof RuntimeException) {
// Because we are rethrowing the original exception, the original stacktrace will be
@@ -179,7 +179,7 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
}
public void showNewJSError(String message, ReadableArray details, int errorCookie) {
- showNewError(message, ExceptionFormatterHelper.jsStackTraceToHtml(details), errorCookie);
+ showNewError(message, StackTraceHelper.convertJsStackTrace(details), errorCookie);
}
public void updateJSError(
@@ -198,8 +198,9 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
errorCookie != mRedBoxDialog.getErrorCookie()) {
return;
}
- mRedBoxDialog.setTitle(message);
- mRedBoxDialog.setDetails(ExceptionFormatterHelper.jsStackTraceToHtml(details));
+ mRedBoxDialog.setExceptionDetails(
+ message,
+ StackTraceHelper.convertJsStackTrace(details));
mRedBoxDialog.show();
}
});
@@ -207,7 +208,7 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
private void showNewError(
final String message,
- final CharSequence details,
+ final StackFrame[] stack,
final int errorCookie) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@@ -222,8 +223,7 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
// show the first and most actionable one.
return;
}
- mRedBoxDialog.setTitle(message);
- mRedBoxDialog.setDetails(details);
+ mRedBoxDialog.setExceptionDetails(message, stack);
mRedBoxDialog.setErrorCookie(errorCookie);
mRedBoxDialog.show();
}
@@ -520,7 +520,7 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
public void run() {
showNewError(
mApplicationContext.getString(R.string.catalyst_remotedbg_error),
- ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
+ StackTraceHelper.convertJavaStackTrace(cause),
JAVA_ERROR_COOKIE);
}
});
@@ -555,13 +555,12 @@ public class DevSupportManager implements NativeModuleCallExceptionHandler {
DebugServerException debugServerException = (DebugServerException) cause;
showNewError(
debugServerException.description,
- ExceptionFormatterHelper.debugServerExcStackTraceToHtml(
- (DebugServerException) cause),
+ StackTraceHelper.convertJavaStackTrace(cause),
JAVA_ERROR_COOKIE);
} else {
showNewError(
mApplicationContext.getString(R.string.catalyst_jsload_error),
- ExceptionFormatterHelper.javaStackTraceToHtml(cause.getStackTrace()),
+ StackTraceHelper.convertJavaStackTrace(cause),
JAVA_ERROR_COOKIE);
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java
deleted file mode 100644
index 89ae7d9bf4..0000000000
--- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ExceptionFormatterHelper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * 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 java.io.File;
-
-import android.text.Html;
-
-import com.facebook.react.bridge.ReadableArray;
-import com.facebook.react.bridge.ReadableMap;
-
-/**
- * Helper class for displaying errors in an eye-catching form (red box).
- */
-/* package */ class ExceptionFormatterHelper {
-
- private static String getStackTraceHtmlComponent(
- String methodName, String filename, int lineNumber, int columnNumber) {
- StringBuilder stringBuilder = new StringBuilder();
- methodName = methodName.replace("<", "<").replace(">", ">");
- stringBuilder.append("")
- .append(methodName)
- .append("
")
- .append(filename)
- .append(":")
- .append(lineNumber);
- if (columnNumber != -1) {
- stringBuilder
- .append(":")
- .append(columnNumber);
- }
- stringBuilder.append("
");
- return stringBuilder.toString();
- }
-
- public static CharSequence jsStackTraceToHtml(ReadableArray stack) {
- StringBuilder stringBuilder = new StringBuilder();
- for (int i = 0; i < stack.size(); i++) {
- ReadableMap frame = stack.getMap(i);
- String methodName = frame.getString("methodName");
- String fileName = new File(frame.getString("file")).getName();
- int lineNumber = frame.getInt("lineNumber");
- int columnNumber = -1;
- if (frame.hasKey("column") && !frame.isNull("column")) {
- columnNumber = frame.getInt("column");
- }
- stringBuilder.append(getStackTraceHtmlComponent(
- methodName, fileName, lineNumber, columnNumber));
- }
- return Html.fromHtml(stringBuilder.toString());
- }
-
- public static CharSequence javaStackTraceToHtml(StackTraceElement[] stack) {
- StringBuilder stringBuilder = new StringBuilder();
- for (int i = 0; i< stack.length; i++) {
- stringBuilder.append(getStackTraceHtmlComponent(
- stack[i].getMethodName(), stack[i].getFileName(), stack[i].getLineNumber(), -1));
-
- }
- return Html.fromHtml(stringBuilder.toString());
- }
-
- public static CharSequence debugServerExcStackTraceToHtml(DebugServerException e) {
- String s = getStackTraceHtmlComponent("", e.fileName, e.lineNumber, e.column);
- return Html.fromHtml(s);
- }
-
-}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java
index 1a3973a3dd..39e48fefe2 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java
@@ -11,28 +11,166 @@ package com.facebook.react.devsupport;
import android.app.Dialog;
import android.content.Context;
-import android.graphics.Typeface;
-import android.text.method.ScrollingMovementMethod;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.view.KeyEvent;
+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.ListView;
import android.widget.TextView;
+import com.facebook.common.logging.FLog;
import com.facebook.react.R;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.common.ReactConstants;
+import com.facebook.react.devsupport.StackTraceHelper.StackFrame;
+
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import org.json.JSONObject;
/**
* Dialog for displaying JS errors in an eye-catching form (red box).
*/
-/* package */ class RedBoxDialog extends Dialog {
+/* package */ class RedBoxDialog extends Dialog implements AdapterView.OnItemClickListener {
private final DevSupportManager mDevSupportManager;
- private TextView mTitle;
- private TextView mDetails;
+ private ListView mStackView;
private Button mReloadJs;
private int mCookie = 0;
+ private static class StackAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_COUNT = 2;
+ private static final int VIEW_TYPE_TITLE = 0;
+ private static final int VIEW_TYPE_STACKFRAME = 1;
+
+ private final String mTitle;
+ private final StackFrame[] mStack;
+
+ private static class FrameViewHolder {
+ private final TextView mMethodView;
+ private final TextView mFileView;
+
+ private FrameViewHolder(View v) {
+ mMethodView = (TextView) v.findViewById(R.id.rn_frame_method);
+ mFileView = (TextView) v.findViewById(R.id.rn_frame_file);
+ }
+ }
+
+ public StackAdapter(String title, StackFrame[] stack) {
+ mTitle = title;
+ mStack = stack;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return position > 0;
+ }
+
+ @Override
+ public int getCount() {
+ return mStack.length + 1;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return position == 0 ? mTitle : mStack[position - 1];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return VIEW_TYPE_COUNT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position == 0 ? VIEW_TYPE_TITLE : VIEW_TYPE_STACKFRAME;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (position == 0) {
+ TextView title = convertView != null
+ ? (TextView) convertView
+ : (TextView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.redbox_item_title, parent, false);
+ title.setText(mTitle);
+ return title;
+ } else {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.redbox_item_frame, parent, false);
+ convertView.setTag(new FrameViewHolder(convertView));
+ }
+ StackFrame frame = mStack[position - 1];
+ FrameViewHolder holder = (FrameViewHolder) convertView.getTag();
+ holder.mMethodView.setText(frame.getMethod());
+ holder.mFileView.setText(frame.getFileName() + ":" + frame.getLine());
+ return convertView;
+ }
+ }
+ }
+
+ private static class OpenStackFrameTask extends AsyncTask {
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+ private final DevSupportManager mDevSupportManager;
+
+ private OpenStackFrameTask(DevSupportManager devSupportManager) {
+ mDevSupportManager = devSupportManager;
+ }
+
+ @Override
+ protected Void doInBackground(StackFrame... stackFrames) {
+ try {
+ String openStackFrameUrl =
+ Uri.parse(mDevSupportManager.getSourceUrl()).buildUpon()
+ .path("/open-stack-frame")
+ .query(null)
+ .build()
+ .toString();
+ OkHttpClient client = new OkHttpClient();
+ for (StackFrame frame: stackFrames) {
+ String payload = stackFrameToJson(frame).toString();
+ RequestBody body = RequestBody.create(JSON, payload);
+ Request request = new Request.Builder().url(openStackFrameUrl).post(body).build();
+ client.newCall(request).execute();
+ }
+ } catch (Exception e) {
+ FLog.e(ReactConstants.TAG, "Could not open stack frame", e);
+ }
+ return null;
+ }
+
+ private static JSONObject stackFrameToJson(StackFrame frame) {
+ return new JSONObject(
+ MapBuilder.of(
+ "file", frame.getFile(),
+ "methodName", frame.getMethod(),
+ "lineNumber", frame.getLine(),
+ "column", frame.getColumn()
+ ));
+ }
+ }
+
protected RedBoxDialog(Context context, DevSupportManager devSupportManager) {
super(context, R.style.Theme_Catalyst_RedBox);
@@ -42,12 +180,9 @@ import com.facebook.react.R;
mDevSupportManager = devSupportManager;
- mTitle = (TextView) findViewById(R.id.catalyst_redbox_title);
- mDetails = (TextView) findViewById(R.id.catalyst_redbox_details);
- mDetails.setTypeface(Typeface.MONOSPACE);
- mDetails.setHorizontallyScrolling(true);
- mDetails.setMovementMethod(new ScrollingMovementMethod());
- mReloadJs = (Button) findViewById(R.id.catalyst_redbox_reloadjs);
+ mStackView = (ListView) findViewById(R.id.rn_redbox_stack);
+ mStackView.setOnItemClickListener(this);
+ mReloadJs = (Button) findViewById(R.id.rn_redbox_reloadjs);
mReloadJs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -56,12 +191,8 @@ import com.facebook.react.R;
});
}
- public void setTitle(String title) {
- mTitle.setText(title);
- }
-
- public void setDetails(CharSequence details) {
- mDetails.setText(details);
+ public void setExceptionDetails(String title, StackFrame[] stack) {
+ mStackView.setAdapter(new StackAdapter(title, stack));
}
public void setErrorCookie(int cookie) {
@@ -72,6 +203,13 @@ import com.facebook.react.R;
return mCookie;
}
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ new OpenStackFrameTask(mDevSupportManager).executeOnExecutor(
+ AsyncTask.THREAD_POOL_EXECUTOR,
+ (StackFrame) mStackView.getAdapter().getItem(position));
+ }
+
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java
new file mode 100644
index 0000000000..5618ba78da
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java
@@ -0,0 +1,127 @@
+/**
+ * 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 java.io.File;
+
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+
+/**
+ * Helper class converting JS and Java stack traces into arrays of {@link StackFrame} objects.
+ */
+/* package */ class StackTraceHelper {
+
+ /**
+ * Represents a generic entry in a stack trace, be it originally from JS or Java.
+ */
+ public static class StackFrame {
+ private final String mFile;
+ private final String mMethod;
+ private final int mLine;
+ private final int mColumn;
+ private final String mFileName;
+
+ private StackFrame(String file, String method, int line, int column) {
+ mFile = file;
+ mMethod = method;
+ mLine = line;
+ mColumn = column;
+ mFileName = new File(file).getName();
+ }
+
+ private StackFrame(String file, String fileName, String method, int line, int column) {
+ mFile = file;
+ mFileName = fileName;
+ mMethod = method;
+ mLine = line;
+ mColumn = column;
+ }
+
+ /**
+ * Get the file this stack frame points to.
+ *
+ * JS traces return the full path to the file here, while Java traces only return the file name
+ * (the path is not known).
+ */
+ public String getFile() {
+ return mFile;
+ }
+
+ /**
+ * Get the name of the method this frame points to.
+ */
+ public String getMethod() {
+ return mMethod;
+ }
+
+ /**
+ * Get the line number this frame points to in the file returned by {@link #getFile()}.
+ */
+ public int getLine() {
+ return mLine;
+ }
+
+ /**
+ * Get the column this frame points to in the file returned by {@link #getFile()}.
+ */
+ public int getColumn() {
+ return mColumn;
+ }
+
+ /**
+ * Get just the name of the file this frame points to.
+ *
+ * For JS traces this is different from {@link #getFile()} in that it only returns the file
+ * name, not the full path. For Java traces there is no difference.
+ */
+ public String getFileName() {
+ return mFileName;
+ }
+ }
+
+ /**
+ * Convert a JavaScript stack trace (see {@code parseErrorStack} JS module) to an array of
+ * {@link StackFrame}s.
+ */
+ public static StackFrame[] convertJsStackTrace(ReadableArray stack) {
+ StackFrame[] result = new StackFrame[stack.size()];
+ for (int i = 0; i < stack.size(); i++) {
+ ReadableMap frame = stack.getMap(i);
+ String methodName = frame.getString("methodName");
+ String fileName = frame.getString("file");
+ int lineNumber = frame.getInt("lineNumber");
+ int columnNumber = -1;
+ if (frame.hasKey("column") && !frame.isNull("column")) {
+ columnNumber = frame.getInt("column");
+ }
+ result[i] = new StackFrame(fileName, methodName, lineNumber, columnNumber);
+ }
+ return result;
+ }
+
+ /**
+ * Convert a {@link Throwable} to an array of {@link StackFrame}s.
+ */
+ public static StackFrame[] convertJavaStackTrace(Throwable exception) {
+ StackTraceElement[] stackTrace = exception.getStackTrace();
+ StackFrame[] result = new StackFrame[stackTrace.length];
+ for (int i = 0; i < stackTrace.length; i++) {
+ result[i] = new StackFrame(
+ stackTrace[i].getClassName(),
+ stackTrace[i].getFileName(),
+ stackTrace[i].getMethodName(),
+ stackTrace[i].getLineNumber(),
+ 0);
+ }
+ return result;
+ }
+
+}
diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml
new file mode 100644
index 0000000000..45923c1c7e
--- /dev/null
+++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_frame.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml
new file mode 100644
index 0000000000..95ef0c926f
--- /dev/null
+++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_item_title.xml
@@ -0,0 +1,10 @@
+
diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml
index b85d4f83d1..faa9a90f04 100644
--- a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml
+++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml
@@ -1,54 +1,20 @@
-
-
-
+
-
-
+ android:id="@+id/rn_redbox_reloadjs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/catalyst_reloadjs"
+ />
+