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 4b27023c0..629a3f12b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -9,9 +9,12 @@ package com.facebook.react.devsupport; +import javax.annotation.Nullable; + import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.devsupport.StackTraceHelper.StackFrame; import com.facebook.react.modules.debug.DeveloperSettings; /** @@ -40,4 +43,6 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { void reloadSettings(); void handleReloadJS(); void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback); + @Nullable String getLastErrorTitle(); + @Nullable StackFrame[] getLastErrorStack(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 0355256e1..dd823f84a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -31,8 +31,6 @@ 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; @@ -110,6 +108,10 @@ public class DevSupportManagerImpl implements DevSupportManager { private boolean mIsShakeDetectorStarted = false; private boolean mIsDevSupportEnabled = false; private @Nullable RedBoxHandler mRedBoxHandler; + private @Nullable String mLastErrorTitle; + private @Nullable StackFrame[] mLastErrorStack; + private int mLastErrorCookie = 0; + private @Nullable ErrorType mLastErrorType; public DevSupportManagerImpl( Context applicationContext, @@ -234,12 +236,12 @@ public class DevSupportManagerImpl implements DevSupportManager { // belongs to the most recent showNewJSError if (mRedBoxDialog == null || !mRedBoxDialog.isShowing() || - errorCookie != mRedBoxDialog.getErrorCookie()) { + errorCookie != mLastErrorCookie) { return; } StackFrame[] stack = StackTraceHelper.convertJsStackTrace(details); mRedBoxDialog.setExceptionDetails(message, stack); - mRedBoxDialog.setErrorCookie(errorCookie); + updateLastErrorInfo(message, stack, errorCookie, ErrorType.JS); // JS errors are reported here after source mapping. if (mRedBoxHandler != null) { mRedBoxHandler.handleRedbox(message, stack, RedBoxHandler.ErrorType.JS); @@ -276,7 +278,7 @@ public class DevSupportManagerImpl implements DevSupportManager { return; } mRedBoxDialog.setExceptionDetails(message, stack); - mRedBoxDialog.setErrorCookie(errorCookie); + 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) { @@ -589,6 +591,27 @@ public class DevSupportManagerImpl implements DevSupportManager { mDevServerHelper.isPackagerRunning(callback); } + @Override + public @Nullable String getLastErrorTitle() { + return mLastErrorTitle; + } + + @Override + public @Nullable StackFrame[] getLastErrorStack() { + return mLastErrorStack; + } + + private void updateLastErrorInfo( + final String message, + final StackFrame[] stack, + final int errorCookie, + final ErrorType errorType) { + mLastErrorTitle = message; + mLastErrorStack = stack; + mLastErrorCookie = errorCookie; + mLastErrorType = errorType; + } + private void reloadJSInProxyMode(final AlertDialog progressDialog) { // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that // anyway diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 5388f98b7..9ec71be18 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -9,9 +9,12 @@ package com.facebook.react.devsupport; +import javax.annotation.Nullable; + import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.devsupport.StackTraceHelper.StackFrame; import com.facebook.react.modules.debug.DeveloperSettings; /** @@ -121,6 +124,16 @@ public class DisabledDevSupportManager implements DevSupportManager { } + @Override + public @Nullable String getLastErrorTitle() { + return null; + } + + @Override + public @Nullable StackFrame[] getLastErrorStack() { + return null; + } + @Override public void handleException(Exception e) { mDefaultNativeModuleCallExceptionHandler.handleException(e); 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 7b9cc1eb4..6079acc62 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialog.java @@ -25,6 +25,7 @@ import android.widget.ListView; import android.widget.TextView; import com.facebook.common.logging.FLog; +import com.facebook.infer.annotation.Assertions; import com.facebook.react.R; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.ReactConstants; @@ -46,7 +47,8 @@ import org.json.JSONObject; private ListView mStackView; private Button mReloadJs; - private int mCookie = 0; + private Button mDismiss; + private Button mCopyToClipboard; private static class StackAdapter extends BaseAdapter { private static final int VIEW_TYPE_COUNT = 2; @@ -124,10 +126,7 @@ import org.json.JSONObject; StackFrame frame = mStack[position - 1]; FrameViewHolder holder = (FrameViewHolder) convertView.getTag(); holder.mMethodView.setText(frame.getMethod()); - final int column = frame.getColumn(); - // If the column is 0, don't show it in red box. - final String columnString = column <= 0 ? "" : ":" + column; - holder.mFileView.setText(frame.getFileName() + ":" + frame.getLine() + columnString); + holder.mFileView.setText(StackTraceHelper.formatFrameSource(frame)); return convertView; } } @@ -175,6 +174,35 @@ import org.json.JSONObject; } } + private static class CopyToHostClipBoardTask extends AsyncTask { + private final DevSupportManager mDevSupportManager; + + private CopyToHostClipBoardTask(DevSupportManager devSupportManager) { + mDevSupportManager = devSupportManager; + } + + @Override + protected Void doInBackground(String... clipBoardString) { + try { + String sendClipBoardUrl = + Uri.parse(mDevSupportManager.getSourceUrl()).buildUpon() + .path("/copy-to-clipboard") + .query(null) + .build() + .toString(); + for (String string: clipBoardString) { + OkHttpClient client = new OkHttpClient(); + RequestBody body = RequestBody.create(null, string); + Request request = new Request.Builder().url(sendClipBoardUrl).post(body).build(); + client.newCall(request).execute(); + } + } catch (Exception e) { + FLog.e(ReactConstants.TAG, "Could not copy to the host clipboard", e); + } + return null; + } + } + protected RedBoxDialog(Context context, DevSupportManager devSupportManager) { super(context, R.style.Theme_Catalyst_RedBox); @@ -187,27 +215,39 @@ import org.json.JSONObject; mStackView = (ListView) findViewById(R.id.rn_redbox_stack); mStackView.setOnItemClickListener(this); - mReloadJs = (Button) findViewById(R.id.rn_redbox_reloadjs); + mReloadJs = (Button) findViewById(R.id.rn_redbox_reload_button); mReloadJs.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mDevSupportManager.handleReloadJS(); } }); + mDismiss = (Button) findViewById(R.id.rn_redbox_dismiss_button); + mDismiss.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + mCopyToClipboard = (Button) findViewById(R.id.rn_redbox_copy_button); + mCopyToClipboard.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String title = mDevSupportManager.getLastErrorTitle(); + StackFrame[] stack = mDevSupportManager.getLastErrorStack(); + Assertions.assertNotNull(title); + Assertions.assertNotNull(stack); + new CopyToHostClipBoardTask(mDevSupportManager).executeOnExecutor( + AsyncTask.THREAD_POOL_EXECUTOR, + StackTraceHelper.formatStackTrace(title, stack)); + } + }); } public void setExceptionDetails(String title, StackFrame[] stack) { mStackView.setAdapter(new StackAdapter(title, stack)); } - public void setErrorCookie(int cookie) { - mCookie = cookie; - } - - public int getErrorCookie() { - return mCookie; - } - @Override public void onItemClick(AdapterView parent, View view, int position, long id) { new OpenStackFrameTask(mDevSupportManager).executeOnExecutor( diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java index aaec57919..007f3607a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/StackTraceHelper.java @@ -124,4 +124,32 @@ public class StackTraceHelper { return result; } + /** + * Format a {@link StackFrame} to a String (method name is not included). + */ + public static String formatFrameSource(StackFrame frame) { + String lineInfo = ""; + final int column = frame.getColumn(); + // If the column is 0, don't show it in red box. + final String columnString = column <= 0 ? "" : ":" + column; + lineInfo += frame.getFileName() + ":" + frame.getLine() + columnString; + return lineInfo; + } + + /** + * Format an array of {@link StackFrame}s with the error title to a String. + */ + public static String formatStackTrace(String title, StackFrame[] stack) { + StringBuilder stackTrace = new StringBuilder(); + stackTrace.append(title).append("\n"); + for (StackFrame frame: stack) { + stackTrace.append(frame.getMethod()) + .append("\n") + .append(" ") + .append(formatFrameSource(frame)) + .append("\n"); + } + + return stackTrace.toString(); + } } diff --git a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml index 02c07ba8c..429cd40ee 100644 --- a/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml +++ b/ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml @@ -11,11 +11,46 @@ android:layout_height="0dp" android:layout_weight="1" /> -