diff --git a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt index 2bc3720bb88..12160788c10 100644 --- a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt +++ b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt @@ -13,10 +13,13 @@ import mozilla.components.concept.engine.EngineSessionState import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.Settings import mozilla.components.concept.engine.history.HistoryTrackingDelegate +import mozilla.components.concept.engine.webextension.WebExtension import org.json.JSONObject +import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoWebExecutor +import org.mozilla.geckoview.WebExtension as GeckoWebExtension /** * Gecko-based implementation of Engine interface. @@ -60,6 +63,24 @@ class GeckoEngine( executor.speculativeConnect(url) } + /** + * See [Engine.installWebExtension]. + */ + override fun installWebExtension( + ext: WebExtension, + onSuccess: ((WebExtension) -> Unit), + onError: ((WebExtension, Throwable) -> Unit) + ) { + val result = runtime.registerWebExtension(GeckoWebExtension(ext.url, ext.id)) + result.then({ + onSuccess(ext) + GeckoResult() + }, { + throwable -> onError(ext, throwable) + GeckoResult() + }) + } + override fun name(): String = "Gecko" /** diff --git a/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt b/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt index 5a9f8b87fd9..0e9369fa2db 100644 --- a/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt +++ b/components/browser/engine-gecko-nightly/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt @@ -8,6 +8,9 @@ import android.content.Context import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy import mozilla.components.concept.engine.UnsupportedSettingException +import mozilla.components.concept.engine.webextension.WebExtension +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -19,12 +22,15 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mozilla.geckoview.ContentBlocking +import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntimeSettings import org.mozilla.geckoview.GeckoSession import org.mozilla.geckoview.GeckoWebExecutor +import org.mozilla.geckoview.WebExtension as GeckoWebExtension import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment +import java.io.IOException @RunWith(RobolectricTestRunner::class) class GeckoEngineTest { @@ -141,9 +147,51 @@ class GeckoEngineTest { val executor: GeckoWebExecutor = mock() val engine = GeckoEngine(context, runtime = runtime, executorProvider = { executor }) - engine.speculativeConnect("https://www.mozilla.org") - verify(executor).speculativeConnect("https://www.mozilla.org") } + + @Test + fun `install web extension successfully`() { + val runtime = mock(GeckoRuntime::class.java) + val engine = GeckoEngine(context, runtime = runtime) + var onSuccessCalled = false + var onErrorCalled = false + var result = GeckoResult() + + `when`(runtime.registerWebExtension(any())).thenReturn(result) + engine.installWebExtension( + WebExtension("test-webext", "resource://android/assets/extensions/test"), + onSuccess = { onSuccessCalled = true }, + onError = { _, _ -> onErrorCalled = true } + ) + result.complete(null) + + val extCaptor = argumentCaptor() + verify(runtime).registerWebExtension(extCaptor.capture()) + assertEquals("test-webext", extCaptor.value.id) + assertEquals("resource://android/assets/extensions/test", extCaptor.value.location) + assertTrue(onSuccessCalled) + assertFalse(onErrorCalled) + } + + @Test + fun `install web extension failure`() { + val runtime = mock(GeckoRuntime::class.java) + val engine = GeckoEngine(context, runtime = runtime) + var onErrorCalled = false + val expected = IOException() + var result = GeckoResult() + + var throwable: Throwable? = null + `when`(runtime.registerWebExtension(any())).thenReturn(result) + engine.installWebExtension(WebExtension("test-webext-error", "resource://android/assets/extensions/error")) { _, e -> + onErrorCalled = true + throwable = e + } + result.completeExceptionally(expected) + + assertTrue(onErrorCalled) + assertEquals(expected, throwable) + } } \ No newline at end of file diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt index f874d6c3cb9..cffb97f989a 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/Engine.kt @@ -6,7 +6,9 @@ package mozilla.components.concept.engine import android.content.Context import android.util.AttributeSet +import mozilla.components.concept.engine.webextension.WebExtension import org.json.JSONObject +import java.lang.UnsupportedOperationException /** * Entry point for interacting with the engine implementation. @@ -56,6 +58,20 @@ interface Engine { */ fun speculativeConnect(url: String) + /** + * Installs the provided extension in this engine. + * + * @param ext the [WebExtension] to install. + * @param onSuccess (optional) callback invoked if the extension was installed successfully. + * @param onError (optional) callback invoked if there was an error installing the extension. + * @throws UnsupportedOperationException if this engine doesn't support web extensions. + */ + fun installWebExtension( + ext: WebExtension, + onSuccess: ((WebExtension) -> Unit) = { }, + onError: ((WebExtension, Throwable) -> Unit) = { _, _ -> } + ): Unit = throw UnsupportedOperationException("Web extension support is not available in this engine") + /** * Provides access to the settings of this engine. */ diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt new file mode 100644 index 00000000000..a3f51785af7 --- /dev/null +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt @@ -0,0 +1,18 @@ +/* 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 + +/** + * Represents a browser extension based on the WebExtension API: + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions + * + * @property id the unique ID of this extension. + * @property url the url pointing to a resources path for locating the extension + * within the APK file e.g. resource://android/assets/extensions/my_web_ext. + */ +data class WebExtension( + val id: String, + val url: String +) diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt new file mode 100644 index 00000000000..9801d11093c --- /dev/null +++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/EngineTest.kt @@ -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.concept.engine + +import android.content.Context +import android.util.AttributeSet +import mozilla.components.concept.engine.webextension.WebExtension +import org.json.JSONObject +import org.junit.Test +import java.lang.UnsupportedOperationException + +class EngineTest { + + @Test(expected = UnsupportedOperationException::class) + fun `throws exception if webextensions not supported`() { + val engine = object : Engine { + override fun createView(context: Context, attrs: AttributeSet?): EngineView { + throw NotImplementedError("Not needed for test") + } + + override fun createSession(private: Boolean): EngineSession { + throw NotImplementedError("Not needed for test") + } + + override fun createSessionState(json: JSONObject): EngineSessionState { + throw NotImplementedError("Not needed for test") + } + + override fun name(): String { + throw NotImplementedError("Not needed for test") + } + + override fun speculativeConnect(url: String) { + throw NotImplementedError("Not needed for test") + } + + override val settings: Settings + get() = throw NotImplementedError("Not needed for test") + } + engine.installWebExtension(WebExtension("my-ext", "resource://path")) + } +} \ No newline at end of file diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt new file mode 100644 index 00000000000..591f79143d1 --- /dev/null +++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/webextension/WebExtensionTest.kt @@ -0,0 +1,23 @@ +/* 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 org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class WebExtensionTest { + + @Test + fun createWebExtension() { + val ext1 = WebExtension("1", "url1") + val ext2 = WebExtension("2", "url2") + + assertNotEquals(ext1, ext2) + assertEquals(ext1, WebExtension("1", "url1")) + assertEquals("1", ext1.id) + assertEquals("url1", ext1.url) + } +} \ No newline at end of file diff --git a/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt b/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt index a6e7085dc19..226c2a73e1e 100644 --- a/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt +++ b/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt @@ -6,12 +6,18 @@ package org.mozilla.samples.browser import android.content.Context import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.webextension.WebExtension +import mozilla.components.support.base.log.Log /** * Helper class for lazily instantiating components needed by the application. */ class Components(private val applicationContext: Context) : DefaultComponents(applicationContext) { override val engine: Engine by lazy { - GeckoEngine(applicationContext, engineSettings) + GeckoEngine(applicationContext, engineSettings).apply { + installWebExtension(WebExtension("mozac-borderify", "resource://android/assets/extensions/borderify/")) { + ext, throwable -> Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install ${ext.id}") + } + } } } diff --git a/samples/browser/src/main/assets/extensions/borderify/borderify.js b/samples/browser/src/main/assets/extensions/borderify/borderify.js new file mode 100644 index 00000000000..06b06b719df --- /dev/null +++ b/samples/browser/src/main/assets/extensions/borderify/borderify.js @@ -0,0 +1 @@ +document.body.style.border = "5px solid red"; \ No newline at end of file diff --git a/samples/browser/src/main/assets/extensions/borderify/manifest.json b/samples/browser/src/main/assets/extensions/borderify/manifest.json new file mode 100644 index 00000000000..c4ef27a8869 --- /dev/null +++ b/samples/browser/src/main/assets/extensions/borderify/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 2, + "name": "Mozilla Android Components - Borderify", + "version": "1.0", + "content_scripts": [ + { + "matches": ["*://*.mozilla.org/*"], + "js": ["borderify.js"] + } + ] +}