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

Closes #4438: Add support for browser.tabs.create()

parent ad99377a
......@@ -24,6 +24,7 @@ import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import org.json.JSONObject
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlockingController
......@@ -33,6 +34,7 @@ import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.WebExtensionController
import java.lang.IllegalStateException
/**
......@@ -146,6 +148,28 @@ class GeckoEngine(
}
}
/**
* See [Engine.registerWebExtensionTabDelegate].
*/
override fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
) {
val tabsDelegate = object : WebExtensionController.TabDelegate {
override fun onNewTab(
webExtension: org.mozilla.geckoview.WebExtension?,
url: String?
): GeckoResult<GeckoSession>? {
val geckoEngineSession = GeckoEngineSession(runtime, openGeckoSession = false)
val geckoWebExtension = webExtension?.let { GeckoWebExtension(it.id, it.location) }
webExtensionTabDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
}
}
runtime.webExtensionController.tabDelegate = tabsDelegate
}
/**
* See [Engine.clearData].
*/
......
......@@ -17,6 +17,7 @@ import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
......@@ -44,6 +45,7 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.StorageController
import org.mozilla.geckoview.WebExtensionController
import org.robolectric.Robolectric
import java.io.IOException
import java.lang.Exception
......@@ -523,6 +525,41 @@ class GeckoEngineTest {
assertEquals(expected, throwable)
}
@Test
fun `register web extension tab delegate`() {
val runtime: GeckoRuntime = mock()
val webExtensionController: WebExtensionController = mock()
whenever(runtime.webExtensionController).thenReturn(webExtensionController)
val webExtensionsTabDelegate: WebExtensionTabDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionTabDelegate(webExtensionsTabDelegate)
val captor = argumentCaptor<WebExtensionController.TabDelegate>()
verify(webExtensionController).tabDelegate = captor.capture()
val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
captor.value.onNewTab(null, null)
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
captor.value.onNewTab(null, "https://www.mozilla.org")
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
val webExt = org.mozilla.geckoview.WebExtension("test")
val geckoExtCap = argumentCaptor<mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension>()
captor.value.onNewTab(webExt, "https://test-moz.org")
verify(webExtensionsTabDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
engineSessionCaptor.capture())
assertNotNull(geckoExtCap.value)
assertEquals(geckoExtCap.value.id, webExt.id)
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
}
@Test(expected = RuntimeException::class)
fun `WHEN GeckoRuntime is shutting down THEN GeckoEngine throws runtime exception`() {
val runtime: GeckoRuntime = mock()
......
......@@ -24,6 +24,7 @@ import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import org.json.JSONObject
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlockingController
......@@ -33,6 +34,7 @@ import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.WebExtensionController
import java.lang.IllegalStateException
/**
......@@ -145,6 +147,28 @@ class GeckoEngine(
}
}
/**
* See [Engine.registerWebExtensionTabDelegate].
*/
override fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
) {
val tabsDelegate = object : WebExtensionController.TabDelegate {
override fun onNewTab(
webExtension: org.mozilla.geckoview.WebExtension?,
url: String?
): GeckoResult<GeckoSession>? {
val geckoEngineSession = GeckoEngineSession(runtime, openGeckoSession = false)
val geckoWebExtension = webExtension?.let { GeckoWebExtension(it.id, it.location) }
webExtensionTabDelegate.onNewTab(geckoWebExtension, url ?: "", geckoEngineSession)
return GeckoResult.fromValue(geckoEngineSession.geckoSession)
}
}
runtime.webExtensionController.tabDelegate = tabsDelegate
}
/**
* See [Engine.clearData].
*/
......
......@@ -18,6 +18,7 @@ import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
......@@ -46,6 +47,7 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoWebExecutor
import org.mozilla.geckoview.StorageController
import org.mozilla.geckoview.WebExtensionController
import org.robolectric.Robolectric
import java.io.IOException
import java.lang.Exception
......@@ -522,6 +524,41 @@ class GeckoEngineTest {
assertEquals(expected, throwable)
}
@Test
fun `register web extension tab delegate`() {
val runtime: GeckoRuntime = mock()
val webExtensionController: WebExtensionController = mock()
whenever(runtime.webExtensionController).thenReturn(webExtensionController)
val webExtensionsTabDelegate: WebExtensionTabDelegate = mock()
val engine = GeckoEngine(context, runtime = runtime)
engine.registerWebExtensionTabDelegate(webExtensionsTabDelegate)
val captor = argumentCaptor<WebExtensionController.TabDelegate>()
verify(webExtensionController).tabDelegate = captor.capture()
val engineSessionCaptor = argumentCaptor<GeckoEngineSession>()
captor.value.onNewTab(null, null)
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq(""), engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
captor.value.onNewTab(null, "https://www.mozilla.org")
verify(webExtensionsTabDelegate).onNewTab(eq(null), eq("https://www.mozilla.org"),
engineSessionCaptor.capture())
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
val webExt = org.mozilla.geckoview.WebExtension("test")
val geckoExtCap = argumentCaptor<mozilla.components.browser.engine.gecko.webextension.GeckoWebExtension>()
captor.value.onNewTab(webExt, "https://test-moz.org")
verify(webExtensionsTabDelegate).onNewTab(geckoExtCap.capture(), eq("https://test-moz.org"),
engineSessionCaptor.capture())
assertNotNull(geckoExtCap.value)
assertEquals(geckoExtCap.value.id, webExt.id)
assertNotNull(engineSessionCaptor.value)
assertFalse(engineSessionCaptor.value.geckoSession.isOpen)
}
@Test(expected = RuntimeException::class)
fun `WHEN GeckoRuntime is shutting down THEN GeckoEngine throws runtime exception`() {
val runtime: GeckoRuntime = mock()
......
......@@ -11,6 +11,7 @@ import mozilla.components.concept.engine.content.blocking.TrackingProtectionExce
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.utils.EngineVersion
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import org.json.JSONObject
import java.lang.UnsupportedOperationException
......@@ -129,6 +130,15 @@ interface Engine {
onError: ((String, Throwable) -> Unit) = { _, _ -> }
): Unit = onError(id, UnsupportedOperationException("Web extension support is not available in this engine"))
/**
* Registers a [WebExtensionTabDelegate] to be notified when web extensions attempt to open/close tabs.
*
* @param webExtensionTabDelegate callback is invoked when a web extension opens a new tab.
*/
fun registerWebExtensionTabDelegate(
webExtensionTabDelegate: WebExtensionTabDelegate
): Unit = throw UnsupportedOperationException("Web extension support is not available in this engine")
/**
* Clears browsing data stored by the engine.
*
......
/* 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.concept.engine.webextension
import mozilla.components.concept.engine.EngineSession
/**
* Notifies applications / other components that a web extension wants to open a new tab via
* browser.tabs.create. Note that browser.tabs.remove is currently not supported:
* https://github.com/mozilla-mobile/android-components/issues/4682
*/
interface WebExtensionTabDelegate {
/**
* Invoked when a web extension opens a new tab.
*
* @param webExtension An instance of [WebExtension] or null if extension was not registered with the engine.
* @param url the target url to be loaded in a new tab.
* @param engineSession an instance of engine session to open a new tab with.
*/
fun onNewTab(webExtension: WebExtension?, url: String, engineSession: EngineSession) = Unit
}
......@@ -30,6 +30,18 @@ permalink: /changelog/
* **firefox-accounts**, **service-fretboard**
* ⚠️ **This is a breaking change**: Due to migration to WorkManager v2.2.0, some classes like `WorkManagerSyncScheduler` and `WorkManagerSyncDispatcher` now expects a `Context` in their constructors.
* **engine**, **engine-gecko-nightly** and **engine-gecko-beta**
* Added `WebExtensionsTabsDelegate` to support `browser.tabs.create()` in web extensions.
```kotlin
GeckoEngine(applicationContext, engineSettings).also {
it.registerWebExtensionTabDelegate(object : WebExtensionTabDelegate {
override fun onNewTab(webExtension: WebExtension?, url: String, engineSession: EngineSession) {
sessionManager.add(Session(url), true, engineSession)
}
})
}
```
# 15.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v14.0.0...v15.0.0)
......
......@@ -6,7 +6,11 @@ package org.mozilla.samples.browser
import android.content.Context
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.feature.webcompat.WebCompatFeature
/**
......@@ -16,6 +20,12 @@ class Components(applicationContext: Context) : DefaultComponents(applicationCon
override val engine: Engine by lazy {
GeckoEngine(applicationContext, engineSettings).also {
WebCompatFeature.install(it)
it.registerWebExtensionTabDelegate(object : WebExtensionTabDelegate {
override fun onNewTab(webExtension: WebExtension?, url: String, engineSession: EngineSession) {
sessionManager.add(Session(url), true, engineSession)
}
})
}
}
}
......@@ -5,7 +5,11 @@ package org.mozilla.samples.browser
import android.content.Context
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionTabDelegate
import mozilla.components.feature.webcompat.WebCompatFeature
import mozilla.components.support.base.log.Log
......@@ -20,6 +24,12 @@ class Components(private val applicationContext: Context) : DefaultComponents(ap
}
WebCompatFeature.install(it)
it.registerWebExtensionTabDelegate(object : WebExtensionTabDelegate {
override fun onNewTab(webExtension: WebExtension?, url: String, engineSession: EngineSession) {
sessionManager.add(Session(url), true, engineSession)
}
})
}
}
}
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