Skip to content

Commit

Permalink
Fall back to app AlertDialog for non AppCompat themes (facebook#44495)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Abbondanzo authored and facebook-github-bot committed May 10, 2024
1 parent bc1a867 commit 44df6df
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
Expand Down Expand Up @@ -40,6 +41,33 @@ public AlertFragment(@Nullable DialogModule.AlertFragmentListener listener, Bund

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.
*
* @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 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).setTitle(arguments.getString(ARG_TITLE));

Expand All @@ -64,6 +92,39 @@ public static Dialog createDialog(
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.
*/
private static Dialog createAppDialog(
Context activityContext, Bundle arguments, DialogInterface.OnClickListener fragment) {
android.app.AlertDialog.Builder builder =
new android.app.AlertDialog.Builder(activityContext)
.setTitle(arguments.getString(ARG_TITLE));

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(getActivity(), getArguments(), this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,14 @@ class DialogModuleTest {

@Before
fun setUp() {
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
activity = activityController.create().start().resume().get()
// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
// raising an exception
activity.setTheme(APP_COMPAT_THEME)

val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
whenever(context.hasActiveReactInstance()).thenReturn(true)
whenever(context.currentActivity).thenReturn(activity)

dialogModule = DialogModule(context)
dialogModule.onHostResume()
setupActivity()
}

@After
fun tearDown() {
activityController.pause().stop().destroy()
}

@Test
fun testIllegalActivityTheme() {
val options = JavaOnlyMap()
activity.setTheme(NON_APP_COMPAT_THEME)

assertThrows(NullPointerException::class.java) { dialogModule.showAlert(options, null, null) }

activity.setTheme(APP_COMPAT_THEME)
}

@Test
fun testAllOptions() {
val options =
Expand All @@ -93,8 +72,7 @@ class DialogModuleTest {

val fragment = getFragment()

assertNotNull("Fragment was not displayed", fragment)
assertFalse(fragment!!.isCancelable)
assertFalse(fragment.isCancelable)

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

val dialog = getFragment()!!.dialog as AlertDialog
val dialog = getFragment().dialog as AlertDialog
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
shadowOf(getMainLooper()).idle()

assertEquals(1, actionCallback.calls)
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.args!![1])
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
assertEquals(DialogInterface.BUTTON_POSITIVE, actionCallback.args?.get(1))
}

@Test
Expand All @@ -127,13 +105,13 @@ class DialogModuleTest {
dialogModule.showAlert(options, null, actionCallback)
shadowOf(getMainLooper()).idle()

val dialog = getFragment()!!.dialog as AlertDialog
val dialog = getFragment().dialog as AlertDialog
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
shadowOf(getMainLooper()).idle()

assertEquals(1, actionCallback.calls)
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.args!![1])
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
assertEquals(DialogInterface.BUTTON_NEGATIVE, actionCallback.args?.get(1))
}

@Test
Expand All @@ -144,13 +122,13 @@ class DialogModuleTest {
dialogModule.showAlert(options, null, actionCallback)
shadowOf(getMainLooper()).idle()

val dialog = getFragment()!!.dialog as AlertDialog
val dialog = getFragment().dialog as AlertDialog
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).performClick()
shadowOf(getMainLooper()).idle()

assertEquals(1, actionCallback.calls)
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args!![0])
assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.args!![1])
assertEquals(DialogModule.ACTION_BUTTON_CLICKED, actionCallback.args?.get(0))
assertEquals(DialogInterface.BUTTON_NEUTRAL, actionCallback.args?.get(1))
}

@Test
Expand All @@ -161,16 +139,52 @@ class DialogModuleTest {
dialogModule.showAlert(options, null, actionCallback)
shadowOf(getMainLooper()).idle()

getFragment()!!.dialog!!.dismiss()
getFragment().dialog?.dismiss()
shadowOf(getMainLooper()).idle()

assertEquals(1, actionCallback.calls)
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args!![0])
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
}

private fun getFragment(): AlertFragment? {
return activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
as? AlertFragment
@Test
fun testNonAppCompatActivityTheme() {
setupActivity(NON_APP_COMPAT_THEME)

val options = JavaOnlyMap()

val actionCallback = SimpleCallback()
dialogModule.showAlert(options, null, actionCallback)
shadowOf(getMainLooper()).idle()

getFragment().dialog?.dismiss()
shadowOf(getMainLooper()).idle()

assertEquals(1, actionCallback.calls)
assertEquals(DialogModule.ACTION_DISMISSED, actionCallback.args?.get(0))
}

private fun setupActivity(theme: Int = APP_COMPAT_THEME) {
activityController = Robolectric.buildActivity(FragmentActivity::class.java)
activity = activityController.create().start().resume().get()

// We must set the theme to a descendant of AppCompat for the AlertDialog to show without
// raising an exception
activity.setTheme(theme)

val context: ReactApplicationContext = mock(ReactApplicationContext::class.java)
whenever(context.hasActiveReactInstance()).thenReturn(true)
whenever(context.currentActivity).thenReturn(activity)

dialogModule = DialogModule(context)
dialogModule.onHostResume()
}

private fun getFragment(): AlertFragment {
val maybeFragment = activity.supportFragmentManager.findFragmentByTag(DialogModule.FRAGMENT_TAG)
if (maybeFragment == null || !(maybeFragment is AlertFragment)) {
error("Fragment was not displayed")
}
return maybeFragment
}

companion object {
Expand Down

0 comments on commit 44df6df

Please sign in to comment.