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

This commit is contained in:
William Durand 2023-10-03 09:59:27 +02:00 committed by mergify[bot]
parent 6bdb2b5b39
commit 1883946a8b
23 changed files with 452 additions and 106 deletions

View file

@ -73,10 +73,24 @@ object InitAction : BrowserAction()
object RestoreCompleteAction : BrowserAction()
/**
* [BrowserAction] implementation for updating state related to whether the extensions process
* spawning has been disabled and a popup is necessary.
* [BrowserAction] implementations to react to extensions process events.
*/
data class ExtensionProcessDisabledPopupAction(val showPopup: Boolean) : BrowserAction()
sealed class ExtensionsProcessAction : BrowserAction() {
/**
* [BrowserAction] to indicate when the crash prompt should be displayed to the user.
*/
data class ShowPromptAction(val show: Boolean) : ExtensionsProcessAction()
/**
* [BrowserAction] to indicate that the process has been re-enabled by the user.
*/
object EnabledAction : ExtensionsProcessAction()
/**
* [BrowserAction] to indicate that the process has been left disabled by the user.
*/
object DisabledAction : ExtensionsProcessAction()
}
/**
* [BrowserAction] implementations to react to system events.

View file

@ -11,6 +11,7 @@ import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.engine.middleware.CrashMiddleware
import mozilla.components.browser.state.engine.middleware.CreateEngineSessionMiddleware
import mozilla.components.browser.state.engine.middleware.EngineDelegateMiddleware
import mozilla.components.browser.state.engine.middleware.ExtensionsProcessMiddleware
import mozilla.components.browser.state.engine.middleware.LinkingMiddleware
import mozilla.components.browser.state.engine.middleware.SuspendMiddleware
import mozilla.components.browser.state.engine.middleware.TabsRemovedMiddleware
@ -48,6 +49,7 @@ object EngineMiddleware {
SuspendMiddleware(scope),
WebExtensionMiddleware(),
CrashMiddleware(),
ExtensionsProcessMiddleware(engine),
) + if (trimMemoryAutomatically) {
listOf(TrimMemoryMiddleware())
} else {

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.engine.middleware
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.concept.engine.Engine
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
/**
* [Middleware] implementation responsible for enabling and disabling the extensions process (spawning).
*
* @property engine An [Engine] instance used for handling extension process spawning.
*/
internal class ExtensionsProcessMiddleware(
private val engine: Engine,
) : Middleware<BrowserState, BrowserAction> {
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
// Pre process actions
next(action)
// Post process actions
when (action) {
is ExtensionsProcessAction.EnabledAction -> {
engine.enableExtensionProcessSpawning()
}
is ExtensionsProcessAction.DisabledAction -> {
engine.disableExtensionProcessSpawning()
}
else -> {
// no-op
}
}
}
}

View file

@ -15,7 +15,7 @@ import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.DebugAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ExtensionProcessDisabledPopupAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.action.HistoryMetadataAction
import mozilla.components.browser.state.action.InitAction
import mozilla.components.browser.state.action.LastAccessAction
@ -76,7 +76,7 @@ internal object BrowserStateReducer {
is HistoryMetadataAction -> HistoryMetadataReducer.reduce(state, action)
is DebugAction -> DebugReducer.reduce(state, action)
is ShoppingProductAction -> ShoppingProductStateReducer.reduce(state, action)
is ExtensionProcessDisabledPopupAction -> state.copy(showExtensionProcessDisabledPopup = action.showPopup)
is ExtensionsProcessAction -> ExtensionsProcessStateReducer.reduce(state, action)
}
}
}

View file

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.state.BrowserState
internal object ExtensionsProcessStateReducer {
fun reduce(state: BrowserState, action: ExtensionsProcessAction): BrowserState = when (action) {
is ExtensionsProcessAction.ShowPromptAction -> state.copy(showExtensionsProcessDisabledPrompt = action.show)
is ExtensionsProcessAction.DisabledAction -> state.copy(extensionsProcessDisabled = true)
is ExtensionsProcessAction.EnabledAction -> state.copy(extensionsProcessDisabled = false)
}
}

View file

@ -49,5 +49,6 @@ data class BrowserState(
val undoHistory: UndoHistoryState = UndoHistoryState(),
val restoreComplete: Boolean = false,
val locale: Locale? = null,
val showExtensionProcessDisabledPopup: Boolean = false,
val showExtensionsProcessDisabledPrompt: Boolean = false,
val extensionsProcessDisabled: Boolean = false,
) : State

View file

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.engine.middleware
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
class ExtensionsProcessMiddlewareTest {
private lateinit var engine: Engine
private lateinit var store: BrowserStore
@Before
fun setUp() {
engine = Mockito.mock()
store = BrowserStore(
middleware = listOf(ExtensionsProcessMiddleware(engine)),
initialState = BrowserState(),
)
}
@Test
fun `WHEN EnabledAction is dispatched THEN enable the process spawning`() {
store.dispatch(ExtensionsProcessAction.EnabledAction).joinBlocking()
Mockito.verify(engine).enableExtensionProcessSpawning()
Mockito.verify(engine, Mockito.never()).disableExtensionProcessSpawning()
}
@Test
fun `WHEN DisabledAction is dispatched THEN disable the process spawning`() {
store.dispatch(ExtensionsProcessAction.DisabledAction).joinBlocking()
Mockito.verify(engine).disableExtensionProcessSpawning()
Mockito.verify(engine, Mockito.never()).enableExtensionProcessSpawning()
}
}

View file

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.state.BrowserState
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ExtensionsProcessStateReducerTest {
@Test
fun `GIVEN ShowPromptAction THEN showExtensionsProcessDisabledPrompt is updated`() {
var state = BrowserState()
assertFalse(state.showExtensionsProcessDisabledPrompt)
state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.ShowPromptAction(show = true))
assertTrue(state.showExtensionsProcessDisabledPrompt)
state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.ShowPromptAction(show = false))
assertFalse(state.showExtensionsProcessDisabledPrompt)
}
@Test
fun `GIVEN EnabledAction THEN extensionsProcessDisabled is set to false`() {
var state = BrowserState(extensionsProcessDisabled = true)
state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.EnabledAction)
assertFalse(state.extensionsProcessDisabled)
}
@Test
fun `GIVEN DisabledAction THEN extensionsProcessDisabled is set to true`() {
var state = BrowserState(extensionsProcessDisabled = false)
state = ExtensionsProcessStateReducer.reduce(state, ExtensionsProcessAction.DisabledAction)
assertTrue(state.extensionsProcessDisabled)
}
}

View file

@ -29,11 +29,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonsProvider
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.FooterViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.HeaderViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
import mozilla.components.support.base.log.logger.Logger
@ -47,6 +50,7 @@ private const val VIEW_HOLDER_TYPE_SECTION = 0
private const val VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION = 1
private const val VIEW_HOLDER_TYPE_ADDON = 2
private const val VIEW_HOLDER_TYPE_FOOTER = 3
private const val VIEW_HOLDER_TYPE_HEADER = 4
/**
* An adapter for displaying add-on items. This will display information related to the state of
@ -66,6 +70,7 @@ class AddonsManagerAdapter(
addons: List<Addon>,
private val style: Style? = null,
private val excludedAddonIDs: List<String> = emptyList(),
private val store: BrowserStore,
) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
private val scope = CoroutineScope(Dispatchers.IO)
private val logger = Logger("AddonsManagerAdapter")
@ -88,6 +93,7 @@ class AddonsManagerAdapter(
VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent)
VIEW_HOLDER_TYPE_FOOTER -> createFooterSectionViewHolder(parent)
VIEW_HOLDER_TYPE_HEADER -> createHeaderSectionViewHolder(parent)
else -> throw IllegalArgumentException("Unrecognized viewType")
}
}
@ -112,6 +118,19 @@ class AddonsManagerAdapter(
return FooterViewHolder(view)
}
private fun createHeaderSectionViewHolder(parent: ViewGroup): CustomViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(
R.layout.mozac_feature_addons_header_section_item,
parent,
false,
)
val restartButton = view.findViewById<TextView>(R.id.restart_button)
return HeaderViewHolder(view, restartButton)
}
private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
@ -159,6 +178,7 @@ class AddonsManagerAdapter(
is Section -> VIEW_HOLDER_TYPE_SECTION
is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION
is FooterSection -> VIEW_HOLDER_TYPE_FOOTER
is HeaderSection -> VIEW_HOLDER_TYPE_HEADER
else -> throw IllegalArgumentException("items[position] has unrecognized type")
}
}
@ -174,6 +194,7 @@ class AddonsManagerAdapter(
item as NotYetSupportedSection,
)
is FooterViewHolder -> bindFooterButton(holder)
is HeaderViewHolder -> bindHeaderButton(holder)
}
}
@ -213,14 +234,20 @@ class AddonsManagerAdapter(
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindFooterButton(
holder: FooterViewHolder,
) {
internal fun bindFooterButton(holder: FooterViewHolder) {
holder.itemView.setOnClickListener {
addonsManagerDelegate.onFindMoreAddonsButtonClicked()
}
}
internal fun bindHeaderButton(holder: HeaderViewHolder) {
holder.restartButton.setOnClickListener {
store.dispatch(ExtensionsProcessAction.EnabledAction)
// Remove the notification.
submitList(currentList.filter { item: Any -> item != HeaderSection })
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindAddon(
holder: AddonViewHolder,
@ -372,6 +399,12 @@ class AddonsManagerAdapter(
}
}
// Calls are safe, except in tests since the store is mocked in most cases.
@Suppress("UNNECESSARY_SAFE_CALL")
if (store?.state?.extensionsProcessDisabled == true) {
itemsWithSections.add(HeaderSection)
}
// Add installed section and addons if available
if (installedAddons.isNotEmpty()) {
itemsWithSections.add(Section(R.string.mozac_feature_addons_enabled, false))
@ -414,6 +447,9 @@ class AddonsManagerAdapter(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal object FooterSection
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal object HeaderSection
/**
* Allows to customize how items should look like.
*/

View file

@ -49,6 +49,14 @@ sealed class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val statusErrorView: View,
) : CustomViewHolder(view)
/**
* A view holder for displaying a section above the list of add-ons.
*/
class HeaderViewHolder(
view: View,
val restartButton: TextView,
) : CustomViewHolder(view)
/**
* A view holder for displaying a section below the list of add-ons.
*/

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="vertical"
tools:ignore="RtlSymmetry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/warning_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleType="center"
app:srcCompat="@drawable/mozac_ic_warning_fill_24"
app:tint="?android:attr/textColorLink"/>
<TextView
android:text="@string/mozac_feature_addons_manager_notification_title_text"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:paddingStart="12dp"
android:paddingEnd="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:text="@string/mozac_feature_addons_manager_notification_content_text"
android:textColor="?android:attr/textColorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp"
android:paddingStart="36dp"/>
<Button
android:id="@+id/restart_button"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:text="@string/mozac_feature_addons_manager_notification_restart_button"
android:textColor="?android:attr/textColorLink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:paddingVertical="8dp"/>
</LinearLayout>

View file

@ -145,6 +145,12 @@
<string name="mozac_feature_addons_addons">Add-ons</string>
<!-- Label for add-ons sub menu item for add-ons manager-->
<string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
<!-- The title of the "crash" notification in the add-ons manager -->
<string name="mozac_feature_addons_manager_notification_title_text">Add-ons are temporarily disabled</string>
<!-- The content of the "crash" notification in the add-ons manager -->
<string name="mozac_feature_addons_manager_notification_content_text">One or more add-ons stopped working, making your system unstable.</string>
<!-- Button to re-enable the add-ons in the "crash" notification -->
<string name="mozac_feature_addons_manager_notification_restart_button">Restart add-ons</string>
<!-- Button in the add-ons manager that opens AMO in a tab -->
<string name="mozac_feature_addons_find_more_addons_button_text">Find more add-ons</string>
<!-- The label of the allow button, this will be shown to the user when an add-on needs new permissions, with the button the user will indicate that they want to accept the new permissions and update the add-on-->

View file

@ -15,6 +15,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.amo.AMOAddonsProvider
@ -51,6 +53,7 @@ class AddonsManagerAdapterTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val scope = coroutinesTestRule.scope
private val dispatcher = coroutinesTestRule.testDispatcher
// We must pass these variables to `bindAddon()` because looking up the version name
// requires package info that we do not have in the test context.
@ -64,7 +67,7 @@ class AddonsManagerAdapterTest {
@Test
fun `createListWithSections`() {
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList())
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), mock(), emptyList(), mock())
val installedAddon: Addon = mock()
val recommendedAddon: Addon = mock()
@ -130,7 +133,7 @@ class AddonsManagerAdapterTest {
val addon = mock<Addon>()
val mockedImageView = spy(ImageView(testContext))
val mockedAddonsProvider = mock<AMOAddonsProvider>()
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList())
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock())
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).then {
throw IOException("Request failed")
}
@ -149,7 +152,7 @@ class AddonsManagerAdapterTest {
val bitmap = mock<Bitmap>()
val mockedImageView = spy(ImageView(testContext))
val mockedAddonsProvider = mock<AMOAddonsProvider>()
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList())
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock())
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
adapter.fetchIcon(addon, mockedImageView, scope).join()
@ -163,7 +166,8 @@ class AddonsManagerAdapterTest {
val bitmap = mock<Bitmap>()
val mockedImageView = spy(ImageView(testContext))
val mockedAddonsProvider = mock<AMOAddonsProvider>()
val adapter = spy(AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList()))
val adapter =
spy(AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock()))
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenAnswer {
runBlocking {
delay(1000)
@ -185,7 +189,7 @@ class AddonsManagerAdapterTest {
whenever(addon.installedState).thenReturn(installedState)
val mockedImageView = spy(ImageView(testContext))
val mockedAddonsProvider = mock<AMOAddonsProvider>()
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList())
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock())
val captor = argumentCaptor<BitmapDrawable>()
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenReturn(null)
@ -239,7 +243,7 @@ class AddonsManagerAdapterTest {
addonNameTextColor = android.R.color.transparent,
addonSummaryTextColor = android.R.color.white,
)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), style, emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -269,7 +273,7 @@ class AddonsManagerAdapterTest {
sectionsTextColor = android.R.color.black,
sectionsTypeFace = mock(),
)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style, emptyList(), mock())
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
@ -292,7 +296,7 @@ class AddonsManagerAdapterTest {
sectionsTextColor = android.R.color.black,
sectionsTypeFace = mock(),
)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style, emptyList(), mock())
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
@ -312,7 +316,7 @@ class AddonsManagerAdapterTest {
sectionsTextColor = android.R.color.black,
sectionsTypeFace = mock(),
)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style, emptyList(), mock())
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
@ -333,7 +337,7 @@ class AddonsManagerAdapterTest {
sectionsTypeFace = mock(),
visibleDividers = false,
)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style, emptyList(), mock())
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
@ -361,7 +365,7 @@ class AddonsManagerAdapterTest {
dividerHeight = dividerHeight,
)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style)
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), style, emptyList(), mock())
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
@ -399,7 +403,7 @@ class AddonsManagerAdapterTest {
createdAt = "",
updatedAt = "",
)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
verify(titleView).setText("id")
@ -417,7 +421,7 @@ class AddonsManagerAdapterTest {
createdAt = "",
updatedAt = "",
)
val adapter = spy(AddonsManagerAdapter(mock(), mock(), listOf(addon)))
val adapter = spy(AddonsManagerAdapter(mock(), mock(), listOf(addon), mock(), emptyList(), mock()))
assertEquals(addon, adapter.addonsMap[addon.id])
@ -449,7 +453,8 @@ class AddonsManagerAdapterTest {
createdAt = "",
updatedAt = "",
)
val adapter = spy(AddonsManagerAdapter(mock(), mock(), listOf(addon1, addon2)))
val adapter =
spy(AddonsManagerAdapter(mock(), mock(), listOf(addon1, addon2), mock(), emptyList(), mock()))
assertEquals(addon1, adapter.addonsMap[addon1.id])
assertEquals(addon2, adapter.addonsMap[addon2.id])
@ -530,7 +535,15 @@ class AddonsManagerAdapterTest {
),
)
val unsupportedAddons = arrayListOf(unsupportedAddon, unsupportedAddonTwo)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, unsupportedAddons)
val adapter = AddonsManagerAdapter(
mock(),
addonsManagerAdapterDelegate,
unsupportedAddons,
mock(),
emptyList(),
mock(),
)
adapter.bindNotYetSupportedSection(unsupportedSectionViewHolder, mock())
verify(unsupportedSectionViewHolder.descriptionView).setText(
testContext.getString(R.string.mozac_feature_addons_unsupported_caption_plural, unsupportedAddons.size),
@ -543,7 +556,8 @@ class AddonsManagerAdapterTest {
@Test
fun bindFooterButton() {
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
val view = View(testContext)
val viewHolder = CustomViewHolder.FooterViewHolder(view)
adapter.bindFooterButton(viewHolder)
@ -552,11 +566,46 @@ class AddonsManagerAdapterTest {
verify(addonsManagerAdapterDelegate).onFindMoreAddonsButtonClicked()
}
@Test
fun bindHeaderButton() {
val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = true))
val adapter = spy(AddonsManagerAdapter(mock(), mock(), emptyList(), mock(), emptyList(), store))
val restartButton = TextView(testContext)
val viewHolder = CustomViewHolder.HeaderViewHolder(View(testContext), restartButton)
adapter.bindHeaderButton(viewHolder)
assertEquals(1, adapter.currentList.size)
viewHolder.restartButton.performClick()
dispatcher.scheduler.advanceUntilIdle()
assertFalse(store.state.extensionsProcessDisabled)
verify(adapter).submitList(emptyList())
}
@Test
fun testNotificationShownWhenProcessIsDisabled() {
val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = true))
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), mock(), emptyList(), store)
val itemsWithSections = adapter.createListWithSections(emptyList())
assertEquals(AddonsManagerAdapter.HeaderSection, itemsWithSections.first())
}
@Test
fun testNotificationNotShownWhenProcessIsEnabled() {
val store = BrowserStore(initialState = BrowserState(extensionsProcessDisabled = false))
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), mock(), emptyList(), store)
val itemsWithSections = adapter.createListWithSections(emptyList())
assertTrue(itemsWithSections.isEmpty())
}
@Test
fun testFindMoreAddonsButtonIsHidden() {
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(false)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
val itemsWithSections = adapter.createListWithSections(emptyList())
assertTrue(itemsWithSections.isEmpty())
@ -566,7 +615,7 @@ class AddonsManagerAdapterTest {
fun testFindMoreAddonsButtonIsVisible() {
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(true)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
val itemsWithSections = adapter.createListWithSections(emptyList())
assertFalse(itemsWithSections.isEmpty())
@ -603,7 +652,7 @@ class AddonsManagerAdapterTest {
)
val addonName = "some addon name"
val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED, addonName)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -646,7 +695,7 @@ class AddonsManagerAdapterTest {
statusErrorView = statusErrorView,
)
val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -684,7 +733,7 @@ class AddonsManagerAdapterTest {
)
val addonName = "some addon name"
val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, addonName)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -727,7 +776,7 @@ class AddonsManagerAdapterTest {
statusErrorView = statusErrorView,
)
val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -765,7 +814,7 @@ class AddonsManagerAdapterTest {
)
val addonName = "some addon name"
val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE, addonName)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
@ -803,7 +852,7 @@ class AddonsManagerAdapterTest {
statusErrorView = statusErrorView,
)
val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE)
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)

View file

@ -12,34 +12,34 @@ import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
/**
* Feature implementation that shows a popup when the extensions process spawning has been
* Feature implementation that shows a prompt when the extensions process spawning has been
* disabled due to the restart threshold being met.
*
* @property store the application's [BrowserStore].
* @property onShowExtensionProcessDisabledPopup a callback invoked when the application should open a
* popup.
* @property onShowExtensionsProcessDisabledPrompt a callback invoked when the application should open a
* prompt.
*/
open class ExtensionProcessDisabledPopupObserver(
open class ExtensionsProcessDisabledPromptObserver(
private val store: BrowserStore,
private val onShowExtensionProcessDisabledPopup: () -> Unit = { },
private val onShowExtensionsProcessDisabledPrompt: () -> Unit = { },
) : LifecycleAwareFeature {
private var popupScope: CoroutineScope? = null
private var promptScope: CoroutineScope? = null
override fun start() {
popupScope = store.flowScoped { flow ->
flow.distinctUntilChangedBy { it.showExtensionProcessDisabledPopup }
promptScope = store.flowScoped { flow ->
flow.distinctUntilChangedBy { it.showExtensionsProcessDisabledPrompt }
.collect { state ->
if (state.showExtensionProcessDisabledPopup) {
if (state.showExtensionsProcessDisabledPrompt) {
// There should only be one active dialog to the user when the extensions
// process spawning is disabled.
onShowExtensionProcessDisabledPopup()
onShowExtensionsProcessDisabledPrompt()
}
}
}
}
override fun stop() {
popupScope?.cancel()
popupScope = null
promptScope?.cancel()
promptScope = null
}
}

View file

@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.ExtensionProcessDisabledPopupAction
import mozilla.components.browser.state.action.ExtensionsProcessAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.selector.allTabs
@ -333,7 +333,7 @@ object WebExtensionSupport {
}
override fun onDisabledExtensionProcessSpawning() {
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
}
},
)

View file

@ -889,7 +889,7 @@ class WebExtensionSupportTest {
val store = BrowserStore()
val engine: Engine = mock()
assertFalse(store.state.showExtensionProcessDisabledPopup)
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
WebExtensionSupport.initialize(engine, store)
@ -898,7 +898,7 @@ class WebExtensionSupportTest {
delegateCaptor.value.onDisabledExtensionProcessSpawning()
store.waitUntilIdle()
assertTrue(store.state.showExtensionProcessDisabledPopup)
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
}
@Test

View file

@ -96,6 +96,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
addonsManagerDelegate = this@AddonsFragment,
addons = addons,
style = style,
store = context.components.store,
)
recyclerView.adapter = adapter
} else {

View file

@ -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)

View file

@ -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

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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()

View file

@ -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 {