diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.java deleted file mode 100644 index 207074b106f..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and 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.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.Window; -import android.widget.FrameLayout; -import androidx.annotation.Nullable; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import com.facebook.common.logging.FLog; -import com.facebook.react.R; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.SurfaceDelegate; -import com.facebook.react.devsupport.interfaces.DevSupportManager; -import com.facebook.react.devsupport.interfaces.RedBoxHandler; -import java.util.Objects; - -/** - * The implementation of SurfaceDelegate with {@link Activity}. This is the default SurfaceDelegate - * for Mobile. - */ -class RedBoxDialogSurfaceDelegate implements SurfaceDelegate { - - private final DoubleTapReloadRecognizer mDoubleTapReloadRecognizer; - private final DevSupportManager mDevSupportManager; - - private @Nullable Dialog mDialog; - private @Nullable RedBoxContentView mRedBoxContentView; - - public RedBoxDialogSurfaceDelegate(DevSupportManager devSupportManager) { - mDevSupportManager = devSupportManager; - mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer(); - } - - @Override - public void createContentView(String appKey) { - // The content view is created in android instead of using react app. Hence the appKey is not - // used here. - RedBoxHandler redBoxHandler = mDevSupportManager.getRedBoxHandler(); - Activity context = mDevSupportManager.getCurrentActivity(); - if (context == null || context.isFinishing()) { - @Nullable String message = mDevSupportManager.getLastErrorTitle(); - FLog.e( - ReactConstants.TAG, - "Unable to launch redbox because react activity " - + "are not available, here is the error that redbox would've displayed: " - + (message != null ? message : "N/A")); - return; - } - // Create a new RedBox when currentActivity get updated - mRedBoxContentView = new RedBoxContentView(context, mDevSupportManager, redBoxHandler); - mRedBoxContentView.init(); - } - - @Override - public boolean isContentViewReady() { - return mRedBoxContentView != null; - } - - @Override - public void destroyContentView() { - mRedBoxContentView = null; - } - - @Override - public void show() { - @Nullable String message = mDevSupportManager.getLastErrorTitle(); - Activity context = mDevSupportManager.getCurrentActivity(); - if (context == null || context.isFinishing()) { - final @Nullable ReactContext reactContext = mDevSupportManager.getCurrentReactContext(); - if (reactContext != null) { - /** - * If the activity isn't available, try again after the next onHostResume(). onHostResume() - * is when the activity gets attached to the react native. - */ - runAfterHostResume(reactContext, this::show); - return; - } - - FLog.e( - ReactConstants.TAG, - "Unable to launch redbox because react activity and react context " - + "is not available, here is the error that redbox would've displayed: " - + (message != null ? message : "N/A")); - return; - } - - if (mRedBoxContentView == null || mRedBoxContentView.getContext() != context) { - // Create a new RedBox when currentActivity get updated - createContentView("RedBox"); - } - - mRedBoxContentView.refreshContentView(); - if (mDialog == null) { - mDialog = - new Dialog(context, R.style.Theme_Catalyst_RedBox) { - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_MENU) { - mDevSupportManager.showDevOptionsDialog(); - return true; - } - if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) { - mDevSupportManager.handleReloadJS(); - } - return super.onKeyUp(keyCode, event); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - Objects.requireNonNull(getWindow()); - // set background color so it will show below transparent system bars on forced - // edge-to-edge - getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK)); - // register insets listener to update margins on the ReactRootView to avoid overlap w/ - // system bars - int insetsType = - WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(); - - ViewCompat.setOnApplyWindowInsetsListener( - mRedBoxContentView, - (view, windowInsetsCompat) -> { - Insets insets = windowInsetsCompat.getInsets(insetsType); - - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); - lp.setMargins(insets.left, insets.top, insets.right, insets.bottom); - - return WindowInsetsCompat.CONSUMED; - }); - } - }; - mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - mDialog.setContentView(mRedBoxContentView); - } - mDialog.show(); - } - - private static void runAfterHostResume(ReactContext reactContext, Runnable runnable) { - reactContext.addLifecycleEventListener( - new LifecycleEventListener() { - @Override - public void onHostResume() { - runnable.run(); - reactContext.removeLifecycleEventListener(this); - } - - @Override - public void onHostPause() {} - - @Override - public void onHostDestroy() {} - }); - } - - @Override - public void hide() { - // dismiss redbox if exists - if (mDialog != null) { - try { - mDialog.dismiss(); - } catch (IllegalArgumentException e) { - FLog.e( - ReactConstants.TAG, "RedBoxDialogSurfaceDelegate: error while dismissing dialog: ", e); - } - destroyContentView(); - mDialog = null; - } - } - - @Override - public boolean isShowing() { - return mDialog != null && mDialog.isShowing(); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.kt new file mode 100644 index 00000000000..2850375582f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/RedBoxDialogSurfaceDelegate.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) Meta Platforms, Inc. and 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.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.view.Window +import android.widget.FrameLayout +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.facebook.common.logging.FLog +import com.facebook.react.R +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.ReactContext +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.SurfaceDelegate +import com.facebook.react.devsupport.interfaces.DevSupportManager + +/** + * The implementation of SurfaceDelegate with [Activity]. This is the default [SurfaceDelegate] for + * Mobile. + */ +internal class RedBoxDialogSurfaceDelegate(private val devSupportManager: DevSupportManager) : + SurfaceDelegate { + private val doubleTapReloadRecognizer = DoubleTapReloadRecognizer() + + private var dialog: Dialog? = null + private var redBoxContentView: RedBoxContentView? = null + + override fun createContentView(appKey: String) { + // The content view is created in android instead of using react app. Hence the appKey is not + // used here. + val redBoxHandler = devSupportManager.redBoxHandler + val context = devSupportManager.currentActivity + if (context == null || context.isFinishing) { + val message = devSupportManager.lastErrorTitle + FLog.e( + ReactConstants.TAG, + "Unable to launch redbox because react activity is not available, here is the error that redbox would've displayed: ${(message ?: "N/A")}") + return + } + + // Create a new RedBox when currentActivity get updated + redBoxContentView = + RedBoxContentView(context, devSupportManager, redBoxHandler).also { it.init() } + } + + override fun isContentViewReady(): Boolean = redBoxContentView != null + + override fun destroyContentView() { + redBoxContentView = null + } + + override fun show() { + val message: String? = devSupportManager.lastErrorTitle + val context: Activity? = devSupportManager.currentActivity + if (context == null || context.isFinishing) { + devSupportManager.currentReactContext?.let { reactContext -> + /** + * If the activity isn't available, try again after the next onHostResume(). onHostResume() + * is when the activity gets attached to the react native. + */ + runAfterHostResume(reactContext) { this.show() } + return + } + FLog.e( + ReactConstants.TAG, + "Unable to launch redbox because react activity and react context is not available, here is the error that redbox would've displayed: ${message ?: "N/A"}") + return + } + + if (redBoxContentView?.context !== context) { + // Create a new RedBox when currentActivity get updated + createContentView("RedBox") + } + + redBoxContentView?.refreshContentView() + if (dialog == null) { + dialog = + object : Dialog(context, R.style.Theme_Catalyst_RedBox) { + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_MENU) { + devSupportManager.showDevOptionsDialog() + return true + } + if (doubleTapReloadRecognizer.didDoubleTapR(keyCode, currentFocus)) { + devSupportManager.handleReloadJS() + } + return super.onKeyUp(keyCode, event) + } + + override fun onCreate(savedInstanceState: Bundle?) { + // set background color so it will show below transparent system bars on forced + // edge-to-edge + checkNotNull(window).setBackgroundDrawable(ColorDrawable(Color.BLACK)) + // register insets listener to update margins on the ReactRootView to avoid + // overlap w/ system bars + val insetsType: Int = + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + + ViewCompat.setOnApplyWindowInsetsListener(checkNotNull(redBoxContentView)) { + view: View, + windowInsetsCompat: WindowInsetsCompat -> + val insets: Insets = windowInsetsCompat.getInsets(insetsType) + val lp: FrameLayout.LayoutParams = view.layoutParams as FrameLayout.LayoutParams + lp.setMargins(insets.left, insets.top, insets.right, insets.bottom) + WindowInsetsCompat.CONSUMED + } + } + } + .apply { + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(checkNotNull(redBoxContentView)) + } + } + dialog?.show() + } + + override fun hide() { + try { + // dismiss redbox if exists + dialog?.dismiss() + } catch (e: IllegalArgumentException) { + FLog.e(ReactConstants.TAG, "RedBoxDialogSurfaceDelegate: error while dismissing dialog: ", e) + } + destroyContentView() + dialog = null + } + + override fun isShowing(): Boolean = dialog?.isShowing == true + + companion object { + private fun runAfterHostResume(reactContext: ReactContext, runnable: Runnable) { + reactContext.addLifecycleEventListener( + object : LifecycleEventListener { + override fun onHostResume() { + runnable.run() + reactContext.removeLifecycleEventListener(this) + } + + override fun onHostPause() = Unit + + override fun onHostDestroy() = Unit + }) + } + } +}