Skip to content

Commit

Permalink
Add Copy and Dismiss Button in RN Android Red Box
Browse files Browse the repository at this point in the history
Summary:
Add "Copy" and "Dismiss" button when the RN Android redbox is shown, consistent with that in RN iOS.
  - "Copy" button copies all the messages shown in the redbox to the host system clipboard, the solution is posting redbox messages to packager and the the packager copies the messages onto the host clipboard.
  - "Dismiss" button always exits the redbox dialog.
  - Add shortcut as "Dismiss (ESC)" and "Reload (R, R).

Notice: Copy button is only supported on Mac OS by now (warning in packager on other platforms), because it's not easy for us to test on Windows or Linux. Will put the codes for other platforms on Github issues, hoping anyone could help test and add this feature, then send us a pull request.

Redbox Dialog in RN Android before:
{F61310489}
Redbox Dialog in RN Android now:
{F61659189}

Follow-up:
- We can adjust the button styles in redboxes.
- We can consider to add shortcut for "Copy" button.

Reviewed By: foghina

Differential Revision: D3392155

fbshipit-source-id: fc5dc2186718cac8706fb3c17d336160e61e3f4e
  • Loading branch information
Siqi Liu authored and Facebook Github Bot 5 committed Jun 30, 2016
1 parent ca0c6db commit dc3fce0
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -40,4 +43,6 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
void reloadSettings();
void handleReloadJS();
void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback);
@Nullable String getLastErrorTitle();
@Nullable StackFrame[] getLastErrorStack();
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
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;

Expand Down Expand Up @@ -110,6 +108,10 @@ private static enum ErrorType {
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,
Expand Down Expand Up @@ -234,12 +236,12 @@ public void run() {
// 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);
Expand Down Expand Up @@ -276,7 +278,7 @@ public void run() {
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) {
Expand Down Expand Up @@ -589,6 +591,27 @@ public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -121,6 +124,16 @@ public void isPackagerRunning(DevServerHelper.PackagerStatusCallback callback) {

}

@Override
public @Nullable String getLastErrorTitle() {
return null;
}

@Override
public @Nullable StackFrame[] getLastErrorStack() {
return null;
}

@Override
public void handleException(Exception e) {
mDefaultNativeModuleCallExceptionHandler.handleException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -46,7 +47,8 @@

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;
Expand Down Expand Up @@ -124,10 +126,7 @@ public View getView(int position, View convertView, ViewGroup parent) {
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;
}
}
Expand Down Expand Up @@ -175,6 +174,35 @@ private static JSONObject stackFrameToJson(StackFrame frame) {
}
}

private static class CopyToHostClipBoardTask extends AsyncTask<String, Void, Void> {
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);

Expand All @@ -187,27 +215,39 @@ protected RedBoxDialog(Context context, DevSupportManager devSupportManager) {

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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,32 @@ public static StackFrame[] convertJavaStackTrace(Throwable exception) {
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();
}
}
43 changes: 39 additions & 4 deletions ReactAndroid/src/main/res/devsupport/layout/redbox_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,46 @@
android:layout_height="0dp"
android:layout_weight="1"
/>
<Button
android:id="@+id/rn_redbox_reloadjs"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/catalyst_reloadjs"
android:orientation="horizontal"
>
<Button
android:id="@+id/rn_redbox_dismiss_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="4dp"
android:text="@string/catalyst_dismiss_button"
android:textColor="@android:color/white"
android:textSize="14sp"
android:alpha="0.5"
style="?android:attr/borderlessButtonStyle"
/>
<Button
android:id="@+id/rn_redbox_reload_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="4dp"
android:text="@string/catalyst_reload_button"
android:textColor="@android:color/white"
android:textSize="14sp"
android:alpha="0.5"
style="?android:attr/borderlessButtonStyle"
/>
<Button
android:id="@+id/rn_redbox_copy_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="4dp"
android:text="@string/catalyst_copy_button"
android:textColor="@android:color/white"
android:textSize="14sp"
android:alpha="0.5"
style="?android:attr/borderlessButtonStyle"
/>
</LinearLayout>
</LinearLayout>
3 changes: 3 additions & 0 deletions ReactAndroid/src/main/res/devsupport/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@
<string name="catalyst_remotedbg_error" project="catalyst" translatable="false">Unable to connect with remote debugger</string>
<string name="catalyst_element_inspector" project="catalyst" translatable="false">Toggle Inspector</string>
<string name="catalyst_heap_capture" project="catalyst" translatable="false">Capture Heap</string>
<string name="catalyst_dismiss_button" project="catalyst" translatable="false">Dismiss (ESC)</string>
<string name="catalyst_reload_button" project="catalyst" translatable="false">Reload (R, R)</string>
<string name="catalyst_copy_button" project="catalyst" translatable="false">Copy</string>
</resources>
28 changes: 28 additions & 0 deletions local-cli/server/middleware/copyToClipBoardMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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.
*/
'use strict';

const copyToClipBoard = require('../util/copyToClipBoard');
var chalk = require('chalk');

/**
* Handle the request from JS to copy contents onto host system clipboard.
* This is only supported on Mac for now.
*/
module.exports = function(req, res, next) {
if (req.url === '/copy-to-clipboard') {
var ret = copyToClipBoard(req.rawBody);
if (!ret) {
console.warn(chalk.red('Copy button is not supported on this platform!'));
}
res.end('OK');
} else {
next();
}
};
2 changes: 2 additions & 0 deletions local-cli/server/runServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const http = require('http');
const loadRawBodyMiddleware = require('./middleware/loadRawBodyMiddleware');
const messageSocket = require('./util/messageSocket.js');
const openStackFrameInEditorMiddleware = require('./middleware/openStackFrameInEditorMiddleware');
const copyToClipBoardMiddleware = require('./middleware/copyToClipBoardMiddleware');
const path = require('path');
const ReactPackager = require('../../packager/react-packager');
const statusPageMiddleware = require('./middleware/statusPageMiddleware.js');
Expand All @@ -33,6 +34,7 @@ function runServer(args, config, readyCallback) {
.use(getDevToolsMiddleware(args, () => wsProxy && wsProxy.isChromeConnected()))
.use(getDevToolsMiddleware(args, () => ms && ms.isChromeConnected()))
.use(openStackFrameInEditorMiddleware(args))
.use(copyToClipBoardMiddleware)
.use(statusPageMiddleware)
.use(systraceProfileMiddleware)
.use(cpuProfilerMiddleware)
Expand Down
Loading

1 comment on commit dc3fce0

@Kennytian
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, well done!

Please sign in to comment.