Bug 1364945 - Fix runtime.openOptionsPage on Firefox for Android.
MozReview-Commit-ID: Envx19jlCjY
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -90,39 +90,60 @@ var ContextMenus = {
Addons.setEnabled(false, this.target.addon);
this.target = null;
},
uninstall: function(event) {
Addons.uninstall(this.target.addon);
this.target = null;
}
+};
+
+function sendEMPong() {
+ Services.obs.notifyObservers(window, "EM-pong");
}
-function init() {
+async function init() {
window.addEventListener("popstate", onPopState);
AddonManager.addInstallListener(Addons);
AddonManager.addAddonListener(Addons);
- Addons.init();
+
+ await Addons.init();
showAddons();
ContextMenus.init();
+
+ Services.obs.addObserver(sendEMPong, "EM-ping");
+
+ // The addons list has been loaded and rendered, send a notification
+ // if the openOptionsPage is waiting to be able to select an addon details page.
+ Services.obs.notifyObservers(window, "EM-loaded");
}
-
function uninit() {
AddonManager.removeInstallListener(Addons);
AddonManager.removeAddonListener(Addons);
+
+ Services.obs.removeObserver(sendEMPong, "EM-ping");
}
function openLink(url) {
let BrowserApp = gChromeWin.BrowserApp;
BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
}
+function openOptionsInTab(url) {
+ let BrowserApp = gChromeWin.BrowserApp;
+ BrowserApp.selectOrAddTab(url, {
+ startsWith: true,
+ selected: true,
+ parentId: BrowserApp.selectedTab.id
+ });
+}
+
function onPopState(aEvent) {
// Called when back/forward is used to change the state of the page
if (aEvent.state) {
// Show the detail page for an addon
const listItem = Addons._getElementForAddon(aEvent.state.id);
if (listItem) {
Addons.showDetails(listItem);
} else {
@@ -133,16 +154,26 @@ function onPopState(aEvent) {
// Clear any previous detail addon
let detailItem = document.querySelector("#addons-details > .addon-item");
detailItem.addon = null;
showAddons();
}
}
+function showAddonDetails(addonId) {
+ const listItem = Addons._getElementForAddon(addonId);
+ if (listItem) {
+ Addons.showDetails(listItem);
+ history.pushState({ id: addonId }, document.title);
+ } else {
+ throw new Error(`Addon not found: ${addonId}`);
+ }
+}
+
function showAddons() {
// Hide the addon options and show the addons list
let details = document.querySelector("#addons-details");
details.classList.add("hidden");
let list = document.querySelector("#addons-list");
list.classList.remove("hidden");
document.documentElement.removeAttribute("details");
@@ -273,39 +304,39 @@ var Addons = {
},
_getElementForAddon: function(aKey) {
let list = document.getElementById("addons-list");
let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]");
return element;
},
- init: function init() {
- let self = this;
- AddonManager.getAllAddons(function(aAddons) {
- // Clear all content before filling the addons
- let list = document.getElementById("addons-list");
- list.innerHTML = "";
+ init: async function init() {
+ const aAddons = await AddonManager.getAllAddons();
+
+ // Clear all content before filling the addons
+ let list = document.getElementById("addons-list");
+ list.innerHTML = "";
+
+ aAddons.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
- aAddons.sort(function(a, b) {
- return a.name.localeCompare(b.name);
- });
- for (let i = 0; i < aAddons.length; i++) {
- // Don't create item for system add-ons.
- if (aAddons[i].isSystem)
- continue;
+ for (let i = 0; i < aAddons.length; i++) {
+ // Don't create item for system add-ons.
+ if (aAddons[i].isSystem)
+ continue;
- let item = self._createItemForAddon(aAddons[i]);
- list.appendChild(item);
- }
+ let item = this._createItemForAddon(aAddons[i]);
+ list.appendChild(item);
+ }
- // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
- let browseItem = self._createBrowseItem();
- list.appendChild(browseItem);
- });
+ // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
+ let browseItem = this._createBrowseItem();
+ list.appendChild(browseItem);
document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this));
document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this));
document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this));
document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this));
document.getElementById("unsigned-learn-more").addEventListener("click", function() {
openLink(Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
@@ -367,27 +398,59 @@ var Addons = {
optionsBox.classList.remove("inner");
// WebExtensions are loaded asynchronously and the optionsURL
// may not be available via listitem when the add-on has just been
// installed, but it is available on the addon if one is set.
detailItem.setAttribute("optionsURL", addon.optionsURL);
this.createWebExtensionOptions(optionsBox, addon.optionsURL, addon.optionsBrowserStyle);
break;
+ case AddonManager.OPTIONS_TYPE_TAB:
+ // Keep the usual layout for any options related the legacy (or system) add-ons
+ // when the options are opened in a new tab from a single button in the addon
+ // details page.
+ optionsBox.classList.add("inner");
+
+ this.createOptionsInTabButton(optionsBox, addon);
+ break;
case AddonManager.OPTIONS_TYPE_INLINE:
// Keep the usual layout for any options related the legacy (or system) add-ons.
optionsBox.classList.add("inner");
this.createInlineOptions(optionsBox, optionsURL, aListItem);
break;
}
showAddonOptions();
},
+ createOptionsInTabButton: function(destination, addon) {
+ let frame = destination.querySelector("iframe#addon-options");
+ let button = destination.querySelector("button#open-addon-options");
+
+ if (frame) {
+ // Remove any existent options frame (e.g. when the addon updates
+ // contains the open_in_tab options for the first time).
+
+ frame.remove();
+ }
+
+ if (!button) {
+ button = document.createElement("button");
+ button.setAttribute("id", "open-addon-options");
+ button.textContent = gStringBundle.GetStringFromName("addon.options");
+ destination.appendChild(button);
+ }
+
+ button.onclick = async () => {
+ const {optionsURL} = addon;
+ openOptionsInTab(optionsURL);
+ };
+ },
+
createWebExtensionOptions: async function(destination, optionsURL, browserStyle) {
let originalHeight;
let frame = document.createElement("iframe");
frame.setAttribute("id", "addon-options");
frame.setAttribute("mozbrowser", "true");
frame.setAttribute("style", "width: 100%; overflow: hidden;");
// Adjust iframe height to the iframe content (also between navigation of multiple options
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1410,16 +1410,55 @@ var BrowserApp = {
tab = this.addTab(aURL, aParams);
} else {
this.selectTab(tab);
}
return tab;
},
+ /**
+ * Open or select a tab with the "about:addons" page and optionally
+ * switch to the details page related to a defined addonId.
+ *
+ * @param {string} addonId
+ */
+ openAddonManager: function openAddonManager({addonId}) {
+ if (addonId) {
+ let emWindow;
+
+ function receivePong(subject, topic, data) {
+ emWindow = subject;
+ };
+
+ Services.obs.addObserver(receivePong, "EM-pong");
+ Services.obs.notifyObservers(null, "EM-ping");
+ Services.obs.removeObserver(receivePong, "EM-pong");
+
+ if (emWindow) {
+ // "about:addons" has been already loaded in a tab.
+ emWindow.showAddonDetails(addonId);
+ } else {
+ // Wait for "about:addons" to be fully loaded.
+ function waitAboutAddons(subject, topic, data) {
+ Services.obs.removeObserver(waitAboutAddons, "EM-loaded");
+ emWindow = subject;
+
+ emWindow.showAddonDetails(addonId);
+ }
+ Services.obs.addObserver(waitAboutAddons, "EM-loaded");
+ }
+ }
+
+ BrowserApp.selectOrAddTab("about:addons", {
+ selected: true,
+ parentId: BrowserApp.selectedTab.id,
+ });
+ },
+
// This method updates the state in BrowserApp after a tab has been selected
// in the Java UI.
_handleTabSelected: function _handleTabSelected(aTab) {
if (this.fullscreenTransitionTab) {
// Defer updating to "fullscreenchange" if tab selection happened during
// a fullscreen transition.
return;
}
--- a/mobile/android/components/extensions/ext-android.js
+++ b/mobile/android/components/extensions/ext-android.js
@@ -43,16 +43,35 @@ extensions.on("page-shutdown", (type, co
if (nativeTab) {
BrowserApp.closeTab(nativeTab);
}
}
}
});
/* eslint-enable mozilla/balanced-listeners */
+global.openOptionsPage = (extension) => {
+ let window = windowTracker.topWindow;
+ if (!window) {
+ return Promise.reject({message: "No browser window available"});
+ }
+
+ let {BrowserApp} = window;
+
+ if (extension.manifest.options_ui.open_in_tab) {
+ BrowserApp.selectOrAddTab(extension.manifest.options_ui.page, {
+ selected: true,
+ parentId: BrowserApp.selectedTab.id,
+ });
+ } else {
+ BrowserApp.openAddonManager({addonId: extension.id});
+ }
+
+ return Promise.resolve();
+};
extensions.registerModules({
browserAction: {
url: "chrome://browser/content/ext-browserAction.js",
schema: "chrome://browser/content/schemas/browser_action.json",
scopes: ["addon_parent"],
manifest: ["browser_action"],
paths: [
--- a/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
@@ -56,16 +56,35 @@ async function clickOnLinkInOptionsPage(
const optionsIframe = content.document.querySelector(`#addon-options`);
optionsIframe.contentDocument.querySelector(selector).click();
}
async function navigateBack() {
content.window.history.back();
}
+function waitDOMContentLoaded(checkUrlCb) {
+ const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
+
+ return new Promise(resolve => {
+ let listener = (event) => {
+ if (checkUrlCb(event.target.defaultView.location.href)) {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
+ resolve();
+ }
+ };
+
+ BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
+ });
+}
+
+function waitAboutAddonsLoaded() {
+ return waitDOMContentLoaded(url => url === "about:addons");
+}
+
add_task(async function test_options_ui_iframe_height() {
let addonID = "test-options-ui@mozilla.org";
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
manifest: {
applications: {
gecko: {id: addonID},
@@ -108,44 +127,30 @@ add_task(async function test_options_ui_
</body>
</html>
`,
},
});
await extension.startup();
- let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
- let BrowserApp = chromeWin.BrowserApp;
+ const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
- let waitAboutAddonsLoaded = new Promise(resolve => {
- let listener = (event) => {
- if (event.target.defaultView.location.href === "about:addons") {
- BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
- resolve();
- }
- };
-
- BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
- });
+ let onceAboutAddonsLoaded = waitAboutAddonsLoaded();
BrowserApp.selectOrAddTab("about:addons", {
selected: true,
parentId: BrowserApp.selectedTab.id,
});
- await waitAboutAddonsLoaded;
+ await onceAboutAddonsLoaded;
is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
"about:addons is the currently selected tab");
- let aboutAddonsWindow = BrowserApp.selectedTab.browser.contentWindow;
-
- is(aboutAddonsWindow.location.href, "about:addons");
-
await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, waitAboutAddonsRendered);
await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, navigateToAddonDetails);
const optionsSizes = await ContentTask.spawn(
BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
);
@@ -185,12 +190,87 @@ add_task(async function test_options_ui_
is(backToOptionsSizes.iframeHeight, optionsSizes.iframeHeight,
`When navigating back, the old iframe size is restored (${backToOptionsSizes.iframeHeight})`);
BrowserApp.closeTab(BrowserApp.selectedTab);
await extension.unload();
});
+add_task(async function test_options_ui_open_aboutaddons_details() {
+ let addonID = "test-options-ui-open-addon-details@mozilla.org";
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "runtime.openOptionsPage") {
+ browser.test.fail(`Received unexpected test message: ${msg}`);
+ return;
+ }
+
+ browser.runtime.openOptionsPage();
+ });
+ }
+
+ function optionsScript() {
+ browser.test.sendMessage("options-page-loaded", window.location.href);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ background,
+ manifest: {
+ applications: {
+ gecko: {id: addonID},
+ },
+ name: "Options UI open addon details Extension",
+ description: "Longer addon description",
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.js": optionsScript,
+ "options.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <h1>Options page</h1>
+ <script src="options.js"><\/script>
+ </body>
+ </html>
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let onceAboutAddonsLoaded = waitAboutAddonsLoaded();
+
+ BrowserApp.selectOrAddTab("about:addons", {
+ selected: true,
+ parentId: BrowserApp.selectedTab.id,
+ });
+
+ await onceAboutAddonsLoaded;
+
+ is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+ "about:addons is the currently selected tab");
+
+ info("Wait runtime.openOptionsPage to open the about:addond details in the existent tab");
+ extension.sendMessage("runtime.openOptionsPage");
+ await extension.awaitMessage("options-page-loaded");
+
+ is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+ "about:addons is still the currently selected tab once the options has been loaded");
+
+ BrowserApp.closeTab(BrowserApp.selectedTab);
+
+ await extension.unload();
+});
+
</script>
</body>
</html>