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 6dd813b6f62b2e..a17763207b53f3 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 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 @@ DeviceInfoModule.class, DevSettingsModule.class, ExceptionsManagerModule.class, + LogBoxModule.class, HeadlessJsTaskSupportModule.class, SourceCodeModule.class, TimingModule.class, @@ -94,6 +96,7 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { DeviceInfoModule.class, DevSettingsModule.class, ExceptionsManagerModule.class, + LogBoxModule.class, HeadlessJsTaskSupportModule.class, SourceCodeModule.class, TimingModule.class, @@ -142,6 +145,8 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext) 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 8d56f3418f5607..2d79c4d3fb205f 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 void toggleElementInspector() { 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 2e3f651fb16e15..5dd361f1d83f91 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.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 void hideRedboxDialog() { } } + 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 4f510fe18895c8..267a455e6ebe4b 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 void addCustomDevOption(String optionName, DevOptionHandler optionHandler @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 00000000000000..c6a5b80e8ddebb --- /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 00000000000000..05e70b069d33fc --- /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 60d45b1ba5b087..02483586b3416f 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 ac04bc6a6dd4e8..43a477e3faddee 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 d15ee8caf7bdf1..8ac1a132df0ff9 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 e9f462c5359d56..cb88e438c9179a 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 + +