Convert com.facebook.react.devsupport.RedBoxDialogSurfaceDelegate to Kotlin (#49091)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49091

Just another Kotlin migration for the devsupport package.

Changelog:
[Internal] [Changed] -

Reviewed By: Abbondanzo

Differential Revision: D68954219

fbshipit-source-id: 4d82a8965207916acec4fa3779627b9e93bb8b10
This commit is contained in:
Nicola Corti
2025-02-04 07:02:40 -08:00
committed by Facebook GitHub Bot
parent 95f9ca5b5b
commit 55b3839183
2 changed files with 159 additions and 188 deletions
@@ -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();
}
}
@@ -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
})
}
}
}