Commit bd9a5127 authored by Simon Chae's avatar Simon Chae
Browse files

Closes #5049: Add WebExtensionToolbarFeature

parent f1717afd
......@@ -11,19 +11,16 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection
import mozilla.components.feature.toolbar.internal.URLRenderer
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
typealias WebExtensionBrowserAction = mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
/**
* Presenter implementation for a toolbar implementation in order to update the toolbar whenever
* the state of the selected session or web extensions changes.
* the state of the selected session.
*/
@Suppress("TooManyFunctions")
class ToolbarPresenter(
......@@ -37,10 +34,6 @@ class ToolbarPresenter(
private var scope: CoroutineScope? = null
// This maps web extension id to [WebExtensionToolbarAction]
@VisibleForTesting
internal val webExtensionBrowserActions = HashMap<String, WebExtensionToolbarAction>()
/**
* Start presenter: Display data in toolbar.
*/
......@@ -48,7 +41,7 @@ class ToolbarPresenter(
renderer.start()
scope = store.flowScoped { flow ->
flow.ifAnyChanged { arrayOf(it.findCustomTabOrSelectedTab(customTabId), it.extensions) }
flow.ifChanged { it.findCustomTabOrSelectedTab(customTabId) }
.collect { state ->
render(state)
}
......@@ -85,38 +78,11 @@ class ToolbarPresenter(
else -> SiteTrackingProtection.OFF_GLOBALLY
}
renderWebExtensionActions(state, tab)
} else {
clear()
}
}
@VisibleForTesting(otherwise = PRIVATE)
internal fun renderWebExtensionActions(state: BrowserState, tab: SessionState) {
val extensionsMap = state.extensions.toMutableMap()
extensionsMap.putAll(tab.extensionState)
val extensions = extensionsMap.values.toList()
extensions.forEach { extension ->
extension.browserAction?.let { extensionAction ->
val existingBrowserAction = webExtensionBrowserActions[extension.id]
if (existingBrowserAction != null) {
existingBrowserAction.browserAction = extensionAction
} else {
val toolbarAction = WebExtensionToolbarAction(
browserAction = extensionAction,
listener = extensionAction.onClick)
toolbar.addBrowserAction(toolbarAction)
webExtensionBrowserActions[extension.id] = toolbarAction
}
toolbar.invalidateActions()
}
}
}
@VisibleForTesting(otherwise = PRIVATE)
internal fun clear() {
renderer.post("")
......
/* 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.feature.toolbar
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
typealias WebExtensionBrowserAction = BrowserAction
/**
* Web extension toolbar implementation that updates the toolbar whenever the state of web
* extensions changes.
*/
class WebExtensionToolbarFeature(
private val toolbar: Toolbar,
private var store: BrowserStore
) : LifecycleAwareFeature {
// This maps web extension id to [WebExtensionToolbarAction]
@VisibleForTesting
internal val webExtensionBrowserActions = HashMap<String, WebExtensionToolbarAction>()
private var scope: CoroutineScope? = null
/**
* Starts observing for the state of web extensions changes
*/
override fun start() {
scope = store.flowScoped { flow ->
flow.ifAnyChanged { arrayOf(it.selectedTab, it.extensions) }
.collect { state ->
state.selectedTab?.let { tab ->
renderWebExtensionActions(state, tab)
}
}
}
}
override fun stop() {
scope?.cancel()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun renderWebExtensionActions(state: BrowserState, tab: SessionState) {
val extensionsMap = state.extensions.toMutableMap()
extensionsMap.putAll(tab.extensionState)
val extensions = extensionsMap.values.toList()
extensions.forEach { extension ->
extension.browserAction?.let { extensionAction ->
val existingBrowserAction = webExtensionBrowserActions[extension.id]
if (existingBrowserAction != null) {
existingBrowserAction.browserAction = extensionAction
} else {
val toolbarAction = WebExtensionToolbarAction(
browserAction = extensionAction,
listener = extensionAction.onClick)
toolbar.addBrowserAction(toolbarAction)
webExtensionBrowserActions[extension.id] = toolbarAction
}
toolbar.invalidateActions()
}
}
}
}
......@@ -4,7 +4,6 @@
package mozilla.components.feature.toolbar
import android.graphics.Color
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
......@@ -15,25 +14,18 @@ import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.TrackingProtectionAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.TrackingProtectionState
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.feature.toolbar.internal.URLRenderer
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.never
......@@ -41,8 +33,6 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
typealias WebExtensionBrowserAction = mozilla.components.concept.engine.webextension.BrowserAction
class ToolbarPresenterTest {
private val testDispatcher = TestCoroutineDispatcher()
......@@ -111,50 +101,6 @@ class ToolbarPresenterTest {
verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
}
@Test
fun `render overridden web extension action from browser state`() {
val defaultBrowserAction =
WebExtensionBrowserAction("default_title", false, mock(), "", "", 0, 0) {}
val overriddenBrowserAction =
WebExtensionBrowserAction("overridden_title", false, mock(), "", "", 0, 0) {}
val toolbar: Toolbar = mock()
val extensions: Map<String, WebExtensionState> = mapOf(
"id" to WebExtensionState("id", "url", defaultBrowserAction)
)
val overriddenExtensions: Map<String, WebExtensionState> = mapOf(
"id" to WebExtensionState("id", "url", overriddenBrowserAction)
)
val store = spy(
BrowserStore(
BrowserState(
tabs = listOf(
createTab(
"https://www.example.org", id = "tab1",
extensions = overriddenExtensions
)
), selectedTabId = "tab1",
extensions = extensions
)
)
)
val toolbarPresenter = spy(ToolbarPresenter(toolbar, store))
toolbarPresenter.renderer = mock()
toolbarPresenter.start()
testDispatcher.advanceUntilIdle()
verify(store).observeManually(any())
verify(toolbarPresenter).render(any())
val delegateCaptor = argumentCaptor<WebExtensionToolbarAction>()
verify(toolbarPresenter.renderer).post("https://www.example.org")
verify(toolbar).addBrowserAction(delegateCaptor.capture())
assertEquals("overridden_title", delegateCaptor.value.browserAction.title)
}
@Test
fun `SecurityInfoState change updates toolbar`() {
val toolbar: Toolbar = mock()
......@@ -472,56 +418,4 @@ class ToolbarPresenterTest {
verify(toolbar).displayProgress(0)
verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE
}
@Test
fun `WebExtensionBrowserAction is replaced when the web extension is already in the toolbar`() {
val toolbarPresenter = ToolbarPresenter(mock(), mock())
val browserAction = BrowserAction(
title = "title",
icon = mock(),
enabled = true,
badgeText = "badgeText",
badgeTextColor = Color.WHITE,
badgeBackgroundColor = Color.BLUE,
uri = "uri"
) {}
val browserActionDisabled = BrowserAction(
title = "title",
icon = mock(),
enabled = false,
badgeText = "badgeText",
badgeTextColor = Color.WHITE,
badgeBackgroundColor = Color.BLUE,
uri = "uri"
) {}
// Verify browser extension toolbar rendering
val browserExtensions = HashMap<String, WebExtensionState>()
browserExtensions["1"] = WebExtensionState(id = "1", browserAction = browserAction)
browserExtensions["2"] = WebExtensionState(id = "2", browserAction = browserActionDisabled)
val browserState = BrowserState(extensions = browserExtensions)
toolbarPresenter.renderWebExtensionActions(browserState, mock())
assertTrue(toolbarPresenter.webExtensionBrowserActions.size == 2)
assertTrue(toolbarPresenter.webExtensionBrowserActions["1"]!!.browserAction.enabled)
assertFalse(toolbarPresenter.webExtensionBrowserActions["2"]!!.browserAction.enabled)
// Verify tab with existing extension in the toolbar to update its BrowserAction
val tabExtensions = HashMap<String, WebExtensionState>()
tabExtensions["3"] = WebExtensionState(id = "1", browserAction = browserActionDisabled)
val tabSessionState = CustomTabSessionState(
content = mock(),
config = mock(),
extensionState = tabExtensions
)
toolbarPresenter.renderWebExtensionActions(browserState, tabSessionState)
assertTrue(toolbarPresenter.webExtensionBrowserActions.size == 2)
assertFalse(toolbarPresenter.webExtensionBrowserActions["1"]!!.browserAction.enabled)
assertFalse(toolbarPresenter.webExtensionBrowserActions["2"]!!.browserAction.enabled)
}
}
/* 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.feature.toolbar
import android.graphics.Color
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
typealias WebExtensionBrowserAction = BrowserAction
class WebExtensionToolbarFeatureTest {
private val testDispatcher = TestCoroutineDispatcher()
@Before
@ExperimentalCoroutinesApi
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
@ExperimentalCoroutinesApi
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `render overridden web extension action from browser state`() {
val defaultBrowserAction =
WebExtensionBrowserAction("default_title", false, mock(), "", "", 0, 0) {}
val overriddenBrowserAction =
WebExtensionBrowserAction("overridden_title", false, mock(), "", "", 0, 0) {}
val toolbar: Toolbar = mock()
val extensions: Map<String, WebExtensionState> = mapOf(
"id" to WebExtensionState("id", "url", defaultBrowserAction)
)
val overriddenExtensions: Map<String, WebExtensionState> = mapOf(
"id" to WebExtensionState("id", "url", overriddenBrowserAction)
)
val store = spy(
BrowserStore(
BrowserState(
tabs = listOf(
createTab(
"https://www.example.org", id = "tab1",
extensions = overriddenExtensions
)
), selectedTabId = "tab1",
extensions = extensions
)
)
)
val webExtToolbarFeature = spy(WebExtensionToolbarFeature(toolbar, store))
webExtToolbarFeature.start()
testDispatcher.advanceUntilIdle()
verify(store).observeManually(any())
verify(webExtToolbarFeature).renderWebExtensionActions(any(), any())
val delegateCaptor = argumentCaptor<WebExtensionToolbarAction>()
verify(toolbar).addBrowserAction(delegateCaptor.capture())
assertEquals("overridden_title", delegateCaptor.value.browserAction.title)
}
@Test
fun `WebExtensionBrowserAction is replaced when the web extension is already in the toolbar`() {
val webExtToolbarFeature = WebExtensionToolbarFeature(mock(), mock())
val browserAction = BrowserAction(
title = "title",
icon = mock(),
enabled = true,
badgeText = "badgeText",
badgeTextColor = Color.WHITE,
badgeBackgroundColor = Color.BLUE,
uri = "uri"
) {}
val browserActionDisabled = BrowserAction(
title = "title",
icon = mock(),
enabled = false,
badgeText = "badgeText",
badgeTextColor = Color.WHITE,
badgeBackgroundColor = Color.BLUE,
uri = "uri"
) {}
// Verify browser extension toolbar rendering
val browserExtensions = HashMap<String, WebExtensionState>()
browserExtensions["1"] = WebExtensionState(id = "1", browserAction = browserAction)
browserExtensions["2"] = WebExtensionState(id = "2", browserAction = browserActionDisabled)
val browserState = BrowserState(extensions = browserExtensions)
webExtToolbarFeature.renderWebExtensionActions(browserState, mock())
assertTrue(webExtToolbarFeature.webExtensionBrowserActions.size == 2)
assertTrue(webExtToolbarFeature.webExtensionBrowserActions["1"]!!.browserAction.enabled)
assertFalse(webExtToolbarFeature.webExtensionBrowserActions["2"]!!.browserAction.enabled)
// Verify tab with existing extension in the toolbar to update its BrowserAction
val tabExtensions = HashMap<String, WebExtensionState>()
tabExtensions["3"] = WebExtensionState(id = "1", browserAction = browserActionDisabled)
val tabSessionState = CustomTabSessionState(
content = mock(),
config = mock(),
extensionState = tabExtensions
)
webExtToolbarFeature.renderWebExtensionActions(browserState, tabSessionState)
assertTrue(webExtToolbarFeature.webExtensionBrowserActions.size == 2)
assertFalse(webExtToolbarFeature.webExtensionBrowserActions["1"]!!.browserAction.enabled)
assertFalse(webExtToolbarFeature.webExtensionBrowserActions["2"]!!.browserAction.enabled)
}
}
......@@ -15,6 +15,7 @@ import mozilla.components.feature.session.ThumbnailsFeature
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.feature.tabs.toolbar.TabsToolbarFeature
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.feature.toolbar.WebExtensionToolbarFeature
import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.samples.browser.ext.components
......@@ -26,6 +27,7 @@ import org.mozilla.samples.browser.integration.ReaderViewIntegration
class BrowserFragment : BaseBrowserFragment(), BackHandler {
private val thumbnailsFeature = ViewBoundFeatureWrapper<ThumbnailsFeature>()
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val layout = super.onCreateView(inflater, container, savedInstanceState)
......@@ -69,6 +71,15 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
view = layout
)
webExtToolbarFeature.set(
feature = WebExtensionToolbarFeature(
layout.toolbar,
components.store
),
owner = this,
view = layout
)
val windowFeature = WindowFeature(components.store, components.tabsUseCases)
lifecycle.addObserver(windowFeature)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment