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 @@ - - - +