diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index a828147e9c0e..7064e5cded68 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -1549,6 +1549,7 @@ package org.mozilla.geckoview { method @UiThread @Nullable public WebExtensionController.PromptDelegate getPromptDelegate(); method @UiThread @Nullable public WebExtensionController.TabDelegate getTabDelegate(); method @NonNull @AnyThread public GeckoResult install(@NonNull String); + method @AnyThread @NonNull public GeckoResult> list(); method @UiThread public void setPromptDelegate(@Nullable WebExtensionController.PromptDelegate); method @UiThread public void setTabDelegate(@Nullable WebExtensionController.TabDelegate); method @NonNull @AnyThread public GeckoResult uninstall(@NonNull WebExtension); diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy.xpi b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy.xpi new file mode 100644 index 000000000000..0268b27df226 Binary files /dev/null and b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy.xpi differ diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/dummy.js b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/dummy.js new file mode 100644 index 000000000000..2a49c0d66576 --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/dummy.js @@ -0,0 +1 @@ +console.log("Hi, I'm a dummy."); diff --git a/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/manifest.json b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/manifest.json new file mode 100644 index 000000000000..d99420b64b1f --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/dummy/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 2, + "name": "Dummy", + "version": "1.0", + "applications": { + "gecko": { + "id": "dummy@tests.mozilla.org" + } + }, + "description": "Doesn't do anything.", + "content_scripts": [ + { + "matches": ["*://*.example.com/*"], + "js": ["dummy.js"] + } + ] +} diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt index 36c226460d22..ad37ec0b63a0 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt @@ -137,9 +137,16 @@ class WebExtensionTest : BaseSessionTest() { assertThat("Content script should have been applied", color as String, equalTo("red")) + var list = sessionRule.waitForResult(controller.list()) + assertEquals(list.size, 1) + assertEquals(list[0].id, borderify.id) + // Unregister WebExtension and check again sessionRule.waitForResult(controller.uninstall(borderify)) + list = sessionRule.waitForResult(controller.list()) + assertEquals(list, emptyList()) + mainSession.reload() sessionRule.waitForPageStop() @@ -149,6 +156,53 @@ class WebExtensionTest : BaseSessionTest() { colorAfter as String, equalTo("")) } + @Test + fun installMultiple() { + // dummy.xpi is not signed, but it could be + sessionRule.setPrefsUntilTestEnd(mapOf( + "xpinstall.signatures.required" to false + )) + + // First, make sure the list starts empty + var list = sessionRule.waitForResult(controller.list()) + assertEquals(list, emptyList()) + + sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { + @AssertCalled(count=2) + override fun onInstallPrompt(extension: WebExtension): GeckoResult { + return GeckoResult.fromValue(AllowOrDeny.ALLOW) + } + }) + + // Install in parallell borderify and dummy + val borderifyResult = controller.install( + "resource://android/assets/web_extensions/borderify.xpi") + val dummyResult = controller.install( + "resource://android/assets/web_extensions/dummy.xpi") + + val (borderify, dummy) = sessionRule.waitForResult( + GeckoResult.allOf(borderifyResult, dummyResult)) + + // Make sure the list is updated accordingly + list = sessionRule.waitForResult(controller.list()) + assertTrue(list.find { it.id == borderify.id } != null) + assertTrue(list.find { it.id == dummy.id } != null) + assertEquals(list.size, 2) + + // Uninstall borderify and verify that it's not in the list anymore + sessionRule.waitForResult(controller.uninstall(borderify)) + + list = sessionRule.waitForResult(controller.list()) + assertEquals(list.size, 1) + assertEquals(list[0].id, dummy.id) + + // Uninstall dummy and make sure the list is now empty + sessionRule.waitForResult(controller.uninstall(dummy)) + + list = sessionRule.waitForResult(controller.list()) + assertEquals(list, emptyList()) + } + private fun testInstallError(name: String, expectedError: Int) { sessionRule.delegateDuringNextWait(object : WebExtensionController.PromptDelegate { @AssertCalled(count = 0) diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java index 62f03bca4247..0148f0b5920b 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java @@ -20,6 +20,7 @@ import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.geckoview.RuntimeTelemetry; import org.mozilla.geckoview.SessionTextInput; import org.mozilla.geckoview.WebExtension; +import org.mozilla.geckoview.WebExtensionController; import org.mozilla.geckoview.test.util.HttpBin; import org.mozilla.geckoview.test.util.RuntimeCreator; import org.mozilla.geckoview.test.util.Environment; @@ -27,6 +28,7 @@ import org.mozilla.geckoview.test.util.UiThreadUtils; import org.mozilla.geckoview.test.util.Callbacks; import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -1191,7 +1193,21 @@ public class GeckoSessionTestRule implements TestRule { } } - protected void cleanupStatement() { + protected void cleanupExtensions() throws Throwable { + WebExtensionController controller = getRuntime().getWebExtensionController(); + List list = waitForResult(controller.list()); + + // Uninstall any left-over extensions + for (WebExtension extension : list) { + waitForResult(controller.uninstall(extension)); + } + + // If an extension was still installed, this test should fail + assertThat("A WebExtension was left installed during this test.", + list.size(), equalTo(0)); + } + + protected void cleanupStatement() throws Throwable { mWaitScopeDelegates.clear(); mTestScopeDelegates.clear(); @@ -1200,6 +1216,7 @@ public class GeckoSessionTestRule implements TestRule { } cleanupSession(mMainSession); + cleanupExtensions(); if (mIgnoreCrash) { deleteCrashDumps(); diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java index bfbf0ae5bfc4..95fd705ac5e3 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java @@ -51,7 +51,13 @@ public class WebExtensionController { mData.remove(id); } - public void put(final String id, final WebExtension extension) { + /** + * Add this extension to the store and update it's current value if it's already present. + * + * @param id the {@link WebExtension} id. + * @param extension the {@link WebExtension} to add to the store. + */ + public void update(final String id, final WebExtension extension) { mData.put(id, extension); } } @@ -392,14 +398,24 @@ public class WebExtensionController { }); } - // TODO: Bug 1600742 make public - GeckoResult> listInstalled() { + /** + * List installed extensions for this {@link GeckoRuntime}. + * + * The returned list can be used to set delegates on the {@link WebExtension} objects using + * {@link WebExtension#setActionDelegate}, {@link WebExtension#setMessageDelegate}. + * + * @return a {@link GeckoResult} that will resolve when the list of extensions is available. + */ + @AnyThread + @NonNull + public GeckoResult> list() { final CallbackResult> result = new CallbackResult>() { @Override public void sendSuccess(final Object response) { final GeckoBundle[] bundles = ((GeckoBundle) response) .getBundleArray("extensions"); final List list = new ArrayList<>(bundles.length); + for (GeckoBundle bundle : bundles) { final WebExtension extension = new WebExtension(bundle); registerWebExtension(extension); @@ -438,7 +454,7 @@ public class WebExtensionController { /* package */ void registerWebExtension(final WebExtension webExtension) { webExtension.setDelegateController(new DelegateController(webExtension)); - mExtensions.put(webExtension.id, webExtension); + mExtensions.update(webExtension.id, webExtension); } /* package */ void handleMessage(final String event, final GeckoBundle message, diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index e222e95e48cf..ea4fa5021947 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -29,6 +29,7 @@ exclude: true - ⚠️ [`WebExtension`][69.5]'s constructor now requires a `WebExtensionController` instance. - Added [`GeckoResult.allOf`][73.10] for consuming a list of results. +- Added [`WebExtensionController.list`][73.11] to list all installed extensions. [73.1]: {{javadoc_uri}}/WebExtensionController.html#install-java.lang.String- @@ -41,6 +42,7 @@ exclude: true [73.8]: {{javadoc_uri}}/LoginStorage.Delegate.html [73.9]: {{javadoc_uri}}/GeckoRuntime.html#setLoginStorageDelegate-org.mozilla.geckoview.LoginStorage.Delegate- [73.10]: {{javadoc_uri}}/GeckoResult.html#allOf-java.util.List- +[73.11]: {{javadoc_uri}}/WebExtensionController.html#list-- ## v72 - Added [`GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture`][72.1]. This indicates @@ -508,4 +510,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]: 919027349db09895822846ea63adacb901b4ab40 +[api-version]: 2d592afef93d47a268448aa5fd3289e03bb1f678 diff --git a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm index 7b1893b5eae9..35cea53e86a8 100644 --- a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm +++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm @@ -240,7 +240,6 @@ class GeckoViewConnection { } function exportExtension(aAddon, aPermissions, aSourceURI) { - const { origins, permissions } = aPermissions; const { creator, description, @@ -272,8 +271,8 @@ function exportExtension(aAddon, aPermissions, aSourceURI) { isEnabled: isActive, isBuiltIn: isBuiltin, metaData: { - permissions, - origins, + permissions: aPermissions ? aPermissions.permissions : [], + origins: aPermissions ? aPermissions.origins : [], description, version, creatorName, @@ -642,8 +641,16 @@ var GeckoViewWebExtension = { } case "GeckoView:WebExtension:List": { - // TODO - aCallback.onError(`Not implemented`); + try { + const addons = await AddonManager.getAddonsByTypes(["extension"]); + const extensions = addons.map(addon => + exportExtension(addon, addon.userPermissions, null) + ); + aCallback.onSuccess({ extensions }); + } catch (ex) { + debug`Failed list ${ex}`; + aCallback.onError(`Unexpected error: ${ex}`); + } break; }