Bug 1599581 - Implement update extension for GeckoView. r=esawin
Wires up the addon manager update process to the GekcoView API. Adds several basic tests for verifying the udpate steps. Adds a new way to automatically bundle new test addons into xpi files and registers them with the example.com server. Differential Revision: https://phabricator.services.mozilla.com/D61501 --HG-- extra : moz-landing-system : lando
|
@ -1530,6 +1530,7 @@ package org.mozilla.geckoview {
|
|||
field public static final int ERROR_NETWORK_FAILURE = -1;
|
||||
field public static final int ERROR_SIGNEDSTATE_REQUIRED = -5;
|
||||
field public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6;
|
||||
field public static final int ERROR_USER_CANCELED = -100;
|
||||
}
|
||||
|
||||
@UiThread public static interface WebExtension.MessageDelegate {
|
||||
|
@ -1603,6 +1604,7 @@ package org.mozilla.geckoview {
|
|||
method @AnyThread public void setTabActive(@NonNull GeckoSession, boolean);
|
||||
method @UiThread public void setTabDelegate(@Nullable WebExtensionController.TabDelegate);
|
||||
method @NonNull @AnyThread public GeckoResult<Void> uninstall(@NonNull WebExtension);
|
||||
method @AnyThread @NonNull public GeckoResult<WebExtension> update(@NonNull WebExtension);
|
||||
}
|
||||
|
||||
public static class WebExtensionController.EnableSource {
|
||||
|
@ -1613,6 +1615,7 @@ package org.mozilla.geckoview {
|
|||
|
||||
@UiThread public static interface WebExtensionController.PromptDelegate {
|
||||
method @Nullable default public GeckoResult<AllowOrDeny> onInstallPrompt(@NonNull WebExtension);
|
||||
method @Nullable default public GeckoResult<AllowOrDeny> onUpdatePrompt(@NonNull WebExtension, @NonNull WebExtension, @NonNull String[], @NonNull String[]);
|
||||
}
|
||||
|
||||
public static interface WebExtensionController.TabDelegate {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
addons = [
|
||||
'update-1',
|
||||
'update-2',
|
||||
'update-with-perms-1',
|
||||
'update-with-perms-2',
|
||||
]
|
||||
|
||||
for addon in addons:
|
||||
indir = 'web_extensions/%s' % addon
|
||||
xpi = '%s.xpi' % indir
|
||||
|
||||
GENERATED_FILES += [xpi]
|
||||
GENERATED_FILES[xpi].script = '../../../../../../toolkit/mozapps/extensions/test/create_xpi.py'
|
||||
GENERATED_FILES[xpi].inputs = [indir]
|
||||
|
||||
TEST_HARNESS_FILES.testing.mochitest.tests.junit += ['!%s' % xpi]
|
|
@ -0,0 +1 @@
|
|||
document.body.style.border = "5px solid red";
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/update-1/icons/border-48.png
Normal file
После Ширина: | Высота: | Размер: 225 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "update",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "update@example.com",
|
||||
"update_url": "https://example.org/tests/junit/update_manifest.json"
|
||||
}
|
||||
},
|
||||
"version": "1.0",
|
||||
"description": "Adds a red border to all webpages matching example.com.",
|
||||
"icons": {
|
||||
"48": "icons/border-48.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.example.com/*"],
|
||||
"js": ["borderify.js"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
document.body.style.border = "5px solid blue";
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/update-2/icons/border-48.png
Normal file
После Ширина: | Высота: | Размер: 225 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "update",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "update@example.com"
|
||||
}
|
||||
},
|
||||
"version": "2.0",
|
||||
"description": "Adds a blue border to all webpages matching example.com.",
|
||||
"icons": {
|
||||
"48": "icons/border-48.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.example.com/*"],
|
||||
"js": ["borderify.js"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
document.body.style.border = "5px solid red";
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/update-with-perms-1/icons/border-48.png
Normal file
После Ширина: | Высота: | Размер: 225 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "update",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "update-with-perms@example.com",
|
||||
"update_url": "https://example.org/tests/junit/update_manifest.json"
|
||||
}
|
||||
},
|
||||
"version": "1.0",
|
||||
"description": "Adds a red border to all webpages matching example.com.",
|
||||
"icons": {
|
||||
"48": "icons/border-48.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.example.com/*"],
|
||||
"js": ["borderify.js"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
document.body.style.border = "5px solid blue";
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/update-with-perms-2/icons/border-48.png
Normal file
После Ширина: | Высота: | Размер: 225 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "update",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "update-with-perms@example.com"
|
||||
}
|
||||
},
|
||||
"version": "2.0",
|
||||
"description": "Adds a blue border to all webpages matching example.com.",
|
||||
"icons": {
|
||||
"48": "icons/border-48.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.example.com/*"],
|
||||
"js": ["borderify.js"]
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"tabs"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"addons": {
|
||||
"update@example.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.0",
|
||||
"update_link": "https://example.org/tests/junit/update-1.xpi"
|
||||
},
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.org/tests/junit/update-2.xpi"
|
||||
}
|
||||
]
|
||||
},
|
||||
"update-with-perms@example.com": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "1.0",
|
||||
"update_link": "https://example.org/tests/junit/update-with-perms-1.xpi"
|
||||
},
|
||||
{
|
||||
"version": "2.0",
|
||||
"update_link": "https://example.org/tests/junit/update-with-perms-2.xpi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,11 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import org.hamcrest.core.StringEndsWith.endsWith
|
||||
import org.hamcrest.core.IsEqual.equalTo
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -1009,4 +1005,239 @@ class WebExtensionTest : BaseSessionTest() {
|
|||
|
||||
fail("The above code should throw.")
|
||||
}
|
||||
|
||||
// Test the basic update extension flow with no new permissions.
|
||||
@Test
|
||||
fun update() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to false,
|
||||
"extensions.install.requireBuiltInCerts" to false,
|
||||
"extensions.update.requireBuiltInCerts" to false
|
||||
))
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// First let's check that the color of the border is empty before loading
|
||||
// the WebExtension
|
||||
assertBodyBorderEqualTo("")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(extension.metaData!!.version, "1.0")
|
||||
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val update1 = sessionRule.waitForResult(
|
||||
controller.install("https://example.org/tests/junit/update-1.xpi"))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was applied by checking the border color
|
||||
assertBodyBorderEqualTo("red")
|
||||
|
||||
val update2 = sessionRule.waitForResult(controller.update(update1));
|
||||
assertEquals(update2.metaData!!.version, "2.0")
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that updated extension changed the border color.
|
||||
assertBodyBorderEqualTo("blue")
|
||||
|
||||
// Unregister WebExtension and check again
|
||||
sessionRule.waitForResult(controller.uninstall(update2))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was not applied after being unregistered
|
||||
assertBodyBorderEqualTo("")
|
||||
}
|
||||
|
||||
// Test extension updating when the new extension has different permissions.
|
||||
@Test
|
||||
fun updateWithPerms() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to false,
|
||||
"extensions.install.requireBuiltInCerts" to false,
|
||||
"extensions.update.requireBuiltInCerts" to false
|
||||
))
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// First let's check that the color of the border is empty before loading
|
||||
// the WebExtension
|
||||
assertBodyBorderEqualTo("")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(extension.metaData!!.version, "1.0")
|
||||
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val update1 = sessionRule.waitForResult(
|
||||
controller.install("https://example.org/tests/junit/update-with-perms-1.xpi"))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was applied by checking the border color
|
||||
assertBodyBorderEqualTo("red")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onUpdatePrompt(currentlyInstalled: WebExtension,
|
||||
updatedExtension: WebExtension,
|
||||
newPermissions: Array<String>,
|
||||
newOrigins: Array<String>): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(currentlyInstalled.metaData!!.version, "1.0")
|
||||
assertEquals(updatedExtension.metaData!!.version, "2.0")
|
||||
assertEquals(newPermissions.size, 1)
|
||||
assertEquals(newPermissions[0], "tabs")
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW);
|
||||
}
|
||||
})
|
||||
|
||||
val update2 = sessionRule.waitForResult(controller.update(update1));
|
||||
assertEquals(update2.metaData!!.version, "2.0")
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that updated extension changed the border color.
|
||||
assertBodyBorderEqualTo("blue")
|
||||
|
||||
// Unregister WebExtension and check again
|
||||
sessionRule.waitForResult(controller.uninstall(update2))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was not applied after being unregistered
|
||||
assertBodyBorderEqualTo("")
|
||||
}
|
||||
|
||||
// Ensure update extension works as expected when there is no update available.
|
||||
@Test
|
||||
fun updateNotAvailable() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to false,
|
||||
"extensions.install.requireBuiltInCerts" to false,
|
||||
"extensions.update.requireBuiltInCerts" to false
|
||||
))
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// First let's check that the color of the border is empty before loading
|
||||
// the WebExtension
|
||||
assertBodyBorderEqualTo("")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(extension.metaData!!.version, "2.0")
|
||||
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val update1 = sessionRule.waitForResult(
|
||||
controller.install("https://example.org/tests/junit/update-2.xpi"))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was applied by checking the border color
|
||||
assertBodyBorderEqualTo("blue")
|
||||
|
||||
val update2 = sessionRule.waitForResult(controller.update(update1))
|
||||
assertNull(update2);
|
||||
|
||||
// Unregister WebExtension and check again
|
||||
sessionRule.waitForResult(controller.uninstall(update1))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was not applied after being unregistered
|
||||
assertBodyBorderEqualTo("")
|
||||
}
|
||||
|
||||
// Test denying an extension update.
|
||||
@Test
|
||||
fun updateDenyPerms() {
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf(
|
||||
"xpinstall.signatures.required" to false,
|
||||
"extensions.install.requireBuiltInCerts" to false,
|
||||
"extensions.update.requireBuiltInCerts" to false
|
||||
))
|
||||
mainSession.loadUri("example.com")
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// First let's check that the color of the border is empty before loading
|
||||
// the WebExtension
|
||||
assertBodyBorderEqualTo("")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onInstallPrompt(extension: WebExtension): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(extension.metaData!!.version, "1.0")
|
||||
|
||||
return GeckoResult.fromValue(AllowOrDeny.ALLOW)
|
||||
}
|
||||
})
|
||||
|
||||
val update1 = sessionRule.waitForResult(
|
||||
controller.install("https://example.org/tests/junit/update-with-perms-1.xpi"))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was applied by checking the border color
|
||||
assertBodyBorderEqualTo("red")
|
||||
|
||||
sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate {
|
||||
@AssertCalled
|
||||
override fun onUpdatePrompt(currentlyInstalled: WebExtension,
|
||||
updatedExtension: WebExtension,
|
||||
newPermissions: Array<String>,
|
||||
newOrigins: Array<String>): GeckoResult<AllowOrDeny> {
|
||||
assertEquals(currentlyInstalled.metaData!!.version, "1.0")
|
||||
assertEquals(updatedExtension.metaData!!.version, "2.0")
|
||||
return GeckoResult.fromValue(AllowOrDeny.DENY);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
sessionRule.waitForResult(controller.update(update1).accept({
|
||||
// We should not be able to update the extension.
|
||||
assertTrue(false)
|
||||
}, { exception ->
|
||||
assertTrue(exception is WebExtension.InstallException)
|
||||
val installException = exception as WebExtension.InstallException
|
||||
assertEquals(installException.code, WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED)
|
||||
}));
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that updated extension changed the border color.
|
||||
assertBodyBorderEqualTo("red")
|
||||
|
||||
// Unregister WebExtension and check again
|
||||
sessionRule.waitForResult(controller.uninstall(update1))
|
||||
|
||||
mainSession.reload()
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
// Check that the WebExtension was not applied after being unregistered
|
||||
assertBodyBorderEqualTo("")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1135,6 +1135,8 @@ public class WebExtension {
|
|||
public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6;
|
||||
/** The extension did not have the expected ID. */
|
||||
public static final int ERROR_INCORRECT_ID = -7;
|
||||
/** The extension install was canceled. */
|
||||
public static final int ERROR_USER_CANCELED = -100;
|
||||
|
||||
/** For testing. */
|
||||
protected ErrorCodes() {}
|
||||
|
@ -1148,7 +1150,8 @@ public class WebExtension {
|
|||
ErrorCodes.ERROR_FILE_ACCESS,
|
||||
ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED,
|
||||
ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE,
|
||||
ErrorCodes.ERROR_INCORRECT_ID
|
||||
ErrorCodes.ERROR_INCORRECT_ID,
|
||||
ErrorCodes.ERROR_USER_CANCELED,
|
||||
})
|
||||
/* package */ @interface Codes {}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_USER_CANCELED;
|
||||
|
||||
public class WebExtensionController {
|
||||
private final static String LOGTAG = "WebExtension";
|
||||
|
||||
|
@ -256,14 +258,31 @@ public class WebExtensionController {
|
|||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Bug 1599581
|
||||
/**
|
||||
* Called whenever an updated extension has new permissions. This is intended as an
|
||||
* opportunity for the app to prompt the user for the new permissions required by
|
||||
* this extension.
|
||||
*
|
||||
* @param currentlyInstalled The {@link WebExtension} that is currently installed.
|
||||
* @param updatedExtension The {@link WebExtension} that will replace the previous extension.
|
||||
* @param newPermissions The new permissions that are needed.
|
||||
* @param newOrigins The new origins that are needed.
|
||||
*
|
||||
* @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW}
|
||||
* if this extension should be update or {@link AllowOrDeny#DENY DENY} if
|
||||
* this extension should not be update. A null value will be interpreted as
|
||||
* {@link AllowOrDeny#DENY DENY}.
|
||||
*/
|
||||
@Nullable
|
||||
default GeckoResult<AllowOrDeny> onUpdatePrompt(
|
||||
WebExtension currentlyInstalled,
|
||||
WebExtension updatedExtension,
|
||||
String[] newPermissions) {
|
||||
@NonNull WebExtension currentlyInstalled,
|
||||
@NonNull WebExtension updatedExtension,
|
||||
@NonNull String[] newPermissions,
|
||||
@NonNull String[] newOrigins) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: Bug 1601420
|
||||
default GeckoResult<AllowOrDeny> onOptionalPrompt(
|
||||
WebExtension extension,
|
||||
|
@ -311,6 +330,11 @@ public class WebExtensionController {
|
|||
|
||||
private static class WebExtensionResult extends GeckoResult<WebExtension>
|
||||
implements EventCallback {
|
||||
/** These states should match gecko's AddonManager.STATE_* constants. */
|
||||
private static class StateCodes {
|
||||
public static final int STATE_CANCELED = 12;
|
||||
}
|
||||
|
||||
private final String mFieldName;
|
||||
|
||||
public WebExtensionResult(final String fieldName) {
|
||||
|
@ -319,6 +343,10 @@ public class WebExtensionController {
|
|||
|
||||
@Override
|
||||
public void sendSuccess(final Object response) {
|
||||
if (response == null) {
|
||||
complete(null);
|
||||
return;
|
||||
}
|
||||
final GeckoBundle bundle = (GeckoBundle) response;
|
||||
complete(new WebExtension(bundle.getBundle(mFieldName)));
|
||||
}
|
||||
|
@ -328,7 +356,11 @@ public class WebExtensionController {
|
|||
if (response instanceof GeckoBundle
|
||||
&& ((GeckoBundle) response).containsKey("installError")) {
|
||||
final GeckoBundle bundle = (GeckoBundle) response;
|
||||
final int errorCode = bundle.getInt("installError");
|
||||
int errorCode = bundle.getInt("installError");
|
||||
final int installState = bundle.getInt("state");
|
||||
if (errorCode == 0 && installState == StateCodes.STATE_CANCELED) {
|
||||
errorCode = ERROR_USER_CANCELED;
|
||||
}
|
||||
completeExceptionally(new WebExtension.InstallException(errorCode));
|
||||
} else {
|
||||
completeExceptionally(new Exception(response.toString()));
|
||||
|
@ -550,8 +582,31 @@ public class WebExtensionController {
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO: Bug 1599581 make public
|
||||
GeckoResult<WebExtension> update(final WebExtension extension) {
|
||||
/**
|
||||
* Update a web extension.
|
||||
*
|
||||
* When checking for an update, GeckoView will download the update manifest that is defined by the
|
||||
* web extension's manifest property
|
||||
* <a href="https://extensionworkshop.com/documentation/manage/updating-your-extension/">browser_specific_settings.gecko.update_url</a>.
|
||||
* If an update is found it will be downloaded and installed. If the extension needs any new
|
||||
* permissions the {@link PromptDelegate#updatePrompt} will be triggered.
|
||||
*
|
||||
* More information about the update manifest format is available
|
||||
* <a href="https://extensionworkshop.com/documentation/manage/updating-your-extension/#manifest-structure">here</a>.
|
||||
*
|
||||
* @param extension The extension to update.
|
||||
*
|
||||
* @return A {@link GeckoResult} that will complete when the update process finishes. If an
|
||||
* update is found and installed successfully, the GeckoResult will return the updated
|
||||
* {@link WebExtension}. If no update is available, null will be returned. If the updated
|
||||
* extension requires new permissions, the {@link PromptDelegate#installPrompt}
|
||||
* will be called.
|
||||
*
|
||||
* @see PromptDelegate#updatePrompt
|
||||
*/
|
||||
@AnyThread
|
||||
@NonNull
|
||||
public GeckoResult<WebExtension> update(final @NonNull WebExtension extension) {
|
||||
final WebExtensionResult result = new WebExtensionResult("extension");
|
||||
|
||||
final GeckoBundle bundle = new GeckoBundle(1);
|
||||
|
@ -561,7 +616,9 @@ public class WebExtensionController {
|
|||
bundle, result);
|
||||
|
||||
return result.then(newExtension -> {
|
||||
registerWebExtension(newExtension);
|
||||
if (newExtension != null) {
|
||||
registerWebExtension(newExtension);
|
||||
}
|
||||
return GeckoResult.fromValue(newExtension);
|
||||
});
|
||||
}
|
||||
|
@ -603,6 +660,9 @@ public class WebExtensionController {
|
|||
} else if ("GeckoView:WebExtension:InstallPrompt".equals(event)) {
|
||||
installPrompt(bundle, callback);
|
||||
return;
|
||||
} else if ("GeckoView:WebExtension:UpdatePrompt".equals(event)) {
|
||||
updatePrompt(bundle, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
final String nativeApp = bundle.getString("nativeApp");
|
||||
|
@ -667,12 +727,47 @@ public class WebExtensionController {
|
|||
}
|
||||
|
||||
promptResponse.accept(allowOrDeny -> {
|
||||
GeckoBundle response = new GeckoBundle(1);
|
||||
if (AllowOrDeny.ALLOW.equals(allowOrDeny)) {
|
||||
response.putBoolean("allow", true);
|
||||
} else {
|
||||
response.putBoolean("allow", false);
|
||||
final GeckoBundle response = new GeckoBundle(1);
|
||||
response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
|
||||
callback.sendSuccess(response);
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePrompt(final GeckoBundle message, final EventCallback callback) {
|
||||
final GeckoBundle currentBundle = message.getBundle("currentlyInstalled");
|
||||
final GeckoBundle updatedBundle = message.getBundle("updatedExtension");
|
||||
final String[] newPermissions = message.getStringArray("newPermissions");
|
||||
final String[] newOrigins = message.getStringArray("newOrigins");
|
||||
if (currentBundle == null || updatedBundle == null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
throw new RuntimeException("Missing bundle");
|
||||
}
|
||||
|
||||
Log.e(LOGTAG, "Missing bundle");
|
||||
return;
|
||||
}
|
||||
|
||||
final WebExtension currentExtension = new WebExtension(currentBundle);
|
||||
currentExtension.setDelegateController(new DelegateController(currentExtension));
|
||||
|
||||
final WebExtension updatedExtension = new WebExtension(updatedBundle);
|
||||
updatedExtension.setDelegateController(new DelegateController(updatedExtension));
|
||||
|
||||
if (mPromptDelegate == null) {
|
||||
Log.e(LOGTAG, "Tried to update extension " + currentExtension.id +
|
||||
" but no delegate is registered");
|
||||
return;
|
||||
}
|
||||
|
||||
final GeckoResult<AllowOrDeny> promptResponse = mPromptDelegate.onUpdatePrompt(
|
||||
currentExtension, updatedExtension, newPermissions, newOrigins);
|
||||
if (promptResponse == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
promptResponse.accept(allowOrDeny -> {
|
||||
final GeckoBundle response = new GeckoBundle(1);
|
||||
response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
|
||||
callback.sendSuccess(response);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ exclude: true
|
|||
- Added ['WebExtension.metaData.optionsUrl'][74.14] and ['WebExtension.metaData.openOptionsPageInTab'][74.15],
|
||||
which is the addon metadata necessary to show their option pages.
|
||||
([bug 1598792]({{bugzilla}}1598792))
|
||||
- Added [`WebExtensionController.update`][74.16] to update extensions. ([bug 1599581]({{bugzilla}}1599581))
|
||||
|
||||
[74.1]: {{javadoc_uri}}/WebExtensionController.html#enable-org.mozilla.geckoview.WebExtension-int-
|
||||
[74.2]: {{javadoc_uri}}/WebExtensionController.html#disable-org.mozilla.geckoview.WebExtension-int-
|
||||
|
@ -59,6 +60,7 @@ exclude: true
|
|||
[74.13]: {{javadoc_uri}}/WebExtensionController.html#setTabActive
|
||||
[74.14]: {{javadoc_uri}}/WebExtension.MetaData.html#optionsUrl
|
||||
[74.15]: {{javadoc_uri}}/WebExtension.MetaData.html#openOptionsPageInTab
|
||||
[74.16]: {{javadoc_uri}}/WebExtensionController.html#update-org.mozilla.geckoview.WebExtension-int-
|
||||
|
||||
## v73
|
||||
- Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to
|
||||
|
@ -569,4 +571,4 @@ exclude: true
|
|||
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
|
||||
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
||||
|
||||
[api-version]: e9de8b39fbe7f7542db5ea472731a696bba5c95c
|
||||
[api-version]: e7913b3fec18f7edb75b81e6bebfb7860ab57c91
|
||||
|
|
|
@ -298,13 +298,13 @@ class ExtensionInstallListener {
|
|||
}
|
||||
|
||||
onDownloadCancelled(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
const { error: installError, state } = aInstall;
|
||||
this.resolve({ installError, state });
|
||||
}
|
||||
|
||||
onDownloadFailed(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
const { error: installError, state } = aInstall;
|
||||
this.resolve({ installError, state });
|
||||
}
|
||||
|
||||
onDownloadEnded() {
|
||||
|
@ -312,13 +312,13 @@ class ExtensionInstallListener {
|
|||
}
|
||||
|
||||
onInstallCancelled(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
const { error: installError, state } = aInstall;
|
||||
this.resolve({ installError, state });
|
||||
}
|
||||
|
||||
onInstallFailed(aInstall) {
|
||||
const { error: installError } = aInstall;
|
||||
this.resolve({ installError });
|
||||
const { error: installError, state } = aInstall;
|
||||
this.resolve({ installError, state });
|
||||
}
|
||||
|
||||
onInstallEnded(aInstall, aAddon) {
|
||||
|
@ -406,6 +406,37 @@ class MobileWindowTracker extends EventEmitter {
|
|||
|
||||
var mobileWindowTracker = new MobileWindowTracker();
|
||||
|
||||
async function updatePromptHandler(aInfo) {
|
||||
const oldPerms = aInfo.existingAddon.userPermissions;
|
||||
if (!oldPerms) {
|
||||
// Updating from a legacy add-on, let it proceed
|
||||
return;
|
||||
}
|
||||
|
||||
const newPerms = aInfo.addon.userPermissions;
|
||||
|
||||
const difference = Extension.comparePermissions(oldPerms, newPerms);
|
||||
|
||||
// If there are no new permissions, just proceed
|
||||
if (!difference.origins.length && !difference.permissions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentlyInstalled = exportExtension(aInfo.existingAddon, oldPerms);
|
||||
const updatedExtension = exportExtension(aInfo.addon, newPerms);
|
||||
const response = await EventDispatcher.instance.sendRequestForResult({
|
||||
type: "GeckoView:WebExtension:UpdatePrompt",
|
||||
currentlyInstalled,
|
||||
updatedExtension,
|
||||
newPermissions: difference.permissions,
|
||||
newOrigins: difference.origins,
|
||||
});
|
||||
|
||||
if (!response.allow) {
|
||||
throw new Error("Extension update rejected.");
|
||||
}
|
||||
}
|
||||
|
||||
var GeckoViewWebExtension = {
|
||||
async registerWebExtension(aId, aUri, allowContentMessaging, aCallback) {
|
||||
const params = {
|
||||
|
@ -564,6 +595,39 @@ var GeckoViewWebExtension = {
|
|||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return A promise resolved with either an AddonInstall object if an update
|
||||
* is available or null if no update is found.
|
||||
*/
|
||||
checkForUpdate(aAddon) {
|
||||
return new Promise(resolve => {
|
||||
const listener = {
|
||||
onUpdateAvailable(aAddon, install) {
|
||||
install.promptHandler = updatePromptHandler;
|
||||
resolve(install);
|
||||
},
|
||||
onNoUpdateAvailable() {
|
||||
resolve(null);
|
||||
},
|
||||
};
|
||||
aAddon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
|
||||
});
|
||||
},
|
||||
|
||||
async updateWebExtension(aId) {
|
||||
const extension = await this.extensionById(aId);
|
||||
|
||||
const install = await this.checkForUpdate(extension);
|
||||
if (!install) {
|
||||
return null;
|
||||
}
|
||||
const promise = new Promise(resolve => {
|
||||
install.addListener(new ExtensionInstallListener(resolve));
|
||||
});
|
||||
install.install();
|
||||
return promise;
|
||||
},
|
||||
|
||||
/* eslint-disable complexity */
|
||||
async onEvent(aEvent, aData, aCallback) {
|
||||
debug`onEvent ${aEvent} ${aData}`;
|
||||
|
@ -749,8 +813,18 @@ var GeckoViewWebExtension = {
|
|||
}
|
||||
|
||||
case "GeckoView:WebExtension:Update": {
|
||||
// TODO
|
||||
aCallback.onError(`Not implemented`);
|
||||
try {
|
||||
const { webExtensionId } = aData;
|
||||
const result = await this.updateWebExtension(webExtensionId);
|
||||
if (result === null || result.extension) {
|
||||
aCallback.onSuccess(result);
|
||||
} else {
|
||||
aCallback.onError(result);
|
||||
}
|
||||
} catch (ex) {
|
||||
debug`Failed update ${ex}`;
|
||||
aCallback.onError(`Unexpected error: ${ex}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ DIRS += [
|
|||
'modules',
|
||||
'themes/core',
|
||||
'themes/geckoview',
|
||||
'geckoview/src/androidTest/assets',
|
||||
'app',
|
||||
'fonts',
|
||||
]
|
||||
|
@ -61,6 +62,7 @@ TEST_DIRS += [
|
|||
TEST_HARNESS_FILES.testing.mochitest.tests.junit += [
|
||||
'geckoview/src/androidTest/assets/www/hello.html',
|
||||
'geckoview/src/androidTest/assets/www/simple_redirect.sjs',
|
||||
'geckoview/src/androidTest/assets/www/update_manifest.json',
|
||||
]
|
||||
|
||||
SPHINX_TREES['/mobile/android'] = 'docs'
|
||||
|
|