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()
|
object RestoreCompleteAction : BrowserAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [BrowserAction] implementation for updating state related to whether the extensions process
|
* [BrowserAction] implementations to react to extensions process events.
|
||||||
* spawning has been disabled and a popup is necessary.
|
|
||||||
*/
|
*/
|
||||||
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.
|
* [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.CrashMiddleware
|
||||||
import mozilla.components.browser.state.engine.middleware.CreateEngineSessionMiddleware
|
import mozilla.components.browser.state.engine.middleware.CreateEngineSessionMiddleware
|
||||||
import mozilla.components.browser.state.engine.middleware.EngineDelegateMiddleware
|
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.LinkingMiddleware
|
||||||
import mozilla.components.browser.state.engine.middleware.SuspendMiddleware
|
import mozilla.components.browser.state.engine.middleware.SuspendMiddleware
|
||||||
import mozilla.components.browser.state.engine.middleware.TabsRemovedMiddleware
|
import mozilla.components.browser.state.engine.middleware.TabsRemovedMiddleware
|
||||||
|
|
@ -48,6 +49,7 @@ object EngineMiddleware {
|
||||||
SuspendMiddleware(scope),
|
SuspendMiddleware(scope),
|
||||||
WebExtensionMiddleware(),
|
WebExtensionMiddleware(),
|
||||||
CrashMiddleware(),
|
CrashMiddleware(),
|
||||||
|
ExtensionsProcessMiddleware(engine),
|
||||||
) + if (trimMemoryAutomatically) {
|
) + if (trimMemoryAutomatically) {
|
||||||
listOf(TrimMemoryMiddleware())
|
listOf(TrimMemoryMiddleware())
|
||||||
} else {
|
} 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.DebugAction
|
||||||
import mozilla.components.browser.state.action.DownloadAction
|
import mozilla.components.browser.state.action.DownloadAction
|
||||||
import mozilla.components.browser.state.action.EngineAction
|
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.HistoryMetadataAction
|
||||||
import mozilla.components.browser.state.action.InitAction
|
import mozilla.components.browser.state.action.InitAction
|
||||||
import mozilla.components.browser.state.action.LastAccessAction
|
import mozilla.components.browser.state.action.LastAccessAction
|
||||||
|
|
@ -76,7 +76,7 @@ internal object BrowserStateReducer {
|
||||||
is HistoryMetadataAction -> HistoryMetadataReducer.reduce(state, action)
|
is HistoryMetadataAction -> HistoryMetadataReducer.reduce(state, action)
|
||||||
is DebugAction -> DebugReducer.reduce(state, action)
|
is DebugAction -> DebugReducer.reduce(state, action)
|
||||||
is ShoppingProductAction -> ShoppingProductStateReducer.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 undoHistory: UndoHistoryState = UndoHistoryState(),
|
||||||
val restoreComplete: Boolean = false,
|
val restoreComplete: Boolean = false,
|
||||||
val locale: Locale? = null,
|
val locale: Locale? = null,
|
||||||
val showExtensionProcessDisabledPopup: Boolean = false,
|
val showExtensionsProcessDisabledPrompt: Boolean = false,
|
||||||
|
val extensionsProcessDisabled: Boolean = false,
|
||||||
) : State
|
) : 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.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
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.Addon
|
||||||
import mozilla.components.feature.addons.AddonsProvider
|
import mozilla.components.feature.addons.AddonsProvider
|
||||||
import mozilla.components.feature.addons.R
|
import mozilla.components.feature.addons.R
|
||||||
import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
|
import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
|
||||||
import mozilla.components.feature.addons.ui.CustomViewHolder.FooterViewHolder
|
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.SectionViewHolder
|
||||||
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
|
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
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_NOT_YET_SUPPORTED_SECTION = 1
|
||||||
private const val VIEW_HOLDER_TYPE_ADDON = 2
|
private const val VIEW_HOLDER_TYPE_ADDON = 2
|
||||||
private const val VIEW_HOLDER_TYPE_FOOTER = 3
|
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
|
* 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>,
|
addons: List<Addon>,
|
||||||
private val style: Style? = null,
|
private val style: Style? = null,
|
||||||
private val excludedAddonIDs: List<String> = emptyList(),
|
private val excludedAddonIDs: List<String> = emptyList(),
|
||||||
|
private val store: BrowserStore,
|
||||||
) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
|
) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
private val logger = Logger("AddonsManagerAdapter")
|
private val logger = Logger("AddonsManagerAdapter")
|
||||||
|
|
@ -88,6 +93,7 @@ class AddonsManagerAdapter(
|
||||||
VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
|
VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
|
||||||
VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent)
|
VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION -> createUnsupportedSectionViewHolder(parent)
|
||||||
VIEW_HOLDER_TYPE_FOOTER -> createFooterSectionViewHolder(parent)
|
VIEW_HOLDER_TYPE_FOOTER -> createFooterSectionViewHolder(parent)
|
||||||
|
VIEW_HOLDER_TYPE_HEADER -> createHeaderSectionViewHolder(parent)
|
||||||
else -> throw IllegalArgumentException("Unrecognized viewType")
|
else -> throw IllegalArgumentException("Unrecognized viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +118,19 @@ class AddonsManagerAdapter(
|
||||||
return FooterViewHolder(view)
|
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 {
|
private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder {
|
||||||
val context = parent.context
|
val context = parent.context
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
|
|
@ -159,6 +178,7 @@ class AddonsManagerAdapter(
|
||||||
is Section -> VIEW_HOLDER_TYPE_SECTION
|
is Section -> VIEW_HOLDER_TYPE_SECTION
|
||||||
is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION
|
is NotYetSupportedSection -> VIEW_HOLDER_TYPE_NOT_YET_SUPPORTED_SECTION
|
||||||
is FooterSection -> VIEW_HOLDER_TYPE_FOOTER
|
is FooterSection -> VIEW_HOLDER_TYPE_FOOTER
|
||||||
|
is HeaderSection -> VIEW_HOLDER_TYPE_HEADER
|
||||||
else -> throw IllegalArgumentException("items[position] has unrecognized type")
|
else -> throw IllegalArgumentException("items[position] has unrecognized type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,6 +194,7 @@ class AddonsManagerAdapter(
|
||||||
item as NotYetSupportedSection,
|
item as NotYetSupportedSection,
|
||||||
)
|
)
|
||||||
is FooterViewHolder -> bindFooterButton(holder)
|
is FooterViewHolder -> bindFooterButton(holder)
|
||||||
|
is HeaderViewHolder -> bindHeaderButton(holder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,14 +234,20 @@ class AddonsManagerAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun bindFooterButton(
|
internal fun bindFooterButton(holder: FooterViewHolder) {
|
||||||
holder: FooterViewHolder,
|
|
||||||
) {
|
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
addonsManagerDelegate.onFindMoreAddonsButtonClicked()
|
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)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun bindAddon(
|
internal fun bindAddon(
|
||||||
holder: AddonViewHolder,
|
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
|
// Add installed section and addons if available
|
||||||
if (installedAddons.isNotEmpty()) {
|
if (installedAddons.isNotEmpty()) {
|
||||||
itemsWithSections.add(Section(R.string.mozac_feature_addons_enabled, false))
|
itemsWithSections.add(Section(R.string.mozac_feature_addons_enabled, false))
|
||||||
|
|
@ -414,6 +447,9 @@ class AddonsManagerAdapter(
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal object FooterSection
|
internal object FooterSection
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
internal object HeaderSection
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to customize how items should look like.
|
* Allows to customize how items should look like.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@ sealed class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
val statusErrorView: View,
|
val statusErrorView: View,
|
||||||
) : CustomViewHolder(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.
|
* 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>
|
<string name="mozac_feature_addons_addons">Add-ons</string>
|
||||||
<!-- Label for add-ons sub menu item for add-ons manager-->
|
<!-- Label for add-ons sub menu item for add-ons manager-->
|
||||||
<string name="mozac_feature_addons_addons_manager">Add-ons Manager</string>
|
<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 -->
|
<!-- 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>
|
<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-->
|
<!-- 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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
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.Addon
|
||||||
import mozilla.components.feature.addons.R
|
import mozilla.components.feature.addons.R
|
||||||
import mozilla.components.feature.addons.amo.AMOAddonsProvider
|
import mozilla.components.feature.addons.amo.AMOAddonsProvider
|
||||||
|
|
@ -51,6 +53,7 @@ class AddonsManagerAdapterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val coroutinesTestRule = MainCoroutineRule()
|
val coroutinesTestRule = MainCoroutineRule()
|
||||||
private val scope = coroutinesTestRule.scope
|
private val scope = coroutinesTestRule.scope
|
||||||
|
private val dispatcher = coroutinesTestRule.testDispatcher
|
||||||
|
|
||||||
// We must pass these variables to `bindAddon()` because looking up the version name
|
// 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.
|
// requires package info that we do not have in the test context.
|
||||||
|
|
@ -64,7 +67,7 @@ class AddonsManagerAdapterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `createListWithSections`() {
|
fun `createListWithSections`() {
|
||||||
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList())
|
val adapter = AddonsManagerAdapter(mock(), mock(), emptyList(), mock(), emptyList(), mock())
|
||||||
|
|
||||||
val installedAddon: Addon = mock()
|
val installedAddon: Addon = mock()
|
||||||
val recommendedAddon: Addon = mock()
|
val recommendedAddon: Addon = mock()
|
||||||
|
|
@ -130,7 +133,7 @@ class AddonsManagerAdapterTest {
|
||||||
val addon = mock<Addon>()
|
val addon = mock<Addon>()
|
||||||
val mockedImageView = spy(ImageView(testContext))
|
val mockedImageView = spy(ImageView(testContext))
|
||||||
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
||||||
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList())
|
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock())
|
||||||
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).then {
|
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).then {
|
||||||
throw IOException("Request failed")
|
throw IOException("Request failed")
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +152,7 @@ class AddonsManagerAdapterTest {
|
||||||
val bitmap = mock<Bitmap>()
|
val bitmap = mock<Bitmap>()
|
||||||
val mockedImageView = spy(ImageView(testContext))
|
val mockedImageView = spy(ImageView(testContext))
|
||||||
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
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)
|
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
|
||||||
|
|
||||||
adapter.fetchIcon(addon, mockedImageView, scope).join()
|
adapter.fetchIcon(addon, mockedImageView, scope).join()
|
||||||
|
|
@ -163,7 +166,8 @@ class AddonsManagerAdapterTest {
|
||||||
val bitmap = mock<Bitmap>()
|
val bitmap = mock<Bitmap>()
|
||||||
val mockedImageView = spy(ImageView(testContext))
|
val mockedImageView = spy(ImageView(testContext))
|
||||||
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
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 {
|
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenAnswer {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
|
|
@ -185,7 +189,7 @@ class AddonsManagerAdapterTest {
|
||||||
whenever(addon.installedState).thenReturn(installedState)
|
whenever(addon.installedState).thenReturn(installedState)
|
||||||
val mockedImageView = spy(ImageView(testContext))
|
val mockedImageView = spy(ImageView(testContext))
|
||||||
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
val mockedAddonsProvider = mock<AMOAddonsProvider>()
|
||||||
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList())
|
val adapter = AddonsManagerAdapter(mockedAddonsProvider, mock(), emptyList(), mock(), emptyList(), mock())
|
||||||
val captor = argumentCaptor<BitmapDrawable>()
|
val captor = argumentCaptor<BitmapDrawable>()
|
||||||
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenReturn(null)
|
whenever(mockedAddonsProvider.getAddonIconBitmap(addon)).thenReturn(null)
|
||||||
|
|
||||||
|
|
@ -239,7 +243,7 @@ class AddonsManagerAdapterTest {
|
||||||
addonNameTextColor = android.R.color.transparent,
|
addonNameTextColor = android.R.color.transparent,
|
||||||
addonSummaryTextColor = android.R.color.white,
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -269,7 +273,7 @@ class AddonsManagerAdapterTest {
|
||||||
sectionsTextColor = android.R.color.black,
|
sectionsTextColor = android.R.color.black,
|
||||||
sectionsTypeFace = mock(),
|
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)
|
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
|
||||||
|
|
||||||
|
|
@ -292,7 +296,7 @@ class AddonsManagerAdapterTest {
|
||||||
sectionsTextColor = android.R.color.black,
|
sectionsTextColor = android.R.color.black,
|
||||||
sectionsTypeFace = mock(),
|
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)
|
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
|
||||||
|
|
||||||
|
|
@ -312,7 +316,7 @@ class AddonsManagerAdapterTest {
|
||||||
sectionsTextColor = android.R.color.black,
|
sectionsTextColor = android.R.color.black,
|
||||||
sectionsTypeFace = mock(),
|
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)
|
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
|
||||||
|
|
||||||
|
|
@ -333,7 +337,7 @@ class AddonsManagerAdapterTest {
|
||||||
sectionsTypeFace = mock(),
|
sectionsTypeFace = mock(),
|
||||||
visibleDividers = false,
|
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)
|
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
|
||||||
|
|
||||||
|
|
@ -361,7 +365,7 @@ class AddonsManagerAdapterTest {
|
||||||
dividerHeight = dividerHeight,
|
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)
|
adapter.bindSection(addonViewHolder, Section(R.string.mozac_feature_addons_disabled_section), position)
|
||||||
|
|
||||||
|
|
@ -399,7 +403,7 @@ class AddonsManagerAdapterTest {
|
||||||
createdAt = "",
|
createdAt = "",
|
||||||
updatedAt = "",
|
updatedAt = "",
|
||||||
)
|
)
|
||||||
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
|
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
|
||||||
|
|
||||||
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
verify(titleView).setText("id")
|
verify(titleView).setText("id")
|
||||||
|
|
@ -417,7 +421,7 @@ class AddonsManagerAdapterTest {
|
||||||
createdAt = "",
|
createdAt = "",
|
||||||
updatedAt = "",
|
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])
|
assertEquals(addon, adapter.addonsMap[addon.id])
|
||||||
|
|
||||||
|
|
@ -449,7 +453,8 @@ class AddonsManagerAdapterTest {
|
||||||
createdAt = "",
|
createdAt = "",
|
||||||
updatedAt = "",
|
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(addon1, adapter.addonsMap[addon1.id])
|
||||||
assertEquals(addon2, adapter.addonsMap[addon2.id])
|
assertEquals(addon2, adapter.addonsMap[addon2.id])
|
||||||
|
|
@ -530,7 +535,15 @@ class AddonsManagerAdapterTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val unsupportedAddons = arrayListOf(unsupportedAddon, unsupportedAddonTwo)
|
val unsupportedAddons = arrayListOf(unsupportedAddon, unsupportedAddonTwo)
|
||||||
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, unsupportedAddons)
|
val adapter = AddonsManagerAdapter(
|
||||||
|
mock(),
|
||||||
|
addonsManagerAdapterDelegate,
|
||||||
|
unsupportedAddons,
|
||||||
|
mock(),
|
||||||
|
emptyList(),
|
||||||
|
mock(),
|
||||||
|
)
|
||||||
|
|
||||||
adapter.bindNotYetSupportedSection(unsupportedSectionViewHolder, mock())
|
adapter.bindNotYetSupportedSection(unsupportedSectionViewHolder, mock())
|
||||||
verify(unsupportedSectionViewHolder.descriptionView).setText(
|
verify(unsupportedSectionViewHolder.descriptionView).setText(
|
||||||
testContext.getString(R.string.mozac_feature_addons_unsupported_caption_plural, unsupportedAddons.size),
|
testContext.getString(R.string.mozac_feature_addons_unsupported_caption_plural, unsupportedAddons.size),
|
||||||
|
|
@ -543,7 +556,8 @@ class AddonsManagerAdapterTest {
|
||||||
@Test
|
@Test
|
||||||
fun bindFooterButton() {
|
fun bindFooterButton() {
|
||||||
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
||||||
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList())
|
val adapter = AddonsManagerAdapter(mock(), addonsManagerAdapterDelegate, emptyList(), mock(), emptyList(), mock())
|
||||||
|
|
||||||
val view = View(testContext)
|
val view = View(testContext)
|
||||||
val viewHolder = CustomViewHolder.FooterViewHolder(view)
|
val viewHolder = CustomViewHolder.FooterViewHolder(view)
|
||||||
adapter.bindFooterButton(viewHolder)
|
adapter.bindFooterButton(viewHolder)
|
||||||
|
|
@ -552,11 +566,46 @@ class AddonsManagerAdapterTest {
|
||||||
verify(addonsManagerAdapterDelegate).onFindMoreAddonsButtonClicked()
|
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
|
@Test
|
||||||
fun testFindMoreAddonsButtonIsHidden() {
|
fun testFindMoreAddonsButtonIsHidden() {
|
||||||
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
||||||
whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(false)
|
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())
|
val itemsWithSections = adapter.createListWithSections(emptyList())
|
||||||
assertTrue(itemsWithSections.isEmpty())
|
assertTrue(itemsWithSections.isEmpty())
|
||||||
|
|
@ -566,7 +615,7 @@ class AddonsManagerAdapterTest {
|
||||||
fun testFindMoreAddonsButtonIsVisible() {
|
fun testFindMoreAddonsButtonIsVisible() {
|
||||||
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
val addonsManagerAdapterDelegate: AddonsManagerAdapterDelegate = mock()
|
||||||
whenever(addonsManagerAdapterDelegate.shouldShowFindMoreAddonsButton()).thenReturn(true)
|
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())
|
val itemsWithSections = adapter.createListWithSections(emptyList())
|
||||||
assertFalse(itemsWithSections.isEmpty())
|
assertFalse(itemsWithSections.isEmpty())
|
||||||
|
|
@ -603,7 +652,7 @@ class AddonsManagerAdapterTest {
|
||||||
)
|
)
|
||||||
val addonName = "some addon name"
|
val addonName = "some addon name"
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED, addonName)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -646,7 +695,7 @@ class AddonsManagerAdapterTest {
|
||||||
statusErrorView = statusErrorView,
|
statusErrorView = statusErrorView,
|
||||||
)
|
)
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.BLOCKLISTED)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -684,7 +733,7 @@ class AddonsManagerAdapterTest {
|
||||||
)
|
)
|
||||||
val addonName = "some addon name"
|
val addonName = "some addon name"
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED, addonName)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -727,7 +776,7 @@ class AddonsManagerAdapterTest {
|
||||||
statusErrorView = statusErrorView,
|
statusErrorView = statusErrorView,
|
||||||
)
|
)
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.NOT_CORRECTLY_SIGNED)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -765,7 +814,7 @@ class AddonsManagerAdapterTest {
|
||||||
)
|
)
|
||||||
val addonName = "some addon name"
|
val addonName = "some addon name"
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE, addonName)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
@ -803,7 +852,7 @@ class AddonsManagerAdapterTest {
|
||||||
statusErrorView = statusErrorView,
|
statusErrorView = statusErrorView,
|
||||||
)
|
)
|
||||||
val addon = makeDisabledAddon(Addon.DisabledReason.INCOMPATIBLE)
|
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)
|
adapter.bindAddon(addonViewHolder, addon, appName, appVersion)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,34 +12,34 @@ import mozilla.components.lib.state.ext.flowScoped
|
||||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
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.
|
* disabled due to the restart threshold being met.
|
||||||
*
|
*
|
||||||
* @property store the application's [BrowserStore].
|
* @property store the application's [BrowserStore].
|
||||||
* @property onShowExtensionProcessDisabledPopup a callback invoked when the application should open a
|
* @property onShowExtensionsProcessDisabledPrompt a callback invoked when the application should open a
|
||||||
* popup.
|
* prompt.
|
||||||
*/
|
*/
|
||||||
open class ExtensionProcessDisabledPopupObserver(
|
open class ExtensionsProcessDisabledPromptObserver(
|
||||||
private val store: BrowserStore,
|
private val store: BrowserStore,
|
||||||
private val onShowExtensionProcessDisabledPopup: () -> Unit = { },
|
private val onShowExtensionsProcessDisabledPrompt: () -> Unit = { },
|
||||||
) : LifecycleAwareFeature {
|
) : LifecycleAwareFeature {
|
||||||
private var popupScope: CoroutineScope? = null
|
private var promptScope: CoroutineScope? = null
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
popupScope = store.flowScoped { flow ->
|
promptScope = store.flowScoped { flow ->
|
||||||
flow.distinctUntilChangedBy { it.showExtensionProcessDisabledPopup }
|
flow.distinctUntilChangedBy { it.showExtensionsProcessDisabledPrompt }
|
||||||
.collect { state ->
|
.collect { state ->
|
||||||
if (state.showExtensionProcessDisabledPopup) {
|
if (state.showExtensionsProcessDisabledPrompt) {
|
||||||
// There should only be one active dialog to the user when the extensions
|
// There should only be one active dialog to the user when the extensions
|
||||||
// process spawning is disabled.
|
// process spawning is disabled.
|
||||||
onShowExtensionProcessDisabledPopup()
|
onShowExtensionsProcessDisabledPrompt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
popupScope?.cancel()
|
promptScope?.cancel()
|
||||||
popupScope = null
|
promptScope = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import mozilla.components.browser.state.action.CustomTabListAction
|
import mozilla.components.browser.state.action.CustomTabListAction
|
||||||
import mozilla.components.browser.state.action.EngineAction
|
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.TabListAction
|
||||||
import mozilla.components.browser.state.action.WebExtensionAction
|
import mozilla.components.browser.state.action.WebExtensionAction
|
||||||
import mozilla.components.browser.state.selector.allTabs
|
import mozilla.components.browser.state.selector.allTabs
|
||||||
|
|
@ -333,7 +333,7 @@ object WebExtensionSupport {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisabledExtensionProcessSpawning() {
|
override fun onDisabledExtensionProcessSpawning() {
|
||||||
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
|
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -889,7 +889,7 @@ class WebExtensionSupportTest {
|
||||||
val store = BrowserStore()
|
val store = BrowserStore()
|
||||||
val engine: Engine = mock()
|
val engine: Engine = mock()
|
||||||
|
|
||||||
assertFalse(store.state.showExtensionProcessDisabledPopup)
|
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
|
val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
|
||||||
WebExtensionSupport.initialize(engine, store)
|
WebExtensionSupport.initialize(engine, store)
|
||||||
|
|
||||||
|
|
@ -898,7 +898,7 @@ class WebExtensionSupportTest {
|
||||||
delegateCaptor.value.onDisabledExtensionProcessSpawning()
|
delegateCaptor.value.onDisabledExtensionProcessSpawning()
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
|
|
||||||
assertTrue(store.state.showExtensionProcessDisabledPopup)
|
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
||||||
addonsManagerDelegate = this@AddonsFragment,
|
addonsManagerDelegate = this@AddonsFragment,
|
||||||
addons = addons,
|
addons = addons,
|
||||||
style = style,
|
style = style,
|
||||||
|
store = context.components.store,
|
||||||
)
|
)
|
||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ import org.mozilla.fenix.GleanMetrics.StartOnHome
|
||||||
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
|
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
|
||||||
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
|
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
|
||||||
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
|
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.BrowsingMode
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||||
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
|
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
|
||||||
|
|
@ -194,8 +194,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||||
WebExtensionPopupObserver(components.core.store, ::openPopup)
|
WebExtensionPopupObserver(components.core.store, ::openPopup)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val extensionProcessDisabledPopupObserver by lazy {
|
private val extensionsProcessDisabledPromptObserver by lazy {
|
||||||
ExtensionProcessDisabledController(this@HomeActivity, components.core.store)
|
ExtensionsProcessDisabledController(this@HomeActivity, components.core.store)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val serviceWorkerSupport by lazy {
|
private val serviceWorkerSupport by lazy {
|
||||||
|
|
@ -347,7 +347,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||||
}
|
}
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
|
|
||||||
lifecycle.addObservers(webExtensionPopupObserver, extensionProcessDisabledPopupObserver, serviceWorkerSupport)
|
lifecycle.addObservers(webExtensionPopupObserver, extensionsProcessDisabledPromptObserver, serviceWorkerSupport)
|
||||||
|
|
||||||
if (shouldAddToRecentsScreen(intent)) {
|
if (shouldAddToRecentsScreen(intent)) {
|
||||||
intent.removeExtra(START_IN_RECENTS_SCREEN)
|
intent.removeExtra(START_IN_RECENTS_SCREEN)
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
|
||||||
runIfFragmentIsAttached {
|
runIfFragmentIsAttached {
|
||||||
if (!shouldRefresh) {
|
if (!shouldRefresh) {
|
||||||
adapter = AddonsManagerAdapter(
|
adapter = AddonsManagerAdapter(
|
||||||
requireContext().components.addonsProvider,
|
addonsProvider = requireContext().components.addonsProvider,
|
||||||
managementView,
|
addonsManagerDelegate = managementView,
|
||||||
addons,
|
addons = addons,
|
||||||
style = createAddonStyle(requireContext()),
|
style = createAddonStyle(requireContext()),
|
||||||
excludedAddonIDs,
|
excludedAddonIDs = excludedAddonIDs,
|
||||||
|
store = requireComponents.core.store,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
binding?.addOnsProgressBar?.isVisible = false
|
binding?.addOnsProgressBar?.isVisible = false
|
||||||
|
|
|
||||||
|
|
@ -11,33 +11,28 @@ import android.widget.TextView
|
||||||
import androidx.annotation.UiContext
|
import androidx.annotation.UiContext
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.LifecycleOwner
|
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.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.Engine
|
|
||||||
import mozilla.components.support.ktx.android.content.appName
|
import mozilla.components.support.ktx.android.content.appName
|
||||||
import mozilla.components.support.webextensions.ExtensionProcessDisabledPopupObserver
|
import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
|
||||||
import org.mozilla.fenix.GleanMetrics.Addons
|
|
||||||
import org.mozilla.fenix.R
|
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 context to show the AlertDialog
|
||||||
* @param store The [BrowserStore] which holds the state for showing the dialog
|
* @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 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
|
* @param appName to be added to the message. Optional and mainly relevant for testing
|
||||||
*/
|
*/
|
||||||
class ExtensionProcessDisabledController(
|
class ExtensionsProcessDisabledController(
|
||||||
@UiContext context: Context,
|
@UiContext context: Context,
|
||||||
store: BrowserStore,
|
store: BrowserStore,
|
||||||
engine: Engine = context.components.core.engine,
|
|
||||||
builder: AlertDialog.Builder = AlertDialog.Builder(context),
|
builder: AlertDialog.Builder = AlertDialog.Builder(context),
|
||||||
appName: String = context.appName,
|
appName: String = context.appName,
|
||||||
) : ExtensionProcessDisabledPopupObserver(
|
) : ExtensionsProcessDisabledPromptObserver(
|
||||||
store,
|
store,
|
||||||
{ presentDialog(context, store, engine, builder, appName) },
|
{ presentDialog(context, store, builder, appName) },
|
||||||
) {
|
) {
|
||||||
override fun onDestroy(owner: LifecycleOwner) {
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
super.onDestroy(owner)
|
super.onDestroy(owner)
|
||||||
|
|
@ -49,21 +44,18 @@ class ExtensionProcessDisabledController(
|
||||||
private var shouldCreateDialog: Boolean = true
|
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,
|
* whether they would like to continue trying or disable extensions. If the user chooses to retry,
|
||||||
* enable the extension process spawning with [Engine.enableExtensionProcessSpawning].
|
* enable the extensions process spawning. Otherwise, disable it.
|
||||||
* Otherwise, call [Engine.disableExtensionProcessSpawning].
|
|
||||||
*
|
*
|
||||||
* @param context to show the AlertDialog
|
* @param context to show the AlertDialog
|
||||||
* @param store The [BrowserStore] which holds the state for showing the dialog
|
* @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 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
|
* @param appName to be added to the message. Necessary to be added as a param for testing
|
||||||
*/
|
*/
|
||||||
private fun presentDialog(
|
private fun presentDialog(
|
||||||
@UiContext context: Context,
|
@UiContext context: Context,
|
||||||
store: BrowserStore,
|
store: BrowserStore,
|
||||||
engine: Engine,
|
|
||||||
builder: AlertDialog.Builder,
|
builder: AlertDialog.Builder,
|
||||||
appName: String,
|
appName: String,
|
||||||
) {
|
) {
|
||||||
|
|
@ -78,15 +70,13 @@ class ExtensionProcessDisabledController(
|
||||||
layout?.apply {
|
layout?.apply {
|
||||||
findViewById<TextView>(R.id.message)?.text = message
|
findViewById<TextView>(R.id.message)?.text = message
|
||||||
findViewById<Button>(R.id.positive)?.setOnClickListener {
|
findViewById<Button>(R.id.positive)?.setOnClickListener {
|
||||||
engine.enableExtensionProcessSpawning()
|
store.dispatch(ExtensionsProcessAction.ShowPromptAction(false))
|
||||||
Addons.extensionsProcessUiRetry.add()
|
store.dispatch(ExtensionsProcessAction.EnabledAction)
|
||||||
store.dispatch(ExtensionProcessDisabledPopupAction(false))
|
|
||||||
onDismissDialog?.invoke()
|
onDismissDialog?.invoke()
|
||||||
}
|
}
|
||||||
findViewById<Button>(R.id.negative)?.setOnClickListener {
|
findViewById<Button>(R.id.negative)?.setOnClickListener {
|
||||||
engine.disableExtensionProcessSpawning()
|
store.dispatch(ExtensionsProcessAction.ShowPromptAction(false))
|
||||||
Addons.extensionsProcessUiDisable.add()
|
store.dispatch(ExtensionsProcessAction.DisabledAction)
|
||||||
store.dispatch(ExtensionProcessDisabledPopupAction(false))
|
|
||||||
onDismissDialog?.invoke()
|
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.ContentAction
|
||||||
import mozilla.components.browser.state.action.DownloadAction
|
import mozilla.components.browser.state.action.DownloadAction
|
||||||
import mozilla.components.browser.state.action.EngineAction
|
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.action.TabListAction
|
||||||
import mozilla.components.browser.state.selector.findTab
|
import mozilla.components.browser.state.selector.findTab
|
||||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
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.internal.TimerId
|
||||||
import mozilla.telemetry.glean.private.NoExtras
|
import mozilla.telemetry.glean.private.NoExtras
|
||||||
import org.mozilla.fenix.Config
|
import org.mozilla.fenix.Config
|
||||||
|
import org.mozilla.fenix.GleanMetrics.Addons
|
||||||
import org.mozilla.fenix.GleanMetrics.Events
|
import org.mozilla.fenix.GleanMetrics.Events
|
||||||
import org.mozilla.fenix.GleanMetrics.Metrics
|
import org.mozilla.fenix.GleanMetrics.Metrics
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
|
@ -136,6 +138,12 @@ class TelemetryMiddleware(
|
||||||
Metrics.hasOpenTabs.set(false)
|
Metrics.hasOpenTabs.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is ExtensionsProcessAction.EnabledAction -> {
|
||||||
|
Addons.extensionsProcessUiRetry.add()
|
||||||
|
}
|
||||||
|
is ExtensionsProcessAction.DisabledAction -> {
|
||||||
|
Addons.extensionsProcessUiDisable.add()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ package org.mozilla.fenix.addons
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.appcompat.app.AlertDialog
|
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.browser.state.store.BrowserStore
|
||||||
import mozilla.components.concept.engine.Engine
|
|
||||||
import mozilla.components.support.test.argumentCaptor
|
import mozilla.components.support.test.argumentCaptor
|
||||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
|
@ -21,39 +20,41 @@ import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.never
|
|
||||||
import org.mockito.Mockito.times
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
class ExtensionProcessDisabledControllerTest {
|
class ExtensionsProcessDisabledControllerTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val coroutinesTestRule = MainCoroutineRule()
|
val coroutinesTestRule = MainCoroutineRule()
|
||||||
private val dispatcher = coroutinesTestRule.testDispatcher
|
private val dispatcher = coroutinesTestRule.testDispatcher
|
||||||
|
|
||||||
@Test
|
@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 store = BrowserStore()
|
||||||
val engine: Engine = mock()
|
|
||||||
val dialog: AlertDialog = mock()
|
val dialog: AlertDialog = mock()
|
||||||
val appName = "TestApp"
|
val appName = "TestApp"
|
||||||
val builder: AlertDialog.Builder = 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>()
|
val buttonsContainerCaptor = argumentCaptor<View>()
|
||||||
|
|
||||||
controller.start()
|
controller.start()
|
||||||
|
|
||||||
whenever(builder.show()).thenReturn(dialog)
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
assertTrue(store.state.showExtensionProcessDisabledPopup)
|
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
|
assertTrue(store.state.extensionsProcessDisabled)
|
||||||
|
|
||||||
verify(builder).setView(buttonsContainerCaptor.capture())
|
verify(builder).setView(buttonsContainerCaptor.capture())
|
||||||
verify(builder).show()
|
verify(builder).show()
|
||||||
|
|
@ -62,32 +63,34 @@ class ExtensionProcessDisabledControllerTest {
|
||||||
|
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
|
|
||||||
verify(engine).enableExtensionProcessSpawning()
|
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
verify(engine, never()).disableExtensionProcessSpawning()
|
assertFalse(store.state.extensionsProcessDisabled)
|
||||||
assertFalse(store.state.showExtensionProcessDisabledPopup)
|
|
||||||
verify(dialog).dismiss()
|
verify(dialog).dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 store = BrowserStore()
|
||||||
val engine: Engine = mock()
|
|
||||||
val appName = "TestApp"
|
val appName = "TestApp"
|
||||||
val dialog: AlertDialog = mock()
|
val dialog: AlertDialog = mock()
|
||||||
val builder: AlertDialog.Builder = 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>()
|
val buttonsContainerCaptor = argumentCaptor<View>()
|
||||||
|
|
||||||
controller.start()
|
controller.start()
|
||||||
|
|
||||||
whenever(builder.show()).thenReturn(dialog)
|
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()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
assertTrue(store.state.showExtensionProcessDisabledPopup)
|
assertTrue(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
|
assertTrue(store.state.extensionsProcessDisabled)
|
||||||
|
|
||||||
verify(builder).setView(buttonsContainerCaptor.capture())
|
verify(builder).setView(buttonsContainerCaptor.capture())
|
||||||
verify(builder).show()
|
verify(builder).show()
|
||||||
|
|
@ -96,20 +99,18 @@ class ExtensionProcessDisabledControllerTest {
|
||||||
|
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
|
|
||||||
assertFalse(store.state.showExtensionProcessDisabledPopup)
|
assertFalse(store.state.showExtensionsProcessDisabledPrompt)
|
||||||
verify(engine, never()).enableExtensionProcessSpawning()
|
assertTrue(store.state.extensionsProcessDisabled)
|
||||||
verify(engine).disableExtensionProcessSpawning()
|
|
||||||
verify(dialog).dismiss()
|
verify(dialog).dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `WHEN dispatching the same event twice THEN the dialog should only be created once`() {
|
fun `WHEN dispatching the same event twice THEN the dialog should only be created once`() {
|
||||||
val store = BrowserStore()
|
val store = BrowserStore()
|
||||||
val engine: Engine = mock()
|
|
||||||
val appName = "TestApp"
|
val appName = "TestApp"
|
||||||
val dialog: AlertDialog = mock()
|
val dialog: AlertDialog = mock()
|
||||||
val builder: AlertDialog.Builder = 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>()
|
val buttonsContainerCaptor = argumentCaptor<View>()
|
||||||
|
|
||||||
controller.start()
|
controller.start()
|
||||||
|
|
@ -117,12 +118,12 @@ class ExtensionProcessDisabledControllerTest {
|
||||||
whenever(builder.show()).thenReturn(dialog)
|
whenever(builder.show()).thenReturn(dialog)
|
||||||
|
|
||||||
// First dispatch...
|
// First dispatch...
|
||||||
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
|
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
|
||||||
dispatcher.scheduler.advanceUntilIdle()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
|
|
||||||
// Second dispatch... without having dismissed the dialog before!
|
// Second dispatch... without having dismissed the dialog before!
|
||||||
store.dispatch(ExtensionProcessDisabledPopupAction(showPopup = true))
|
store.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true))
|
||||||
dispatcher.scheduler.advanceUntilIdle()
|
dispatcher.scheduler.advanceUntilIdle()
|
||||||
store.waitUntilIdle()
|
store.waitUntilIdle()
|
||||||
|
|
||||||
|
|
@ -6,10 +6,13 @@ package org.mozilla.fenix.telemetry
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import mozilla.components.browser.state.action.ContentAction
|
import mozilla.components.browser.state.action.ContentAction
|
||||||
import mozilla.components.browser.state.action.EngineAction
|
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.action.TabListAction
|
||||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||||
import mozilla.components.browser.state.state.BrowserState
|
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.RecoverableTab
|
||||||
import mozilla.components.browser.state.state.recover.TabState
|
import mozilla.components.browser.state.state.recover.TabState
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.Engine
|
||||||
import mozilla.components.service.glean.testing.GleanTestRule
|
import mozilla.components.service.glean.testing.GleanTestRule
|
||||||
import mozilla.components.support.base.android.Clock
|
import mozilla.components.support.base.android.Clock
|
||||||
import mozilla.components.support.test.ext.joinBlocking
|
import mozilla.components.support.test.ext.joinBlocking
|
||||||
|
|
@ -33,6 +37,7 @@ import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.GleanMetrics.Addons
|
||||||
import org.mozilla.fenix.GleanMetrics.Events
|
import org.mozilla.fenix.GleanMetrics.Events
|
||||||
import org.mozilla.fenix.GleanMetrics.Metrics
|
import org.mozilla.fenix.GleanMetrics.Metrics
|
||||||
import org.mozilla.fenix.components.AppStore
|
import org.mozilla.fenix.components.AppStore
|
||||||
|
|
@ -78,8 +83,11 @@ class TelemetryMiddlewareTest {
|
||||||
searchState = searchState,
|
searchState = searchState,
|
||||||
timerId = timerId,
|
timerId = timerId,
|
||||||
)
|
)
|
||||||
|
val engine: Engine = mockk()
|
||||||
|
every { engine.enableExtensionProcessSpawning() } just runs
|
||||||
|
every { engine.disableExtensionProcessSpawning() } just runs
|
||||||
store = BrowserStore(
|
store = BrowserStore(
|
||||||
middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine = mockk()),
|
middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine),
|
||||||
initialState = BrowserState(),
|
initialState = BrowserState(),
|
||||||
)
|
)
|
||||||
appStore = AppStore()
|
appStore = AppStore()
|
||||||
|
|
@ -417,6 +425,28 @@ class TelemetryMiddlewareTest {
|
||||||
|
|
||||||
verify { metrics.track(Event.GrowthData.FirstUriLoadForDay) }
|
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 {
|
internal class FakeClock : Clock.Delegate {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue