Commit c0f857b5 authored by MozLando's avatar MozLando
Browse files

Merge #5121



5121: Closes #5090: Wire up GeckoView action delegate for BrowserActions r=psymoon,Amejia481 a=csadilek

This follows the same pattern as content/background messaging, with the only difference that we have to be able to register action handlers for each extension on every session. The latter is the reason I moved the session-specific action out of the `EngineObserver` into our web extension module. Ultimately, we want to get rid of`EngineObserver` anyway once we fully migrated to browser-state.

I am also going to add a browser action to our sample-browser extension, but I will do this as part of #4791, as this PR is already pretty big.

We will get new GV API for installing extensions i.e different calls for built-in and third-party extensions. For now I've also introduced a `supportActions` boolean, similar to our `allowContentMessaging` to be able to configure per extension if we need to hook up action delegates. We really don't need that for our current extensions, so it makes sense not to register all these handlers.
Co-authored-by: default avatarChristian Sadilek <christian.sadilek@gmail.com>
parents 9b8e2760 a9d4e2d2
......@@ -136,10 +136,11 @@ class GeckoEngine(
id: String,
url: String,
allowContentMessaging: Boolean,
supportActions: Boolean,
onSuccess: ((WebExtension) -> Unit),
onError: ((String, Throwable) -> Unit)
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
GeckoWebExtension(id, url, allowContentMessaging, supportActions).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
......
......@@ -6,6 +6,7 @@ package mozilla.components.browser.engine.gecko.webextension
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtension
......@@ -21,13 +22,14 @@ class GeckoWebExtension(
id: String,
url: String,
allowContentMessaging: Boolean = true,
supportActions: Boolean = false,
val nativeExtension: GeckoNativeWebExtension = GeckoNativeWebExtension(
url,
id,
createWebExtensionFlags(allowContentMessaging)
),
private val connectedPorts: MutableMap<PortId, Port> = mutableMapOf()
) : WebExtension(id, url) {
) : WebExtension(id, url, supportActions) {
/**
* Uniquely identifies a port using its name and the session it
......@@ -142,6 +144,11 @@ class GeckoWebExtension(
connectedPorts.remove(portId)
}
}
// Not yet supported in beta
override fun registerActionHandler(actionHandler: ActionHandler) = Unit
override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) = Unit
override fun hasActionHandler(session: EngineSession) = false
}
/**
......
......@@ -35,7 +35,13 @@ class GeckoWebExtensionTest {
val portCaptor = argumentCaptor<Port>()
val portDelegateCaptor = argumentCaptor<WebExtension.PortDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -86,7 +92,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
assertFalse(extension.hasContentMessageHandler(session, "mozacTest"))
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -138,7 +150,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -158,7 +176,13 @@ class GeckoWebExtensionTest {
val nativeGeckoWebExt: WebExtension = mock()
val messageHandler: MessageHandler = mock()
val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
......
......@@ -144,10 +144,11 @@ class GeckoEngine(
id: String,
url: String,
allowContentMessaging: Boolean,
supportActions: Boolean,
onSuccess: ((WebExtension) -> Unit),
onError: ((String, Throwable) -> Unit)
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
GeckoWebExtension(id, url, allowContentMessaging, supportActions).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
......
......@@ -5,13 +5,19 @@
package mozilla.components.browser.engine.gecko.webextension
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.browser.engine.gecko.await
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.support.base.log.logger.Logger
import org.json.JSONObject
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.WebExtension as GeckoNativeWebExtension
import org.mozilla.geckoview.WebExtension.Action as GeckoNativeWebExtensionAction
/**
* Gecko-based implementation of [WebExtension], wrapping the native web
......@@ -21,13 +27,16 @@ class GeckoWebExtension(
id: String,
url: String,
allowContentMessaging: Boolean = true,
supportActions: Boolean = false,
val nativeExtension: GeckoNativeWebExtension = GeckoNativeWebExtension(
url,
id,
createWebExtensionFlags(allowContentMessaging)
),
private val connectedPorts: MutableMap<PortId, Port> = mutableMapOf()
) : WebExtension(id, url) {
) : WebExtension(id, url, supportActions) {
private val logger = Logger("GeckoWebExtension")
/**
* Uniquely identifies a port using its name and the session it
......@@ -142,6 +151,64 @@ class GeckoWebExtension(
connectedPorts.remove(portId)
}
}
/**
* See [WebExtension.registerActionHandler].
*/
override fun registerActionHandler(actionHandler: ActionHandler) {
if (!supportActions) {
logger.error("Attempt to register default action handler but browser and page " +
"action support is turned off for this extension: $id")
return
}
val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
override fun onBrowserAction(
ext: GeckoNativeWebExtension,
// Session will always be null here for the global default delegate
session: GeckoSession?,
action: GeckoNativeWebExtensionAction
) {
actionHandler.onBrowserAction(this@GeckoWebExtension, null, action.toBrowserAction())
}
}
nativeExtension.setActionDelegate(actionDelegate)
}
/**
* See [WebExtension.registerActionHandler].
*/
override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) {
if (!supportActions) {
logger.error("Attempt to register action handler on session but browser and page " +
"action support is turned off for this extension: $id")
return
}
val actionDelegate = object : GeckoNativeWebExtension.ActionDelegate {
override fun onBrowserAction(
ext: GeckoNativeWebExtension,
geckoSession: GeckoSession?,
action: GeckoNativeWebExtensionAction
) {
actionHandler.onBrowserAction(this@GeckoWebExtension, session, action.toBrowserAction())
}
}
val geckoSession = (session as GeckoEngineSession).geckoSession
geckoSession.setWebExtensionActionDelegate(nativeExtension, actionDelegate)
}
/**
* See [WebExtension.hasActionHandler].
*/
override fun hasActionHandler(session: EngineSession): Boolean {
val geckoSession = (session as GeckoEngineSession).geckoSession
return geckoSession.getWebExtensionActionDelegate(nativeExtension) != null
}
}
/**
......@@ -172,3 +239,14 @@ private fun createWebExtensionFlags(allowContentMessaging: Boolean): Long {
GeckoNativeWebExtension.Flags.NONE
}
}
private fun GeckoNativeWebExtensionAction.toBrowserAction() =
BrowserAction(
title ?: "",
enabled ?: true,
{ size -> icon?.get(size)?.await() },
badgeText ?: "",
badgeTextColor ?: 0,
badgeBackgroundColor ?: 0,
{ click() }
)
......@@ -6,6 +6,8 @@ package mozilla.components.browser.engine.gecko.webextension
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.support.test.argumentCaptor
......@@ -17,8 +19,10 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mozilla.geckoview.GeckoSession
......@@ -35,9 +39,15 @@ class GeckoWebExtensionTest {
val portCaptor = argumentCaptor<Port>()
val portDelegateCaptor = argumentCaptor<WebExtension.PortDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
// Verify messages are forwarded to message handler
......@@ -86,7 +96,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
assertFalse(extension.hasContentMessageHandler(session, "mozacTest"))
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -138,7 +154,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -158,7 +180,13 @@ class GeckoWebExtensionTest {
val nativeGeckoWebExt: WebExtension = mock()
val messageHandler: MessageHandler = mock()
val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -173,4 +201,82 @@ class GeckoWebExtensionTest {
verify(port).disconnect()
assertNull(extension.getConnectedPort("mozacTest"))
}
}
\ No newline at end of file
@Test
fun `register global default action handler`() {
val nativeGeckoWebExt: WebExtension = mock()
val actionHandler: ActionHandler = mock()
val actionDelegateCaptor = argumentCaptor<WebExtension.ActionDelegate>()
val actionCaptor = argumentCaptor<BrowserAction>()
val nativeBrowserAction: WebExtension.Action = mock()
// Verify actions will not be acted on when not supported
val extensionWithActions = GeckoWebExtension(
"mozacTest",
"url",
false,
false,
nativeGeckoWebExt
)
extensionWithActions.registerActionHandler(actionHandler)
verify(nativeGeckoWebExt, never()).setActionDelegate(actionDelegateCaptor.capture())
// Create extension and register global default action handler
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerActionHandler(actionHandler)
verify(nativeGeckoWebExt).setActionDelegate(actionDelegateCaptor.capture())
// Verify that browser actions are forwarded to the handler
actionDelegateCaptor.value.onBrowserAction(nativeGeckoWebExt, null, nativeBrowserAction)
verify(actionHandler).onBrowserAction(eq(extension), eq(null), actionCaptor.capture())
// We don't have access to the native WebExtension.Action fields and
// can't mock them either, but we can verify that we've linked
// the actions by simulating a click.
actionCaptor.value.onClick()
verify(nativeBrowserAction).click()
}
@Test
fun `register session-specific action handler`() {
val session: GeckoEngineSession = mock()
val geckoSession: GeckoSession = mock()
whenever(session.geckoSession).thenReturn(geckoSession)
val nativeGeckoWebExt: WebExtension = mock()
val actionHandler: ActionHandler = mock()
val actionDelegateCaptor = argumentCaptor<WebExtension.ActionDelegate>()
val actionCaptor = argumentCaptor<BrowserAction>()
val nativeBrowserAction: WebExtension.Action = mock()
// Create extension and register action handler for session
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerActionHandler(session, actionHandler)
verify(geckoSession).setWebExtensionActionDelegate(eq(nativeGeckoWebExt), actionDelegateCaptor.capture())
whenever(geckoSession.getWebExtensionActionDelegate(nativeGeckoWebExt)).thenReturn(actionDelegateCaptor.value)
assertTrue(extension.hasActionHandler(session))
// Verify that browser actions are forwarded to the handler
actionDelegateCaptor.value.onBrowserAction(nativeGeckoWebExt, null, nativeBrowserAction)
verify(actionHandler).onBrowserAction(eq(extension), eq(session), actionCaptor.capture())
// We don't have access to the native WebExtension.Action fields and
// can't mock them either, but we can verify that we've linked
// the actions by simulating a click.
actionCaptor.value.onClick()
verify(nativeBrowserAction).click()
}
}
......@@ -136,10 +136,11 @@ class GeckoEngine(
id: String,
url: String,
allowContentMessaging: Boolean,
supportActions: Boolean,
onSuccess: ((WebExtension) -> Unit),
onError: ((String, Throwable) -> Unit)
) {
GeckoWebExtension(id, url, allowContentMessaging).also { ext ->
GeckoWebExtension(id, url, allowContentMessaging, supportActions).also { ext ->
runtime.registerWebExtension(ext.nativeExtension).then({
webExtensionDelegate?.onInstalled(ext)
onSuccess(ext)
......
......@@ -6,6 +6,7 @@ package mozilla.components.browser.engine.gecko.webextension
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtension
......@@ -21,13 +22,14 @@ class GeckoWebExtension(
id: String,
url: String,
allowContentMessaging: Boolean = true,
supportActions: Boolean = false,
val nativeExtension: GeckoNativeWebExtension = GeckoNativeWebExtension(
url,
id,
createWebExtensionFlags(allowContentMessaging)
),
private val connectedPorts: MutableMap<PortId, Port> = mutableMapOf()
) : WebExtension(id, url) {
) : WebExtension(id, url, supportActions) {
/**
* Uniquely identifies a port using its name and the session it
......@@ -132,6 +134,11 @@ class GeckoWebExtension(
connectedPorts.remove(portId)
}
}
// Not yet supported in release
override fun registerActionHandler(actionHandler: ActionHandler) = Unit
override fun registerActionHandler(session: EngineSession, actionHandler: ActionHandler) = Unit
override fun hasActionHandler(session: EngineSession) = false
}
/**
......
......@@ -35,7 +35,13 @@ class GeckoWebExtensionTest {
val portCaptor = argumentCaptor<Port>()
val portDelegateCaptor = argumentCaptor<WebExtension.PortDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -86,7 +92,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
assertFalse(extension.hasContentMessageHandler(session, "mozacTest"))
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -138,7 +150,13 @@ class GeckoWebExtensionTest {
whenever(session.geckoSession).thenReturn(geckoSession)
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerContentMessageHandler(session, "mozacTest", messageHandler)
verify(geckoSession).setMessageDelegate(eq(nativeGeckoWebExt), messageDelegateCaptor.capture(), eq("mozacTest"))
......@@ -158,7 +176,13 @@ class GeckoWebExtensionTest {
val nativeGeckoWebExt: WebExtension = mock()
val messageHandler: MessageHandler = mock()
val messageDelegateCaptor = argumentCaptor<WebExtension.MessageDelegate>()
val extension = GeckoWebExtension("mozacTest", "url", true, nativeGeckoWebExt)
val extension = GeckoWebExtension(
id = "mozacTest",
url = "url",
allowContentMessaging = true,
supportActions = true,
nativeExtension = nativeGeckoWebExt
)
extension.registerBackgroundMessageHandler("mozacTest", messageHandler)
verify(nativeGeckoWebExt).setMessageDelegate(messageDelegateCaptor.capture(), eq("mozacTest"))
......
......@@ -13,7 +13,6 @@ import mozilla.components.browser.session.engine.request.LoadRequestOption
import mozilla.components.browser.session.ext.syncDispatch
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TrackingProtectionAction
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
......@@ -24,7 +23,6 @@ import mozilla.components.concept.engine.media.Media
import mozilla.components.concept.engine.media.RecordingDevice
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.webextension.BrowserAction
import mozilla.components.concept.engine.window.WindowRequest
import mozilla.components.support.base.observer.Consumable
......@@ -220,16 +218,6 @@ internal class EngineObserver(
media.unregisterObservers()
}
override fun onBrowserActionChange(webExtensionId: String, action: BrowserAction) {
store?.dispatch(
WebExtensionAction.UpdateTabBrowserAction(
session.id,
webExtensionId,
action
)
<