Skip to content

Commit 297ded9

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Fall back to app AlertDialog for non AppCompat themes (#44495)
Summary: Pull Request resolved: #44495 ## Summary Migrates the `AlertFragment` from `android.app.AlertDialog` to `androidx.appcompat.app.AlertDialog`. This backports tons of fixes that have gone into the AlertDialog component over the years, including proper line wrapping of button text, dark mode support, alignment of buttons, etc. This change provides a fallback to the original `android.app.AlertDialog` if the current activity is not an AppCompat descendant. ## For consideration - Alert dialog themes may no longer need the `android` namespace, meaning themes can now be specified as `alertDialogTheme` rather than `android:alertDialogTheme`. ## Changelog: [Android] [Changed] - Migrated `AlertFragment` dialog builder to use `androidx.appcompat` Reviewed By: zeyap Differential Revision: D57113950 fbshipit-source-id: ba5109c9d79b6ceb042ff93eebe796a2d14ebd63
1 parent 600d3f6 commit 297ded9

File tree

2 files changed

+118
-42
lines changed

2 files changed

+118
-42
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/dialog/AlertFragment.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
import android.app.Dialog;
1212
import android.content.Context;
1313
import android.content.DialogInterface;
14+
import android.content.res.TypedArray;
1415
import android.os.Bundle;
1516
import androidx.annotation.Nullable;
1617
import androidx.appcompat.app.AlertDialog;
1718
import androidx.fragment.app.DialogFragment;
19+
import com.facebook.infer.annotation.Nullsafe;
1820

1921
/** A fragment used to display the dialog. */
22+
@Nullsafe(Nullsafe.Mode.LOCAL)
2023
public class AlertFragment extends DialogFragment implements DialogInterface.OnClickListener {
2124

2225
/* package */ static final String ARG_TITLE = "title";
@@ -40,6 +43,35 @@ public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bund
4043

4144
public static Dialog createDialog(
4245
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
46+
if (isAppCompatTheme(activityContext)) {
47+
return createAppCompatDialog(activityContext, arguments, fragment);
48+
} else {
49+
return createAppDialog(activityContext, arguments, fragment);
50+
}
51+
}
52+
53+
/**
54+
* Checks if the current activity is a descendant of an AppCompat theme. This check is required to
55+
* safely display an AppCompat dialog. If the current activity is not a descendant of an AppCompat
56+
* theme and we attempt to render an AppCompat dialog, this will cause a crash.
57+
*
58+
* @returns true if the current activity is a descendant of an AppCompat theme.
59+
*/
60+
private static boolean isAppCompatTheme(Context activityContext) {
61+
TypedArray attributes =
62+
activityContext.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme);
63+
boolean isAppCompat =
64+
attributes.hasValue(androidx.appcompat.R.styleable.AppCompatTheme_windowActionBar);
65+
attributes.recycle();
66+
return isAppCompat;
67+
}
68+
69+
/**
70+
* Creates a dialog compatible only with AppCompat activities. This function should be kept in
71+
* sync with {@link createAppDialog}.
72+
*/
73+
private static Dialog createAppCompatDialog(
74+
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
4375
AlertDialog.Builder builder =
4476
new AlertDialog.Builder(activityContext).setTitle(arguments.getString(ARG_TITLE));
4577

@@ -64,9 +96,42 @@ public static Dialog createDialog(
6496
return builder.create();
6597
}
6698

99+
/**
100+
* Creates a dialog compatible with non-AppCompat activities. This function should be kept in sync
101+
* with {@link createAppCompatDialog}.
102+
*
103+
* @deprecated non-AppCompat dialogs are deprecated and will be removed in a future version.
104+
*/
105+
private static Dialog createAppDialog(
106+
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
107+
android.app.AlertDialog.Builder builder =
108+
new android.app.AlertDialog.Builder(activityContext)
109+
.setTitle(arguments.getString(ARG_TITLE));
110+
111+
if (arguments.containsKey(ARG_BUTTON_POSITIVE)) {
112+
builder.setPositiveButton(arguments.getString(ARG_BUTTON_POSITIVE), fragment);
113+
}
114+
if (arguments.containsKey(ARG_BUTTON_NEGATIVE)) {
115+
builder.setNegativeButton(arguments.getString(ARG_BUTTON_NEGATIVE), fragment);
116+
}
117+
if (arguments.containsKey(ARG_BUTTON_NEUTRAL)) {
118+
builder.setNeutralButton(arguments.getString(ARG_BUTTON_NEUTRAL), fragment);
119+
}
120+
// if both message and items are set, Android will only show the message
121+
// and ignore the items argument entirely
122+
if (arguments.containsKey(ARG_MESSAGE)) {
123+
builder.setMessage(arguments.getString(ARG_MESSAGE));
124+
}
125+
if (arguments.containsKey(ARG_ITEMS)) {
126+
builder.setItems(arguments.getCharSequenceArray(ARG_ITEMS), fragment);
127+
}
128+
129+
return builder.create();
130+
}
131+
67132
@Override
68133
public Dialog onCreateDialog(Bundle savedInstanceState) {
69-
return createDialog(getActivity(), getArguments(), this);
134+
return createDialog(requireActivity(), requireArguments(), this);
70135
}
71136

72137
@Override

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/dialog/DialogModuleTest.kt

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,38 +47,14 @@ class DialogModuleTest {
4747

4848
@Before
4949
fun setUp() {
50-
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
51-
activity = activityController.create().start().resume().get()
52-
// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
53-
// raising an exception
54-
activity.setTheme(APP_COMPAT_THEME)
55-
56-
val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
57-
whenever(context.hasActiveReactInstance()).thenReturn(true)
58-
whenever(context.currentActivity).thenReturn(activity)
59-
60-
dialogModule = DialogModule(context)
61-
dialogModule.onHostResume()
50+
setupActivity()
6251
}
6352

6453
@After
6554
fun tearDown() {
6655
activityController.pause().stop().destroy()
6756
}
6857

69-
@Test
70-
fun testIllegalActivityTheme() {
71-
val options = JavaOnlyMap()
72-
activity.setTheme(NON_APP_COMPAT_THEME)
73-
74-
assertThrows(NullPointerException::class.java) {
75-
dialogModule.showAlert(options, null, null)
76-
shadowOf(getMainLooper()).idle()
77-
}
78-
79-
activity.setTheme(APP_COMPAT_THEME)
80-
}
81-
8258
@Test
8359
fun testAllOptions() {
8460
val options =
@@ -96,8 +72,7 @@ class DialogModuleTest {
9672

9773
val fragment = getFragment()
9874

99-
assertNotNull("Fragment was not displayed", fragment)
100-
assertFalse(fragment!!.isCancelable)
75+
assertFalse(fragment.isCancelable)
10176

10277
val dialog = fragment.dialog as AlertDialog
10378
assertEquals("OK", dialog.getButton(DialogInterface.BUTTON_POSITIVE).text.toString())
@@ -113,13 +88,13 @@ class DialogModuleTest {
11388
dialogModule.showAlert(options, null, actionCallback)
11489
shadowOf(getMainLooper()).idle()
11590

116-
val dialog = getFragment()!!.dialog as AlertDialog
91+
val dialog = getFragment().dialog as AlertDialog
11792
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
11893
shadowOf(getMainLooper()).idle()
11994

12095
assertEquals(1, actionCallback.calls)
121-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
122-
assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.args!![1])
96+
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
97+
assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.args?.get(1))
12398
}
12499

125100
@Test
@@ -130,13 +105,13 @@ class DialogModuleTest {
130105
dialogModule.showAlert(options, null, actionCallback)
131106
shadowOf(getMainLooper()).idle()
132107

133-
val dialog = getFragment()!!.dialog as AlertDialog
108+
val dialog = getFragment().dialog as AlertDialog
134109
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
135110
shadowOf(getMainLooper()).idle()
136111

137112
assertEquals(1, actionCallback.calls)
138-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
139-
assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.args!![1])
113+
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
114+
assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.args?.get(1))
140115
}
141116

142117
@Test
@@ -147,13 +122,13 @@ class DialogModuleTest {
147122
dialogModule.showAlert(options, null, actionCallback)
148123
shadowOf(getMainLooper()).idle()
149124

150-
val dialog = getFragment()!!.dialog as AlertDialog
125+
val dialog = getFragment().dialog as AlertDialog
151126
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()
152127
shadowOf(getMainLooper()).idle()
153128

154129
assertEquals(1, actionCallback.calls)
155-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
156-
assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.args!![1])
130+
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
131+
assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.args?.get(1))
157132
}
158133

159134
@Test
@@ -164,16 +139,52 @@ class DialogModuleTest {
164139
dialogModule.showAlert(options, null, actionCallback)
165140
shadowOf(getMainLooper()).idle()
166141

167-
getFragment()!!.dialog!!.dismiss()
142+
getFragment().dialog?.dismiss()
143+
shadowOf(getMainLooper()).idle()
144+
145+
assertEquals(1, actionCallback.calls)
146+
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
147+
}
148+
149+
@Test
150+
fun testNonAppCompatActivityTheme() {
151+
setupActivity(NON_APP_COMPAT_THEME)
152+
153+
val options = JavaOnlyMap()
154+
155+
val actionCallback = SimpleCallback()
156+
dialogModule.showAlert(options, null, actionCallback)
157+
shadowOf(getMainLooper()).idle()
158+
159+
getFragment().dialog?.dismiss()
168160
shadowOf(getMainLooper()).idle()
169161

170162
assertEquals(1, actionCallback.calls)
171-
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args!![0])
163+
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
172164
}
173165

174-
private fun getFragment(): AlertFragment? {
175-
return activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
176-
as? AlertFragment
166+
private fun setupActivity(theme: Int = APP_COMPAT_THEME) {
167+
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
168+
activity = activityController.create().start().resume().get()
169+
170+
// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
171+
// raising an exception
172+
activity.setTheme(theme)
173+
174+
val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
175+
whenever(context.hasActiveReactInstance()).thenReturn(true)
176+
whenever(context.currentActivity).thenReturn(activity)
177+
178+
dialogModule = DialogModule(context)
179+
dialogModule.onHostResume()
180+
}
181+
182+
private fun getFragment(): AlertFragment {
183+
val maybeFragment = activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
184+
if (maybeFragment == null || !(maybeFragment is AlertFragment)) {
185+
error("Fragment was not displayed")
186+
}
187+
return maybeFragment
177188
}
178189

179190
companion object {

0 commit comments

Comments
 (0)