Skip to content

Commit

Permalink
Closes mozilla-mobile#2150: Add engine API to install WebExtensions
Browse files Browse the repository at this point in the history
  • Loading branch information
csadilek committed Feb 26, 2019
1 parent bd23c8d commit a8d6fa3
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Void>()
}, {
throwable -> onError(ext, throwable)
GeckoResult<Void>()
})
}

override fun name(): String = "Gecko"

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -146,4 +152,48 @@ class GeckoEngineTest {

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<Void>()

`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<GeckoWebExtension>()
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<Void>()

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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"))
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
10 changes: 10 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ permalink: /changelog/
* [Gecko](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Gecko.kt)
* [Configuration](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Config.kt)

* **browser-engine-gecko-nightly**
* Added API to install web extensions:

```kotlin
val borderify = WebExtension("borderify", "resource://android/assets/extensions/borderify/")
engine.installWebExtension(borderify) {
ext, throwable -> Log.log(Log.Priority.ERROR, "MyApp", throwable, "Failed to install ${ext.id}")
}
```

* **feature-toolbar**
* Added ability to color parts of the domain (e.g. [registrable domain](https://url.spec.whatwg.org/#host-registrable-domain)) by providing a `UrlRenderConfiguration`:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
document.body.style.border = "5px solid red";
11 changes: 11 additions & 0 deletions samples/browser/src/main/assets/extensions/borderify/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"manifest_version": 2,
"name": "Mozilla Android Components - Borderify",
"version": "1.0",
"content_scripts": [
{
"matches": ["*://*.mozilla.org/*"],
"js": ["borderify.js"]
}
]
}

0 comments on commit a8d6fa3

Please sign in to comment.