--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -74,16 +74,28 @@ pref("extensions.webextensions.default-c
// Require signed add-ons by default
pref("xpinstall.signatures.required", true);
pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
// Dictionary download preference
pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
+// When we display a doorhanger to update firefox, there will be a "What's new"
+// link which will point to this URL.
+#ifdef RELEASE_OR_BETA
+pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/{targetVersion}/releasenotes/");
+#else
+#ifdef NIGHTLY_BUILD
+pref("app.releaseNotesURL", "https://blog.nightly.mozilla.org/");
+#else
+pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/{targetVersion}/auroranotes/");
+#endif
+#endif
+
// At startup, should we check to see if the installation
// date is older than some threshold
pref("app.update.checkInstallTime", true);
// The number of days a binary is permitted to be old without checking is defined in
// firefox-branding.js (app.update.checkInstallTime.days)
// The minimum delay in seconds for the timer to fire between the notification
@@ -112,16 +124,19 @@ pref("app.update.log", false);
// The number of general background check failures to allow before notifying the
// user of the failure. User initiated update checks always notify the user of
// the failure.
pref("app.update.backgroundMaxErrors", 10);
// Whether or not app updates are enabled
pref("app.update.enabled", true);
+// Whether or not to use the simplified doorhanger application update UI.
+pref("app.update.doorhanger", true);
+
// If set to true, the Update Service will automatically download updates when
// app updates are enabled per the app.update.enabled preference and if the user
// can apply updates.
pref("app.update.auto", true);
// If set to true, the Update Service will present no UI for any event.
pref("app.update.silent", false);
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -258,19 +258,19 @@ var gFxAccounts = {
this.panelUIFooter.setAttribute("fxastatus", "signedin");
this.panelUILabel.setAttribute("label", userData.email);
}
if (profileInfoEnabled) {
this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
}
}
if (showErrorBadge) {
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
} else {
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+ PanelUI.removeNotification("fxa-needs-authentication");
}
}
let updateWithProfile = (profile) => {
if (profileInfoEnabled) {
if (profile.displayName) {
this.panelUILabel.setAttribute("label", profile.displayName);
}
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1139,17 +1139,17 @@ toolbarpaletteitem[place="palette"][hidd
#customization-palette .toolbarpaletteitem-box {
-moz-box-pack: center;
-moz-box-flex: 1;
width: 10em;
max-width: 10em;
}
-#main-window[customizing=true] #PanelUI-update-status {
+#main-window[customizing=true] .PanelUI-notification-menu-item {
display: none;
}
/* UI Tour */
@keyframes uitour-wobble {
from {
transform: rotate(0deg) translateX(3px) rotate(0deg);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1357,18 +1357,16 @@ var gBrowserInit = {
gSyncUI.init();
gFxAccounts.init();
if (AppConstants.MOZ_DATA_REPORTING)
gDataNotificationInfoBar.init();
gBrowserThumbnails.init();
- gMenuButtonBadgeManager.init();
-
gMenuButtonUpdateBadge.init();
window.addEventListener("mousemove", MousePosTracker);
window.addEventListener("dragover", MousePosTracker);
gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
gNavToolbox.addEventListener("customizationchange", CustomizationHandler);
gNavToolbox.addEventListener("customizationending", CustomizationHandler);
@@ -1522,18 +1520,16 @@ var gBrowserInit = {
CompactTheme.uninit();
TrackingProtection.uninit();
RefreshBlocker.uninit();
gMenuButtonUpdateBadge.uninit();
- gMenuButtonBadgeManager.uninit();
-
SidebarUI.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {
this._cancelDelayedStartup();
} else {
if (Win7Features)
@@ -2561,196 +2557,304 @@ function SetPageProxyState(aState, aOpti
}
}
function PageProxyClickHandler(aEvent) {
if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
middleMousePaste(aEvent);
}
-var gMenuButtonBadgeManager = {
- BADGEID_APPUPDATE: "update",
- BADGEID_DOWNLOAD: "download",
- BADGEID_FXA: "fxa",
-
- fxaBadge: null,
- downloadBadge: null,
- appUpdateBadge: null,
+// Setup the hamburger button badges for updates, if enabled.
+var gMenuButtonUpdateBadge = {
+ kTopics: [
+ "update-staged",
+ "update-downloaded",
+ "update-available",
+ "update-error",
+ ],
+
+ timeouts: [],
+ manualWorkflowStarted: null,
+
+ tryGetPref(type, name, defaultValue) {
+ try {
+ return Services.prefs[`get${type}Pref`](name);
+ } catch (e) {
+ return defaultValue;
+ }
+ },
+
+ get enabled() {
+ return Services.prefs.getBoolPref("app.update.doorhanger");
+ },
+
+ get repromptDownloadWaitTime() {
+ return this.tryGetPref("Int",
+ "app.update.repromptDownloadWaitTime",
+ 60 * 1000);
+ },
+
+ get launchInstallerPopupWaitTime() {
+ return this.tryGetPref("Int",
+ "app.update.launchInstallerPromptWaitTime",
+ 5 * 60 * 1000);
+ },
+
+ get badgeWaitTime() {
+ return this.tryGetPref("Int",
+ "app.update.promptBadgeWaitTime",
+ 2 * 24 * 3600 * 1000); // 2 days
+ },
init() {
- PanelUI.panel.addEventListener("popupshowing", this, true);
- },
-
- uninit() {
- PanelUI.panel.removeEventListener("popupshowing", this, true);
- },
-
- handleEvent(e) {
- if (e.type === "popupshowing") {
- this.clearBadges();
- }
- },
-
- _showBadge() {
- let badgeToShow = this.downloadBadge || this.appUpdateBadge || this.fxaBadge;
-
- if (badgeToShow) {
- PanelUI.menuButton.setAttribute("badge-status", badgeToShow);
- } else {
- PanelUI.menuButton.removeAttribute("badge-status");
- }
- },
-
- _changeBadge(badgeId, badgeStatus = null) {
- if (badgeId == this.BADGEID_APPUPDATE) {
- this.appUpdateBadge = badgeStatus;
- } else if (badgeId == this.BADGEID_DOWNLOAD) {
- this.downloadBadge = badgeStatus;
- } else if (badgeId == this.BADGEID_FXA) {
- this.fxaBadge = badgeStatus;
- } else {
- Cu.reportError("The badge ID '" + badgeId + "' is unknown!");
- }
- this._showBadge();
- },
-
- addBadge(badgeId, badgeStatus) {
- if (!badgeStatus) {
- Cu.reportError("badgeStatus must be defined");
- return;
- }
- this._changeBadge(badgeId, badgeStatus);
- },
-
- removeBadge(badgeId) {
- this._changeBadge(badgeId);
- },
-
- clearBadges() {
- this.appUpdateBadge = null;
- this.downloadBadge = null;
- this.fxaBadge = null;
- this._showBadge();
- }
-};
-
-// Setup the hamburger button badges for updates, if enabled.
-var gMenuButtonUpdateBadge = {
- enabled: false,
- badgeWaitTime: 0,
- timer: null,
- cancelObserverRegistered: false,
-
- init() {
- try {
- this.enabled = Services.prefs.getBoolPref("app.update.badge");
- } catch (e) {}
if (this.enabled) {
- try {
- this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime");
- } catch (e) {
- this.badgeWaitTime = 345600; // 4 days
- }
- Services.obs.addObserver(this, "update-staged", false);
- Services.obs.addObserver(this, "update-downloaded", false);
+ this.kTopics.forEach(t => {
+ Services.obs.addObserver(this, t, false);
+ });
}
},
uninit() {
- if (this.timer)
- this.timer.cancel();
if (this.enabled) {
- Services.obs.removeObserver(this, "update-staged");
- Services.obs.removeObserver(this, "update-downloaded");
- this.enabled = false;
- }
- if (this.cancelObserverRegistered) {
- Services.obs.removeObserver(this, "update-canceled");
- this.cancelObserverRegistered = false;
- }
- },
-
- onMenuPanelCommand(event) {
- if (event.originalTarget.getAttribute("update-status") === "succeeded") {
- // restart the app
- let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
- .createInstance(Ci.nsISupportsPRBool);
- Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
-
- if (!cancelQuit.data) {
- Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+ this.kTopics.forEach(t => {
+ Services.obs.removeObserver(this, t);
+ });
+ }
+
+ PanelUI.removeNotification(/^update-/);
+ this.clearCallbacks();
+ },
+
+ clearCallbacks() {
+ this.timeouts.forEach(t => clearTimeout(t));
+ this.timeouts = [];
+
+ Downloads.getList(Downloads.ALL).then(list => list.removeView(this));
+ },
+
+ addTimeout(time, callback) {
+ this.timeouts.push(setTimeout(() => {
+ this.clearCallbacks();
+ callback();
+ }, time));
+ },
+
+ replaceReleaseNotes(update, whatsNewId) {
+ let whatsNewLink = document.getElementById(whatsNewId);
+ if (update && update.appVersion) {
+ let releaseNotesURL = Services.urlFormatter.formatURLPref("app.releaseNotesURL");
+ let targetVersion = update.appVersion;
+ releaseNotesURL = releaseNotesURL.replace("{targetVersion}", targetVersion);
+ whatsNewLink.href = releaseNotesURL;
+ whatsNewLink.hidden = false;
+ } else {
+ whatsNewLink.href = null;
+ whatsNewLink.hidden = true;
+ }
+ },
+
+ requestRestart() {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ if (!cancelQuit.data) {
+ Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+ }
+ },
+
+ openManualUpdateUrl() {
+ let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ openUILinkIn(manualUpdateUrl, "tab");
+ this.manualWorkflowStarted = Date.now();
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.addView(this))
+ .then(null, Cu.reportError);
+ },
+
+ showRestartNotification(dismissed) {
+ let action = {
+ callback: () => { gMenuButtonUpdateBadge.requestRestart(); }
+ };
+
+ PanelUI.showNotification("update-restart", action, [], { dismissed });
+ },
+
+ showUpdateAvailableNotification(update, dismissed) {
+ let action = {
+ callback: () => {
+ let updateService = Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService);
+ updateService.downloadUpdate(update, true);
}
- } else {
- // open the page for manual update
- let url = Services.urlFormatter.formatURLPref("app.update.url.manual");
- openUILinkIn(url, "tab");
- }
+ };
+
+ this.replaceReleaseNotes(update, "update-available-whats-new");
+
+ PanelUI.showNotification("update-available", action, [], { dismissed });
+ },
+
+ showLaunchInstallerNotification(dismissed) {
+ let action = {
+ callback: () => {
+ if (download.succeeded) {
+ download.launch();
+ } else {
+ download.launchWhenSucceeded = true;
+ }
+ }
+ };
+
+ PanelUI.showNotification("update-launch", action, [], { dismissed });
+ },
+
+ showManualUpdateNotification(update, dismissed) {
+ let action = {
+ callback: () => {
+ let self = gMenuButtonUpdateBadge;
+ self.openManualUpdateUrl();
+
+ this.addTimeout(this.repromptDownloadWaitTime, () => {
+ this.showManualUpdateNotification(true);
+ });
+ }
+ };
+
+ this.replaceReleaseNotes(update, "update-manual-whats-new");
+
+ PanelUI.showNotification("update-manual", action, [], { dismissed });
+ },
+
+ handleUpdateError(update, status) {
+ switch (status) {
+ case "all-downloads-failed":
+ case "check-attempts-exceeded":
+ case "elevation-canceled":
+ case "unknown":
+ // Background update has failed, let's show the UI responsible for
+ // prompting the user to update manually.
+ this.clearCallbacks();
+ this.showManualUpdateNotification(update, false);
+ break;
+ }
+ },
+
+ handleUpdateDownloaded(update, status) {
+ switch (status) {
+ case "pending-service":
+ case "success":
+ this.clearCallbacks();
+ let badgeWaitTime = this.badgeWaitTime;
+ let doorhangerWaitTime = update.promptWaitTime * 1000;
+
+ if (badgeWaitTime < doorhangerWaitTime) {
+ this.addTimeout(badgeWaitTime, () => {
+ this.showRestartNotification(true);
+
+ let remainingTime = doorhangerWaitTime - badgeWaitTime;
+ this.addTimeout(remainingTime, () => {
+ this.showRestartNotification(false);
+ });
+ });
+ } else {
+ this.addTimeout(doorhangerWaitTime, () => {
+ this.showRestartNotification(false);
+ });
+ }
+ break;
+ }
+ },
+
+ handleUpdateAvailable(update, status) {
+ switch (status) {
+ case "show-prompt":
+ // Background update has failed, let's show the UI responsible for
+ // prompting the user to update manually.
+ this.clearCallbacks();
+ this.showUpdateAvailableNotification(update, false);
+ break;
+ }
+ },
+
+ handleUpdateStaged(update, status) {
+ handleUpdateDownloaded(status);
},
observe(subject, topic, status) {
- if (topic == "update-canceled") {
- this.reset();
+ if (!this.enabled) {
return;
}
- if (status == "failed") {
- // Background update has failed, let's show the UI responsible for
- // prompting the user to update manually.
- this.uninit();
- this.displayBadge(false);
+
+ let update = subject && subject.QueryInterface(Ci.nsIUpdate);
+
+ switch (topic) {
+ case "update-available":
+ this.handleUpdateAvailable(update, status);
+ break;
+ case "update-downloaded":
+ this.handleUpdateDownloaded(update, status);
+ break;
+ case "update-staged":
+ this.handleUpdateStaged(update, status);
+ break;
+ case "update-error":
+ this.handleUpdateError(update, status);
+ break;
+ }
+ },
+
+ onDownloadAdded(download) {
+ if (download.startTime < this.manualWorkflowStarted) {
+ // This download was from before we prompted them, so it's likely not
+ // the installer we directed them toward.
return;
}
- // Give the user badgeWaitTime seconds to react before prompting.
- this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
- this.timer.TYPE_ONE_SHOT);
- // The timer callback will call uninit() when it completes.
- },
-
- notify() {
- // If the update is successfully applied, or if the updater has fallen back
- // to non-staged updates, add a badge to the hamburger menu to indicate an
- // update will be applied once the browser restarts.
- this.uninit();
- this.displayBadge(true);
- },
-
- displayBadge(succeeded) {
- let status = succeeded ? "succeeded" : "failed";
- let badgeStatus = "update-" + status;
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, badgeStatus);
-
- let stringId;
- let updateButtonText;
- if (succeeded) {
- let brandBundle = document.getElementById("bundle_brand");
- let brandShortName = brandBundle.getString("brandShortName");
- stringId = "appmenu.restartNeeded.description";
- updateButtonText = gNavigatorBundle.getFormattedString(stringId,
- [brandShortName]);
- Services.obs.addObserver(this, "update-canceled", false);
- this.cancelObserverRegistered = true;
- } else {
- stringId = "appmenu.updateFailed.description";
- updateButtonText = gNavigatorBundle.getString(stringId);
- }
-
- let updateButton = document.getElementById("PanelUI-update-status");
- updateButton.setAttribute("label", updateButtonText);
- updateButton.setAttribute("update-status", status);
- updateButton.hidden = false;
+ let tryGetBaseDomain = uriString => {
+ try {
+ let uri = NetUtil.newURI(uriString);
+ return Services.eTLD.getBaseDomain(uri);
+ } catch (e) {
+ Cu.reportError(`Bad URI in onDownloadAdded: [${uriString}] (${e})`);
+ return null;
+ }
+ };
+
+ let referrerBaseDomain = tryGetBaseDomain(download.source.referrer);
+ let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
+ let manualUpdateBaseDomain = tryGetBaseDomain(manualUpdateUrl);
+
+ // Assume that the first download from the same base domain as our manual
+ // update URL is the installer. This is a bit clunky, but it should
+ // generally work just fine, especially since this listener will be
+ // cancelled if they don't download the installer within
+ // repromptDownloadWaitTime.
+ if (referrerBaseDomain && referrerBaseDomain == manualUpdateBaseDomain) {
+ this.clearCallbacks();
+
+ // This download is already going to launch when it finishes, so don't
+ // bother showing the user anything.
+ if (download.launchWhenSucceeded) {
+ return;
+ }
+
+ PanelUI.removeNotification("update-manual");
+ this.showLaunchInstallerNotification(true);
+
+ // Give the user launchInstallerPopupWaitTime before escalating the badge
+ // into a popup
+ this.addTimeout(this.launchInstallerPopupWaitTime, () => {
+ this.showLaunchInstallerNotification(false);
+ });
+ }
},
reset() {
- gMenuButtonBadgeManager.removeBadge(
- gMenuButtonBadgeManager.BADGEID_APPUPDATE);
- let updateButton = document.getElementById("PanelUI-update-status");
- updateButton.hidden = true;
- this.uninit();
- this.init();
+ PanelUI.removeNotification(/^update-/);
+ this.clearCallbacks();
}
};
// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND = 4;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND = 5;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -489,17 +489,16 @@ tags = mcb
tags = psm
[browser_mcb_redirect.js]
tags = mcb
[browser_windowactivation.js]
[browser_contextmenu_childprocess.js]
[browser_bug963945.js]
[browser_domFullscreen_fullscreenMode.js]
tags = fullscreen
-[browser_menuButtonBadgeManager.js]
[browser_newTabDrop.js]
[browser_newWindowDrop.js]
[browser_csp_block_all_mixedcontent.js]
tags = mcb
[browser_newwindow_focus.js]
skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
[browser_bug1299667.js]
[browser_close_dependent_tabs.js]
deleted file mode 100644
--- a/browser/base/content/test/general/browser_menuButtonBadgeManager.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* 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/. */
-
-var menuButton = document.getElementById("PanelUI-menu-button");
-
-add_task(function* testButtonActivities() {
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
- is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
- is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
- is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-failed");
- is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-severe");
- is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-warning");
- is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
-
- gMenuButtonBadgeManager.addBadge("unknownbadge", "attr");
- is(menuButton.getAttribute("badge-status"), "download-warning", "Should not have changed badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
- is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE);
- is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-
- yield PanelUI.show();
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
- PanelUI.hide();
-
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
- gMenuButtonBadgeManager.clearBadges();
- is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (clearBadges called)");
-});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ head.js
+ update.sjs
+ sharedUpdateXML.js
+ simple.mar
+ downloadPage.html
+
+[browser_noUpdates.js]
+[browser_updatesBasicPrompt.js]
+[browser_updatesBasicPromptNoStaging.js]
+[browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadSizes.js]
+[browser_updatesCompletePatchApplyFailure.js]
+[browser_updatesCompletePatchWithBadCompleteSize.js]
+[browser_updatesDownloadReprompt.js]
+[browser_updatesLaunchManualInstaller.js]
+[browser_updatesMalformedXml.js]
+[browser_updatesPartialPatchApplyFailure.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js]
+[browser_updatesPartialPatchWithBadPartialSize.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_noUpdates.js
@@ -0,0 +1,10 @@
+add_task(function* testNoUpdates() {
+ let updateParams = "noUpdates=1";
+
+ yield runUpdateTest(updateParams, 1, []);
+
+ yield sleep(100);
+
+ is(PanelUI.activeNotification, null,
+ "No notification should be present if there are no updates.")
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesBasicPrompt.js
@@ -0,0 +1,22 @@
+add_task(function* testBasicPrompt() {
+ let updateParams = "showPrompt=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-available",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-available-whats-new");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ }
+ },
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesBasicPromptNoStaging.js
@@ -0,0 +1,26 @@
+add_task(function* testBasicPromptNoStaging() {
+ pushPref("Bool", PREF_APP_UPDATE_STAGING_ENABLED, false);
+
+ let updateParams = "showPrompt=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-available",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-available-whats-new");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ }
+ },
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+
+ popPref(PREF_APP_UPDATE_STAGING_ENABLED);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadCompleteSize() {
+ let updateParams = "invalidCompleteSize=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadPartialSize() {
+ let updateParams = "invalidPartialSize=1&promptWaitTime=0";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
@@ -0,0 +1,25 @@
+add_task(function* testCompleteAndPartialPatchesWithBadSizes() {
+ let updateParams = "invalidPartialSize=1&invalidCompleteSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompletePatchApplyFailure.js
@@ -0,0 +1,28 @@
+add_task(function* testCompletePatchApplyFailure() {
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null);
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
@@ -0,0 +1,25 @@
+add_task(function* testCompletePatchWithBadCompleteSize() {
+ let updateParams = "completePatchOnly=1&invalidCompleteSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesDownloadReprompt.js
@@ -0,0 +1,31 @@
+add_task(function* testMalformedXml() {
+ const maxBackgroundErrors = 1;
+ pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+ pushPref("Int", PREF_APP_UPDATE_REPROMPTDOWNLOADWAITTIME, 0);
+
+ let updateParams = "xmlMalformed=1";
+
+ yield runUpdateTest(updateParams, maxBackgroundErrors, [
+ {
+ notificationId: "update-manual",
+ button: "button",
+ },
+ {
+ // since we set the reprompt time to 0, we should see this pop up immediately
+ notificationId: "update-manual",
+ button: "secondarybutton",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, true,
+ "The what's new link should be hidden since we don't know the version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gMenuButtonUpdateBadge.reset();
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesLaunchManualInstaller.js
@@ -0,0 +1,37 @@
+add_task(function* testMalformedXml() {
+ const maxBackgroundErrors = 1;
+ pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+ pushPref("Int", PREF_APP_UPDATE_LAUNCHINSTALLERPROMPTWAITTIME, 0);
+ pushPref("Int", PREF_APP_SECURITY_DIALOGENABLEDELAY, 0);
+
+ let updateParams = "xmlMalformed=1";
+
+ yield runUpdateTest(updateParams, maxBackgroundErrors, [
+ {
+ notificationId: "update-manual",
+ button: "button",
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ content.document.getElementById('download-link').click();
+ });
+
+ let win = yield waitForWindow("chrome://mozapps/content/downloads/unknownContentType.xul");
+ executeSoon(() => win.document.documentElement.getButton("accept").click());
+ }
+ },
+ {
+ notificationId: "update-launch",
+ button: "secondarybutton",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, true,
+ "The what's new link should be hidden since we don't know the version.");
+ },
+ cleanup() {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gMenuButtonUpdateBadge.reset();
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesMalformedXml.js
@@ -0,0 +1,27 @@
+add_task(function* testMalformedXml() {
+ const maxBackgroundErrors = 10;
+ pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+
+ let updateParams = "xmlMalformed=1";
+
+ yield runUpdateTest(updateParams, maxBackgroundErrors, [
+ {
+ // if we fail 10 check attempts, then we want to just show the user a manual update
+ // workflow.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, true,
+ "The what's new link should be hidden since we don't know the version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ gMenuButtonUpdateBadge.reset();
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailure.js
@@ -0,0 +1,29 @@
+add_task(function* testPartialPatchApplyFailure() {
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
@@ -0,0 +1,22 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteAvailable() {
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", SERVICE_URL, null, null,
+ null, "false");
+
+ let promptWaitTime = "0";
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false",
+ null, null, null, null, promptWaitTime);
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ notificationId: "update-restart",
+ button: "secondarybutton",
+ cleanup() {
+ PanelUI.removeNotification(/.*/);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteValidationFailure() {
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", SERVICE_URL, "MD5",
+ null, "1234",
+ "false");
+
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ yield runUpdateProcessingTest(updates, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchWithBadPartialSize.js
@@ -0,0 +1,25 @@
+add_task(function* testPartialPatchWithBadPartialSize() {
+ let updateParams = "partialPatchOnly=1&invalidPartialSize=1";
+
+ yield runUpdateTest(updateParams, 1, [
+ {
+ // if we have only an invalid patch, then something's wrong and we don't have an automatic
+ // way to fix it, so show the manual update doorhanger.
+ notificationId: "update-manual",
+ button: "button",
+ beforeClick() {
+ let whatsNewLink = document.getElementById("update-manual-whats-new");
+ is(whatsNewLink.hidden, false,
+ "We should have a what's new link if we had an update.");
+ is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+ "The what's new link points to the release notes for the update version.");
+ },
+ *cleanup() {
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.selectedBrowser.currentURI.spec,
+ URL_MANUAL_UPDATE, "Landed on manual update page.")
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+ },
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/downloadPage.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Download page</title>
+</head>
+<body>
+<!-- just use simple.mar since we have it available and it will result in a download dialog -->
+<a id="download-link" href="http://example.com/browser/browser/base/content/test/simpleUpdate/simple.mar" data-link-type="download">
+ Download
+</a>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/head.js
@@ -0,0 +1,696 @@
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const IS_MACOSX = ("nsILocalFileMac" in Ci);
+const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
+
+const KEY_UPDROOT = "UpdRootD";
+
+const PREF_APP_UPDATE_URL = "app.update.url";
+const PREF_APP_UPDATE_DOORHANGER = "app.update.doorhanger";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
+const PREF_APP_RELEASE_NOTES_URL = "app.releaseNotesURL";
+const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual";
+const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_REPROMPTDOWNLOADWAITTIME = "app.update.repromptDownloadWaitTime";
+const PREF_APP_UPDATE_LAUNCHINSTALLERPROMPTWAITTIME = "app.update.launchInstallerPromptWaitTime";
+const PREF_APP_SECURITY_DIALOGENABLEDELAY = "security.dialog_enable_delay";
+
+const PREFBRANCH_APP_PARTNER = "app.partner.";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+const NS_GRE_DIR = "GreD";
+const NS_GRE_BIN_DIR = "GreBinD";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_EXECUTABLE_FILE = "XREExeF";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+
+const DIR_PATCH = "0";
+const DIR_TOBEDELETED = "tobedeleted";
+const DIR_UPDATES = "updates";
+const DIR_UPDATED = IS_MACOSX ? "Updated.app" : "updated";
+
+const BIN_SUFFIX = (IS_WIN ? ".exe" : "");
+const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
+const FILE_APPLICATION_INI = "application.ini";
+const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
+const FILE_LAST_UPDATE_LOG = "last-update.log";
+const FILE_UPDATE_SETTINGS_INI = "update-settings.ini";
+const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak";
+const FILE_UPDATER_INI = "updater.ini";
+const FILE_UPDATES_XML = "updates.xml";
+const FILE_UPDATE_LOG = "update.log";
+const FILE_UPDATE_MAR = "update.mar";
+const FILE_UPDATE_STATUS = "update.status";
+const FILE_UPDATE_TEST = "update.test";
+const FILE_UPDATE_VERSION = "update.version";
+
+const PERMS_FILE = FileUtils.PERMS_FILE;
+const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY;
+
+const UPDATE_SETTINGS_CONTENTS = "[Settings]\n" +
+ "ACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n";
+
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+const DEFAULT_UPDATE_VERSION = "999999.0";
+
+const URL_HOST = "http://example.com"
+const URL_HTTP_UPDATE_XML = "http://example.com/browser/browser/base/content/test/simpleUpdate/update.sjs";
+
+const URL_MANUAL_UPDATE = "http://example.com/browser/browser/base/content/test/simpleUpdate/downloadPage.html";
+
+var gURLData = "http://example.com/browser/browser/base/content/test/simpleUpdate/";
+
+const DATA_URI_SPEC = "chrome://mochitests/content/browser/browser/base/content/test/simpleUpdate/";
+
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this);
+
+const REL_PATH_DATA = "browser/browser/base/content/test/simpleUpdate/";
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+
+XPCOMUtils.defineLazyGetter(this, "gUpdateService", function test_gAUS() {
+ return Cc["@mozilla.org/updates/update-service;1"].
+ getService(Ci.nsIApplicationUpdateService).
+ QueryInterface(Ci.nsITimerCallback).
+ QueryInterface(Ci.nsIObserver).
+ QueryInterface(Ci.nsIUpdateCheckListener);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUpdateManager",
+ "@mozilla.org/updates/update-manager;1",
+ "nsIUpdateManager");
+
+XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
+ return Services.prefs.getDefaultBranch(null);
+});
+
+let gRembemberedPrefs = [];
+
+/**
+ * Sets the app.update.url default preference.
+ *
+ * @param aURL
+ * The update url. If not specified 'URL_HOST + "/update.xml"' will be
+ * used.
+ */
+function setUpdateURL(aURL) {
+ let url = aURL ? aURL : URL_HOST + "/update.xml";
+ pushPref("Char", PREF_APP_UPDATE_URL, url, gDefaultPrefBranch);
+}
+
+/**
+ * Gets the update version info for the update url parameters to send to
+ * update.sjs.
+ *
+ * @param aAppVersion (optional)
+ * The application version for the update snippet. If not specified the
+ * current application version will be used.
+ * @return The url parameters for the application and platform version to send
+ * to update.sjs.
+ */
+function getVersionParams(aAppVersion) {
+ let appInfo = Services.appinfo;
+ return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
+}
+
+/**
+ * Removes the updates.xml file, active-update.xml file, and all files and
+ * sub-directories in the updates directory except for the "0" sub-directory.
+ * This prevents some tests from failing due to files being left behind when the
+ * tests are interrupted.
+ */
+function removeUpdateDirsAndFiles() {
+ let file = getUpdatesXMLFile(true);
+ try {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (e) {
+ logTestInfo("Unable to remove file. Path: " + file.path +
+ ", Exception: " + e);
+ }
+
+ file = getUpdatesXMLFile(false);
+ try {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (e) {
+ logTestInfo("Unable to remove file. Path: " + file.path +
+ ", Exception: " + e);
+ }
+
+ // This fails sporadically on Mac OS X so wrap it in a try catch
+ let updatesDir = getUpdatesDir();
+ try {
+ cleanUpdatesDir(updatesDir);
+ } catch (e) {
+ logTestInfo("Unable to remove files / directories from directory. Path: " +
+ updatesDir.path + ", Exception: " + e);
+ }
+}
+
+/**
+ * Removes all files and sub-directories in the updates directory except for
+ * the "0" sub-directory.
+ *
+ * @param aDir
+ * nsIFile for the directory to be deleted.
+ */
+function cleanUpdatesDir(aDir) {
+ if (!aDir.exists()) {
+ return;
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.isDirectory()) {
+ if (entry.leafName == DIR_PATCH && entry.parent.leafName == DIR_UPDATES) {
+ cleanUpdatesDir(entry);
+ entry.permissions = PERMS_DIRECTORY;
+ } else {
+ try {
+ entry.remove(true);
+ return;
+ } catch (e) {
+ }
+ cleanUpdatesDir(entry);
+ entry.permissions = PERMS_DIRECTORY;
+ try {
+ entry.remove(true);
+ } catch (e) {
+ logTestInfo("cleanUpdatesDir: unable to remove directory. Path: " +
+ entry.path + ", Exception: " + e);
+ throw (e);
+ }
+ }
+ } else {
+ entry.permissions = PERMS_FILE;
+ try {
+ entry.remove(false);
+ } catch (e) {
+ logTestInfo("cleanUpdatesDir: unable to remove file. Path: " +
+ entry.path + ", Exception: " + e);
+ throw (e);
+ }
+ }
+ }
+}
+
+/**
+ * Deletes a directory and its children. First it tries nsIFile::Remove(true).
+ * If that fails it will fall back to recursing, setting the appropriate
+ * permissions, and deleting the current entry.
+ *
+ * @param aDir
+ * nsIFile for the directory to be deleted.
+ */
+function removeDirRecursive(aDir) {
+ if (!aDir.exists()) {
+ return;
+ }
+
+ try {
+ debugDump("attempting to remove directory. Path: " + aDir.path);
+ aDir.remove(true);
+ return;
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Exception: " + e);
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.isDirectory()) {
+ removeDirRecursive(entry);
+ } else {
+ entry.permissions = PERMS_FILE;
+ try {
+ debugDump("attempting to remove file. Path: " + entry.path);
+ entry.remove(false);
+ } catch (e) {
+ logTestInfo("error removing file. Exception: " + e);
+ throw (e);
+ }
+ }
+ }
+
+ aDir.permissions = PERMS_DIRECTORY;
+ aDir.remove(true);
+}
+
+/* Reloads the update metadata from disk */
+function reloadUpdateManagerData() {
+ gUpdateManager.QueryInterface(Ci.nsIObserver).
+ observe(null, "um-reload-update-data", "");
+}
+
+/**
+ * Returns the Gecko Runtime Engine directory where files other than executable
+ * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/
+ * and the installation directory on all other platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine directory.
+ */
+function getGREDir() {
+ return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the application base directory.
+ *
+ * @return nsIFile object for the application base directory.
+ */
+function getAppBaseDir() {
+ return Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent;
+}
+
+/**
+ * Logs TEST-INFO messages when DEBUG_AUS_TEST evaluates to true.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function debugDump(aText, aCaller) {
+ if (DEBUG_AUS_TEST) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ logTestInfo(aText, caller);
+ }
+}
+
+/**
+ * Returns either the active or regular update database XML file.
+ *
+ * @param isActiveUpdate
+ * If true this will return the active-update.xml otherwise it will
+ * return the updates.xml file.
+ */
+function getUpdatesXMLFile(aIsActiveUpdate) {
+ let file = getUpdatesRootDir();
+ file.append(aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML);
+ return file;
+}
+
+/**
+ * Gets the root directory for the updates directory.
+ *
+ * @return nsIFile for the updates root directory.
+ */
+function getUpdatesRootDir() {
+ return Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the updates directory.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesDir() {
+ let dir = getUpdatesRootDir();
+ dir.append(DIR_UPDATES);
+ return dir;
+}
+
+/**
+ * Gets the directory for update patches.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesPatchDir() {
+ let dir = getUpdatesDir();
+ dir.append(DIR_PATCH);
+ return dir;
+}
+
+/**
+ * Logs TEST-INFO messages.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function logTestInfo(aText, aCaller) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ let now = new Date();
+ let hh = now.getHours();
+ let mm = now.getMinutes();
+ let ss = now.getSeconds();
+ let ms = now.getMilliseconds();
+ let time = (hh < 10 ? "0" + hh : hh) + ":" +
+ (mm < 10 ? "0" + mm : mm) + ":" +
+ (ss < 10 ? "0" + ss : ss) + ":";
+ if (ms < 10) {
+ time += "00";
+ } else if (ms < 100) {
+ time += "0";
+ }
+ time += ms;
+ let msg = time + " | TEST-INFO | " + caller.filename + " | [" + caller.name +
+ " : " + caller.lineNumber + "] " + aText;
+ info(msg);
+}
+
+/**
+ * Clean up updates list and the updates directory.
+ */
+function cleanUpUpdates() {
+ gUpdateManager.activeUpdate = null;
+ gUpdateManager.saveUpdates();
+
+ removeUpdateDirsAndFiles();
+}
+
+function sleep(milliseconds) {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+}
+
+function runUpdateTest(updateParams, checkAttempts, steps) {
+ return Task.spawn(function*() {
+ registerCleanupFunction(() => {
+ popPrefs();
+ gMenuButtonUpdateBadge.uninit();
+ gMenuButtonUpdateBadge.init();
+ });
+
+ pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+ pushPref("Int", PREF_APP_UPDATE_IDLETIME, 0);
+ pushPref("Char", PREF_APP_RELEASE_NOTES_URL, "http://example.com/{targetVersion}");
+ pushPref("Char", PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE);
+
+ let url = URL_HTTP_UPDATE_XML +
+ "?" + updateParams +
+ getVersionParams();
+
+ setUpdateURL(url);
+
+ executeSoon(() => {
+ Task.spawn(function*() {
+ gUpdateService.checkForBackgroundUpdates();
+ for (var i = 0; i < checkAttempts - 1; i++) {
+ yield waitForEvent("update-error", "check-attempt-failed");
+ gUpdateService.checkForBackgroundUpdates();
+ }
+ });
+ });
+
+ for (let step of steps) {
+ yield processStep(step);
+ }
+
+ cleanUpUpdates();
+ });
+}
+
+function runUpdateProcessingTest(updates, steps) {
+ return Task.spawn(function*() {
+ pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+ pushPref("Int", PREF_APP_UPDATE_IDLETIME, 0);
+ pushPref("Char", PREF_APP_RELEASE_NOTES_URL, "http://example.com/{targetVersion}");
+ pushPref("Char", PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE);
+
+ registerCleanupFunction(() => {
+ popPrefs();
+ gMenuButtonUpdateBadge.reset();
+ });
+
+ pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+
+ for (let step of steps) {
+ yield processStep(step);
+ }
+
+ cleanUpUpdates();
+ });
+}
+
+function removeUpdateFile(name) {
+ let versionFile = FileUtils.getDir(KEY_UPDROOT, ["updates", "0"], true).clone();
+ versionFile.append(name);
+
+ if (versionFile.exists()) {
+ versionFile.remove(false);
+ }
+}
+
+/**
+ * Writes the updates specified to either the active-update.xml or the
+ * updates.xml.
+ *
+ * @param aContent
+ * The updates represented as a string to write to the XML file.
+ * @param isActiveUpdate
+ * If true this will write to the active-update.xml otherwise it will
+ * write to the updates.xml file.
+ */
+function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) {
+ writeStringToFile(getUpdatesXMLFile(aIsActiveUpdate), aContent);
+}
+
+/**
+ * Writes the current update operation/state to a file in the patch
+ * directory, indicating to the patching system that operations need
+ * to be performed.
+ *
+ * @param aStatus
+ * The status value to write.
+ */
+function writeStatusFile(aStatus) {
+ let file = getUpdatesPatchDir();
+ file.append(FILE_UPDATE_STATUS);
+ writeStringToFile(file, aStatus + "\n");
+}
+
+/**
+ * Writes a string of text to a file. A newline will be appended to the data
+ * written to the file. This function only works with ASCII text.
+ */
+function writeStringToFile(file, text) {
+ let fos = FileUtils.openSafeFileOutputStream(file);
+ text += "\n";
+ try {
+ fos.write(text, text.length);
+ } catch (e) {
+ throw new Error(`Error writing file: [${file.path}]. Message: ${e}`);
+ }
+ FileUtils.closeSafeFileOutputStream(fos);
+}
+
+function processStep({notificationId, button, beforeClick, cleanup}) {
+ return Task.spawn(function*() {
+ yield waitForEvent(`panelUI-${notificationId}`, "doorhanger-shown");
+
+ let notification = document.getElementById(`PanelUI-${notificationId}-notification`);
+ is(notification.hidden, false, `${notificationId} notification is showing`);
+ if (beforeClick) {
+ yield Task.spawn(beforeClick);
+ }
+
+ let buttonEl = document.getAnonymousElementByAttribute(notification, "anonid", button);
+
+ buttonEl.click();
+
+ if (cleanup) {
+ yield Task.spawn(cleanup);
+ }
+ });
+}
+
+/**
+ * Sets a preference's value in a way that it is remembered and can be set back to what it
+ * was previously.
+ *
+ * @param type
+ * {Int, Char, Bool, etc.} as in getCharPref.
+ * @param name
+ * The name of the preference.
+ * @param value
+ * The value to temporarily give the preference.
+ * @param prefBranch
+ * Optional pref branch to use.
+ */
+function pushPref(type, name, value, prefBranch) {
+ if (!prefBranch) {
+ prefBranch = Services.prefs;
+ }
+
+ let oldValue;
+ try {
+ oldValue = prefBranch[`get${type}Pref`](name);
+ } catch (e) {
+ oldValue = null;
+ }
+ gRembemberedPrefs.push({type, name, oldValue, prefBranch});
+ prefBranch[`set${type}Pref`](name, value);
+}
+
+function popPrefs() {
+ for (let {type, name, oldValue, prefBranch} of gRembemberedPrefs.reverse()) {
+ if (oldValue === null) {
+ prefBranch.clearUserPref(name);
+ } else {
+ prefBranch[`set${type}Pref`](name, oldValue);
+ }
+ }
+
+ gRembemberedPrefs = [];
+}
+
+function popPref(name) {
+ let remembered = gRembemberedPrefs.reverse().find(p => p.name == name);
+ if (remembered) {
+ let {type, oldValue} = remembered;
+
+ if (oldValue === null) {
+ Services.prefs.clearUserPref(name);
+ } else {
+ Services.prefs[`set${type}Pref`](name, oldValue);
+ }
+
+ gRembemberedPrefs = gRembemberedPrefs.slice(gRembemberedPrefs.indexOf(remembered), 1);
+ }
+}
+
+/**
+ * Waits for the specified topic and (optionally) status.
+ * @param topic
+ * String representing the topic to wait for.
+ * @param status
+ * Optional String representing the status on said topic to wait for.
+ * @return A promise which will resolve the first time an event occurs on the specified
+ * topic, and (optionally) with the specified status.
+ */
+function waitForEvent(topic, status = null) {
+ return new Promise(resolve => Services.obs.addObserver({
+ observe(subject, innerTopic, innerStatus) {
+ if (!status || status == innerStatus) {
+ Services.obs.removeObserver(this, topic);
+ resolve();
+ }
+ }
+ }, topic, false))
+}
+
+/* Triggers post-update processing */
+function testPostUpdateProcessing() {
+ gUpdateService.observe(null, "test-post-update-processing", "");
+}
+
+function addWindowListener(aURL, aCallback) {
+}
+
+/**
+ * Waits for a window with the specified url to load.
+ *
+ * @param url
+ * The url to wait for.
+ * @return A promise which will resolve when a window with the specified url is loaded.
+ * If another url is loaded, this will result in an assertion failure.
+ */
+function waitForWindow(url) {
+ return new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow(xulWindow) {
+ info("window opened, waiting for focus");
+ Services.wm.removeListener(this);
+
+ var domwindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ waitForFocus(function() {
+ is(domwindow.document.location.href, url, "should have seen the right window open");
+ resolve(domwindow);
+ }, domwindow);
+ },
+ onCloseWindow(xulWindow) { },
+ onWindowTitleChange(xulWindow, newTitle) { }
+ });
+ })
+}
+
+/**
+ * Constructs a string representing an update element for a local update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aPatches
+ * String representing the application update patches.
+ * @param aServiceURL (optional)
+ * The update's xml url.
+ * If not specified it will default to 'http://test_service/'.
+ * @param aIsCompleteUpdate (optional)
+ * The string 'true' if this update was a complete update or the string
+ * 'false' if this update was a partial update.
+ * If not specified it will default to 'true'.
+ * @param aChannel (optional)
+ * The update channel name.
+ * If not specified it will default to the default preference value of
+ * app.update.channel.
+ * @param aForegroundDownload (optional)
+ * The string 'true' if this update was manually downloaded or the
+ * string 'false' if this update was automatically downloaded.
+ * If not specified it will default to 'true'.
+ * @param aPreviousAppVersion (optional)
+ * The application version prior to applying the update.
+ * If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getLocalUpdateString(aPatches, aType, aName, aDisplayVersion,
+ aAppVersion, aBuildID, aDetailsURL, aServiceURL,
+ aInstallDate, aStatusText, aIsCompleteUpdate,
+ aChannel, aForegroundDownload, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aPreviousAppVersion,
+ aCustom1, aCustom2) {
+ let serviceURL = aServiceURL ? aServiceURL : "http://test_service/";
+ let installDate = aInstallDate ? aInstallDate : "1238441400314";
+ let statusText = aStatusText ? aStatusText : "Install Pending";
+ let isCompleteUpdate =
+ typeof aIsCompleteUpdate == "string" ? aIsCompleteUpdate : "true";
+ let channel = aChannel ? aChannel
+ : gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ let foregroundDownload =
+ typeof aForegroundDownload == "string" ? aForegroundDownload : "true";
+ let previousAppVersion = aPreviousAppVersion ? "previousAppVersion=\"" +
+ aPreviousAppVersion + "\" "
+ : "";
+ return getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+ aDetailsURL, aShowPrompt, aShowNeverForVersion,
+ aPromptWaitTime, aBackgroundInterval, aCustom1, aCustom2) +
+ " " +
+ previousAppVersion +
+ "serviceURL=\"" + serviceURL + "\" " +
+ "installDate=\"" + installDate + "\" " +
+ "statusText=\"" + statusText + "\" " +
+ "isCompleteUpdate=\"" + isCompleteUpdate + "\" " +
+ "channel=\"" + channel + "\" " +
+ "foregroundDownload=\"" + foregroundDownload + "\">" +
+ aPatches +
+ " </update>";
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/sharedUpdateXML.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Helper functions for creating xml strings used by application update tests.
+ *
+ * !IMPORTANT - This file contains everything needed (along with dependencies)
+ * by the updates.sjs file used by the mochitest-chrome tests. Since xpcshell
+ * used by the http server is launched with -v 170 this file must not use
+ * features greater than JavaScript 1.7.
+ */
+
+/* eslint-disable no-undef */
+
+const FILE_SIMPLE_MAR = "simple.mar";
+const SIZE_SIMPLE_MAR = "1031";
+const MD5_HASH_SIMPLE_MAR = "1f8c038577bb6845d94ccec4999113ee";
+const SHA1_HASH_SIMPLE_MAR = "5d49a672c87f10f31d7e326349564a11272a028b";
+const SHA256_HASH_SIMPLE_MAR = "1aabbed5b1dd6e16e139afc5b43d479e254e0c26" +
+ "3c8fb9249c0a1bb93071c5fb";
+const SHA384_HASH_SIMPLE_MAR = "26615014ea034af32ef5651492d5f493f5a7a1a48522e" +
+ "d24c366442a5ec21d5ef02e23fb58d79729b8ca2f9541" +
+ "99dd53";
+const SHA512_HASH_SIMPLE_MAR = "922e5ae22081795f6e8d65a3c508715c9a314054179a8" +
+ "bbfe5f50dc23919ad89888291bc0a07586ab17dd0304a" +
+ "b5347473601127571c66f61f5080348e05c36b";
+
+const STATE_NONE = "null";
+const STATE_DOWNLOADING = "downloading";
+const STATE_PENDING = "pending";
+const STATE_PENDING_SVC = "pending-service";
+const STATE_APPLYING = "applying";
+const STATE_APPLIED = "applied";
+const STATE_APPLIED_SVC = "applied-service";
+const STATE_SUCCEEDED = "succeeded";
+const STATE_DOWNLOAD_FAILED = "download-failed";
+const STATE_FAILED = "failed";
+
+const LOADSOURCE_ERROR_WRONG_SIZE = 2;
+const CRC_ERROR = 4;
+const READ_ERROR = 6;
+const WRITE_ERROR = 7;
+const MAR_CHANNEL_MISMATCH_ERROR = 22;
+const VERSION_DOWNGRADE_ERROR = 23;
+const INVALID_APPLYTO_DIR_STAGED_ERROR = 72;
+const INVALID_APPLYTO_DIR_ERROR = 74;
+
+const STATE_FAILED_DELIMETER = ": ";
+
+const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE =
+ STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE;
+const STATE_FAILED_CRC_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + CRC_ERROR;
+const STATE_FAILED_READ_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR;
+const STATE_FAILED_WRITE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR;
+const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR;
+const STATE_FAILED_VERSION_DOWNGRADE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR;
+
+/**
+ * Constructs a string representing a remote update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a remote update xml file.
+ */
+function getRemoteUpdatesXMLString(aUpdates) {
+ return "<?xml version=\"1.0\"?>\n" +
+ "<updates>\n" +
+ aUpdates +
+ "</updates>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aPatches
+ * String representing the application update patches.
+ * @return The string representing an update element for an update xml file.
+ */
+function getRemoteUpdateString(aPatches, aType, aName, aDisplayVersion,
+ aAppVersion, aBuildID, aDetailsURL, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aCustom1, aCustom2) {
+ return getUpdateString(aType, aName, aDisplayVersion, aAppVersion,
+ aBuildID, aDetailsURL, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aCustom1, aCustom2) + ">\n" +
+ aPatches +
+ " </update>\n";
+}
+
+/**
+ * Constructs a string representing a patch element for a remote update xml
+ * file. See getPatchString for parameter information not provided below.
+ *
+ * @return The string representing a patch element for a remote update xml file.
+ */
+function getRemotePatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+ return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) +
+ "/>\n";
+}
+
+/**
+ * Constructs a string representing a local update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a local update xml file.
+ */
+function getLocalUpdatesXMLString(aUpdates) {
+ if (!aUpdates || aUpdates == "") {
+ return "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"/>";
+ }
+ return ("<updates xmlns=\"http://www.mozilla.org/2005/app-update\">" +
+ aUpdates +
+ "</updates>").replace(/>\s+\n*</g, '><');
+}
+
+/**
+ * Constructs a string representing a patch element for a local update xml file.
+ * See getPatchString for parameter information not provided below.
+ *
+ * @param aSelected (optional)
+ * Whether this patch is selected represented or not. The string 'true'
+ * denotes selected and the string 'false' denotes not selected.
+ * If not specified it will default to the string 'true'.
+ * @param aState (optional)
+ * The patch's state.
+ * If not specified it will default to STATE_SUCCEEDED.
+ * @return The string representing a patch element for a local update xml file.
+ */
+function getLocalPatchString(aType, aURL, aHashFunction, aHashValue, aSize,
+ aSelected, aState) {
+ let selected = typeof aSelected == "string" ? aSelected : "true";
+ let state = aState ? aState : STATE_SUCCEEDED;
+ return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + " " +
+ "selected=\"" + selected + "\" " +
+ "state=\"" + state + "\"/>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file.
+ *
+ * @param aType (optional)
+ * The update's type which should be major or minor. If not specified it
+ * will default to 'major'.
+ * @param aName (optional)
+ * The update's name.
+ * If not specified it will default to 'App Update Test'.
+ * @param aDisplayVersion (optional)
+ * The update's display version.
+ * If not specified it will default to 'version #' where # is the value
+ * of DEFAULT_UPDATE_VERSION.
+ * @param aAppVersion (optional)
+ * The update's application version.
+ * If not specified it will default to the value of
+ * DEFAULT_UPDATE_VERSION.
+ * @param aBuildID (optional)
+ * The update's build id.
+ * If not specified it will default to '20080811053724'.
+ * @param aDetailsURL (optional)
+ * The update's details url.
+ * If not specified it will default to 'http://test_details/' due to due
+ * to bug 470244.
+ * @param aShowPrompt (optional)
+ * Whether to show the prompt for the update when auto update is
+ * enabled.
+ * If not specified it will not be present and the update service will
+ * default to false.
+ * @param aShowNeverForVersion (optional)
+ * Whether to show the 'No Thanks' button in the update prompt.
+ * If not specified it will not be present and the update service will
+ * default to false.
+ * @param aPromptWaitTime (optional)
+ * Override for the app.update.promptWaitTime preference.
+ * @param aBackgroundInterval (optional)
+ * Override for the app.update.download.backgroundInterval preference.
+ * @param aCustom1 (optional)
+ * A custom attribute name and attribute value to add to the xml.
+ * Example: custom1_attribute="custom1 value"
+ * If not specified it will not be present.
+ * @param aCustom2 (optional)
+ * A custom attribute name and attribute value to add to the xml.
+ * Example: custom2_attribute="custom2 value"
+ * If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+ aDetailsURL, aShowPrompt, aShowNeverForVersion,
+ aPromptWaitTime, aBackgroundInterval, aCustom1,
+ aCustom2) {
+ let type = aType ? aType : "major";
+ let name = aName ? aName : "App Update Test";
+ let displayVersion = aDisplayVersion ? "displayVersion=\"" +
+ aDisplayVersion + "\" "
+ : "";
+ let appVersion = "appVersion=\"" +
+ (aAppVersion ? aAppVersion : DEFAULT_UPDATE_VERSION) +
+ "\" ";
+ let buildID = aBuildID ? aBuildID : "20080811053724";
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+// let detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : "";
+ let detailsURL = "detailsURL=\"" +
+ (aDetailsURL ? aDetailsURL
+ : "http://test_details/") + "\" ";
+ let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : "";
+ let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" +
+ aShowNeverForVersion + "\" "
+ : "";
+ let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime +
+ "\" "
+ : "";
+ let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" +
+ aBackgroundInterval + "\" "
+ : "";
+ let custom1 = aCustom1 ? aCustom1 + " " : "";
+ let custom2 = aCustom2 ? aCustom2 + " " : "";
+ return " <update type=\"" + type + "\" " +
+ "name=\"" + name + "\" " +
+ displayVersion +
+ appVersion +
+ detailsURL +
+ showPrompt +
+ showNeverForVersion +
+ promptWaitTime +
+ backgroundInterval +
+ custom1 +
+ custom2 +
+ "buildID=\"" + buildID + "\"";
+}
+
+/**
+ * Constructs a string representing a patch element for an update xml file.
+ *
+ * @param aType (optional)
+ * The patch's type which should be complete or partial.
+ * If not specified it will default to 'complete'.
+ * @param aURL (optional)
+ * The patch's url to the mar file.
+ * If not specified it will default to the value of:
+ * gURLData + FILE_SIMPLE_MAR
+ * @param aHashFunction (optional)
+ * The patch's hash function used to verify the mar file.
+ * If not specified it will default to 'MD5'.
+ * @param aHashValue (optional)
+ * The patch's hash value used to verify the mar file.
+ * If not specified it will default to the value of MD5_HASH_SIMPLE_MAR
+ * which is the MD5 hash value for the file specified by FILE_SIMPLE_MAR.
+ * @param aSize (optional)
+ * The patch's file size for the mar file.
+ * If not specified it will default to the file size for FILE_SIMPLE_MAR
+ * specified by SIZE_SIMPLE_MAR.
+ * @return The string representing a patch element for an update xml file.
+ */
+function getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+ let type = aType ? aType : "complete";
+ let url = aURL ? aURL : gURLData + FILE_SIMPLE_MAR;
+ let hashFunction = aHashFunction ? aHashFunction : "MD5";
+ let hashValue = aHashValue ? aHashValue : MD5_HASH_SIMPLE_MAR;
+ let size = aSize ? aSize : SIZE_SIMPLE_MAR;
+ return " <patch type=\"" + type + "\" " +
+ "URL=\"" + url + "\" " +
+ "hashFunction=\"" + hashFunction + "\" " +
+ "hashValue=\"" + hashValue + "\" " +
+ "size=\"" + size + "\"";
+}
new file mode 100644
index 0000000000000000000000000000000000000000..b2ccbd8d2552c0ed4c7e941ad543fd8157d5cace
GIT binary patch
literal 1031
zc%1Wf3^HV3V2)=10~U55!3e>O49eg9y`xiZt^4&r=*)b!e+hFL!w<1qzPYNh?ZY*5
z!EYO-LsY_hRGWUqEcpFI|MtZf;_FpT?Y`7}Fx>Z(d7<s+Y_=;*Y`gdIzmh9(z54ED
z$%E6Yqa=36%NkCdKD#vN#HF)OPDBbw7<RsTmGVy7F<_hI-?J;;o<F9Q$LwjiN55;&
z8TDK571R|ce6Bew@oLktZ;3f2Rgc#OJz_cGa-)TtN%dI8`^ys~j)y((E6^^m;`%<h
z_fM6KsKYhe+$aB|1+}f_KAI7}{+yGt>qpUCz2~hu=5>=b=Pomu+rGJL=6vn;%eQjp
zuWhcfNLp-_ZeIMQWO})R$+RxhYdbyIs}oZ-CRAlZ!y*GjR}>@{XQbxj=$51wmoR8C
zP#ZW!Wmp<IMydrzp54B80t16fcmsn#0MkMSF^2VO_Z1WvN+d-tm}HztbCKwhWOHkF
zo7pkvkk^Vw&cB$-oD?NwIGQ(lEh#9?u3X?6BpZ~;A;l`ZU~P}P>tS{sy}}9Wu4ObA
z2^dOPzDlY46MItRp_8-Oy>AW!Y-vh??{5YM{_8+X85kOYmM&8}&!E7dxbhX_#Dx=<
z`S>&|3m=&!D$6U*5n3@fV?o2idv<zHx(XI5oOm$%8^W9=2a4t}Ffh3T%~51vV^CA^
z0h(gSaDkC2rOIfYQ>9MM7v^2lkKUQ+C!(ThAhR4PuuoV06=7h|z7F*_8_3^4%^|8v
z8MbN;HnW<7U8Q&#%oaRc-?4Pgf_#NVU(Rs^iEcmJvS5uuN1pn5-`&%q+YAqwsN4*_
z{4$iC)4?fcIzPlFqkH>ZK{2}on0h*ZbSaQtQd*FbSdv<1q?enRmzf4iN<h)SXtKu0
zvP?xl!UG{24@!XgMX9;@WvMB;X_+~x#UNF?fP^)=s!%XH1juwuNpS-z1*!ZBB(gv%
Q3yM;c^K%PwQcF@9080a99smFU
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/update.sjs
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Server side http server script for application update tests.
+ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const REL_PATH_DATA = "browser/browser/base/content/test/simpleUpdate/";
+
+function getTestDataFile(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).get("CurWorkD", Ci.nsILocalFile);
+ let pathParts = REL_PATH_DATA.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ file.append(pathParts[i]);
+ }
+ if (aFilename) {
+ file.append(aFilename);
+ }
+ return file;
+}
+
+function loadHelperScript() {
+ let scriptFile = getTestDataFile("sharedUpdateXML.js");
+ let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
+ let scriptSpec = io.newFileURI(scriptFile).spec;
+ let scriptloader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ scriptloader.loadSubScript(scriptSpec, this);
+}
+loadHelperScript();
+
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/browser/browser/base/content/test/simpleUpdate/update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+
+const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
+var gTimer;
+
+function handleRequest(aRequest, aResponse) {
+
+ let params = { };
+ if (aRequest.queryString) {
+ params = parseQueryString(aRequest.queryString);
+ }
+
+ let statusCode = params.statusCode ? parseInt(params.statusCode) : 200;
+ let statusReason = params.statusReason ? params.statusReason : "OK";
+ aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason);
+ aResponse.setHeader("Cache-Control", "no-cache", false);
+
+ // When a mar download is started by the update service it can finish
+ // downloading before the ui has loaded. By specifying a serviceURL for the
+ // update patch that points to this file and has a slowDownloadMar param the
+ // mar will be downloaded asynchronously which will allow the ui to load
+ // before the download completes.
+ if (params.slowDownloadMar) {
+ aResponse.processAsync();
+ aResponse.setHeader("Content-Type", "binary/octet-stream");
+ aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+ var continueFile = getTestDataFile("continue");
+ var contents = readFileBytes(getTestDataFile(FILE_SIMPLE_MAR));
+ gTimer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ gTimer.initWithCallback(function(aTimer) {
+ if (continueFile.exists()) {
+ gTimer.cancel();
+ aResponse.write(contents);
+ aResponse.finish();
+ }
+ }, SLOW_MAR_DOWNLOAD_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ return;
+ }
+
+ if (params.uiURL) {
+ let remoteType = "";
+ if (!params.remoteNoTypeAttr && params.uiURL == "BILLBOARD") {
+ remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
+ }
+ aResponse.write("<html><head><meta http-equiv=\"content-type\" content=" +
+ "\"text/html; charset=utf-8\"></head><body" +
+ remoteType + ">" + params.uiURL +
+ "<br><br>this is a test mar that will not affect your " +
+ "build.</body></html>");
+ return;
+ }
+
+ if (params.xmlMalformed) {
+ aResponse.write("xml error");
+ return;
+ }
+
+ if (params.noUpdates) {
+ aResponse.write(getRemoteUpdatesXMLString(""));
+ return;
+ }
+
+ if (params.unsupported) {
+ aResponse.write(getRemoteUpdatesXMLString(" <update type=\"major\" " +
+ "unsupported=\"true\" " +
+ "detailsURL=\"" + URL_HOST +
+ "\"></update>\n"));
+ return;
+ }
+
+
+ let size;
+ let patches = "";
+ if (!params.partialPatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
+ patches += getRemotePatchString("complete", SERVICE_URL, "SHA512",
+ SHA512_HASH_SIMPLE_MAR, size);
+ }
+
+ if (!params.completePatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
+ patches += getRemotePatchString("partial", SERVICE_URL, "SHA512",
+ SHA512_HASH_SIMPLE_MAR, size);
+ }
+
+ let type = params.type ? params.type : "major";
+ let name = params.name ? params.name : "App Update Test";
+ let appVersion = params.appVersion ? params.appVersion : "999999.9";
+ let displayVersion = params.displayVersion ? params.displayVersion
+ : "version " + appVersion;
+ let buildID = params.buildID ? params.buildID : "01234567890123";
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+// let detailsURL = params.showDetails ? URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" : null;
+ let detailsURL = URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS";
+ let showPrompt = params.showPrompt ? "true" : null;
+ let showNever = params.showNever ? "true" : null;
+ let promptWaitTime = params.promptWaitTime ? params.promptWaitTime : null;
+
+ let updates = getRemoteUpdateString(patches, type, "App Update Test",
+ displayVersion, appVersion, buildID,
+ detailsURL, showPrompt, showNever,
+ promptWaitTime);
+ aResponse.write(getRemoteUpdatesXMLString(updates));
+}
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param aQueryString
+ * The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ * queryString.
+ */
+function parseQueryString(aQueryString) {
+ let paramArray = aQueryString.split("&");
+ let regex = /^([^=]+)=(.*)$/;
+ let params = {};
+ for (let i = 0, sz = paramArray.length; i < sz; i++) {
+ let match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+/**
+ * Reads the binary contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The contents of the file as a string.
+ */
+function readFileBytes(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ let bis = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ bis.setInputStream(fis);
+ let data = [];
+ let count = fis.available();
+ while (count > 0) {
+ let bytes = bis.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (bytes.length == 0) {
+ throw "Nothing read from input stream!";
+ }
+ }
+ data.join('');
+ fis.close();
+ return data.toString();
+}
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -17,16 +17,17 @@ MOCHITEST_CHROME_MANIFESTS += [
BROWSER_CHROME_MANIFESTS += [
'content/test/alerts/browser.ini',
'content/test/captivePortal/browser.ini',
'content/test/general/browser.ini',
'content/test/newtab/browser.ini',
'content/test/plugins/browser.ini',
'content/test/popupNotifications/browser.ini',
'content/test/referrer/browser.ini',
+ 'content/test/simpleUpdate/browser.ini',
'content/test/social/browser.ini',
'content/test/tabcrashed/browser.ini',
'content/test/tabPrompts/browser.ini',
'content/test/tabs/browser.ini',
'content/test/urlbar/browser.ini',
'content/test/webrtc/browser.ini',
]
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -11,19 +11,31 @@
noautofocus="true">
<panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
<panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
<vbox id="PanelUI-contents-scroller">
<vbox id="PanelUI-contents" class="panelUI-grid"/>
</vbox>
<footer id="PanelUI-footer">
- <toolbarbutton id="PanelUI-update-status"
- oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
+ <toolbarbutton id="PanelUI-update-available-menu-item"
+ wrap="true"
+ label="&updateAvailable.panelUI.label;"
+ hidden="true"/>
+ <toolbarbutton id="PanelUI-update-manual-menu-item"
wrap="true"
+ label="&updateManual.panelUI.label;"
+ hidden="true"/>
+ <toolbarbutton id="PanelUI-update-launch-menu-item"
+ wrap="true"
+ label="&updateLaunch.panelUI.label;"
+ hidden="true"/>
+ <toolbarbutton id="PanelUI-update-restart-menu-item"
+ wrap="true"
+ label="&updateRestart.panelUI.label;"
hidden="true"/>
<hbox id="PanelUI-footer-fxa">
<hbox id="PanelUI-fxa-status"
defaultlabel="&fxaSignIn.label;"
signedinTooltiptext="&fxaSignedIn.tooltip;"
tooltiptext="&fxaSignedIn.tooltip;"
errorlabel="&fxaSignInError.label;"
unverifiedlabel="&fxaUnverified.label;"
@@ -400,8 +412,86 @@
<description>&panicButton.thankyou.msg1;</description>
<description>&panicButton.thankyou.msg2;</description>
</vbox>
</hbox>
<button label="&panicButton.thankyou.buttonlabel;"
id="panic-button-success-closebutton"
oncommand="PanicButtonNotifier.close()"/>
</panel>
+
+<panel id="PanelUI-notification-popup"
+ class="popup-notification-panel"
+ type="arrow"
+ position="after_start"
+ hidden="true"
+ orient="vertical"
+ noautofocus="true"
+ noautohide="true"
+ role="alert"/>
+
+<popupnotification id="PanelUI-update-available-notification"
+ popupid="update-available"
+ label="&updateAvailable.header.message;"
+ buttonlabel="&updateAvailable.acceptButton.label;"
+ buttonaccesskey="&updateAvailable.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateAvailable.cancelButton.label;"
+ secondarybuttonaccesskey="&updateAvailable.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-available-notification-content" orient="vertical">
+ <description id="update-available-description">&updateAvailable.message;
+ <label id="update-available-whats-new" class="text-link" value="&updateAvailable.whatsnew.label;" />
+ </description>
+ </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-manual-notification"
+ popupid="update-manual"
+ label="&updateManual.header.message;"
+ buttonlabel="&updateManual.acceptButton.label;"
+ buttonaccesskey="&updateManual.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateManual.cancelButton.label;"
+ secondarybuttonaccesskey="&updateManual.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-manual-notification-content" orient="vertical">
+ <description id="update-manual-description">&updateManual.message;
+ <label id="update-manual-whats-new" class="text-link" value="&updateManual.whatsnew.label;" />
+ </description>
+ </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-launch-notification"
+ popupid="update-launch"
+ label="&updateLaunch.header.message;"
+ buttonlabel="&updateLaunch.acceptButton.label;"
+ buttonaccesskey="&updateLaunch.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateLaunch.cancelButton.label;"
+ secondarybuttonaccesskey="&updateLaunch.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-launch-notification-content" orient="vertical">
+ <description id="update-launch-description">&updateLaunch.message;</description>
+ </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-restart-notification"
+ popupid="update-restart"
+ label="&updateRestart.header.message;"
+ buttonlabel="&updateRestart.acceptButton.label;"
+ buttonaccesskey="&updateRestart.acceptButton.accesskey;"
+ closebuttonhidden="true"
+ secondarybuttonlabel="&updateRestart.cancelButton.label;"
+ secondarybuttonaccesskey="&updateRestart.cancelButton.accesskey;"
+ dropmarkerhidden="true"
+ checkboxhidden="true"
+ hidden="true">
+ <popupnotificationcontent id="update-restart-notification-content" orient="vertical">
+ <description id="update-restart-description">&updateRestart.message;</description>
+ </popupnotificationcontent>
+</popupnotification>
\ No newline at end of file
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -27,37 +27,46 @@ const PanelUI = {
get kElements() {
return {
contents: "PanelUI-contents",
mainView: "PanelUI-mainView",
multiView: "PanelUI-multiView",
helpView: "PanelUI-helpView",
menuButton: "PanelUI-menu-button",
panel: "PanelUI-popup",
- scroller: "PanelUI-contents-scroller"
+ notificationPanel: "PanelUI-notification-popup",
+ scroller: "PanelUI-contents-scroller",
+ footer: "PanelUI-footer"
};
},
_initialized: false,
init() {
for (let [k, v] of Object.entries(this.kElements)) {
// Need to do fresh let-bindings per iteration
let getKey = k;
let id = v;
this.__defineGetter__(getKey, function() {
delete this[getKey];
return this[getKey] = document.getElementById(id);
});
}
+ this.notifications = [];
this.menuButton.addEventListener("mousedown", this);
this.menuButton.addEventListener("keypress", this);
this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
+ window.addEventListener("fullscreen", this);
window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
CustomizableUI.addListener(this);
+
+ for (let event of this.kEvents) {
+ this.notificationPanel.addEventListener(event, this);
+ }
+
this._initialized = true;
},
_eventListenersAdded: false,
_ensureEventListenersAdded() {
if (this._eventListenersAdded)
return;
this._addEventListeners();
@@ -70,16 +79,17 @@ const PanelUI = {
this.helpView.addEventListener("ViewShowing", this._onHelpViewShow);
this._eventListenersAdded = true;
},
uninit() {
for (let event of this.kEvents) {
this.panel.removeEventListener(event, this);
+ this.notificationPanel.removeEventListener(event, this);
}
this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
CustomizableUI.removeListener(this);
this._overlayScrollListenerBoundFn = null;
},
@@ -152,68 +162,135 @@ const PanelUI = {
anchor = aEvent.target;
}
this.panel.addEventListener("popupshown", function onPopupShown() {
this.removeEventListener("popupshown", onPopupShown);
resolve();
});
- let iconAnchor =
- document.getAnonymousElementByAttribute(anchor, "class",
- "toolbarbutton-icon");
- this.panel.openPopup(iconAnchor || anchor);
+ anchor = this._getPanelAnchor(anchor);
+ this.panel.openPopup(anchor);
}, (reason) => {
console.error("Error showing the PanelUI menu", reason);
});
});
},
+ showNotification(id, mainAction, secondaryActions = [], options = {}) {
+ let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
+ let existingIndex = this.notifications.findIndex(n => n.id == id);
+ if (existingIndex != -1) {
+ this.notifications.splice(existingIndex, 1);
+ }
+
+ // we don't want to clobber doorhanger notifications just to show a badge,
+ // so don't dismiss any of them and the badge will show once the doorhanger
+ // gets resolved.
+ if (!options.badgeOnly && !options.dismissed) {
+ this.notifications.forEach(n => { n.dismissed = true; });
+ }
+
+ // since notifications are generally somewhat pressing, the ideal case is that
+ // we never have two notifications at once. However, in the event that we do,
+ // it's more likely that the older notification has been sitting around for a
+ // bit, and so we don't want to hide the new notification behind it. Thus,
+ // we want our notifications to behave like a stack instead of a queue.
+ this.notifications.unshift(notification);
+ this._updateNotifications();
+ return notification;
+ },
+
+ showBadgeOnlyNotification(id) {
+ return this.showNotification(id, null, null, { badgeOnly: true });
+ },
+
+ removeNotification(id) {
+ let notifications;
+ if (typeof id == "string") {
+ notifications = this.notifications.filter(n => n.id == id);
+ } else {
+ // if it's not a string, assume RegExp
+ notifications = this.notifications.filter(n => id.test(n.id));
+ }
+
+ notifications.forEach(n => {
+ this._removeNotification(n);
+ });
+ this._updateNotifications();
+ },
+
/**
* If the menu panel is being shown, hide it.
*/
hide() {
if (document.documentElement.hasAttribute("customizing")) {
return;
}
this.panel.hidePopup();
},
handleEvent(aEvent) {
+ if (aEvent.target == this.notificationPanel) {
+ let doorhanger = this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
+ switch (aEvent.type) {
+ case "popupshowing":
+ this._notify(doorhanger.id, "doorhanger-showing");
+ break;
+ case "popupshown":
+ this._notify(doorhanger.id, "doorhanger-shown");
+ break;
+ }
+ }
+
// Ignore context menus and menu button menus showing and hiding:
if (aEvent.type.startsWith("popup") &&
aEvent.target != this.panel) {
return;
}
switch (aEvent.type) {
case "popupshowing":
this._adjustLabelsForAutoHyphens();
// Fall through
case "popupshown":
// Fall through
case "popuphiding":
// Fall through
case "popuphidden":
+ this._updateNotifications();
this._updatePanelButton(aEvent.target);
break;
case "mousedown":
if (aEvent.button == 0)
this.toggle(aEvent);
break;
case "keypress":
this.toggle(aEvent);
break;
+ case "fullscreen":
+ this._updateNotifications();
+ break;
}
},
get isReady() {
return !!this._isReady;
},
+ get isNotificationPanelOpen() {
+ let panelState = this.notificationPanel.state;
+
+ return panelState == "showing" || panelState == "open";
+ },
+
+ get activeNotification() {
+ return this._activeNotification || null;
+ },
+
/**
* Registering the menu panel is done lazily for performance reasons. This
* method is exposed so that CustomizationMode can force panel-readyness in the
* event that customization mode is started before the panel has been opened
* by the user.
*
* @param aCustomizing (optional) set to true if this was called while entering
* customization mode. If that's the case, we trust that customization
@@ -389,24 +466,23 @@ const PanelUI = {
panelRemover();
return;
}
viewShown = true;
CustomizableUI.addPanelCloseListeners(tempPanel);
tempPanel.addEventListener("popuphidden", panelRemover);
- let iconAnchor =
- document.getAnonymousElementByAttribute(aAnchor, "class",
- "toolbarbutton-icon");
+ let anchor = this._getPanelAnchor(aAnchor);
- if (iconAnchor && aAnchor.id) {
- iconAnchor.setAttribute("consumeanchor", aAnchor.id);
+ if (aAnchor != anchor && aAnchor.id) {
+ anchor.setAttribute("consumeanchor", aAnchor.id);
}
- tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
+
+ tempPanel.openPopup(anchor, "bottomcenter topright");
}
}),
/**
* NB: The enable- and disableSingleSubviewPanelAnimations methods only
* affect the hiding/showing animations of single-subview panels (tempPanel
* in the showSubView method).
*/
@@ -534,16 +610,225 @@ const PanelUI = {
quitButton.setAttribute("tooltiptext", tooltipString);
},
_overlayScrollListenerBoundFn: null,
_overlayScrollListener(aMQL) {
ScrollbarSampler.resetSystemScrollbarWidth();
this._scrollWidth = null;
},
+
+
+ _updateNotifications() {
+ if (!this.notifications.length) {
+ this._clearAllNotifications();
+ this.notificationPanel.hidePopup();
+ this._activeNotification = null;
+ return;
+ }
+
+ if (window.fullScreen) {
+ this.notificationPanel.hidePopup();
+ return;
+ }
+
+ let doorhangers =
+ this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
+
+ if (this.panel.state == "showing" || this.panel.state == "open") {
+ // if the menu is already showing, then we need to dismiss all notifications
+ // since we don't want their doorhangers competing for attention
+ doorhangers.forEach(n => { n.dismissed = true; })
+ this.notificationPanel.hidePopup();
+ this._clearBadges();
+ if (!this.notifications[0].options.badgeOnly) {
+ this._showMenuItem(this.notifications[0]);
+ }
+ } else if (doorhangers.length > 0) {
+ this._clearBadges();
+ this._showNotificationPanel(doorhangers[0]);
+ } else {
+ this.notificationPanel.hidePopup();
+ this._showBadge(this.notifications[0]);
+ this._showMenuItem(this.notifications[0]);
+ }
+ },
+
+ _showNotificationPanel(notification) {
+ this._refreshNotificationPanel(notification);
+
+ if (this.isNotificationPanelOpen) {
+ return;
+ }
+
+ let anchor = this._getPanelAnchor(this.menuButton);
+
+ this.notificationPanel.hidden = false;
+ this.notificationPanel.openPopup(anchor, "bottomcenter topright");
+
+ this._activeNotification = notification;
+ },
+
+ _clearNotificationPanel() {
+ for (let popupnotification of this.notificationPanel.children) {
+ popupnotification.hidden = true;
+ popupnotification.notification = null;
+ }
+ },
+
+ _clearAllNotifications() {
+ this._clearNotificationPanel();
+ this._clearBadges();
+ this._clearMenuItems();
+ },
+
+ _refreshNotificationPanel(notification) {
+ this._clearNotificationPanel();
+
+ let popupnotificationID = this._getPopupId(notification);
+ let popupnotification = document.getElementById(popupnotificationID);
+
+ popupnotification.setAttribute("id", popupnotificationID);
+ popupnotification.setAttribute("buttoncommand", "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');");
+ popupnotification.setAttribute("secondarybuttoncommand", "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');");
+
+ popupnotification.notification = notification;
+
+ this.notificationPanel.appendChild(popupnotification);
+ popupnotification.hidden = false;
+ },
+
+ _showBadge(notification) {
+ let badgeStatus = this._getBadgeStatus(notification);
+ this.menuButton.setAttribute("badge-status", badgeStatus);
+ this._notify(notification.id, "badge-shown");
+
+ this._activeNotification = notification;
+ },
+
+ _showMenuItem(notification) {
+ this._clearMenuItems();
+
+ let menuItemId = this._getMenuItemId(notification);
+ let menuItem = document.getElementById(menuItemId);
+ if (menuItem) {
+ menuItem.notification = notification;
+ menuItem.setAttribute("oncommand", "PanelUI._onNotificationMenuItemSelected(event)");
+ menuItem.classList.add("PanelUI-notification-menu-item");
+ menuItem.hidden = false;
+ menuItem.fromPanelUINotifications = true;
+ this._notify(notification.id, "menu-item-shown");
+ }
+
+ this._activeNotification = notification;
+ },
+
+ _clearBadges() {
+ this.menuButton.removeAttribute("badge-status");
+ },
+
+ _clearMenuItems() {
+ for (let child of this.footer.children) {
+ if (child.fromPanelUINotifications) {
+ child.notification = null;
+ child.hidden = true;
+ }
+ }
+ },
+
+ _removeNotification(notification) {
+ // This notification may already be removed, in which case let's just fail
+ // silently.
+ let notifications = this.notifications;
+ if (!notifications)
+ return;
+
+ var index = notifications.indexOf(notification);
+ if (index == -1)
+ return;
+
+ // remove the notification
+ notifications.splice(index, 1);
+ },
+
+ _onNotificationButtonEvent(event, type) {
+ let notificationEl = getNotificationFromElement(event.originalTarget);
+
+ if (!notificationEl)
+ throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
+
+ if (!notificationEl.notification)
+ throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
+
+ let notification = notificationEl.notification;
+
+ let action = notification.mainAction;
+
+ if (type == "secondarybuttoncommand") {
+ action = notification.secondaryActions[0];
+ }
+
+ let dismiss = true;
+ if (action) {
+ try {
+ action.callback();
+ if (action === notification.mainAction) {
+ this._notify(notification.id, "main-action");
+ } else {
+ this._notify(notification.id, "secondary-action");
+ }
+ } catch (error) {
+ Cu.reportError(error);
+ }
+
+ dismiss = action.dismiss;
+ }
+
+ if (dismiss) {
+ notification.dismissed = true;
+ } else {
+ this._removeNotification(notification);
+ }
+ this._updateNotifications();
+ },
+
+ _onNotificationMenuItemSelected(event) {
+ let target = event.originalTarget;
+ if (!target.notification)
+ throw "menucommand target has no associated action/notification";
+
+ event.stopPropagation();
+
+ try {
+ target.notification.mainAction.callback();
+ this._notify(notification.id, "main-action");
+ } catch (error) {
+ Cu.reportError(error);
+ }
+
+ this._removeNotification(target.notification);
+ this._updateNotifications();
+ },
+
+ _getPopupId(notification) { return "PanelUI-" + notification.id + "-notification"; },
+
+ _getBadgeStatus(notification) { return notification.id; },
+
+ _getMenuItemId(notification) { return "PanelUI-" + notification.id + "-menu-item"; },
+
+ _getPanelAnchor(candidate) {
+ let iconAnchor =
+ document.getAnonymousElementByAttribute(candidate, "class",
+ "toolbarbutton-icon");
+ return iconAnchor || candidate;
+ },
+
+ _notify(topic, status) {
+ Services.obs.notifyObservers(null, "panelUI-" + topic, status);
+ }
};
XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
/**
* Gets the currently selected locale for display.
* @return the selected locale or "en-US" if none is selected
*/
@@ -551,8 +836,29 @@ function getLocale() {
try {
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
return chromeRegistry.getSelectedLocale("browser");
} catch (ex) {
return "en-US";
}
}
+
+function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
+ this.id = id;
+ this.mainAction = mainAction;
+ this.secondaryActions = secondaryActions;
+ this.options = options;
+ this.dismissed = this.options.dismissed || false;
+}
+
+function getNotificationFromElement(aElement) {
+ // Need to find the associated notification object, which is a bit tricky
+ // since it isn't associated with the element directly - this is kind of
+ // gross and very dependent on the structure of the popupnotification
+ // binding's content.
+ let notificationEl;
+ let parent = aElement;
+ while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
+ notificationEl = parent;
+ }
+ return notificationEl;
+}
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -145,10 +145,11 @@ tags = fullscreen
skip-if = os == "mac"
[browser_1087303_button_preferences.js]
[browser_1089591_still_customizable_after_reset.js]
[browser_1096763_seen_widgets_post_reset.js]
[browser_1161838_inserted_new_default_buttons.js]
[browser_bootstrapped_custom_toolbar.js]
[browser_customizemode_contextmenu_menubuttonstate.js]
[browser_panel_toggle.js]
+[browser_panelUINotifications.js]
[browser_switch_to_customize_mode.js]
[browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -0,0 +1,272 @@
+"use strict";
+
+/**
+ * Tests that when we click on the main call-to-action of the doorhanger, the provided
+ * action is called, and the doorhanger removed.
+ */
+add_task(function* testMainActionCalled() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = PanelUI.notificationPanel.children;
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ mainActionButton.click();
+
+ ok(mainActionCalled, "Main action callback was called");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * This tests that when we click the secondary action for a notification,
+ * it will display the badge for that notification on the PanelUI menu button.
+ * Once we click on this button, we should see an item in the menu which will
+ * call our main action.
+ */
+add_task(function* testSecondaryActionWorkflow() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; },
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = PanelUI.notificationPanel.children;
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+ let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ yield PanelUI.hide();
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
+
+ yield PanelUI.show();
+ menuItem.click();
+ ok(mainActionCalled, "Main action callback was called");
+
+ PanelUI.removeNotification(/.*/);
+ });
+});
+
+/**
+ * We want to ensure a few things with this:
+ * - Adding a doorhanger will make a badge disappear
+ * - once the notification for the doorhanger is resolved (removed, not just dismissed),
+ * then we display any other badges that are remaining.
+ */
+add_task(function* testInteractionWithBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; },
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = PanelUI.notificationPanel.children;
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+ let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ menuItem.click();
+ ok(mainActionCalled, "Main action callback was called");
+
+ is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+ PanelUI.removeNotification(/.*/);
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * This tests that adding a badge will not dismiss any existing doorhangers.
+ */
+add_task(function* testAddingBadgeWhileDoorhangerIsShowing() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ PanelUI.showNotification("update-manual", mainAction);
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = PanelUI.notificationPanel.children;
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ mainActionButton.click();
+
+ ok(mainActionCalled, "Main action callback was called");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+ PanelUI.removeNotification(/.*/);
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that badges operate like a stack.
+ */
+add_task(function* testMultipleBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+ let menuButton = doc.getElementById("PanelUI-menu-button");
+
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
+
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ PanelUI.showBadgeOnlyNotification("update-succeeded");
+ is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
+
+ PanelUI.showBadgeOnlyNotification("update-failed");
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ PanelUI.showBadgeOnlyNotification("download-severe");
+ is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
+
+ PanelUI.showBadgeOnlyNotification("download-warning");
+ is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
+
+ PanelUI.removeNotification(/^download-/);
+ is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+ PanelUI.removeNotification(/^update-/);
+ is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+ PanelUI.removeNotification(/^fxa-/);
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+
+ yield PanelUI.show();
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
+ PanelUI.hide();
+
+ PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ PanelUI.showBadgeOnlyNotification("update-succeeded");
+ PanelUI.removeNotification(/.*/);
+ is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that non-badges also operate like a stack.
+ */
+add_task(function* testMultipleNonBadges() {
+ yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+ let doc = browser.ownerDocument;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ let updateManualAction = {
+ called: false,
+ callback: () => { updateManualAction.called = true; },
+ };
+ let updateRestartAction = {
+ called: false,
+ callback: () => { updateRestartAction.called = true; },
+ };
+
+ PanelUI.showNotification("update-manual", updateManualAction);
+
+ let notifications;
+ let doorhanger;
+
+ isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+ notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ PanelUI.showNotification("update-restart", updateRestartAction);
+
+ isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+ notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ doorhanger = notifications[0];
+ is(doorhanger.id, "PanelUI-update-restart-notification", "PanelUI is displaying the update-restart notification.");
+
+ let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ secondaryActionButton.click();
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is displaying on PanelUI button.");
+
+ let menuItem;
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is hidden on PanelUI button.");
+ menuItem = doc.getElementById("PanelUI-update-restart-menu-item");
+ is(menuItem.hidden, false, "update-restart menu item is showing.");
+
+ menuItem.click();
+ ok(updateRestartAction.called, "update-restart main action callback was called");
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is displaying on PanelUI button.");
+
+ yield PanelUI.show();
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
+ menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+ is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+ menuItem.click();
+ ok(updateManualAction.called, "update-manual main action callback was called");
+ });
+});
+
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -470,23 +470,23 @@ const DownloadsIndicatorView = {
// Check if the downloads button is in the menu panel, to determine which
// button needs to get a badge.
let widgetGroup = CustomizableUI.getWidget("downloads-button");
let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
if (aValue == DownloadsCommon.ATTENTION_NONE) {
this.indicator.removeAttribute("attention");
if (inMenu) {
- gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
+ PanelUI.removeNotification(/^download-/);
}
} else {
this.indicator.setAttribute("attention", aValue);
if (inMenu) {
let badgeClass = "download-" + aValue;
- gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass);
+ PanelUI.showBadgeOnlyNotification(badgeClass);
}
}
}
return aValue;
},
_attention: DownloadsCommon.ATTENTION_NONE,
//////////////////////////////////////////////////////////////////////////////
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -884,8 +884,45 @@ you can use these alternative items. Oth
<!ENTITY panicButton.view.forgetButton "Forget!">
<!ENTITY panicButton.thankyou.msg1 "Your recent history is cleared.">
<!ENTITY panicButton.thankyou.msg2 "Safe browsing!">
<!ENTITY panicButton.thankyou.buttonlabel "Thanks!">
<!ENTITY emeLearnMoreContextMenu.label "Learn more about DRM…">
<!ENTITY emeLearnMoreContextMenu.accesskey "D">
+
+<!ENTITY updateAvailable.message "Update your &brandShorterName; for the latest in speed and privacy.">
+<!ENTITY updateAvailable.whatsnew.label "See what's new.">
+<!ENTITY updateAvailable.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateAvailable.header.message "A new &brandShorterName; update is available.">
+<!ENTITY updateAvailable.acceptButton.label "Download Update">
+<!ENTITY updateAvailable.acceptButton.accesskey "D">
+<!ENTITY updateAvailable.cancelButton.label "Not Now">
+<!ENTITY updateAvailable.cancelButton.accesskey "N">
+<!ENTITY updateAvailable.panelUI.label "Download &brandShorterName; update">
+
+<!ENTITY updateManual.message "Download a fresh copy of &brandShorterName; and we'll help you to install it.">
+<!ENTITY updateManual.whatsnew.label "See what's new.">
+<!ENTITY updateManual.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateManual.header.message "&brandShorterName; can't update to the latest version.">
+<!ENTITY updateManual.acceptButton.label "Download &brandShorterName;">
+<!ENTITY updateManual.acceptButton.accesskey "D">
+<!ENTITY updateManual.cancelButton.label "Not Now">
+<!ENTITY updateManual.cancelButton.accesskey "N">
+<!ENTITY updateManual.panelUI.label "Download a fresh copy of &brandShorterName;">
+
+<!ENTITY updateRestart.message "After a quick restart, &brandShorterName; will restore all your open tabs and windows.">
+<!ENTITY updateRestart.header.message "Restart &brandShorterName; to apply update.">
+<!ENTITY updateRestart.acceptButton.label "Restart and Restore">
+<!ENTITY updateRestart.acceptButton.accesskey "R">
+<!ENTITY updateRestart.cancelButton.label "Not Now">
+<!ENTITY updateRestart.cancelButton.accesskey "N">
+<!ENTITY updateRestart.panelUI.label "Restart &brandShorterName; to apply update">
+
+<!ENTITY updateLaunch.message "Once you launch the installer, you'll have the latest version of &brandShorterName;.">
+<!ENTITY updateLaunch.header.message "Launch the &brandShorterName; installer you just downloaded.">
+<!ENTITY updateLaunch.acceptButton.label "Launch Installer">
+<!ENTITY updateLaunch.acceptButton.accesskey "L">
+<!ENTITY updateLaunch.cancelButton.label "Not Now">
+<!ENTITY updateLaunch.cancelButton.accesskey "N">
+<!ENTITY updateLaunch.panelUI.label "Launch &brandShorterName; installer">
+
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -636,25 +636,16 @@ flashHang.helpButton.accesskey = L
# be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
customizeTips.tip0.hint = Hint
customizeTips.tip0.learnMore = Learn more
# LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
customizeMode.tabTitle = Customize %S
-# LOCALIZATION NOTE(appmenu.*.description, appmenu.*.label): these are used for
-# the appmenu labels and buttons that appear when an update is staged for
-# installation or a background update has failed and a manual download is required.
-# %S is brandShortName
-appmenu.restartNeeded.description = Restart %S to apply updates
-appmenu.updateFailed.description = Background update failed, please download update
-appmenu.restartBrowserButton.label = Restart %S
-appmenu.downloadUpdateButton.label = Download Update
-
# LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
readingList.promo.firstUse.readerView.title = Reader View
readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
# LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
# %1$S will be replaced with a link, the text of which is
# appMenuRemoteTabs.mobilePromo.android and the link will be to
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -105,24 +105,44 @@
background-size: contain;
border: none;
}
#PanelUI-menu-button[badge-status="download-success"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
display: none;
}
-#PanelUI-menu-button[badge-status="update-succeeded"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
+#PanelUI-menu-button[badge-status="update-available"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-manual"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-launch"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-restart"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
- height: 13px;
+ border-radius: 50%;
+ box-shadow: none;
+ border: 1px solid -moz-dialog;
+ /* "!important" is necessary to override the rule in toolbarbutton.css */
+ margin: -9px 0 0 !important;
+ margin-inline-end: -6px !important;
+ min-width: 16px;
+ min-height: 16px;
}
-#PanelUI-menu-button[badge-status="update-failed"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
- background: #D90000 url(chrome://browser/skin/update-badge-failed.svg) no-repeat center;
- height: 13px;
+#PanelUI-update-restart-menu-item::after,
+#PanelUI-update-download-menu-item::after,
+#PanelUI-update-launch-menu-item::after,
+#PanelUI-update-manual-menu-item::after {
+ background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
+ border-radius: 50%;
+}
+
+#PanelUI-update-restart-menu-item,
+#PanelUI-update-download-menu-item,
+#PanelUI-update-launch-menu-item,
+#PanelUI-update-manual-menu-item {
+ list-style-image: url(chrome://branding/content/icon16.png);
}
#PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
#PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
box-shadow: none;
filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
}
@@ -432,25 +452,25 @@ toolbaritem[cui-areatype="menu-panel"][s
toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"] > iframe {
margin: 4px auto;
}
#PanelUI-multiView[viewtype="subview"] > .panel-viewcontainer > .panel-viewstack > .panel-mainview > #PanelUI-mainView {
background-color: var(--arrowpanel-dimmed);
}
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-status,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-label,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-icon,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > toolbarseparator,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-customize,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-help:not([panel-multiview-anchor="true"]) {
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .panel-wide-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
+#PanelUI-multiView[viewtype="subview"] .PanelUI-notification-menu-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-avatar,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-label,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-icon,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-inner > toolbarseparator,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-customize,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-help:not([panel-multiview-anchor="true"]) {
opacity: .5;
}
/*
* XXXgijs: this is a workaround for a layout issue that was caused by these iframes,
* which was affecting subview display. Because of this, we're hiding the iframe *only*
* when displaying a subview. The discerning user might notice this, but it's not nearly
* as bad as the brokenness.
@@ -556,37 +576,24 @@ toolbarpaletteitem[place="palette"] > to
width: 47px;
padding-top: 1px;
display: block;
text-align: center;
position: relative;
top: 25%;
}
-#PanelUI-update-status[update-status]::after {
+.PanelUI-notification-menu-item::after {
content: "";
- width: 14px;
- height: 14px;
+ width: 16px;
+ height: 16px;
margin-inline-end: 16.5px;
- box-shadow: 0px 1px 0px rgba(255,255,255,.2) inset, 0px -1px 0px rgba(0,0,0,.1) inset, 0px 1px 0px rgba(12,27,38,.2);
- border-radius: 2px;
- background-size: contain;
display: -moz-box;
}
-#PanelUI-update-status[update-status="succeeded"]::after {
- background-image: url(chrome://browser/skin/update-badge.svg);
- background-color: #74BF43;
-}
-
-#PanelUI-update-status[update-status="failed"]::after {
- background-image: url(chrome://browser/skin/update-badge-failed.svg);
- background-color: #D90000;
-}
-
#PanelUI-fxa-status {
display: flex;
flex: 1 1 0%;
width: 1px;
}
#PanelUI-footer-inner,
#PanelUI-footer-fxa:not([hidden]) {
@@ -607,17 +614,17 @@ toolbarpaletteitem[place="palette"] > to
-moz-appearance: none;
}
#PanelUI-footer-inner:hover > toolbarseparator,
#PanelUI-footer-fxa:hover > toolbarseparator {
margin: 0;
}
-#PanelUI-update-status,
+.PanelUI-notification-menu-item,
#PanelUI-help,
#PanelUI-fxa-label,
#PanelUI-fxa-icon,
#PanelUI-customize,
#PanelUI-quit {
margin: 0;
padding: 11px 0;
box-sizing: border-box;
@@ -625,49 +632,46 @@ toolbarpaletteitem[place="palette"] > to
-moz-appearance: none;
box-shadow: none;
border: none;
border-radius: 0;
transition: background-color;
-moz-box-orient: horizontal;
}
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
border-top: 1px solid var(--panel-separator-color);
-}
-
-#PanelUI-update-status {
border-bottom: 1px solid transparent;
margin-bottom: -1px;
}
-#PanelUI-update-status > .toolbarbutton-text {
+.PanelUI-notification-menu-item > .toolbarbutton-text {
width: 0; /* Fancy cropping solution for flexbox. */
}
#PanelUI-help,
#PanelUI-quit {
min-width: 46px;
}
-#PanelUI-update-status > .toolbarbutton-text,
+.PanelUI-notification-menu-item > .toolbarbutton-text,
#PanelUI-fxa-label > .toolbarbutton-text,
#PanelUI-customize > .toolbarbutton-text {
margin: 0;
padding: 0 6px;
text-align: start;
}
#PanelUI-help > .toolbarbutton-text,
#PanelUI-quit > .toolbarbutton-text,
#PanelUI-fxa-avatar > .toolbarbutton-text {
display: none;
}
-#PanelUI-update-status > .toolbarbutton-icon,
+.PanelUI-notification-menu-item > .toolbarbutton-icon,
#PanelUI-fxa-label > .toolbarbutton-icon,
#PanelUI-fxa-icon > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon {
margin-inline-end: 0;
}
@@ -683,26 +687,24 @@ toolbarpaletteitem[place="palette"] > to
border-inline-start-style: none;
}
#PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
#PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label {
padding-inline-start: 0px;
}
-#PanelUI-update-status {
+/* descend from #PanelUI-footer to add specificity, or else the
+ padding-inline-start will be overridden */
+#PanelUI-footer > .PanelUI-notification-menu-item {
width: calc(@menuPanelWidth@ + 30px);
padding-inline-start: 15px;
border-inline-start-style: none;
}
-#PanelUI-update-status {
- list-style-image: url(chrome://branding/content/icon16.png);
-}
-
#PanelUI-fxa-label,
#PanelUI-fxa-icon {
list-style-image: url(chrome://browser/skin/sync-horizontalbar.png);
}
#PanelUI-remotetabs {
--panel-ui-sync-illustration-height: 157.5px;
}
@@ -939,44 +941,29 @@ toolbarpaletteitem[place="palette"] > to
background-color: hsl(42,94%,85%);
}
#PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
background-color: hsl(42,94%,82%);
box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
}
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
color: black;
-}
-
-#PanelUI-update-status[update-status="succeeded"] {
background-color: hsla(96,65%,75%,.5);
}
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover {
+.PanelUI-notification-menu-item:not([disabled]):hover {
background-color: hsla(96,65%,75%,.8);
}
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover:active {
+.PanelUI-notification-menu-item:not([disabled]):hover:active {
background-color: hsl(96,65%,75%);
}
-#PanelUI-update-status[update-status="failed"] {
- background-color: hsla(359,69%,84%,.5);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover {
- background-color: hsla(359,69%,84%,.8);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover:active {
- background-color: hsl(359,69%,84%);
-}
-
#PanelUI-quit:not([disabled]):hover {
background-color: #d94141;
outline-color: #c23a3a;
}
#PanelUI-quit:not([disabled]):hover:active {
background-color: #ad3434;
outline-color: #992e2e;
@@ -1670,17 +1657,20 @@ menuitem[checked="true"].subviewbutton >
}
#PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after,
toolbarbutton[panel-multiview-anchor="true"]:-moz-locale-dir(rtl) {
background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl@2x.png),
linear-gradient(rgba(255,255,255,0.3), transparent);
}
- #PanelUI-update-status {
+ #PanelUI-update-restart-menu-item,
+ #PanelUI-update-download-menu-item,
+ #PanelUI-update-launch-menu-item,
+ #PanelUI-update-manual-menu-item {
list-style-image: url(chrome://branding/content/icon32.png);
}
#PanelUI-fxa-label,
#PanelUI-fxa-icon {
list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
}
@@ -1707,17 +1697,17 @@ menuitem[checked="true"].subviewbutton >
#PanelUI-fxa-label,
#PanelUI-fxa-icon,
#PanelUI-customize,
#PanelUI-help,
#PanelUI-quit {
-moz-image-region: rect(0, 32px, 32px, 0);
}
- #PanelUI-update-status > .toolbarbutton-icon,
+ .PanelUI-notification-menu-item > .toolbarbutton-icon,
#PanelUI-fxa-label > .toolbarbutton-icon,
#PanelUI-fxa-icon > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon {
width: 16px;
}
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -253,17 +253,17 @@ html|*#webRTC-previewVideo {
.plugin-icon.plugin-blocked {
list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin-blocked);
}
#notification-popup-box[hidden] {
/* Override display:none to make the pluginBlockedNotification animation work
when showing the notification repeatedly. */
- display: -moz-box;
+ display: -moz-box;j
visibility: collapse;
}
#plugins-notification-icon.plugin-blocked[showing] {
animation: pluginBlockedNotification 500ms ease 0s 5 alternate both;
}
@keyframes pluginBlockedNotification {
@@ -315,8 +315,17 @@ html|*#webRTC-previewVideo {
-moz-image-region: rect(0px, 32px, 32px, 0px);
}
.translation-icon.in-use {
-moz-image-region: rect(0px, 64px, 32px, 32px);
}
}
%endif
+
+/* UPDATE */
+.popup-notification-icon[popupid="update-available"],
+.popup-notification-icon[popupid="update-manual"],
+.popup-notification-icon[popupid="update-launch"],
+.popup-notification-icon[popupid="update-restart"] {
+ background: #74BF43 url(chrome://browser/skin/notification-icons.svg#update) no-repeat center;
+ border-radius: 50%;
+}
\ No newline at end of file
--- a/browser/themes/shared/notification-icons.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -40,16 +40,22 @@
}
#camera-indicator,
#microphone-indicator,
#screen-indicator {
fill: white;
fill-opacity: 1;
}
+
+ #update-icon {
+ stroke: #fff;
+ stroke-width: 3px;
+ stroke-linecap: round;
+ }
</style>
<defs>
<path id="camera-icon" d="m 2,23 a 3,3 0 0 0 3,3 l 14,0 a 3,3 0 0 0 3,-3 l 0,-4 6,5.5 c 0.5,0.5 1,0.7 2,0.5 l 0,-18 c -1,-0.2 -1.5,0 -2,0.5 l -6,5.5 0,-4 a 3,3 0 0 0 -3,-3 l -14,0 a 3,3 0 0 0 -3,3 z" />
<path id="desktop-notification-icon" d="m 2,20 a 4,4 0 0 0 4,4 l 13,0 7,7 0,-7 a 4,4 0 0 0 4,-4 l 0,-12 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 5,-2 a 1,1 0 1 1 0,-2 l 10,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 14,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 18,0 a 1,1 0 1 1 0,2 z" />
<path id="geo-linux-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 4,2.1 a 10,10 0 0 0 8,8 l 0,-4 4,0 0,4 a 10,10 0 0 0 8,-8 l -4,0 0,-4 4,0 a 10,10 0 0 0 -8,-8 l 0,4 -4,0 0,-4 a 10,10 0 0 0 -8,8 l 4,0 0,4 z" />
<path id="geo-linux-detailed-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 3,2.1 a 11,11 0 0 0 9,9 l 1,-5 2,0 1,5 a 11,11 0 0 0 9,-9 l -5,-1 0,-2 5,-1 a 11,11 0 0 0 -9,-9 l -1,5 -2,0 -1,-5 a 11,11 0 0 0 -9,9 l 5,1 0,2 z" />
<path id="geo-osx-icon" d="m 0,16 16,0 0,16 12,-28 z" />
@@ -58,16 +64,17 @@
<path id="indexedDB-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 2,0 0,-4 -2,0 0,-16 20,0 0,16 -2,0 0,4 2,0 a 4,4 0 0 0 4,-4 l 0,-16 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 8,-2 6,7 6,-7 -4,0 0,-8 -4,0 0,8 z" />
<path id="login-icon" d="m 2,26 0,4 6,0 0,-2 2,0 0,-2 1,0 0,-1 2,0 0,-3 2,0 2.5,-2.5 1.5,1.5 3,-3 a 8,8 0 1 0 -8,-8 l -3,3 2,2 z m 20,-18.1 a 2,2 0 1 1 0,0.2 z" />
<path id="login-detailed-icon" d="m 1,27 0,3.5 a 0.5,0.5 0 0 0 0.5,0.5 l 5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1.5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-2 2,0 2.5,-2.5 q 0.5,-0.5 1,0 l 1,1 c 0.5,0.5 1,0.5 1.5,-0.5 l 1,-2 a 9,9 0 1 0 -8,-8 l -2,1 c -1,0.5 -1,1 -0.5,1.5 l 1.5,1.5 q 0.5,0.5 0,1 z m 21,-19.1 a 2,2 0 1 1 0,0.2 z" />
<path id="microphone-icon" d="m 8,14 0,4 a 8,8 0 0 0 6,7.7 l 0,2.3 -2,0 a 2,2 0 0 0 -2,2 l 12,0 a 2,2 0 0 0 -2,-2 l -2,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 -2,0 0,4 a 6,6 0 0 1 -12,0 l 0,-4 z m 4,4 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
<path id="microphone-detailed-icon" d="m 8,18 a 8,8 0 0 0 6,7.7 l 0,2.3 -1,0 a 3,2 0 0 0 -3,2 l 12,0 a 3,2 0 0 0 -3,-2 l -1,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 a 1,1 0 0 0 -2,0 l 0,4 a 6,6 0 0 1 -12,0 l 0,-4 a 1,1 0 0 0 -2,0 z m 4,0 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
<path id="plugin-icon" d="m 2,26 a 2,2 0 0 0 2,2 l 24,0 a 2,2 0 0 0 2,-2 l 0,-16 a 2,2 0 0 0 -2,-2 l -24,0 a 2,2 0 0 0 -2,2 z m 2,-20 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z m 14,0 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z" />
<path id="popup-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 8,0 a 10,10 0 0 1 -2,-4 l -4,0 a 2,2 0 0 1 -2,-2 l 0,-12 18,0 0,2 a 10,10 0 0 1 4,2 l 0,-8 a 4,4 0 0 0 -4,-4 l -18,0 a 4,4 0 0 0 -4,4 z m 12,-2.1 a 8,8 0 1 1 0,0.2 m 10.7,-4.3 a 5,5 0 0 0 -6.9,6.9 z m -5.4,8.4 a 5,5 0 0 0 6.9,-6.9 z" />
<path id="screen-icon" d="m 2,18 a 2,2 0 0 0 2,2 l 2,0 0,-6 a 4,4 0 0 1 4,-4 l 14,0 0,-6 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z m 6,10 a 2,2 0 0 0 2,2 l 18,0 a 2,2 0 0 0 2,-2 l 0,-14 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z" />
+ <path id="update-icon" d="M 16,9 L 16,24 M 16,9 L 11,14 M 16,9 L 21,14" />
<clipPath id="clip">
<path d="m 0,0 0,31 31,-31 z m 6,32 26,0 0,-26 z"/>
</clipPath>
</defs>
<use id="camera" xlink:href="#camera-icon" />
<use id="camera-sharing" xlink:href="#camera-icon"/>
@@ -95,11 +102,12 @@
<use id="microphone-detailed" xlink:href="#microphone-detailed-icon" />
<use id="plugin" xlink:href="#plugin-icon" />
<use id="plugin-blocked" class="blocked" xlink:href="#plugin-icon" />
<use id="popup" xlink:href="#popup-icon" />
<use id="screen" xlink:href="#screen-icon" />
<use id="screen-sharing" xlink:href="#screen-icon"/>
<use id="screen-indicator" xlink:href="#screen-icon"/>
<use id="screen-blocked" class="blocked" xlink:href="#screen-icon" />
+ <use id="update" xlink:href="#update-icon" />
<path id="strikeout" d="m 2,28 2,2 26,-26 -2,-2 z"/>
</svg>
--- a/browser/themes/shared/update-badge.svg
+++ b/browser/themes/shared/update-badge.svg
@@ -1,6 +1,8 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="10px" height="10px">
- <polygon points="4,9 4,5 2,5 5,1 8,5 6,5 6,9" fill="#fff"/>
+ <line x1="5" x2="5" y1="9" y2="2" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+ <line x1="5" x2="2" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+ <line x1="5" x2="8" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
</svg>
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -33,16 +33,17 @@ const PREF_APP_UPDATE_ENABLED
const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
const PREF_APP_UPDATE_POSTUPDATE = "app.update.postupdate";
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors";
const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors";
+const PREF_APP_UPDATE_DOORHANGER = "app.update.doorhanger";
const PREF_APP_UPDATE_SILENT = "app.update.silent";
const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout";
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
const PREF_APP_UPDATE_URL = "app.update.url";
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
@@ -1185,16 +1186,19 @@ function handleUpdateFailure(update, err
Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt).
showUpdateError(update);
writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
return true;
}
if (update.errorCode == ELEVATION_CANCELED) {
+
+ Services.obs.notifyObservers(update, "update-error", "elevation-canceled");
+
let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0);
cancelations++;
Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
if (AppConstants.platform == "macosx") {
let osxCancelations = getPref("getIntPref",
PREF_APP_UPDATE_CANCELATIONS_OSX, 0);
osxCancelations++;
Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX,
@@ -1208,16 +1212,17 @@ function handleUpdateFailure(update, err
cleanupActiveUpdate();
} else {
writeStatusFile(getUpdatesDir(),
update.state = STATE_PENDING_ELEVATE);
}
update.statusText = gUpdateBundle.GetStringFromName("elevationFailure");
update.QueryInterface(Ci.nsIWritablePropertyBag);
update.setProperty("patchingFailed", "elevationFailure");
+
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt);
prompter.showUpdateError(update);
} else {
writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
}
return true;
}
@@ -1279,16 +1284,18 @@ function handleFallbackToCompleteUpdate(
LOG("handleFallbackToCompleteUpdate - install of partial patch " +
"failed, downloading complete patch");
var status = Cc["@mozilla.org/updates/update-service;1"].
getService(Ci.nsIApplicationUpdateService).
downloadUpdate(update, !postStaging);
if (status == STATE_NONE)
cleanupActiveUpdate();
} else {
+ Services.obs.notifyObservers(update, "update-error", "unknown");
+
LOG("handleFallbackToCompleteUpdate - install of complete or " +
"only one patch offered failed.");
}
update.QueryInterface(Ci.nsIWritablePropertyBag);
update.setProperty("patchingFailed", oldType);
}
function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
@@ -2159,19 +2166,22 @@ UpdateService.prototype = {
errCount++;
Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
// Don't allow the preference to set a value greater than 20 for max errors.
let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20);
if (errCount >= maxErrors) {
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
createInstance(Ci.nsIUpdatePrompt);
+ Services.obs.notifyObservers(update, "update-error", "check-attempts-exceeded");
prompter.showUpdateError(update);
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
} else {
+ Services.obs.notifyObservers(update, "update-error", "check-attempt-failed");
+
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
}
},
/**
* Called when a connection should be resumed
*/
_attemptResume: function AUS_attemptResume() {
@@ -2519,23 +2529,29 @@ UpdateService.prototype = {
if (update.unsupported) {
LOG("UpdateService:_selectAndInstallUpdate - update not supported for " +
"this system");
if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) {
LOG("UpdateService:_selectAndInstallUpdate - notifying that the " +
"update is not supported for this system");
this._showPrompt(update);
}
+
+ Services.obs.notifyObservers(null, "update-available", "unsupported");
+
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
return;
}
if (!getCanApplyUpdates()) {
LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " +
"apply updates... prompting");
+
+ Services.obs.notifyObservers(null, "update-available", "cant-apply");
+
this._showPrompt(update);
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
return;
}
/**
* From this point on there are two possible outcomes:
* 1. download and install the update automatically
@@ -2551,24 +2567,30 @@ UpdateService.prototype = {
* Update Type Outcome
* Major Notify
* Minor Auto Install
*/
if (update.showPrompt) {
LOG("UpdateService:_selectAndInstallUpdate - prompting because the " +
"update snippet specified showPrompt");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET);
+
+ Services.obs.notifyObservers(update, "update-available", "show-prompt");
+
this._showPrompt(update);
return;
}
if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) {
LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " +
"install is disabled");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
+
+ Services.obs.notifyObservers(update, "update-available", "show-prompt");
+
this._showPrompt(update);
return;
}
LOG("UpdateService:_selectAndInstallUpdate - download the update");
let status = this.downloadUpdate(update, true);
if (status == STATE_NONE) {
cleanupActiveUpdate();
@@ -3104,17 +3126,17 @@ UpdateManager.prototype = {
if (update.state == STATE_APPLIED && shouldUseService()) {
writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE);
}
// Send an observer notification which the update wizard uses in
// order to update its UI.
LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " +
"the update was staged. state: " + update.state + ", status: " + status);
- Services.obs.notifyObservers(null, "update-staged", update.state);
+ Services.obs.notifyObservers(update, "update-staged", update.state);
if (AppConstants.platform == "gonk") {
// Do this after everything else, since it will likely cause the app to
// shut down.
if (update.state == STATE_APPLIED) {
// Notify the user that an update has been staged and is ready for
// installation (i.e. that they should restart the application). We do
// not notify on failed update attempts.
@@ -3233,18 +3255,16 @@ Checker.prototype = {
/**
* See nsIUpdateService.idl
*/
checkForUpdates: function UC_checkForUpdates(listener, force) {
LOG("Checker: checkForUpdates, force: " + force);
if (!listener)
throw Cr.NS_ERROR_NULL_POINTER;
- Services.obs.notifyObservers(null, "update-check-start", null);
-
var url = this.getUpdateURL(force);
if (!url || (!this.enabled && !force))
return;
this._request = new XMLHttpRequest();
this._request.open("GET", url, true);
this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
// Prevent the request from reading from the cache.
@@ -3339,16 +3359,17 @@ Checker.prototype = {
* The nsIDOMEvent for the load
*/
onLoad: function UC_onLoad(event) {
LOG("Checker:onLoad - request completed downloading document");
try {
// Analyze the resulting DOM and determine the set of updates.
var updates = this._updates;
+
LOG("Checker:onLoad - number of updates available: " + updates.length);
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
}
// Tell the callback about the updates
this._callback.onCheckComplete(event.target, updates, updates.length);
@@ -4135,16 +4156,19 @@ Downloader.prototype = {
cleanupActiveUpdate();
} else {
allFailed = false;
}
}
if (allFailed) {
LOG("Downloader:onStopRequest - all update patch downloads failed");
+
+ Services.obs.notifyObservers(this._update, "update-error", "all-downloads-failed");
+
// If the update UI is not open (e.g. the user closed the window while
// downloading) and if at any point this was a foreground download
// notify the user about the error. If the update was a background
// update there is no notification since the user won't be expecting it.
if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) {
this._update.QueryInterface(Ci.nsIWritablePropertyBag);
if (this._update.getProperty("foregroundDownload") == "true") {
let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
@@ -4260,33 +4284,39 @@ UpdatePrompt.prototype = {
null, null);
},
/**
* See nsIUpdateService.idl
*/
showUpdateAvailable: function UP_showUpdateAvailable(update) {
if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
this._getUpdateWindow() || this._getAltUpdateWindow()) {
return;
}
this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
UPDATE_WINDOW_NAME, "updatesavailable", update);
},
/**
* See nsIUpdateService.idl
*/
showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) {
if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) {
return;
}
- // Trigger the display of the hamburger menu badge.
- Services.obs.notifyObservers(null, "update-downloaded", update.state);
+
+ // Trigger the display of the hamburger doorhanger.
+ Services.obs.notifyObservers(update, "update-downloaded", update.state);
+
+ if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+ return;
+ }
if (this._getAltUpdateWindow())
return;
if (background) {
this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
UPDATE_WINDOW_NAME, "finishedBackground", update);
} else {
@@ -4295,16 +4325,17 @@ UpdatePrompt.prototype = {
}
},
/**
* See nsIUpdateService.idl
*/
showUpdateError: function UP_showUpdateError(update) {
if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
this._getAltUpdateWindow())
return;
// In some cases, we want to just show a simple alert dialog.
// Replace with Array.prototype.includes when it has stabilized.
if (update.state == STATE_FAILED &&
(WRITE_ERRORS.indexOf(update.errorCode) != -1 ||
update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR ||
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -809,16 +809,17 @@ function setupPrefs() {
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) {
gAppUpdateStagingEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED);
}
Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
Services.prefs.setIntPref(PREF_APP_UPDATE_IDLETIME, 0);
Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0);
Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_DOORHANGER, false);
}
/**
* Restores files that were backed up for the tests and general file cleanup.
*/
function resetFiles() {
// Restore the backed up updater-settings.ini if it exists.
let baseAppDir = getGREDir();
@@ -902,16 +903,20 @@ function resetPrefs() {
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
}
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDMAXERRORS)) {
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS);
}
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DOORHANGER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_DOORHANGER);
+ }
+
try {
Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER);
} catch (e) {
}
}
function setupTimer(aTestTimeout) {
gTestTimeout = aTestTimeout;
--- a/toolkit/mozapps/update/tests/data/shared.js
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -19,16 +19,18 @@ const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTE
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout";
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
const PREF_APP_UPDATE_SILENT = "app.update.silent";
const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
const PREF_APP_UPDATE_URL = "app.update.url";
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
+const PREF_APP_UPDATE_DOORHANGER = "app.update.doorhanger";
+
const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
const PREFBRANCH_APP_PARTNER = "app.partner.";
const PREF_DISTRIBUTION_ID = "distribution.id";
const PREF_DISTRIBUTION_VERSION = "distribution.version";
const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";