Skip to content

Commit ca51a4b

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Fall back to app AlertDialog for non AppCompat themes (facebook#44495)
Summary: Pull Request resolved: facebook#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
1 parent ccc267b commit ca51a4b

File tree

2 files changed

+116
-39
lines changed

2 files changed

+116
-39
lines changed

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

Lines changed: 64 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,33 @@ 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.
55+
*
56+
* @returns true if the current activity is a descendant of an AppCompat theme.
57+
*/
58+
private static boolean isAppCompatTheme(Context activityContext) {
59+
TypedArray attributes =
60+
activityContext.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme);
61+
boolean isAppCompat =
62+
attributes.hasValue(androidx.appcompat.R.styleable.AppCompatTheme_windowActionBar);
63+
attributes.recycle();
64+
return isAppCompat;
65+
}
66+
67+
/**
68+
* Creates a dialog compatible only with AppCompat activities. This function should be kept in
69+
* sync with {@link createAppDialog}.
70+
*/
71+
private static Dialog createAppCompatDialog(
72+
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
4373
AlertDialog.Builder builder =
4474
new AlertDialog.Builder(activityContext).setTitle(arguments.getString(ARG_TITLE));
4575

@@ -64,9 +94,42 @@ public static Dialog createDialog(
6494
return builder.create();
6595
}
6696

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

72135
@Override

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

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +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) { dialogModule.showAlert(options, null, null) }
75-
76-
activity.setTheme(APP_COMPAT_THEME)
77-
}
78-
7958
@Test
8059
fun testAllOptions() {
8160
val options =
@@ -93,8 +72,7 @@ class DialogModuleTest {
9372

9473
val fragment = getFragment()
9574

96-
assertNotNull("Fragment was not displayed", fragment)
97-
assertFalse(fragment!!.isCancelable)
75+
assertFalse(fragment.isCancelable)
9876

9977
val dialog = fragment.dialog as AlertDialog
10078
assertEquals("OK", dialog.getButton(DialogInterface.BUTTON_POSITIVE).text.toString())
@@ -110,13 +88,13 @@ class DialogModuleTest {
11088
dialogModule.showAlert(options, null, actionCallback)
11189
shadowOf(getMainLooper()).idle()
11290

113-
val dialog = getFragment()!!.dialog as AlertDialog
91+
val dialog = getFragment().dialog as AlertDialog
11492
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
11593
shadowOf(getMainLooper()).idle()
11694

11795
assertEquals(1, actionCallback.calls)
118-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
119-
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))
12098
}
12199

122100
@Test
@@ -127,13 +105,13 @@ class DialogModuleTest {
127105
dialogModule.showAlert(options, null, actionCallback)
128106
shadowOf(getMainLooper()).idle()
129107

130-
val dialog = getFragment()!!.dialog as AlertDialog
108+
val dialog = getFragment().dialog as AlertDialog
131109
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
132110
shadowOf(getMainLooper()).idle()
133111

134112
assertEquals(1, actionCallback.calls)
135-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
136-
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))
137115
}
138116

139117
@Test
@@ -144,13 +122,13 @@ class DialogModuleTest {
144122
dialogModule.showAlert(options, null, actionCallback)
145123
shadowOf(getMainLooper()).idle()
146124

147-
val dialog = getFragment()!!.dialog as AlertDialog
125+
val dialog = getFragment().dialog as AlertDialog
148126
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()
149127
shadowOf(getMainLooper()).idle()
150128

151129
assertEquals(1, actionCallback.calls)
152-
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
153-
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))
154132
}
155133

156134
@Test
@@ -161,16 +139,52 @@ class DialogModuleTest {
161139
dialogModule.showAlert(options, null, actionCallback)
162140
shadowOf(getMainLooper()).idle()
163141

164-
getFragment()!!.dialog!!.dismiss()
142+
getFragment().dialog?.dismiss()
165143
shadowOf(getMainLooper()).idle()
166144

167145
assertEquals(1, actionCallback.calls)
168-
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args!![0])
146+
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
169147
}
170148

171-
private fun getFragment(): AlertFragment? {
172-
return activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
173-
as? AlertFragment
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()
160+
shadowOf(getMainLooper()).idle()
161+
162+
assertEquals(1, actionCallback.calls)
163+
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
164+
}
165+
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
174188
}
175189

176190
companion object {

0 commit comments

Comments
 (0)