xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java (#50630)

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

Changelog: [Internal] - Convert AlertFragment to Kotlin

Reviewed By: mlord93

Differential Revision: D72729380

fbshipit-source-id: f7d8ee4cd5938c453b865aea60b6f0533bd8fe0e
This commit is contained in:
Peter Abbondanzo
2025-04-10 18:26:47 -07:00
committed by Facebook GitHub Bot
parent 7101adf611
commit c51c4b1922
3 changed files with 216 additions and 207 deletions
@@ -2890,15 +2890,19 @@ public abstract interface class com/facebook/react/modules/debug/interfaces/Deve
public abstract fun setStartSamplingProfilerOnInit (Z)V
}
public class com/facebook/react/modules/dialog/AlertFragment : androidx/fragment/app/DialogFragment, android/content/DialogInterface$OnClickListener {
public final class com/facebook/react/modules/dialog/AlertFragment : androidx/fragment/app/DialogFragment, android/content/DialogInterface$OnClickListener {
public static final field Companion Lcom/facebook/react/modules/dialog/AlertFragment$Companion;
public fun <init> ()V
public fun <init> (Lcom/facebook/react/modules/dialog/DialogModule$AlertFragmentListener;Landroid/os/Bundle;)V
public static fun createDialog (Landroid/content/Context;Landroid/os/Bundle;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/Dialog;
public static final fun createDialog (Landroid/content/Context;Landroid/os/Bundle;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/Dialog;
public fun onClick (Landroid/content/DialogInterface;I)V
public fun onCreateDialog (Landroid/os/Bundle;)Landroid/app/Dialog;
public fun onDismiss (Landroid/content/DialogInterface;)V
}
public final class com/facebook/react/modules/dialog/AlertFragment$Companion {
public final fun createDialog (Landroid/content/Context;Landroid/os/Bundle;Landroid/content/DialogInterface$OnClickListener;)Landroid/app/Dialog;
}
public final class com/facebook/react/modules/dialog/DialogModule : com/facebook/fbreact/specs/NativeDialogManagerAndroidSpec, com/facebook/react/bridge/LifecycleEventListener {
public static final field Companion Lcom/facebook/react/modules/dialog/DialogModule$Companion;
public static final field NAME Ljava/lang/String;
@@ -1,204 +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.modules.dialog;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.fragment.app.DialogFragment;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.R;
/** A fragment used to display the dialog. */
@Nullsafe(Nullsafe.Mode.LOCAL)
public class AlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
/* package */ static final String ARG_TITLE = "title";
/* package */ static final String ARG_MESSAGE = "message";
/* package */ static final String ARG_BUTTON_POSITIVE = "button_positive";
/* package */ static final String ARG_BUTTON_NEGATIVE = "button_negative";
/* package */ static final String ARG_BUTTON_NEUTRAL = "button_neutral";
/* package */ static final String ARG_ITEMS = "items";
private final @Nullable DialogModule.AlertFragmentListener mListener;
public AlertFragment() {
mListener = null;
}
@SuppressLint("ValidFragment")
public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bundle arguments) {
mListener = listener;
setArguments(arguments);
}
public static Dialog createDialog(
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
if (isAppCompatTheme(activityContext)) {
return createAppCompatDialog(activityContext, arguments, fragment);
} else {
return createAppDialog(activityContext, arguments, fragment);
}
}
/**
* Checks if the current activity is a descendant of an AppCompat theme. This check is required to
* safely display an AppCompat dialog. If the current activity is not a descendant of an AppCompat
* theme and we attempt to render an AppCompat dialog, this will cause a crash.
*
* @returns true if the current activity is a descendant of an AppCompat theme.
*/
private static boolean isAppCompatTheme(Context activityContext) {
TypedArray attributes =
activityContext.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme);
boolean isAppCompat =
attributes.hasValue(androidx.appcompat.R.styleable.AppCompatTheme_windowActionBar);
attributes.recycle();
return isAppCompat;
}
/**
* Creates a custom dialog title View that has the role of "Heading" and focusable for
* accessibility purposes.
*
* @returns accessible TextView title
*/
private static View getAccessibleTitle(Context activityContext, String titleText) {
LayoutInflater inflater = LayoutInflater.from(activityContext);
// This layout matches the sizing and styling of AlertDialog's title_template (minus the icon)
// since the whole thing gets tossed out when setting a custom title
View titleContainer = inflater.inflate(R.layout.alert_title_layout, null);
TextView accessibleTitle =
Assertions.assertNotNull(titleContainer.findViewById(R.id.alert_title));
accessibleTitle.setText(titleText);
accessibleTitle.setFocusable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
accessibleTitle.setAccessibilityHeading(true);
} else {
ViewCompat.setAccessibilityDelegate(
accessibleTitle,
new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
View view, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(accessibleTitle, info);
info.setHeading(true);
}
});
}
return titleContainer;
}
/**
* Creates a dialog compatible only with AppCompat activities. This function should be kept in
* sync with {@link createAppDialog}.
*/
private static Dialog createAppCompatDialog(
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
AlertDialog.Builder builder = new AlertDialog.Builder(activityContext);
if (arguments.containsKey(ARG_TITLE)) {
String title = Assertions.assertNotNull(arguments.getString(ARG_TITLE));
View accessibleTitle = getAccessibleTitle(activityContext, title);
builder.setCustomTitle(accessibleTitle);
}
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
}
// if both message and items are set, Android will only show the message
// and ignore the items argument entirely
if (arguments.containsKey(ARG_MESSAGE)) {
builder.setMessage(arguments.getString(ARG_MESSAGE));
}
if (arguments.containsKey(ARG_ITEMS)) {
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment);
}
return builder.create();
}
/**
* Creates a dialog compatible with non-AppCompat activities. This function should be kept in sync
* with {@link createAppCompatDialog}.
*
* @deprecated non-AppCompat dialogs are deprecated and will be removed in a future version.
*/
@Deprecated(since = "0.75.0", forRemoval = true)
private static Dialog createAppDialog(
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(activityContext);
if (arguments.containsKey(ARG_TITLE)) {
String title = Assertions.assertNotNull(arguments.getString(ARG_TITLE));
View accessibleTitle = getAccessibleTitle(activityContext, title);
builder.setCustomTitle(accessibleTitle);
}
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
}
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
}
// if both message and items are set, Android will only show the message
// and ignore the items argument entirely
if (arguments.containsKey(ARG_MESSAGE)) {
builder.setMessage(arguments.getString(ARG_MESSAGE));
}
if (arguments.containsKey(ARG_ITEMS)) {
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment);
}
return builder.create();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return createDialog(requireActivity(), requireArguments(), this);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mListener != null) {
mListener.onClick(dialog, which);
}
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mListener != null) {
mListener.onDismiss(dialog);
}
}
}
@@ -0,0 +1,209 @@
/*
* 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.modules.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.fragment.app.DialogFragment
import com.facebook.infer.annotation.Assertions
import com.facebook.react.R
import com.facebook.react.modules.dialog.DialogModule.AlertFragmentListener
/** A fragment used to display the dialog. */
@SuppressLint("fragment_subclass_nonempty_constructor")
public class AlertFragment : DialogFragment, DialogInterface.OnClickListener {
private val listener: AlertFragmentListener?
public constructor() {
listener = null
}
@SuppressLint("ValidFragment")
internal constructor(listener: AlertFragmentListener?, arguments: Bundle?) {
this.listener = listener
setArguments(arguments)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
createDialog(requireActivity(), requireArguments(), this)
override fun onClick(dialog: DialogInterface, which: Int) {
listener?.onClick(dialog, which)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
listener?.onDismiss(dialog)
}
public companion object {
internal const val ARG_TITLE: String = "title"
internal const val ARG_MESSAGE: String = "message"
internal const val ARG_BUTTON_POSITIVE: String = "button_positive"
internal const val ARG_BUTTON_NEGATIVE: String = "button_negative"
internal const val ARG_BUTTON_NEUTRAL: String = "button_neutral"
internal const val ARG_ITEMS: String = "items"
@JvmStatic
@Suppress("DEPRECATION")
public fun createDialog(
activityContext: Context,
arguments: Bundle,
fragment: DialogInterface.OnClickListener
): Dialog =
if (isAppCompatTheme(activityContext)) {
createAppCompatDialog(activityContext, arguments, fragment)
} else {
createAppDialog(activityContext, arguments, fragment)
}
/**
* Checks if the current activity is a descendant of an AppCompat theme. This check is required
* to safely display an AppCompat dialog. If the current activity is not a descendant of an
* AppCompat theme and we attempt to render an AppCompat dialog, this will cause a crash.
*
* @returns true if the current activity is a descendant of an AppCompat theme.
*/
private fun isAppCompatTheme(activityContext: Context): Boolean {
val attributes =
activityContext.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme)
val isAppCompat =
attributes.hasValue(androidx.appcompat.R.styleable.AppCompatTheme_windowActionBar)
attributes.recycle()
return isAppCompat
}
/**
* Creates a custom dialog title View that has the role of "Heading" and focusable for
* accessibility purposes.
*
* @returns accessible TextView title
*/
private fun getAccessibleTitle(activityContext: Context, titleText: String): View {
val inflater = LayoutInflater.from(activityContext)
// This layout matches the sizing and styling of AlertDialog's title_template (minus the icon)
// since the whole thing gets tossed out when setting a custom title
val titleContainer: View = inflater.inflate(R.layout.alert_title_layout, null)
val accessibleTitle: TextView =
Assertions.assertNotNull<TextView>(
titleContainer.findViewById<TextView>(R.id.alert_title))
accessibleTitle.text = titleText
accessibleTitle.isFocusable = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
accessibleTitle.isAccessibilityHeading = true
} else {
ViewCompat.setAccessibilityDelegate(
accessibleTitle,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
view: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(accessibleTitle, info)
info.isHeading = true
}
})
}
return titleContainer
}
/**
* Creates a dialog compatible only with AppCompat activities. This function should be kept in
* sync with [createAppDialog].
*/
private fun createAppCompatDialog(
activityContext: Context,
arguments: Bundle,
fragment: DialogInterface.OnClickListener
): Dialog {
val builder = AlertDialog.Builder(activityContext)
if (arguments.containsKey(ARG_TITLE)) {
val title = Assertions.assertNotNull<String>(arguments.getString(ARG_TITLE))
val accessibleTitle = getAccessibleTitle(activityContext, title)
builder.setCustomTitle(accessibleTitle)
}
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment)
}
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment)
}
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment)
}
// if both message and items are set, Android will only show the message
// and ignore the items argument entirely
if (arguments.containsKey(ARG_MESSAGE)) {
builder.setMessage(arguments.getString(ARG_MESSAGE))
}
if (arguments.containsKey(ARG_ITEMS)) {
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment)
}
return builder.create()
}
/**
* Creates a dialog compatible with non-AppCompat activities. This function should be kept in
* sync with [createAppCompatDialog].
*/
@Deprecated(
"non-AppCompat dialogs are deprecated and will be removed in a future version.",
replaceWith = ReplaceWith("createAppCompatDialog(activityContext, arguments, fragment)"),
)
private fun createAppDialog(
activityContext: Context,
arguments: Bundle,
fragment: DialogInterface.OnClickListener
): Dialog {
val builder = android.app.AlertDialog.Builder(activityContext)
if (arguments.containsKey(ARG_TITLE)) {
val title = Assertions.assertNotNull<String>(arguments.getString(ARG_TITLE))
val accessibleTitle = getAccessibleTitle(activityContext, title)
builder.setCustomTitle(accessibleTitle)
}
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment)
}
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment)
}
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment)
}
// if both message and items are set, Android will only show the message
// and ignore the items argument entirely
if (arguments.containsKey(ARG_MESSAGE)) {
builder.setMessage(arguments.getString(ARG_MESSAGE))
}
if (arguments.containsKey(ARG_ITEMS)) {
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment)
}
return builder.create()
}
}
}