Add NativeLogBox module on Android

Summary:
This diff adds a NativeLogBox module implementation on Android to manage rendering LogBox the way we render RedBox, except rendering a React Native component instead of a native view.

The strategy here is:
- initialize: will create a React rootview and render it.
- show: will add the rootview to a dialog and display the dialog.
- hide: will remove the rootview from it's parent, dismiss the dialog, and release the reference to the activity to prevent leaks.

Most of this is copied from the way RedBox works, the difference here is that we eagerly initialize the rootview with the `initialize` function so that it's warm by the time the dialog needs to render.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D18768517

fbshipit-source-id: 2510d6c186ccf73153ef9372c736c9e0c71bbc7d
This commit is contained in:
Rick Hanlon
2019-12-10 02:28:06 -08:00
committed by Facebook Github Bot
parent 6b22a4e802
commit e272089524
10 changed files with 203 additions and 0 deletions
@@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.devsupport.LogBoxModule;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.annotations.ReactModuleList;
import com.facebook.react.module.model.ReactModuleInfo;
@@ -49,6 +50,7 @@ import java.util.Map;
DeviceInfoModule.class,
DevSettingsModule.class,
ExceptionsManagerModule.class,
LogBoxModule.class,
HeadlessJsTaskSupportModule.class,
SourceCodeModule.class,
TimingModule.class,
@@ -94,6 +96,7 @@ import java.util.Map;
DeviceInfoModule.class,
DevSettingsModule.class,
ExceptionsManagerModule.class,
LogBoxModule.class,
HeadlessJsTaskSupportModule.class,
SourceCodeModule.class,
TimingModule.class,
@@ -142,6 +145,8 @@ import java.util.Map;
return new DevSettingsModule(reactContext, mReactInstanceManager.getDevSupportManager());
case ExceptionsManagerModule.NAME:
return new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager());
case LogBoxModule.NAME:
return new LogBoxModule(reactContext, mReactInstanceManager.getDevSupportManager());
case HeadlessJsTaskSupportModule.NAME:
return new HeadlessJsTaskSupportModule(reactContext);
case SourceCodeModule.NAME:
@@ -304,6 +304,27 @@ public class ReactInstanceManager {
public JavaScriptExecutorFactory getJavaScriptExecutorFactory() {
return ReactInstanceManager.this.getJSExecutorFactory();
}
@Override
public @Nullable View createRootView(String appKey) {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
ReactRootView rootView = new ReactRootView(currentActivity);
rootView.startReactApplication(ReactInstanceManager.this, appKey, null);
return rootView;
}
return null;
}
@Override
public void destroyRootView(View rootView) {
if (rootView instanceof ReactRootView) {
((ReactRootView) rootView).unmountReactApplication();
}
}
};
}
@@ -19,6 +19,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.hardware.SensorManager;
import android.util.Pair;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.Nullable;
@@ -377,6 +378,14 @@ public class DevSupportManagerImpl
}
}
public @Nullable View createRootView(String appKey) {
return mReactInstanceManagerHelper.createRootView(appKey);
}
public void destroyRootView(View rootView) {
mReactInstanceManagerHelper.destroyRootView(rootView);
}
private void hideDevOptionsDialog() {
if (mDevOptionsDialog != null) {
mDevOptionsDialog.dismiss();
@@ -7,6 +7,7 @@
package com.facebook.react.devsupport;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
@@ -40,6 +41,14 @@ public class DisabledDevSupportManager implements DevSupportManager {
@Override
public void showNewJSError(String message, ReadableArray details, int errorCookie) {}
@Override
public @Nullable View createRootView(String appKey) {
return null;
}
@Override
public void destroyRootView(View rootView) {}
@Override
public void updateJSError(String message, ReadableArray details, int errorCookie) {}
@@ -0,0 +1,24 @@
/*
* Copyright (c) Facebook, Inc. and its 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.View;
import android.view.Window;
import com.facebook.react.R;
/** Dialog for displaying JS errors in LogBox. */
public class LogBoxDialog extends Dialog {
public LogBoxDialog(Activity context, View reactRootView) {
super(context, R.style.Theme_Catalyst_LogBox);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(reactRootView);
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (c) Facebook, Inc. and its 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.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.module.annotations.ReactModule;
@ReactModule(name = LogBoxModule.NAME)
public class LogBoxModule extends ReactContextBaseJavaModule {
public static final String NAME = "LogBox";
private final DevSupportManager mDevSupportManager;
private @Nullable View mReactRootView;
private @Nullable LogBoxDialog mLogBoxDialog;
public LogBoxModule(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext);
mDevSupportManager = devSupportManager;
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mReactRootView == null) {
mReactRootView = mDevSupportManager.createRootView("LogBox");
if (mReactRootView == null) {
FLog.e(
ReactConstants.TAG,
"Unable to launch logbox because react was unable to create the root view");
}
}
}
});
}
@Override
public String getName() {
return NAME;
}
@ReactMethod
public void show() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mLogBoxDialog == null) {
Activity context = getCurrentActivity();
if (context == null || context.isFinishing()) {
FLog.e(
ReactConstants.TAG,
"Unable to launch logbox because react activity "
+ "is not available, here is the error that logbox would've displayed: ");
return;
}
mLogBoxDialog = new LogBoxDialog(context, mReactRootView);
mLogBoxDialog.setCancelable(false);
mLogBoxDialog.show();
}
}
});
}
@ReactMethod
public void hide() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mLogBoxDialog != null) {
if (mReactRootView.getParent() != null) {
((ViewGroup) mReactRootView.getParent()).removeView(mReactRootView);
}
mLogBoxDialog.dismiss();
mLogBoxDialog = null;
}
}
});
}
@Override
public void onCatalystInstanceDestroy() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mReactRootView != null) {
mDevSupportManager.destroyRootView(mReactRootView);
mReactRootView = null;
}
}
});
}
}
@@ -8,6 +8,7 @@
package com.facebook.react.devsupport;
import android.app.Activity;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
@@ -32,4 +33,9 @@ public interface ReactInstanceManagerDevHelper {
Activity getCurrentActivity();
JavaScriptExecutorFactory getJavaScriptExecutorFactory();
@Nullable
View createRootView(String appKey);
void destroyRootView(View rootView);
}
@@ -7,6 +7,7 @@
package com.facebook.react.devsupport.interfaces;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
@@ -25,6 +26,11 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
void addCustomDevOption(String optionName, DevOptionHandler optionHandler);
@Nullable
View createRootView(String appKey);
void destroyRootView(View rootView);
void showNewJSError(String message, ReadableArray details, int errorCookie);
void updateJSError(final String message, final ReadableArray details, final int errorCookie);