forked from mirrors/gecko-dev
Bug 1847088 - Show a notification in the Fenix add-on manager when the extensions process spawning is disabled
This commit is contained in:
parent
6bdb2b5b39
commit
1883946a8b
23 changed files with 452 additions and 106 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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-->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
|||
addonsManagerDelegate = this@AddonsFragment,
|
||||
addons = addons,
|
||||
style = style,
|
||||
store = context.components.store,
|
||||
)
|
||||
recyclerView.adapter = adapter
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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…
Reference in a new issue