Bug 1853177 - Avoid multiple crash dialogs to be created and shown

fenix/119.0
William Durand 9 months ago committed by mergify[bot]
parent 5a683806c9
commit b3c8e1f3ee

@ -10,6 +10,7 @@ import android.widget.Button
import android.widget.TextView
import androidx.annotation.UiContext
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.LifecycleOwner
import mozilla.components.browser.state.action.ExtensionProcessDisabledPopupAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
@ -20,52 +21,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.WebExtensionController
/**
* Present a dialog to the user notifying of extension process spawning disabled and also asking
* whether they would like to continue trying or disable extensions. If the user chooses to retry,
* enable the extension process spawning with [WebExtensionController.enableExtensionProcessSpawning].
*
* @param context to show the AlertDialog
* @param store The [BrowserStore] which holds the state for showing the dialog
* @param webExtensionController to call when a user enables the process spawning
* @param builder to use for creating the dialog which can be styled as needed
* @param appName to be added to the message. Necessary to be added as a param for testing
*/
private fun presentDialog(
@UiContext context: Context,
store: BrowserStore,
engine: Engine,
builder: AlertDialog.Builder,
appName: String,
) {
val message = context.getString(R.string.addon_process_crash_dialog_message, appName)
var onDismissDialog: (() -> Unit)? = null
val layout = LayoutInflater.from(context)
.inflate(R.layout.crash_extension_dialog, null, false)
layout?.apply {
findViewById<TextView>(R.id.message)?.text = message
findViewById<Button>(R.id.positive)?.setOnClickListener {
engine.enableExtensionProcessSpawning()
Addons.extensionsProcessUiRetry.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
onDismissDialog?.invoke()
}
findViewById<Button>(R.id.negative)?.setOnClickListener {
Addons.extensionsProcessUiDisable.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
onDismissDialog?.invoke()
}
}
builder.apply {
setCancelable(false)
setView(layout)
setTitle(R.string.addon_process_crash_dialog_title)
}
val dialog = builder.show()
onDismissDialog = { dialog?.dismiss() }
}
/**
* Controller for showing the user a dialog when the the extension process spawning has been disabled.
*
@ -84,4 +39,68 @@ class ExtensionProcessDisabledController(
) : ExtensionProcessDisabledPopupObserver(
store,
{ presentDialog(context, store, engine, builder, appName) },
)
) {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
// In case the activity gets destroyed, we want to re-create the dialog.
shouldCreateDialog = true
}
companion object {
private var shouldCreateDialog: Boolean = true
/**
* Present a dialog to the user notifying of extension process spawning disabled and also asking
* whether they would like to continue trying or disable extensions. If the user chooses to retry,
* enable the extension process spawning with [WebExtensionController.enableExtensionProcessSpawning].
*
* @param context to show the AlertDialog
* @param store The [BrowserStore] which holds the state for showing the dialog
* @param webExtensionController to call when a user enables the process spawning
* @param builder to use for creating the dialog which can be styled as needed
* @param appName to be added to the message. Necessary to be added as a param for testing
*/
private fun presentDialog(
@UiContext context: Context,
store: BrowserStore,
engine: Engine,
builder: AlertDialog.Builder,
appName: String,
) {
if (!shouldCreateDialog) {
return
}
val message = context.getString(R.string.addon_process_crash_dialog_message, appName)
var onDismissDialog: (() -> Unit)? = null
val layout = LayoutInflater.from(context)
.inflate(R.layout.crash_extension_dialog, null, false)
layout?.apply {
findViewById<TextView>(R.id.message)?.text = message
findViewById<Button>(R.id.positive)?.setOnClickListener {
engine.enableExtensionProcessSpawning()
Addons.extensionsProcessUiRetry.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
onDismissDialog?.invoke()
}
findViewById<Button>(R.id.negative)?.setOnClickListener {
Addons.extensionsProcessUiDisable.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
onDismissDialog?.invoke()
}
}
builder.apply {
setCancelable(false)
setView(layout)
setTitle(R.string.addon_process_crash_dialog_title)
}
val dialog = builder.show()
shouldCreateDialog = false
onDismissDialog = {
dialog?.dismiss()
shouldCreateDialog = true
}
}
}
}

@ -22,6 +22,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -40,8 +41,7 @@ class ExtensionProcessDisabledControllerTest {
val dialog: AlertDialog = mock()
val appName = "TestApp"
val builder: AlertDialog.Builder = mock()
val controller =
ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val controller = ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
@ -74,8 +74,7 @@ class ExtensionProcessDisabledControllerTest {
val appName = "TestApp"
val dialog: AlertDialog = mock()
val builder: AlertDialog.Builder = mock()
val controller =
ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val controller = ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
@ -100,4 +99,36 @@ class ExtensionProcessDisabledControllerTest {
verify(engine, never()).enableExtensionProcessSpawning()
verify(dialog).dismiss()
}
@Test
fun `WHEN dispatching the same event twice THEN the dialog should only be created once`() {
val store = BrowserStore()
val engine: Engine = mock()
val appName = "TestApp"
val dialog: AlertDialog = mock()
val builder: AlertDialog.Builder = mock()
val controller = ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
whenever(builder.show()).thenReturn(dialog)
// First dispatch...
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
// Second dispatch... without having dismissed the dialog before!
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(builder).setView(buttonsContainerCaptor.capture())
verify(builder, times(1)).show()
// Click a button to dismiss the dialog.
buttonsContainerCaptor.value.findViewById<Button>(R.id.negative).performClick()
store.waitUntilIdle()
}
}

Loading…
Cancel
Save