From e2720895245366aaf25d63e10bfa9995253bad00 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Tue, 10 Dec 2019 02:28:06 -0800 Subject: [PATCH] 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 --- .../facebook/react/CoreModulesPackage.java | 5 + .../facebook/react/ReactInstanceManager.java | 21 ++++ .../devsupport/DevSupportManagerImpl.java | 9 ++ .../devsupport/DisabledDevSupportManager.java | 9 ++ .../react/devsupport/LogBoxDialog.java | 24 ++++ .../react/devsupport/LogBoxModule.java | 110 ++++++++++++++++++ .../ReactInstanceManagerDevHelper.java | 6 + .../interfaces/DevSupportManager.java | 6 + .../src/main/res/devsupport/values/colors.xml | 1 + .../src/main/res/devsupport/values/styles.xml | 12 ++ 10 files changed, 203 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxDialog.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxModule.java diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 6dd813b6f62..a17763207b5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -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: diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java index 8d56f3418f5..2d79c4d3fb2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java @@ -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(); + } + } }; } 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 2e3f651fb16..5dd361f1d83 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -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(); 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 4f510fe1889..267a455e6eb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -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) {} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxDialog.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxDialog.java new file mode 100644 index 00000000000..c6a5b80e8dd --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxDialog.java @@ -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); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxModule.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxModule.java new file mode 100644 index 00000000000..05e70b069d3 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/LogBoxModule.java @@ -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; + } + } + }); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java index 60d45b1ba5b..02483586b34 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/ReactInstanceManagerDevHelper.java @@ -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); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java index ac04bc6a6dd..43a477e3fad 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevSupportManager.java @@ -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); diff --git a/ReactAndroid/src/main/res/devsupport/values/colors.xml b/ReactAndroid/src/main/res/devsupport/values/colors.xml index d15ee8caf7b..8ac1a132df0 100644 --- a/ReactAndroid/src/main/res/devsupport/values/colors.xml +++ b/ReactAndroid/src/main/res/devsupport/values/colors.xml @@ -1,4 +1,5 @@ #eecc0000 + #ffffffff diff --git a/ReactAndroid/src/main/res/devsupport/values/styles.xml b/ReactAndroid/src/main/res/devsupport/values/styles.xml index e9f462c5359..cb88e438c91 100644 --- a/ReactAndroid/src/main/res/devsupport/values/styles.xml +++ b/ReactAndroid/src/main/res/devsupport/values/styles.xml @@ -9,10 +9,22 @@ @android:anim/fade_out @android:color/white + +