Adding interface to wrap surface interactions for LogBox

Summary:
This diff introduces a new interface named `SurfaceDelegate`. The interface abstracts the API for interacting with a surface, which is required for platforms other than mobile to implement how it wants to show and hide a surface. For existing Mobile use cases, the `LogBoxDialogSurfaceDelegate` is provided as a fallback solution so everything still works.

Changelog:
[Android][Added] - Add SurfaceDelegate abstraction to support interaction in multiple platforms and provide default implementation in LogBoxModule

Reviewed By: mdvacca

Differential Revision: D31132285

fbshipit-source-id: 13315a8bc5b7bcaee9b5e53ef5c6f6cc8cb01f31
This commit is contained in:
Xin Chen
2021-09-28 14:57:37 -07:00
committed by Facebook GitHub Bot
parent f2eecae63d
commit 652cb541a5
14 changed files with 288 additions and 53 deletions
@@ -76,6 +76,7 @@ import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.react.devsupport.DevSupportManagerFactory;
@@ -232,7 +233,8 @@ public class ReactInstanceManager {
int minTimeLeftInFrameForNonBatchedOperationMs,
@Nullable JSIModulePackage jsiModulePackage,
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers,
@Nullable ReactPackageTurboModuleManagerDelegate.Builder tmmDelegateBuilder) {
@Nullable ReactPackageTurboModuleManagerDelegate.Builder tmmDelegateBuilder,
@Nullable SurfaceDelegateFactory surfaceDelegateFactory) {
FLog.d(TAG, "ReactInstanceManager.ctor()");
initializeSoLoaderIfNecessary(applicationContext);
@@ -259,7 +261,8 @@ public class ReactInstanceManager {
redBoxHandler,
devBundleDownloadListener,
minNumShakes,
customPackagerCommandHandlers);
customPackagerCommandHandlers,
surfaceDelegateFactory);
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
mBridgeIdleDebugListener = bridgeIdleDebugListener;
mLifecycleState = initialLifecycleState;
@@ -22,6 +22,7 @@ import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.NotThreadSafeBridgeIdleDebugListener;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.DefaultDevSupportManagerFactory;
import com.facebook.react.devsupport.DevSupportManagerFactory;
import com.facebook.react.devsupport.RedBoxHandler;
@@ -63,6 +64,7 @@ public class ReactInstanceManagerBuilder {
private @Nullable JSIModulePackage mJSIModulesPackage;
private @Nullable Map<String, RequestHandler> mCustomPackagerCommandHandlers;
private @Nullable ReactPackageTurboModuleManagerDelegate.Builder mTMMDelegateBuilder;
private @Nullable SurfaceDelegateFactory mSurfaceDelegateFactory;
/* package protected */ ReactInstanceManagerBuilder() {}
@@ -195,6 +197,20 @@ public class ReactInstanceManagerBuilder {
return this;
}
/**
* When the {@link SurfaceDelegateFactory} is provided, it will be used for native modules to get
* a {@link SurfaceDelegate} to interact with the platform specific surface that they that needs
* to be rendered in. For mobile platform this is default to be null so that these modules will
* need to provide a default surface delegate. One example of such native module is LogBoxModule,
* which is rendered in mobile platform with LogBoxDialog, while in VR platform with custom layer
* provided by runtime.
*/
public ReactInstanceManagerBuilder setSurfaceDelegateFactory(
@Nullable final SurfaceDelegateFactory surfaceDelegateFactory) {
mSurfaceDelegateFactory = surfaceDelegateFactory;
return this;
}
/**
* Sets the initial lifecycle state of the host. For example, if the host is already resumed at
* creation time, we wouldn't expect an onResume call until we get an onPause call.
@@ -322,7 +338,8 @@ public class ReactInstanceManagerBuilder {
mMinTimeLeftInFrameForNonBatchedOperationMs,
mJSIModulesPackage,
mCustomPackagerCommandHandlers,
mTMMDelegateBuilder);
mTMMDelegateBuilder,
mSurfaceDelegateFactory);
}
private JavaScriptExecutorFactory getDefaultJSExecutorFactory(
@@ -15,6 +15,8 @@ import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.DevSupportManagerFactory;
import com.facebook.react.devsupport.RedBoxHandler;
import com.facebook.react.uimanager.UIImplementationProvider;
@@ -71,6 +73,7 @@ public abstract class ReactNativeHost {
.setUseDeveloperSupport(getUseDeveloperSupport())
.setDevSupportManagerFactory(getDevSupportManagerFactory())
.setRequireActivity(getShouldRequireActivity())
.setSurfaceDelegateFactory(getSurfaceDelegateFactory())
.setRedBoxHandler(getRedBoxHandler())
.setJavaScriptExecutorFactory(getJavaScriptExecutorFactory())
.setUIImplementationProvider(getUIImplementationProvider())
@@ -132,6 +135,21 @@ public abstract class ReactNativeHost {
return true;
}
/**
* Return the {@link SurfaceDelegateFactory} used by NativeModules to get access to a {@link
* SurfaceDelegate} to interact with a surface. By default in the mobile platform the {@link
* SurfaceDelegate} it returns is null, and the NativeModule needs to implement its own {@link
* SurfaceDelegate} to decide how it would interact with its own container surface.
*/
public SurfaceDelegateFactory getSurfaceDelegateFactory() {
return new SurfaceDelegateFactory() {
@Override
public @Nullable SurfaceDelegate createSurfaceDelegate(String moduleName) {
return null;
}
};
}
/**
* Returns the name of the main module. Determines the URL used to fetch the JS bundle from Metro.
* It is only used when dev support is enabled. This is the first file to be executed once the
@@ -0,0 +1,40 @@
/*
* 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.common;
/**
* Interface for handling a surface in React Native. In mobile platform a surface can be any
* container that holds some {@link View}. For example, a Dialog can be a surface to wrap content
* view object as needed. In VR platform, a surface is provided by Shell panel app sdk, which
* requires custom logic to show/hide. NativeModules requires a surface will delegate interactions
* with the surface to a SurfaceDelegate.
*/
public interface SurfaceDelegate {
/**
* Create the React content view that uses the appKey as the React application name
*
* @param appKey
*/
void createContentView(String appKey);
/**
* Check if the content view is created and ready to be shown
*
* @return true if the content view is ready to be shown
*/
boolean isContentViewReady();
/** Destroy the React content view to avoid memory leak */
void destroyContentView();
/** Show the surface containing the React content view */
void show();
/** Hide the surface containing the React content view */
void hide();
}
@@ -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.common;
/**
* Factory to create a {@link SurfaceDelegate}. The moduleName is needed to help the factory decide
* which surface to return {@link SurfaceDelegate} that the given module should use to interact
* with.
*/
public interface SurfaceDelegateFactory {
/**
* Create a {@link SurfaceDelegate} instance which is used to interact with a surface of platform
* the app is running in.
*
* @param moduleName the module name that will be using the surface
* @return {@link SurfaceDelegate} instance
*/
SurfaceDelegate createSurfaceDelegate(String moduleName);
}
@@ -57,6 +57,7 @@ rn_android_library(
react_native_dep("third-party/android/androidx:annotation"),
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/modules/debug:interfaces"),
],
)
@@ -22,6 +22,7 @@ import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.common.futures.SimpleSettableFuture;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
@@ -71,7 +72,8 @@ public final class BridgeDevSupportManager extends DevSupportManagerBase {
@Nullable RedBoxHandler redBoxHandler,
@Nullable DevBundleDownloadListener devBundleDownloadListener,
int minNumShakes,
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers) {
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers,
@Nullable SurfaceDelegateFactory surfaceDelegateFactory) {
super(
applicationContext,
reactInstanceManagerHelper,
@@ -80,7 +82,8 @@ public final class BridgeDevSupportManager extends DevSupportManagerBase {
redBoxHandler,
devBundleDownloadListener,
minNumShakes,
customPackagerCommandHandlers);
customPackagerCommandHandlers,
surfaceDelegateFactory);
if (getDevSettings().isStartSamplingProfilerOnInit()) {
// Only start the profiler. If its already running, there is an error
@@ -9,6 +9,7 @@ package com.facebook.react.devsupport;
import android.content.Context;
import androidx.annotation.Nullable;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.packagerconnection.RequestHandler;
@@ -41,6 +42,7 @@ public class DefaultDevSupportManagerFactory implements DevSupportManagerFactory
null,
null,
minNumShakes,
null,
null);
}
@@ -53,7 +55,8 @@ public class DefaultDevSupportManagerFactory implements DevSupportManagerFactory
@Nullable RedBoxHandler redBoxHandler,
@Nullable DevBundleDownloadListener devBundleDownloadListener,
int minNumShakes,
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers) {
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers,
@Nullable SurfaceDelegateFactory surfaceDelegateFactory) {
if (!enableOnCreate) {
return new DisabledDevSupportManager();
}
@@ -76,7 +79,8 @@ public class DefaultDevSupportManagerFactory implements DevSupportManagerFactory
RedBoxHandler.class,
DevBundleDownloadListener.class,
int.class,
Map.class);
Map.class,
SurfaceDelegateFactory.class);
return (DevSupportManager)
constructor.newInstance(
applicationContext,
@@ -86,7 +90,8 @@ public class DefaultDevSupportManagerFactory implements DevSupportManagerFactory
redBoxHandler,
devBundleDownloadListener,
minNumShakes,
customPackagerCommandHandlers);
customPackagerCommandHandlers,
surfaceDelegateFactory);
} catch (Exception e) {
throw new RuntimeException(
"Requested enabled DevSupportManager, but BridgeDevSupportManager class was not found"
@@ -41,6 +41,8 @@ import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.DebugServerException;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.ShakeDetector;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener;
import com.facebook.react.devsupport.interfaces.BundleLoadCallback;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
@@ -118,6 +120,7 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
private @Nullable Map<String, RequestHandler> mCustomPackagerCommandHandlers;
private @Nullable Activity currentActivity;
private @Nullable final SurfaceDelegateFactory mSurfaceDelegateFactory;
public DevSupportManagerBase(
Context applicationContext,
@@ -127,7 +130,8 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
@Nullable RedBoxHandler redBoxHandler,
@Nullable DevBundleDownloadListener devBundleDownloadListener,
int minNumShakes,
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers) {
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers,
@Nullable SurfaceDelegateFactory surfaceDelegateFactory) {
mReactInstanceDevHelper = reactInstanceDevHelper;
mApplicationContext = applicationContext;
mJSAppBundleName = packagerPathForJSBundleName;
@@ -202,7 +206,8 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
mRedBoxHandler = redBoxHandler;
mDevLoadingViewController = new DevLoadingViewController(reactInstanceDevHelper);
}
mSurfaceDelegateFactory = surfaceDelegateFactory;
};
protected abstract String getUniqueTag();
@@ -1204,4 +1209,18 @@ public abstract class DevSupportManagerBase implements DevSupportManager {
DevSupportManager.PackagerLocationCustomizer packagerLocationCustomizer) {
mPackagerLocationCustomizer = packagerLocationCustomizer;
}
@Override
public @Nullable Activity getCurrentActivity() {
return mReactInstanceDevHelper.getCurrentActivity();
}
@Override
public @Nullable SurfaceDelegate createSurfaceDelegate(String moduleName) {
if (mSurfaceDelegateFactory == null) {
return null;
}
return mSurfaceDelegateFactory.createSurfaceDelegate(moduleName);
}
}
@@ -9,6 +9,7 @@ package com.facebook.react.devsupport;
import android.content.Context;
import androidx.annotation.Nullable;
import com.facebook.react.common.SurfaceDelegateFactory;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.packagerconnection.RequestHandler;
@@ -23,5 +24,6 @@ public interface DevSupportManagerFactory {
@Nullable RedBoxHandler redBoxHandler,
@Nullable DevBundleDownloadListener devBundleDownloadListener,
int minNumShakes,
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers);
@Nullable Map<String, RequestHandler> customPackagerCommandHandlers,
@Nullable SurfaceDelegateFactory surfaceDelegateFactory);
}
@@ -7,11 +7,13 @@
package com.facebook.react.devsupport;
import android.app.Activity;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.devsupport.interfaces.BundleLoadCallback;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback;
@@ -175,4 +177,14 @@ public class DisabledDevSupportManager implements DevSupportManager {
public void handleException(Exception e) {
mDefaultNativeModuleCallExceptionHandler.handleException(e);
}
@Override
public @Nullable Activity getCurrentActivity() {
return null;
}
@Override
public @Nullable SurfaceDelegate createSurfaceDelegate(String moduleName) {
return null;
}
}
@@ -0,0 +1,92 @@
/*
* 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.infer.annotation.Assertions;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.util.RNLog;
/**
* The implementation of SurfaceDelegate with {@link Activity}. This is the default SurfaceDelegate
* for Mobile.
*/
public class LogBoxDialogSurfaceDelegate implements SurfaceDelegate {
private @Nullable View mReactRootView;
private @Nullable LogBoxDialog mDialog;
private final DevSupportManager mDevSupportManager;
LogBoxDialogSurfaceDelegate(DevSupportManager devSupportManager) {
mDevSupportManager = devSupportManager;
}
@Override
public void createContentView(String appKey) {
Assertions.assertCondition(
appKey.equals("LogBox"), "This surface manager can only create LogBox React application");
mReactRootView = mDevSupportManager.createRootView("LogBox");
if (mReactRootView == null) {
RNLog.e("Unable to launch logbox because react was unable to create the root view");
}
}
@Override
public boolean isContentViewReady() {
return mReactRootView != null;
}
@Override
public void destroyContentView() {
if (mReactRootView != null) {
mDevSupportManager.destroyRootView(mReactRootView);
mReactRootView = null;
}
}
@Override
public void show() {
if (isSurfaceVisible() || !isContentViewReady()) {
return;
}
final @Nullable Activity context = mDevSupportManager.getCurrentActivity();
if (context == null || context.isFinishing()) {
RNLog.e(
"Unable to launch logbox because react activity "
+ "is not available, here is the error that logbox would've displayed: ");
return;
}
mDialog = new LogBoxDialog(context, mReactRootView);
mDialog.setCancelable(false);
mDialog.show();
}
@Override
public void hide() {
if (!isSurfaceVisible()) {
return;
}
if (mReactRootView != null && mReactRootView.getParent() != null) {
((ViewGroup) mReactRootView.getParent()).removeView(mReactRootView);
}
mDialog.dismiss();
mDialog = null;
}
private boolean isSurfaceVisible() {
return mDialog != null;
}
}
@@ -7,16 +7,13 @@
package com.facebook.react.devsupport;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.facebook.fbreact.specs.NativeLogBoxSpec;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.util.RNLog;
@ReactModule(name = LogBoxModule.NAME)
public class LogBoxModule extends NativeLogBoxSpec {
@@ -24,24 +21,30 @@ public class LogBoxModule extends NativeLogBoxSpec {
public static final String NAME = "LogBox";
private final DevSupportManager mDevSupportManager;
private @Nullable View mReactRootView;
private @Nullable LogBoxDialog mLogBoxDialog;
private final SurfaceDelegate mSurfaceDelegate;
/**
* LogBoxModule can be rendered in different surface. By default, it will use LogBoxDialog to wrap
* the content of logs. In other platform (for example VR), a surfaceDelegate can be provided so
* that the content can be wrapped in custom surface.
*/
public LogBoxModule(ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext);
mDevSupportManager = devSupportManager;
@Nullable SurfaceDelegate surfaceDelegate = devSupportManager.createSurfaceDelegate(NAME);
if (surfaceDelegate != null) {
mSurfaceDelegate = surfaceDelegate;
} else {
mSurfaceDelegate = new LogBoxDialogSurfaceDelegate(devSupportManager);
}
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mReactRootView == null && mDevSupportManager != null) {
mReactRootView = mDevSupportManager.createRootView("LogBox");
if (mReactRootView == null) {
RNLog.e("Unable to launch logbox because react was unable to create the root view");
}
}
mSurfaceDelegate.createContentView("LogBox");
}
});
}
@@ -53,26 +56,17 @@ public class LogBoxModule extends NativeLogBoxSpec {
@Override
public void show() {
if (mReactRootView != null) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mLogBoxDialog == null && mReactRootView != null) {
Activity context = getCurrentActivity();
if (context == null || context.isFinishing()) {
RNLog.e(
"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();
}
}
});
if (!mSurfaceDelegate.isContentViewReady()) {
return;
}
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
mSurfaceDelegate.show();
}
});
}
@Override
@@ -81,13 +75,7 @@ public class LogBoxModule extends NativeLogBoxSpec {
new Runnable() {
@Override
public void run() {
if (mLogBoxDialog != null) {
if (mReactRootView != null && mReactRootView.getParent() != null) {
((ViewGroup) mReactRootView.getParent()).removeView(mReactRootView);
}
mLogBoxDialog.dismiss();
mLogBoxDialog = null;
}
mSurfaceDelegate.hide();
}
});
}
@@ -98,10 +86,7 @@ public class LogBoxModule extends NativeLogBoxSpec {
new Runnable() {
@Override
public void run() {
if (mReactRootView != null) {
mDevSupportManager.destroyRootView(mReactRootView);
mReactRootView = null;
}
mSurfaceDelegate.destroyContentView();
}
});
}
@@ -7,11 +7,13 @@
package com.facebook.react.devsupport.interfaces;
import android.app.Activity;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.SurfaceDelegate;
import com.facebook.react.modules.debug.interfaces.DeveloperSettings;
import java.io.File;
@@ -107,4 +109,16 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
}
void setPackagerLocationCustomizer(PackagerLocationCustomizer packagerLocationCustomizer);
@Nullable
Activity getCurrentActivity();
/**
* Create the surface delegate that the provided module should use to interact with
*
* @param moduleName the module name that helps decide which surface it should interact with
* @return a {@link SurfaceDelegate} instance
*/
@Nullable
SurfaceDelegate createSurfaceDelegate(String moduleName);
}