Bug 1847088 - Show a notification in the Fenix add-on manager when the extensions process spawning is disabled

fenix/120.0
William Durand 8 months ago committed by mergify[bot]
parent 742b7ca33b
commit b69eb9037e

@ -91,7 +91,7 @@ import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.addons.ExtensionProcessDisabledController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledController
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
@ -194,8 +194,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
WebExtensionPopupObserver(components.core.store, ::openPopup)
}
private val extensionProcessDisabledPopupObserver by lazy {
ExtensionProcessDisabledController(this@HomeActivity, components.core.store)
private val extensionsProcessDisabledPromptObserver by lazy {
ExtensionsProcessDisabledController(this@HomeActivity, components.core.store)
}
private val serviceWorkerSupport by lazy {
@ -347,7 +347,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
supportActionBar?.hide()
lifecycle.addObservers(webExtensionPopupObserver, extensionProcessDisabledPopupObserver, serviceWorkerSupport)
lifecycle.addObservers(webExtensionPopupObserver, extensionsProcessDisabledPromptObserver, serviceWorkerSupport)
if (shouldAddToRecentsScreen(intent)) {
intent.removeExtra(START_IN_RECENTS_SCREEN)

@ -114,11 +114,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
runIfFragmentIsAttached {
if (!shouldRefresh) {
adapter = AddonsManagerAdapter(
requireContext().components.addonsProvider,
managementView,
addons,
addonsProvider = requireContext().components.addonsProvider,
addonsManagerDelegate = managementView,
addons = addons,
style = createAddonStyle(requireContext()),
excludedAddonIDs,
excludedAddonIDs = excludedAddonIDs,
store = requireComponents.core.store,
)
}
binding?.addOnsProgressBar?.isVisible = false

@ -11,33 +11,28 @@ 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.action.ExtensionsProcessAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.webextensions.ExtensionProcessDisabledPopupObserver
import org.mozilla.fenix.GleanMetrics.Addons
import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
/**
* Controller for showing the user a dialog when the the extension process spawning has been disabled.
* Controller for showing the user a dialog when the the extensions process spawning has been disabled.
*
* @param context to show the AlertDialog
* @param store The [BrowserStore] which holds the state for showing the dialog
* @param engine An [Engine] instance used for handling extension process spawning.
* @param builder to use for creating the dialog which can be styled as needed
* @param appName to be added to the message. Optional and mainly relevant for testing
*/
class ExtensionProcessDisabledController(
class ExtensionsProcessDisabledController(
@UiContext context: Context,
store: BrowserStore,
engine: Engine = context.components.core.engine,
builder: AlertDialog.Builder = AlertDialog.Builder(context),
appName: String = context.appName,
) : ExtensionProcessDisabledPopupObserver(
) : ExtensionsProcessDisabledPromptObserver(
store,
{ presentDialog(context, store, engine, builder, appName) },
{ presentDialog(context, store, builder, appName) },
) {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
@ -49,21 +44,18 @@ class ExtensionProcessDisabledController(
private var shouldCreateDialog: Boolean = true
/**
* Present a dialog to the user notifying of extension process spawning disabled and also asking
* Present a dialog to the user notifying of extensions 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 [Engine.enableExtensionProcessSpawning].
* Otherwise, call [Engine.disableExtensionProcessSpawning].
* enable the extensions process spawning. Otherwise, disable it.
*
* @param context to show the AlertDialog
* @param store The [BrowserStore] which holds the state for showing the dialog
* @param engine An [Engine] instance used for handling extension 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,
) {
@ -78,15 +70,13 @@ class ExtensionProcessDisabledController(
layout?.apply {
findViewById<TextView>(R.id.message)?.text = message
findViewById<Button>(R.id.positive)?.setOnClickListener {
engine.enableExtensionProcessSpawning()
Addons.extensionsProcessUiRetry.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
store.dispatch(ExtensionsProcessAction.ShowPromptAction(false))
store.dispatch(ExtensionsProcessAction.EnabledAction)
onDismissDialog?.invoke()
}
findViewById<Button>(R.id.negative)?.setOnClickListener {
engine.disableExtensionProcessSpawning()
Addons.extensionsProcessUiDisable.add()
store.dispatch(ExtensionProcessDisabledPopupAction(false))
store.dispatch(ExtensionsProcessAction.ShowPromptAction(false))
store.dispatch(ExtensionsProcessAction.DisabledAction)
onDismissDialog?.invoke()
}
}

@ -9,6 +9,7 @@ import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTab
@ -23,6 +24,7 @@ import mozilla.components.support.base.log.logger.Logger
import mozilla.telemetry.glean.internal.TimerId
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.components.metrics.Event
@ -136,6 +138,12 @@ class TelemetryMiddleware(
Metrics.hasOpenTabs.set(false)
}
}
is ExtensionsProcessAction.EnabledAction -> {
Addons.extensionsProcessUiRetry.add()
}
is ExtensionsProcessAction.DisabledAction -> {
Addons.extensionsProcessUiDisable.add()
}
else -> {
// no-op
}

@ -7,9 +7,8 @@ package org.mozilla.fenix.addons
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import mozilla.components.browser.state.action.ExtensionProcessDisabledPopupAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.robolectric.testContext
@ -21,39 +20,41 @@ import org.junit.Rule
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
@RunWith(FenixRobolectricTestRunner::class)
class ExtensionProcessDisabledControllerTest {
class ExtensionsProcessDisabledControllerTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `WHEN showExtensionProcessDisabledPopup is true AND positive button clicked then enable extension process spawning`() {
fun `WHEN showExtensionsProcessDisabledPrompt is true AND positive button clicked then enable extension process spawning`() {
val store = BrowserStore()
val engine: Engine = mock()
val dialog: AlertDialog = mock()
val appName = "TestApp"
val builder: AlertDialog.Builder = mock()
val controller = ExtensionProcessDisabledController(testContext, store, engine, builder, appName)
val controller = ExtensionsProcessDisabledController(testContext, store, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
whenever(builder.show()).thenReturn(dialog)
assertFalse(store.state.showExtensionProcessDisabledPopup)
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
assertFalse(store.state.extensionsProcessDisabled)
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
// Pretend the process has been disabled and we show the dialog.
store.dispatch(ExtensionsProcessAction.DisabledAction)
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertTrue(store.state.showExtensionProcessDisabledPopup)
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
assertTrue(store.state.extensionsProcessDisabled)
verify(builder).setView(buttonsContainerCaptor.capture())
verify(builder).show()
@ -62,32 +63,34 @@ class ExtensionProcessDisabledControllerTest {
store.waitUntilIdle()
verify(engine).enableExtensionProcessSpawning()
verify(engine, never()).disableExtensionProcessSpawning()
assertFalse(store.state.showExtensionProcessDisabledPopup)
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
assertFalse(store.state.extensionsProcessDisabled)
verify(dialog).dismiss()
}
@Test
fun `WHEN showExtensionProcessDisabledPopup is true AND negative button clicked then dismiss without enabling extension process spawning`() {
fun `WHEN showExtensionsProcessDisabledPrompt is true AND negative button clicked then dismiss without enabling extension process spawning`() {
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 controller = ExtensionsProcessDisabledController(testContext, store, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
whenever(builder.show()).thenReturn(dialog)
assertFalse(store.state.showExtensionProcessDisabledPopup)
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
assertFalse(store.state.extensionsProcessDisabled)
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
// Pretend the process has been disabled and we show the dialog.
store.dispatch(ExtensionsProcessAction.DisabledAction)
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertTrue(store.state.showExtensionProcessDisabledPopup)
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
assertTrue(store.state.extensionsProcessDisabled)
verify(builder).setView(buttonsContainerCaptor.capture())
verify(builder).show()
@ -96,20 +99,18 @@ class ExtensionProcessDisabledControllerTest {
store.waitUntilIdle()
assertFalse(store.state.showExtensionProcessDisabledPopup)
verify(engine, never()).enableExtensionProcessSpawning()
verify(engine).disableExtensionProcessSpawning()
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
assertTrue(store.state.extensionsProcessDisabled)
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 controller = ExtensionsProcessDisabledController(testContext, store, builder, appName)
val buttonsContainerCaptor = argumentCaptor<View>()
controller.start()
@ -117,12 +118,12 @@ class ExtensionProcessDisabledControllerTest {
whenever(builder.show()).thenReturn(dialog)
// First dispatch...
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
// Second dispatch... without having dismissed the dialog before!
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()

@ -6,10 +6,13 @@ package org.mozilla.fenix.telemetry
import androidx.test.core.app.ApplicationProvider
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.state.BrowserState
@ -17,6 +20,7 @@ import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.browser.state.state.recover.TabState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.base.android.Clock
import mozilla.components.support.test.ext.joinBlocking
@ -33,6 +37,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.components.AppStore
@ -78,8 +83,11 @@ class TelemetryMiddlewareTest {
searchState = searchState,
timerId = timerId,
)
val engine: Engine = mockk()
every { engine.enableExtensionProcessSpawning() } just runs
every { engine.disableExtensionProcessSpawning() } just runs
store = BrowserStore(
middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine = mockk()),
middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine),
initialState = BrowserState(),
)
appStore = AppStore()
@ -417,6 +425,28 @@ class TelemetryMiddlewareTest {
verify { metrics.track(Event.GrowthData.FirstUriLoadForDay) }
}
@Test
fun `WHEN EnabledAction is dispatched THEN enable the process spawning`() {
assertNull(Addons.extensionsProcessUiRetry.testGetValue())
assertNull(Addons.extensionsProcessUiDisable.testGetValue())
store.dispatch(ExtensionsProcessAction.EnabledAction).joinBlocking()
assertEquals(1, Addons.extensionsProcessUiRetry.testGetValue())
assertNull(Addons.extensionsProcessUiDisable.testGetValue())
}
@Test
fun `WHEN DisabledAction is dispatched THEN disable the process spawning`() {
assertNull(Addons.extensionsProcessUiRetry.testGetValue())
assertNull(Addons.extensionsProcessUiDisable.testGetValue())
store.dispatch(ExtensionsProcessAction.DisabledAction).joinBlocking()
assertEquals(1, Addons.extensionsProcessUiDisable.testGetValue())
assertNull(Addons.extensionsProcessUiRetry.testGetValue())
}
}
internal class FakeClock : Clock.Delegate {

Loading…
Cancel
Save