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
This commit is contained in:
Brendan Dahl 2020-02-06 01:58:45 +00:00
Родитель 69c87f6cb2
Коммит fe7cce925e
25 изменённых файлов: 584 добавлений и 31 удалений

Просмотреть файл

@ -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";

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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";

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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";

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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";

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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'