This commit is contained in:
Ryan VanderMeulen 2015-03-25 13:43:32 -04:00
Родитель e88da8056c 0f0c55ff2e
Коммит fff7cc416b
141 изменённых файлов: 3753 добавлений и 2023 удалений

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

@ -202,7 +202,7 @@ var Connection = Class({
},
poolFor: function(id) {
for (let pool of this.pools.values()) {
if pool.has(id)
if (pool.has(id))
return pool;
}
},
@ -797,7 +797,7 @@ var Tab = Client.from({
"storageActor": "storage",
"gcliActor": "gcli",
"memoryActor": "memory",
"eventLoopLag": "eventLoopLag"
"eventLoopLag": "eventLoopLag",
"trace": "trace", // missing
}

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

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

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>

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

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

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

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="93f9ba577f68d772093987c2f1c0a4ae293e1802"/>

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

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

@ -17,7 +17,7 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>

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

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "aebfbd998041e960cea0468533c0b5041b504850",
"git_revision": "508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "26282fe3ad19972a8d84cdc7eee85f73b6cfcc4e",
"revision": "19761d0d782f0b33d2605bae9aa2e135e592d622",
"repo_path": "integration/gaia-central"
}

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

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="ed2cf97a6c37a4bbd0bbbbffe06ec7136d8c79ff"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="b685e3aab4fde7624d78993877a8f7910f2a5f06"/>

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="52775e03a2d8532429dff579cb2cd56718e488c3">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aebfbd998041e960cea0468533c0b5041b504850"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="508b8d48fb5ecf08bf0e5b4fef42bc48b770e7f2"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="86cd7486d8e50eaac8ef6fe2f51f09d25194577b"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

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

@ -37,6 +37,8 @@ pref("extensions.minCompatibleAppVersion", "4.0");
// extensions.checkCompatibility=false has been set.
pref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "29.0a1");
pref("xpinstall.customConfirmationUI", true);
// Preferences for AMO integration
pref("extensions.getAddons.cache.enabled", true);
pref("extensions.getAddons.maxResults", 15);
@ -1700,9 +1702,9 @@ pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");

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

@ -48,9 +48,15 @@ const gXPInstallObserver = {
timeout: Date.now() + 30000
};
try {
options.originHost = installInfo.originatingURI.host;
} catch (e) {
// originatingURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs.
}
switch (aTopic) {
case "addon-install-disabled":
notificationID = "xpinstall-disabled"
case "addon-install-disabled": {
notificationID = "xpinstall-disabled";
if (gPrefService.prefIsLocked("xpinstall.enabled")) {
messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked");
@ -70,18 +76,15 @@ const gXPInstallObserver = {
PopupNotifications.show(browser, notificationID, messageString, anchorID,
action, null, options);
break;
case "addon-install-blocked":
let originatingHost;
try {
originatingHost = installInfo.originatingURI.host;
} catch (ex) {
break; }
case "addon-install-blocked": {
if (!options.originHost) {
// Need to deal with missing originatingURI and with about:/data: URIs more gracefully,
// see bug 1063418 - but for now, bail:
return;
}
messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning",
[brandShortName, originatingHost]);
messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage",
[brandShortName]);
let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI");
action = {
@ -96,9 +99,9 @@ const gXPInstallObserver = {
secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
PopupNotifications.show(browser, notificationID, messageString, anchorID,
action, null, options);
break;
case "addon-install-started":
var needsDownload = function needsDownload(aInstall) {
break; }
case "addon-install-started": {
let needsDownload = function needsDownload(aInstall) {
return aInstall.state != AddonManager.STATE_DOWNLOADED;
}
// If all installs have already been downloaded then there is no need to
@ -106,25 +109,36 @@ const gXPInstallObserver = {
if (!installInfo.installs.some(needsDownload))
return;
notificationID = "addon-progress";
messageString = gNavigatorBundle.getString("addonDownloading");
messageString = gNavigatorBundle.getString("addonDownloadingAndVerifying");
messageString = PluralForm.get(installInfo.installs.length, messageString);
messageString = messageString.replace("#1", installInfo.installs.length);
options.installs = installInfo.installs;
options.contentWindow = browser.contentWindow;
options.sourceURI = browser.currentURI;
options.eventCallback = function(aEvent) {
if (aEvent != "removed")
return;
options.contentWindow = null;
options.sourceURI = null;
options.eventCallback = (aEvent) => {
switch (aEvent) {
case "removed":
options.contentWindow = null;
options.sourceURI = null;
break;
}
};
PopupNotifications.show(browser, notificationID, messageString, anchorID,
null, null, options);
break;
case "addon-install-failed":
let notification = PopupNotifications.show(browser, notificationID, messageString,
anchorID, null, null, options);
notification._startTime = Date.now();
let cancelButton = document.getElementById("addon-progress-cancel");
cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
let acceptButton = document.getElementById("addon-progress-accept");
acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
break; }
case "addon-install-failed": {
// TODO This isn't terribly ideal for the multiple failure case
for (let install of installInfo.installs) {
let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) &&
installInfo.originatingURI.host;
let host = options.originHost;
if (!host)
host = (install.sourceURI instanceof Ci.nsIStandardURL) &&
install.sourceURI.host;
@ -147,9 +161,100 @@ const gXPInstallObserver = {
PopupNotifications.show(browser, notificationID, messageString, anchorID,
action, null, options);
}
break;
case "addon-install-complete":
var needsRestart = installInfo.installs.some(function(i) {
this._removeProgressNotification(browser);
break; }
case "addon-install-confirmation": {
options.eventCallback = (aEvent) => {
switch (aEvent) {
case "removed":
if (installInfo) {
for (let install of installInfo.installs)
install.cancel();
}
this.acceptInstallation = null;
break;
case "shown":
let addonList = document.getElementById("addon-install-confirmation-content");
while (addonList.firstChild)
addonList.firstChild.remove();
for (let install of installInfo.installs) {
let container = document.createElement("hbox");
let name = document.createElement("label");
let author = document.createElement("label");
name.setAttribute("value", install.addon.name);
author.setAttribute("value", !install.addon.creator ? "" :
gNavigatorBundle.getFormattedString("addonConfirmInstall.author", [install.addon.creator]));
name.setAttribute("class", "addon-install-confirmation-name");
author.setAttribute("class", "addon-install-confirmation-author");
container.appendChild(name);
container.appendChild(author);
addonList.appendChild(container);
}
this.acceptInstallation = () => {
for (let install of installInfo.installs)
install.install();
installInfo = null;
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH);
};
break;
}
};
messageString = gNavigatorBundle.getString("addonConfirmInstall.message");
messageString = PluralForm.get(installInfo.installs.length, messageString);
messageString = messageString.replace("#1", brandShortName);
messageString = messageString.replace("#2", installInfo.installs.length);
let cancelButton = document.getElementById("addon-install-confirmation-cancel");
cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label");
cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey");
let acceptButton = document.getElementById("addon-install-confirmation-accept");
acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label");
acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey");
let showNotification = () => {
// The download may have been cancelled during the security delay
if (!PopupNotifications.getNotification("addon-progress", browser))
return;
let tab = gBrowser.getTabForBrowser(browser);
if (tab)
gBrowser.selectedTab = tab;
if (PopupNotifications.isPanelOpen) {
let rect = document.getElementById("addon-progress-notification").getBoundingClientRect();
let notification = document.getElementById("addon-install-confirmation-notification");
notification.style.minHeight = rect.height + "px";
}
PopupNotifications.show(browser, notificationID, messageString, anchorID,
action, null, options);
this._removeProgressNotification(browser);
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
};
let downloadDuration = 0;
let progressNotification = PopupNotifications.getNotification("addon-progress", browser);
if (progressNotification)
downloadDuration = Date.now() - progressNotification._startTime;
let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration;
if (securityDelay > 0)
setTimeout(showNotification, securityDelay);
else
showNotification();
break; }
case "addon-install-complete": {
let needsRestart = installInfo.installs.some(function(i) {
return i.addon.pendingOperations != AddonManager.PENDING_NONE;
});
@ -180,8 +285,13 @@ const gXPInstallObserver = {
PopupNotifications.show(browser, notificationID, messageString, anchorID,
action, null, options);
break;
break; }
}
},
_removeProgressNotification(aBrowser) {
let notification = PopupNotifications.getNotification("addon-progress", aBrowser);
if (notification)
notification.remove();
}
};

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

@ -1223,6 +1223,7 @@ var gBrowserInit = {
Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false);
Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false);
Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation", false);
Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
@ -1534,6 +1535,7 @@ var gBrowserInit = {
Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
Services.obs.removeObserver(gXPInstallObserver, "addon-install-confirmation");
Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
window.messageManager.removeMessageListener("Browser:URIFixup", gKeywordURIFixup);
window.messageManager.removeMessageListener("Browser:LoadURI", RedirectLoad);
@ -2103,6 +2105,12 @@ function getShortcutOrURIAndPostData(aURL, aCallback) {
let keyword = aURL;
let param = "";
// XXX Bug 1100294 will remove this little hack by using an async version of
// PlacesUtils.getURLAndPostDataForKeyword(). For now we simulate an async
// execution with at least a setTimeout(fn, 0).
let originalCallback = aCallback;
aCallback = data => setTimeout(() => originalCallback(data));
let offset = aURL.indexOf(" ");
if (offset > 0) {
keyword = aURL.substr(0, offset);
@ -4551,8 +4559,12 @@ var TabsProgressListener = {
aFlags) {
// Filter out location changes caused by anchor navigation
// or history.push/pop/replaceState.
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
// Reader mode actually cares about these:
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage("Reader:PushState");
return;
}
// Filter out location changes in sub documents.
if (!aWebProgress.isTopLevel)

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

@ -489,6 +489,7 @@ let AboutReaderListener = {
addEventListener("pageshow", this, false);
addEventListener("pagehide", this, false);
addMessageListener("Reader:ParseDocument", this);
addMessageListener("Reader:PushState", this);
},
receiveMessage: function(message) {
@ -497,6 +498,10 @@ let AboutReaderListener = {
this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
content.document.location = "about:reader?url=" + encodeURIComponent(message.data.url);
break;
case "Reader:PushState":
this.updateReaderButton();
break;
}
},
@ -519,6 +524,7 @@ let AboutReaderListener = {
// Update the toolbar icon to show the "reader active" icon.
sendAsyncMessage("Reader:UpdateReaderButton");
new AboutReader(global, content, this._articlePromise);
this._articlePromise = null;
}
break;
@ -529,19 +535,23 @@ let AboutReaderListener = {
case "pageshow":
// If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
// event, so we need to rely on "pageshow" in this case.
if (!aEvent.persisted) {
break;
if (aEvent.persisted) {
this.updateReaderButton();
}
// Fall through.
break;
case "DOMContentLoaded":
if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) {
return;
}
this.updateReaderButton();
break;
let isArticle = ReaderMode.isProbablyReaderable(content.document);
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: isArticle });
}
}
},
updateReaderButton: function() {
if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader) {
return;
}
let isArticle = ReaderMode.isProbablyReaderable(content.document);
sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: isArticle });
},
};
AboutReaderListener.init();

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

@ -10,7 +10,6 @@
<popupnotification id="webRTC-shareDevices-notification" hidden="true">
<popupnotificationcontent id="webRTC-selectCamera" orient="vertical">
<separator class="thin"/>
<label value="&getUserMedia.selectCamera.label;"
accesskey="&getUserMedia.selectCamera.accesskey;"
control="webRTC-selectCamera-menulist"/>
@ -20,7 +19,6 @@
</popupnotificationcontent>
<popupnotificationcontent id="webRTC-selectWindowOrScreen" orient="vertical">
<separator class="thin"/>
<label id="webRTC-selectWindow-label"
control="webRTC-selectWindow-menulist"/>
<menulist id="webRTC-selectWindow-menulist"
@ -31,7 +29,6 @@
</popupnotificationcontent>
<popupnotificationcontent id="webRTC-selectMicrophone" orient="vertical">
<separator class="thin"/>
<label value="&getUserMedia.selectMicrophone.label;"
accesskey="&getUserMedia.selectMicrophone.accesskey;"
control="webRTC-selectMicrophone-menulist"/>
@ -42,9 +39,7 @@
</popupnotification>
<popupnotification id="webapps-install-progress-notification" hidden="true">
<popupnotificationcontent id="webapps-install-progress-content" orient="vertical" align="start">
<separator class="thin"/>
</popupnotificationcontent>
<popupnotificationcontent id="webapps-install-progress-content" orient="vertical" align="start"/>
</popupnotification>
<popupnotification id="servicesInstall-notification" hidden="true">
@ -55,7 +50,6 @@
<popupnotification id="pointerLock-notification" hidden="true">
<popupnotificationcontent orient="vertical" align="start">
<separator class="thin"/>
<label id="pointerLock-cancel">&pointerLock.notification.message;</label>
</popupnotificationcontent>
</popupnotification>
@ -73,3 +67,18 @@
<popupnotificationcontent orient="vertical"/>
</popupnotification>
#endif
<popupnotification id="addon-progress-notification" hidden="true">
<button id="addon-progress-cancel"
oncommand="this.parentNode.cancel();"/>
<button id="addon-progress-accept" disabled="true"/>
</popupnotification>
<popupnotification id="addon-install-confirmation-notification" hidden="true">
<popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/>
<button id="addon-install-confirmation-cancel"
oncommand="PopupNotifications.getNotification('addon-install-confirmation').remove();"/>
<button id="addon-install-confirmation-accept"
oncommand="gXPInstallObserver.acceptInstallation();
PopupNotifications.getNotification('addon-install-confirmation').remove();"/>
</popupnotification>

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

@ -452,7 +452,6 @@ skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test un
skip-if = e10s # Bug 1093941 - ESC reverted the location bar value - Got foobar, expected example.com
[browser_urlbarSearchSingleWordNotification.js]
[browser_urlbarStop.js]
skip-if = e10s # Bug 1093941 - test calls gBrowser.contentWindow.stop
[browser_urlbarTrimURLs.js]
[browser_urlbar_search_healthreport.js]
[browser_utilityOverlay.js]

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

@ -3,13 +3,16 @@
"use strict";
add_task(function(){
add_task(function* () {
// Test that changing the URL in a pinned tab works correctly
let TEST_LINK_INITIAL = "about:";
let TEST_LINK_CHANGED = "about:support";
let appTab = gBrowser.addTab(TEST_LINK_INITIAL);
let browser = appTab.linkedBrowser;
yield BrowserTestUtils.browserLoaded(browser);
gBrowser.pinTab(appTab);
is(appTab.pinned, true, "Tab was successfully pinned");
@ -20,9 +23,8 @@ add_task(function(){
gURLBar.focus();
gURLBar.value = TEST_LINK_CHANGED;
let promisePageload = promiseTabLoadEvent(appTab);
goButton.click();
yield promisePageload;
yield BrowserTestUtils.browserLoaded(browser);
is(appTab.linkedBrowser.currentURI.spec, TEST_LINK_CHANGED,
"New page loaded in the app tab");

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

@ -5,9 +5,8 @@
const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
const TESTROOT2 = "http://example.org/browser/toolkit/mozapps/extensions/test/xpinstall/";
const SECUREROOT = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
const PROGRESS_NOTIFICATION = "addon-progress-notification";
const PROGRESS_NOTIFICATION = "addon-progress";
var rootDir = getRootDirectory(gTestPath);
var path = rootDir.split('/');
@ -22,7 +21,15 @@ const CHROMEROOT = croot;
var gApp = document.getElementById("bundle_brand").getString("brandShortName");
var gVersion = Services.appinfo.version;
var check_notification;
function get_observer_topic(aNotificationId) {
let topic = aNotificationId;
if (topic == "xpinstall-disabled")
topic = "addon-install-disabled";
else if (topic == "addon-progress")
topic = "addon-install-started";
return topic;
}
function wait_for_progress_notification(aCallback) {
wait_for_notification(PROGRESS_NOTIFICATION, aCallback, "popupshowing");
@ -30,19 +37,45 @@ function wait_for_progress_notification(aCallback) {
function wait_for_notification(aId, aCallback, aEvent = "popupshown") {
info("Waiting for " + aId + " notification");
check_notification = function() {
let topic = get_observer_topic(aId);
function observer(aSubject, aTopic, aData) {
// Ignore the progress notification unless that is the notification we want
if (aId != PROGRESS_NOTIFICATION && PopupNotifications.panel.childNodes[0].id == PROGRESS_NOTIFICATION)
if (aId != PROGRESS_NOTIFICATION &&
aTopic == get_observer_topic(PROGRESS_NOTIFICATION))
return;
PopupNotifications.panel.removeEventListener(aEvent, check_notification, false);
Services.obs.removeObserver(observer, topic);
if (PopupNotifications.isPanelOpen)
executeSoon(verify);
else
PopupNotifications.panel.addEventListener(aEvent, event_listener);
}
function event_listener() {
// Ignore the progress notification unless that is the notification we want
if (aId != PROGRESS_NOTIFICATION &&
PopupNotifications.panel.childNodes[0].id == PROGRESS_NOTIFICATION + "-notification")
return;
PopupNotifications.panel.removeEventListener(aEvent, event_listener);
verify();
}
function verify() {
info("Saw a notification");
ok(PopupNotifications.isPanelOpen, "Panel should be open");
is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
if (PopupNotifications.panel.childNodes.length)
is(PopupNotifications.panel.childNodes[0].id, aId, "Should have seen the right notification");
if (PopupNotifications.panel.childNodes.length) {
is(PopupNotifications.panel.childNodes[0].id,
aId + "-notification", "Should have seen the right notification");
}
aCallback(PopupNotifications.panel);
};
PopupNotifications.panel.addEventListener(aEvent, check_notification, false);
}
Services.obs.addObserver(observer, topic, false);
}
function wait_for_notification_close(aCallback) {
@ -53,35 +86,6 @@ function wait_for_notification_close(aCallback) {
}, false);
}
function wait_for_install_dialog(aCallback) {
info("Waiting for install dialog");
Services.wm.addListener({
onOpenWindow: function(aXULWindow) {
info("Install dialog opened, waiting for focus");
Services.wm.removeListener(this);
var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
waitForFocus(function() {
info("Saw install dialog");
is(domwindow.document.location.href, XPINSTALL_URL, "Should have seen the right window open");
// Override the countdown timer on the accept button
var button = domwindow.document.documentElement.getButton("accept");
button.disabled = false;
aCallback(domwindow);
}, domwindow);
},
onCloseWindow: function(aXULWindow) {
},
onWindowTitleChange: function(aXULWindow, aNewTitle) {
}
});
}
function wait_for_single_notification(aCallback) {
function inner_waiter() {
info("Waiting for single notification");
@ -114,7 +118,7 @@ function test_disabled_install() {
Services.prefs.setBoolPref("xpinstall.enabled", false);
// Wait for the disabled notification
wait_for_notification("xpinstall-disabled-notification", function(aPanel) {
wait_for_notification("xpinstall-disabled", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Enable", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -151,18 +155,19 @@ function test_disabled_install() {
function test_blocked_install() {
// Wait for the blocked notification
wait_for_notification("addon-install-blocked-notification", function(aPanel) {
wait_for_notification("addon-install-blocked", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Allow", "Should have seen the right button");
is(notification.getAttribute("originhost"), "example.com",
"Should have seen the right origin host");
is(notification.getAttribute("label"),
gApp + " prevented this site (example.com) from asking you to install " +
"software on your computer.",
gApp + " prevented this site from asking you to install software on your computer.",
"Should have seen the right message");
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -178,7 +183,7 @@ function test_blocked_install() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
// Click on Allow
@ -188,7 +193,6 @@ function test_blocked_install() {
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
notification = aPanel.childNodes[0];
is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
});
var triggers = encodeURIComponent(JSON.stringify({
@ -201,10 +205,15 @@ function test_blocked_install() {
function test_whitelisted_install() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
gBrowser.selectedTab = originalTab;
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
is(gBrowser.selectedTab, tab,
"tab selected in response to the addon-install-confirmation notification");
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -221,7 +230,7 @@ function test_whitelisted_install() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -231,7 +240,9 @@ function test_whitelisted_install() {
var triggers = encodeURIComponent(JSON.stringify({
"XPI": "unsigned.xpi"
}));
gBrowser.selectedTab = gBrowser.addTab();
let originalTab = gBrowser.selectedTab;
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
},
@ -239,7 +250,7 @@ function test_failed_download() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the failed notification
wait_for_notification("addon-install-failed-notification", function(aPanel) {
wait_for_notification("addon-install-failed", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.getAttribute("label"),
"The add-on could not be downloaded because of a connection failure " +
@ -266,7 +277,7 @@ function test_corrupt_file() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the failed notification
wait_for_notification("addon-install-failed-notification", function(aPanel) {
wait_for_notification("addon-install-failed", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.getAttribute("label"),
"The add-on downloaded from example.com could not be installed " +
@ -293,7 +304,7 @@ function test_incompatible() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the failed notification
wait_for_notification("addon-install-failed-notification", function(aPanel) {
wait_for_notification("addon-install-failed", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.getAttribute("label"),
"XPI Test could not be installed because it is not compatible with " +
@ -320,9 +331,9 @@ function test_restartless() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.getAttribute("label"),
"XPI Test has been installed successfully.",
@ -341,7 +352,7 @@ function test_restartless() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -359,9 +370,9 @@ function test_multiple() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -382,7 +393,7 @@ function test_multiple() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -401,9 +412,9 @@ function test_url() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -419,7 +430,7 @@ function test_url() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -467,7 +478,7 @@ function test_wronghost() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-failed-notification", function(aPanel) {
wait_for_notification("addon-install-failed", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.getAttribute("label"),
"The add-on downloaded from example.com could not be installed " +
@ -488,9 +499,9 @@ function test_reload() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -523,7 +534,7 @@ function test_reload() {
gBrowser.loadURI(TESTROOT2 + "enabled.html");
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -541,9 +552,9 @@ function test_theme() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
@ -566,7 +577,7 @@ function test_theme() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -582,13 +593,13 @@ function test_theme() {
function test_renotify_blocked() {
// Wait for the blocked notification
wait_for_notification("addon-install-blocked-notification", function(aPanel) {
wait_for_notification("addon-install-blocked", function(aPanel) {
let notification = aPanel.childNodes[0];
wait_for_notification_close(function () {
info("Timeouts after this probably mean bug 589954 regressed");
executeSoon(function () {
wait_for_notification("addon-install-blocked-notification", function(aPanel) {
wait_for_notification("addon-install-blocked", function(aPanel) {
AddonManager.getAllInstalls(function(aInstalls) {
is(aInstalls.length, 2, "Should be two pending installs");
aInstalls[0].cancel();
@ -619,9 +630,9 @@ function test_renotify_installed() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
// Dismiss the notification
wait_for_notification_close(function () {
// Install another
@ -629,11 +640,11 @@ function test_renotify_installed() {
// Wait for the progress notification
wait_for_progress_notification(function(aPanel) {
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
wait_for_notification("addon-install-confirmation", function(aPanel) {
info("Timeouts after this probably mean bug 589954 regressed");
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
wait_for_notification("addon-install-complete", function(aPanel) {
AddonManager.getAllInstalls(function(aInstalls) {
is(aInstalls.length, 1, "Should be one pending installs");
aInstalls[0].cancel();
@ -644,7 +655,7 @@ function test_renotify_installed() {
});
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -656,7 +667,7 @@ function test_renotify_installed() {
aPanel.hidePopup();
});
aWindow.document.documentElement.acceptDialog();
document.getElementById("addon-install-confirmation-accept").click();
});
});
@ -670,7 +681,7 @@ function test_renotify_installed() {
gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
},
function test_cancel_restart() {
function test_cancel() {
function complete_install(callback) {
let url = TESTROOT + "slowinstall.sjs?continue=true"
NetUtil.asyncFetch(url, callback || (() => {}));
@ -687,10 +698,9 @@ function test_cancel_restart() {
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
isnot(notification, aPanel.childNodes[0], "Should have reconstructed the notification UI");
notification = aPanel.childNodes[0];
is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
let button = document.getAnonymousElementByAttribute(notification, "anonid", "cancel");
let button = document.getElementById("addon-progress-cancel");
// Wait for the install to fully cancel
let install = notification.notification.options.installs[0];
@ -699,45 +709,15 @@ function test_cancel_restart() {
install.removeListener(this);
executeSoon(function() {
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
isnot(notification, aPanel.childNodes[0], "Should have reconstructed the notification UI");
notification = aPanel.childNodes[0];
is(notification.id, "addon-install-cancelled-notification", "Should have seen the cancelled notification");
ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
// Wait for the install confirmation dialog
wait_for_install_dialog(function(aWindow) {
// Wait for the complete notification
wait_for_notification("addon-install-complete-notification", function(aPanel) {
let notification = aPanel.childNodes[0];
is(notification.button.label, "Restart Now", "Should have seen the right button");
is(notification.getAttribute("label"),
"XPI Test will be installed after you restart " + gApp + ".",
"Should have seen the right message");
AddonManager.getAllInstalls(function(aInstalls) {
is(aInstalls.length, 0, "Should be no pending install");
AddonManager.getAllInstalls(function(aInstalls) {
is(aInstalls.length, 1, "Should be one pending install");
aInstalls[0].cancel();
Services.perms.remove("example.com", "install");
wait_for_notification_close(runNextTest);
gBrowser.removeTab(gBrowser.selectedTab);
});
});
aWindow.document.documentElement.acceptDialog();
Services.perms.remove("example.com", "install");
gBrowser.removeTab(gBrowser.selectedTab);
runNextTest();
});
// Restart the download
EventUtils.synthesizeMouseAtCenter(notification.button, {});
// Should be back to a progress notification
ok(PopupNotifications.isPanelOpen, "Notification should still be open");
is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification");
notification = aPanel.childNodes[0];
is(notification.id, "addon-progress-notification", "Should have seen the progress notification");
complete_install();
});
}
});
@ -764,7 +744,7 @@ function test_failed_security() {
});
// Wait for the blocked notification
wait_for_notification("addon-install-blocked-notification", function(aPanel) {
wait_for_notification("addon-install-blocked", function(aPanel) {
let notification = aPanel.childNodes[0];
// Click on Allow
@ -841,6 +821,7 @@ function test() {
Services.prefs.setBoolPref("extensions.logging.enabled", true);
Services.prefs.setBoolPref("extensions.strictCompatibility", true);
Services.prefs.setBoolPref("extensions.install.requireSecureOrigin", false);
Services.prefs.setIntPref("security.dialog_enable_delay", 0);
Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
@ -850,7 +831,6 @@ function test() {
registerCleanupFunction(function() {
// Make sure no more test parts run in case we were timed out
TESTS = [];
PopupNotifications.panel.removeEventListener("popupshown", check_notification, false);
AddonManager.getAllInstalls(function(aInstalls) {
aInstalls.forEach(function(aInstall) {
@ -861,6 +841,7 @@ function test() {
Services.prefs.clearUserPref("extensions.logging.enabled");
Services.prefs.clearUserPref("extensions.strictCompatibility");
Services.prefs.clearUserPref("extensions.install.requireSecureOrigin");
Services.prefs.clearUserPref("security.dialog_enable_delay");
Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");

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

@ -4,209 +4,211 @@
const TEST_VALUE = "example.com";
const START_VALUE = "example.org";
let gFocusManager = Services.focus;
function test() {
waitForExplicitFinish();
registerCleanupFunction(function () {
Services.prefs.clearUserPref("browser.altClickSave");
});
add_task(function* setup() {
Services.prefs.setBoolPref("browser.altClickSave", true);
runAltLeftClickTest();
}
registerCleanupFunction(() => {
Services.prefs.clearUserPref("browser.altClickSave");
});
});
// Monkey patch saveURL to avoid dealing with file save code paths
var oldSaveURL = saveURL;
saveURL = function() {
add_task(function* alt_left_click_test() {
info("Running test: Alt left click");
// Monkey patch saveURL() to avoid dealing with file save code paths.
let oldSaveURL = saveURL;
let saveURLPromise = new Promise(resolve => {
saveURL = () => {
// Restore old saveURL() value.
saveURL = oldSaveURL;
resolve();
};
});
triggerCommand(true, {altKey: true});
yield saveURLPromise;
ok(true, "SaveURL was called");
is(gURLBar.value, "", "Urlbar reverted to original value");
saveURL = oldSaveURL;
runShiftLeftClickTest();
}
function runAltLeftClickTest() {
info("Running test: Alt left click");
triggerCommand(true, { altKey: true });
}
function runShiftLeftClickTest() {
let listener = new BrowserWindowListener(getBrowserURL(), function(aWindow) {
Services.wm.removeListener(listener);
addPageShowListener(aWindow.gBrowser.selectedBrowser, function() {
executeSoon(function () {
info("URL should be loaded in a new window");
is(gURLBar.value, "", "Urlbar reverted to original value");
is(gFocusManager.focusedElement, null, "There should be no focused element");
is(gFocusManager.focusedWindow, aWindow.gBrowser.contentWindow, "Content window should be focused");
is(aWindow.gURLBar.textValue, TEST_VALUE, "New URL is loaded in new window");
aWindow.close();
// Continue testing when the original window has focus again.
whenWindowActivated(window, runNextTest);
});
}, "http://example.com/");
});
Services.wm.addListener(listener);
});
add_task(function* shift_left_click_test() {
info("Running test: Shift left click");
triggerCommand(true, { shiftKey: true });
}
function runNextTest() {
let test = gTests.shift();
if (!test) {
finish();
return;
let newWindowPromise = promiseWaitForNewWindow();
triggerCommand(true, {shiftKey: true});
let win = yield newWindowPromise;
// Wait for the initial browser to load.
let browser = win.gBrowser.selectedBrowser;
yield BrowserTestUtils.browserLoaded(browser);
info("URL should be loaded in a new window");
is(gURLBar.value, "", "Urlbar reverted to original value");
is(Services.focus.focusedElement, null, "There should be no focused element");
is(Services.focus.focusedWindow, win.gBrowser.contentWindow, "Content window should be focused");
is(win.gURLBar.textValue, TEST_VALUE, "New URL is loaded in new window");
// Cleanup.
yield promiseWindowClosed(win);
});
add_task(function* right_click_test() {
info("Running test: Right click on go button");
// Add a new tab.
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
triggerCommand(true, {button: 2});
// Right click should do nothing (context menu will be shown).
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
// Cleanup.
gBrowser.removeCurrentTab();
});
add_task(function* shift_accel_left_click_test() {
info("Running test: Shift+Ctrl/Cmd left click on go button");
// Add a new tab.
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
let loadStartedPromise = promiseLoadStarted();
triggerCommand(true, {accelKey: true, shiftKey: true});
yield loadStartedPromise;
// Check the load occurred in a new background tab.
info("URL should be loaded in a new background tab");
is(gURLBar.value, "", "Urlbar reverted to original value");
ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
is(gBrowser.selectedTab, tab, "Focus did not change to the new tab");
// Select the new background tab
gBrowser.selectedTab = gBrowser.selectedTab.nextSibling;
is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
// Cleanup.
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
});
add_task(function* load_in_current_tab_test() {
let tests = [
{desc: "Simple return keypress"},
{desc: "Left click on go button", click: true},
{desc: "Ctrl/Cmd+Return keypress", event: {accelKey: true}},
{desc: "Alt+Return keypress in a blank tab", event: {altKey: true}}
];
for (let test of tests) {
info(`Running test: ${test.desc}`);
// Add a new tab.
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
// Trigger a load and check it occurs in the current tab.
let loadStartedPromise = promiseLoadStarted();
triggerCommand(test.click || false, test.event || {});
yield loadStartedPromise;
info("URL should be loaded in the current tab");
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
is(Services.focus.focusedElement, null, "There should be no focused element");
is(Services.focus.focusedWindow, gBrowser.contentWindow, "Content window should be focused");
is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
// Cleanup.
gBrowser.removeCurrentTab();
}
});
info("Running test: " + test.desc);
// Tab will be blank if test.startValue is null
let tab = gBrowser.selectedTab = gBrowser.addTab(test.startValue);
addPageShowListener(gBrowser.selectedBrowser, function() {
triggerCommand(test.click, test.event);
test.check(tab);
add_task(function* load_in_new_tab_test() {
let tests = [
{desc: "Ctrl/Cmd left click on go button", click: true, event: {accelKey: true}},
{desc: "Alt+Return keypress in a dirty tab", event: {altKey: true}, url: START_VALUE}
];
// Clean up
while (gBrowser.tabs.length > 1)
gBrowser.removeTab(gBrowser.selectedTab)
runNextTest();
});
}
for (let test of tests) {
info(`Running test: ${test.desc}`);
let gTests = [
{ desc: "Right click on go button",
click: true,
event: { button: 2 },
check: function(aTab) {
// Right click should do nothing (context menu will be shown)
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
}
},
// Add a new tab.
let tab = gBrowser.selectedTab = gBrowser.addTab(test.url || "about:blank");
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
{ desc: "Left click on go button",
click: true,
event: {},
check: checkCurrent
},
// Trigger a load and check it occurs in the current tab.
let tabSelectedPromise = promiseNewTabSelected();
triggerCommand(test.click || false, test.event || {});
yield tabSelectedPromise;
{ desc: "Ctrl/Cmd left click on go button",
click: true,
event: { accelKey: true },
check: checkNewTab
},
// Check the load occurred in a new tab.
info("URL should be loaded in a new focused tab");
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
is(Services.focus.focusedElement, null, "There should be no focused element");
is(Services.focus.focusedWindow, gBrowser.contentWindow, "Content window should be focused");
isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
{ desc: "Shift+Ctrl/Cmd left click on go button",
click: true,
event: { accelKey: true, shiftKey: true },
check: function(aTab) {
info("URL should be loaded in a new background tab");
is(gURLBar.value, "", "Urlbar reverted to original value");
ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
is(gBrowser.selectedTab, aTab, "Focus did not change to the new tab");
// Select the new background tab
gBrowser.selectedTab = gBrowser.selectedTab.nextSibling;
is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
}
},
{ desc: "Simple return keypress",
event: {},
check: checkCurrent
},
{ desc: "Alt+Return keypress in a blank tab",
event: { altKey: true },
check: checkCurrent
},
{ desc: "Alt+Return keypress in a dirty tab",
event: { altKey: true },
check: checkNewTab,
startValue: START_VALUE
},
{ desc: "Ctrl/Cmd+Return keypress",
event: { accelKey: true },
check: checkCurrent
// Cleanup.
gBrowser.removeCurrentTab();
gBrowser.removeCurrentTab();
}
]
});
let gGoButton = document.getElementById("urlbar-go-button");
function triggerCommand(aClick, aEvent) {
function triggerCommand(shouldClick, event) {
gURLBar.value = TEST_VALUE;
gURLBar.focus();
if (aClick) {
if (shouldClick) {
is(gURLBar.getAttribute("pageproxystate"), "invalid",
"page proxy state must be invalid for go button to be visible");
EventUtils.synthesizeMouseAtCenter(gGoButton, aEvent);
let goButton = document.getElementById("urlbar-go-button");
EventUtils.synthesizeMouseAtCenter(goButton, event);
} else {
EventUtils.synthesizeKey("VK_RETURN", event);
}
else
EventUtils.synthesizeKey("VK_RETURN", aEvent);
}
/* Checks that the URL was loaded in the current tab */
function checkCurrent(aTab) {
info("URL should be loaded in the current tab");
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
is(gFocusManager.focusedElement, null, "There should be no focused element");
is(gFocusManager.focusedWindow, gBrowser.contentWindow, "Content window should be focused");
is(gBrowser.selectedTab, aTab, "New URL was loaded in the current tab");
}
/* Checks that the URL was loaded in a new focused tab */
function checkNewTab(aTab) {
info("URL should be loaded in a new focused tab");
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
is(gFocusManager.focusedElement, null, "There should be no focused element");
is(gFocusManager.focusedWindow, gBrowser.contentWindow, "Content window should be focused");
isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab");
}
function addPageShowListener(browser, cb, expectedURL) {
browser.addEventListener("pageshow", function pageShowListener() {
info("pageshow: " + browser.currentURI.spec);
if (expectedURL && browser.currentURI.spec != expectedURL)
return; // ignore pageshows for non-expected URLs
browser.removeEventListener("pageshow", pageShowListener, false);
cb();
function promiseLoadStarted() {
return new Promise(resolve => {
gBrowser.addTabsProgressListener({
onStateChange(browser, webProgress, req, flags, status) {
if (flags & Ci.nsIWebProgressListener.STATE_START) {
gBrowser.removeTabsProgressListener(this);
resolve();
}
}
});
});
}
function whenWindowActivated(win, cb) {
if (Services.focus.activeWindow == win) {
executeSoon(cb);
return;
}
win.addEventListener("activate", function onActivate() {
win.removeEventListener("activate", onActivate);
executeSoon(cb);
function promiseNewTabSelected() {
return new Promise(resolve => {
gBrowser.tabContainer.addEventListener("TabSelect", function onSelect() {
gBrowser.tabContainer.removeEventListener("TabSelect", onSelect);
resolve();
});
});
}
function BrowserWindowListener(aURL, aCallback) {
this.callback = aCallback;
this.url = aURL;
}
BrowserWindowListener.prototype = {
onOpenWindow: function(aXULWindow) {
let cb = () => this.callback(domwindow);
let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
function promiseWaitForNewWindow() {
return new Promise(resolve => {
let listener = {
onOpenWindow(xulWindow) {
let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let numWait = 2;
function maybeRunCallback() {
if (--numWait == 0)
cb();
}
Services.wm.removeListener(listener);
whenDelayedStartupFinished(win, () => resolve(win));
},
whenWindowActivated(domwindow, maybeRunCallback);
whenDelayedStartupFinished(domwindow, maybeRunCallback);
},
onCloseWindow: function(aXULWindow) {},
onWindowTitleChange: function(aXULWindow, aNewTitle) {}
onCloseWindow() {},
onWindowTitleChange() {}
};
Services.wm.addListener(listener);
});
}

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

@ -1,69 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_VALUE = "example.com/\xF7?\xF7";
const START_VALUE = "example.com/%C3%B7?%C3%B7";
function test() {
waitForExplicitFinish();
runNextTest();
}
function locationBarEnter(aEvent, aClosure) {
executeSoon(function() {
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", aEvent);
addPageShowListener(aClosure);
});
}
function runNextTest() {
let test = gTests.shift();
if (!test) {
finish();
return;
}
info("Running test: " + test.desc);
add_task(function* () {
info("Simple return keypress");
let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
addPageShowListener(function() {
locationBarEnter(test.event, function() {
test.check(tab);
// Clean up
while (gBrowser.tabs.length > 1)
gBrowser.removeTab(gBrowser.selectedTab)
runNextTest();
});
});
}
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
let gTests = [
{ desc: "Simple return keypress",
event: {},
check: checkCurrent
},
{ desc: "Alt+Return keypress",
event: { altKey: true },
check: checkNewTab,
},
]
function checkCurrent(aTab) {
// Check url bar and selected tab.
is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
is(gBrowser.selectedTab, aTab, "New URL was loaded in the current tab");
}
is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
function checkNewTab(aTab) {
// Cleanup.
gBrowser.removeCurrentTab();
});
add_task(function* () {
info("Alt+Return keypress");
let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
// Check url bar and selected tab.
is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab");
}
function addPageShowListener(aFunc) {
gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() {
gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false);
aFunc();
});
}
isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
// Cleanup.
gBrowser.removeTab(tab);
gBrowser.removeCurrentTab();
});

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

@ -60,13 +60,15 @@ function* runURLBarSearchTest(valueToOpen, expectSearch, expectNotification, aWi
}
add_task(function* test_navigate_full_domain() {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
yield* runURLBarSearchTest("www.mozilla.org", false, false);
gBrowser.removeTab(tab);
});
add_task(function* test_navigate_numbers() {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
yield* runURLBarSearchTest("1234", true, false);
gBrowser.removeTab(tab);
});
@ -84,7 +86,8 @@ function get_test_function_for_localhost_with_hostname(hostName, isPrivate) {
win = window;
}
let browser = win.gBrowser;
let tab = browser.selectedTab = browser.addTab();
let tab = browser.selectedTab = browser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
Services.prefs.setBoolPref(pref, false);
yield* runURLBarSearchTest(hostName, true, true, win);
@ -102,7 +105,8 @@ function get_test_function_for_localhost_with_hostname(hostName, isPrivate) {
browser.removeTab(tab);
// Now try again with the pref set.
tab = browser.selectedTab = browser.addTab();
tab = browser.selectedTab = browser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
// In a private window, the notification should appear again.
yield* runURLBarSearchTest(hostName, isPrivate, isPrivate, win);
browser.removeTab(tab);
@ -122,7 +126,8 @@ add_task(get_test_function_for_localhost_with_hostname("localhost."));
add_task(get_test_function_for_localhost_with_hostname("localhost", true));
add_task(function* test_navigate_invalid_url() {
let tab = gBrowser.selectedTab = gBrowser.addTab();
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
yield* runURLBarSearchTest("mozilla is awesome", true, false);
gBrowser.removeTab(tab);
});

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

@ -1,40 +1,69 @@
"use strict";
const goodURL = "http://mochi.test:8888/";
const badURL = "http://mochi.test:8888/whatever.html";
function test() {
waitForExplicitFinish();
add_task(function* () {
gBrowser.selectedTab = gBrowser.addTab(goodURL);
gBrowser.selectedBrowser.addEventListener("load", onload, true);
}
function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page");
typeAndSubmit(badURL);
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects loading page");
gBrowser.contentWindow.stop();
yield typeAndSubmitAndStop(badURL);
is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page after stop()");
gBrowser.removeCurrentTab();
gBrowser.selectedTab = gBrowser.addTab("about:blank");
is(gURLBar.textValue, "", "location bar is empty");
typeAndSubmit(badURL);
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects loading page");
gBrowser.contentWindow.stop();
yield typeAndSubmitAndStop(badURL);
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
gBrowser.removeCurrentTab();
});
finish();
}
function typeAndSubmit(value) {
gBrowser.userTypedValue = value;
function typeAndSubmitAndStop(url) {
gBrowser.userTypedValue = url;
URLBarSetURI();
is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
let promise = waitForDocLoadAndStopIt();
gURLBar.handleCommand();
return promise;
}
function waitForDocLoadAndStopIt() {
function content_script() {
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let progressListener = {
onStateChange(webProgress, req, flags, status) {
if (flags & Ci.nsIWebProgressListener.STATE_START) {
wp.removeProgressListener(progressListener);
/* Hammer time. */
content.stop();
/* Let the parent know we're done. */
sendAsyncMessage("{MSG}");
}
},
QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
};
let wp = docShell.QueryInterface(Ci.nsIWebProgress);
wp.addProgressListener(progressListener, wp.NOTIFY_ALL);
}
return new Promise(resolve => {
const MSG = "test:waitForDocLoadAndStopIt";
const SCRIPT = content_script.toString().replace("{MSG}", MSG);
let mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript("data:,(" + SCRIPT + ")();", true);
mm.addMessageListener(MSG, function onComplete() {
mm.removeMessageListener(MSG, onComplete);
resolve();
});
});
}

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

@ -39,7 +39,8 @@ add_task(function* test_healthreport_search_recording() {
}
}
let tab = gBrowser.addTab();
let tab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
gBrowser.selectedTab = tab;
let searchStr = "firefox health report";

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

@ -358,18 +358,10 @@
gBrowser.selectedBrowser.focus();
let isMouseEvent = aTriggeringEvent instanceof MouseEvent;
let altEnter = !isMouseEvent && aTriggeringEvent && aTriggeringEvent.altKey;
if (altEnter) {
// XXX This was added a long time ago, and I'm not sure why it is
// necessary. Alt+Enter's default action might cause a system beep,
// or something like that?
aTriggeringEvent.preventDefault();
aTriggeringEvent.stopPropagation();
}
// If the current tab is empty, ignore Alt+Enter (just reuse this tab)
altEnter = altEnter && !isTabEmpty(gBrowser.selectedTab);
let altEnter = !isMouseEvent && aTriggeringEvent &&
aTriggeringEvent.altKey && !isTabEmpty(gBrowser.selectedTab);
if (isMouseEvent || altEnter) {
// Use the standard UI link behaviors for clicks or Alt+Enter
@ -1573,16 +1565,17 @@
<xul:image class="popup-notification-icon"
xbl:inherits="popupid,src=icon"/>
<xul:vbox flex="1">
<xul:description class="popup-notification-description addon-progress-description"
xbl:inherits="xbl:text=label"/>
<xul:label class="popup-notification-originHost header"
xbl:inherits="value=originhost"
crop="end"/>
<xul:description class="popup-notification-description"
xbl:inherits="xbl:text=label,popupid"/>
<xul:progressmeter anonid="progressmeter" flex="1" mode="undetermined" class="popup-progress-meter"/>
<xul:label anonid="progresstext" class="popup-progress-label" flex="1" crop="end"/>
<xul:spacer flex="1"/>
<xul:hbox align="center">
<xul:progressmeter anonid="progressmeter" flex="1" mode="undetermined" class="popup-progress-meter"/>
<xul:button anonid="cancel" class="popup-progress-cancel" oncommand="document.getBindingParent(this).cancel()"/>
</xul:hbox>
<xul:label anonid="progresstext" class="popup-progress-label"/>
<xul:hbox class="popup-notification-button-container"
pack="end" align="center">
<children includes="button"/>
<xul:button anonid="button"
class="popup-notification-menubutton"
type="menu-button"
@ -1606,7 +1599,8 @@
</content>
<implementation>
<constructor><![CDATA[
this.cancelbtn.setAttribute("tooltiptext", gNavigatorBundle.getString("addonDownloadCancelTooltip"));
if (!this.notification)
return;
this.notification.options.installs.forEach(function(aInstall) {
aInstall.addListener(this);
@ -1631,9 +1625,6 @@
<field name="progresstext" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "progresstext");
</field>
<field name="cancelbtn" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "cancel");
</field>
<field name="DownloadUtils" readonly="true">
let utils = {};
Components.utils.import("resource://gre/modules/DownloadUtils.jsm", utils);
@ -1642,6 +1633,9 @@
<method name="destroy">
<body><![CDATA[
if (!this.notification)
return;
this.notification.options.installs.forEach(function(aInstall) {
aInstall.removeListener(this);
}, this);
@ -1686,17 +1680,12 @@
let status = null;
[status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last);
this.progresstext.value = status;
this.progresstext.value = this.progresstext.tooltipText = status;
]]></body>
</method>
<method name="cancel">
<body><![CDATA[
// Cache these as cancelling the installs will remove this
// notification which will drop these references
let browser = this.notification.browser;
let sourceURI = this.notification.options.sourceURI;
let installs = this.notification.options.installs;
installs.forEach(function(aInstall) {
try {
@ -1707,35 +1696,15 @@
}
}, this);
let anchorID = "addons-notification-icon";
let notificationID = "addon-install-cancelled";
let messageString = gNavigatorBundle.getString("addonDownloadCancelled");
messageString = PluralForm.get(installs.length, messageString);
let buttonText = gNavigatorBundle.getString("addonDownloadRestart");
buttonText = PluralForm.get(installs.length, buttonText);
let action = {
label: buttonText,
accessKey: gNavigatorBundle.getString("addonDownloadRestart.accessKey"),
callback: function() {
let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"].
getService(Ci.amIWebInstallListener);
if (weblistener.onWebInstallRequested(browser, sourceURI,
installs, installs.length)) {
installs.forEach(function(aInstall) {
aInstall.install();
});
}
}
};
PopupNotifications.show(browser, notificationID, messageString,
anchorID, action);
PopupNotifications.remove(this.notification);
]]></body>
</method>
<method name="updateProgress">
<body><![CDATA[
if (!this.notification)
return;
let downloadingCount = 0;
let progress = 0;
let maxProgress = 0;
@ -1752,7 +1721,13 @@
if (downloadingCount == 0) {
this.destroy();
PopupNotifications.remove(this.notification);
if (Preferences.get("xpinstall.customConfirmationUI", false)) {
this.progressmeter.mode = "undetermined";
this.progresstext.value = this.progresstext.tooltipText =
gNavigatorBundle.getString("addonDownloadVerifying");
} else {
PopupNotifications.remove(this.notification);
}
}
else {
this.setProgress(progress, maxProgress);

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

@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
"resource://gre/modules/PageMetadata.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
@ -844,6 +846,24 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Gets the metadata related to the currently selected tab in
* the most recent window.
*
* @param {Function} A callback that is passed the metadata.
*/
getSelectedTabMetadata: {
value: function(callback) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) {
win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
let pageData = msg.json;
callback(cloneValueInto(pageData, targetWindow));
});
win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
}
},
/**
* Associates a session-id and a call-id with a window for debugging.
*

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

@ -170,25 +170,71 @@ body {
/* Rooms */
.rooms {
min-height: 100px;
padding: 0 1rem;
}
.rooms > h1 {
font-weight: bold;
color: #999;
padding: .5rem 1rem;
}
.rooms > p {
padding: .5rem 0;
margin: 0;
}
.rooms > p > .btn {
.rooms > div > .context {
margin: .5rem 0 0;
background-color: #DEEFF7;
border-radius: 3px 3px 0 0;
padding: .5rem;
}
.rooms > div > .context > .context-enabled {
margin-bottom: .5rem;
display: block;
}
.rooms > div > .context > .context-enabled > input {
-moz-margin-start: 0;
}
.rooms > div > .context > .context-preview {
float: right;
width: 100px;
max-height: 200px;
-moz-margin-start: 10px;
margin-bottom: 10px;
}
body[dir=rtl] .rooms > div > .context > .context-preview {
float: left;
}
.rooms > div > .context > .context-preview[src=""] {
display: none;
}
.rooms > div > .context > .context-description {
display: block;
color: #707070;
}
.rooms > div > .context > .context-url {
display: block;
color: #59A1D7;
clear: both;
}
.rooms > div > .btn {
display: block;
font-size: 1rem;
margin: 0 auto;
margin: 0 auto .5rem;
width: 100%;
padding: .5rem 1rem;
border-radius: 0 0 3px 3px;
}
/* Remove when bug 1142671 is backed out. */
.rooms > div > :not(.context) + .btn {
border-radius: 3px;
margin-top: 0.5rem;
}
.room-list {
@ -197,6 +243,8 @@ body {
overflow: auto;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
margin-left: -1rem;
margin-right: -1rem;
}
.room-list:empty {

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

@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) {
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
userDisplayName: React.PropTypes.string.isRequired // for room creation
@ -666,7 +667,8 @@ loop.panel = (function(_, mozL10n) {
);
}, this)
),
React.createElement("p", null,
React.createElement("div", null,
React.createElement(ContextInfo, {mozLoop: this.props.mozLoop}),
React.createElement("button", {className: "btn btn-info new-room-button",
onClick: this.handleCreateButtonClick,
disabled: this._hasPendingOperation()},
@ -678,6 +680,60 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Context info that is offered to be part of a Room.
*/
var ContextInfo = React.createClass({displayName: "ContextInfo",
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
},
mixins: [sharedMixins.DocumentVisibilityMixin],
getInitialState: function() {
return {
previewImage: "",
description: "",
url: ""
};
},
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
},
render: function() {
if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") ||
!this.state.url) {
return null;
}
return (
React.createElement("div", {className: "context"},
React.createElement("label", {className: "context-enabled"},
React.createElement("input", {type: "checkbox"}),
mozL10n.get("context_offer_label")
),
React.createElement("img", {className: "context-preview", src: this.state.previewImage}),
React.createElement("span", {className: "context-description"}, this.state.description),
React.createElement("span", {className: "context-url"}, this.state.url)
)
);
}
});
/**
* Panel view.
*/
@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) {
React.createElement(Tab, {name: "rooms"},
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
store: this.props.roomStore,
userDisplayName: this._getUserDisplayName()}),
userDisplayName: this._getUserDisplayName(),
mozLoop: this.props.mozLoop}),
React.createElement(ToSView, null)
),
React.createElement(Tab, {name: "contacts"},
@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
ContextInfo: ContextInfo,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

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

@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) {
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
userDisplayName: React.PropTypes.string.isRequired // for room creation
@ -666,13 +667,68 @@ loop.panel = (function(_, mozL10n) {
/>;
}, this)
}</div>
<p>
<div>
<ContextInfo mozLoop={this.props.mozLoop} />
<button className="btn btn-info new-room-button"
onClick={this.handleCreateButtonClick}
disabled={this._hasPendingOperation()}>
{mozL10n.get("rooms_new_room_button_label")}
</button>
</p>
</div>
</div>
);
}
});
/**
* Context info that is offered to be part of a Room.
*/
var ContextInfo = React.createClass({
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
},
mixins: [sharedMixins.DocumentVisibilityMixin],
getInitialState: function() {
return {
previewImage: "",
description: "",
url: ""
};
},
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
},
render: function() {
if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") ||
!this.state.url) {
return null;
}
return (
<div className="context">
<label className="context-enabled">
<input type="checkbox"/>
{mozL10n.get("context_offer_label")}
</label>
<img className="context-preview" src={this.state.previewImage}/>
<span className="context-description">{this.state.description}</span>
<span className="context-url">{this.state.url}</span>
</div>
);
}
@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) {
<Tab name="rooms">
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomStore}
userDisplayName={this._getUserDisplayName()}/>
userDisplayName={this._getUserDisplayName()}
mozLoop={this.props.mozLoop}/>
<ToSView />
</Tab>
<Tab name="contacts">
@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
ContextInfo: ContextInfo,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

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

@ -333,6 +333,10 @@ loop.store.ActiveRoomStore = (function() {
});
this._setRefreshTimeout(actionData.expires);
// Only send media telemetry on one side of the call: the desktop side.
actionData["sendTwoWayMediaTelemetry"] = this._isDesktop;
this._sdkDriver.connectSession(actionData);
this._mozLoop.addConversationContext(this._storeState.windowId,

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

@ -270,7 +270,8 @@ loop.store = loop.store || {};
this.sdkDriver.connectSession({
apiKey: state.apiKey,
sessionId: state.sessionId,
sessionToken: state.sessionToken
sessionToken: state.sessionToken,
sendTwoWayMediaTelemetry: state.outgoing // only one side of the call
});
this.mozLoop.addConversationContext(
state.windowId,

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

@ -37,7 +37,6 @@ loop.OTSdkDriver = (function() {
}
this.connections = {};
this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_UNINITIALIZED);
this.dispatcher.register(this, [
"setupStreamElements",
@ -210,12 +209,20 @@ loop.OTSdkDriver = (function() {
* - sessionId: The OT session ID
* - apiKey: The OT API key
* - sessionToken: The token for the OT session
* - sendTwoWayMediaTelemetry: boolean should we send telemetry on length
* of media sessions. Callers should ensure
* that this is only set for one side of the
* session so that things don't get
* double-counted.
*
* @param {Object} sessionData The session data for setting up the OT session.
*/
connectSession: function(sessionData) {
this.session = this.sdk.initSession(sessionData.sessionId);
this._sendTwoWayMediaTelemetry = !!sessionData.sendTwoWayMediaTelemetry;
this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_UNINITIALIZED);
this.session.on("connectionCreated", this._onConnectionCreated.bind(this));
this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
this.session.on("streamDestroyed", this._onRemoteStreamDestroyed.bind(this));
@ -468,17 +475,17 @@ loop.OTSdkDriver = (function() {
* analogous in order to follow the principle of least surprise for
* people consuming this code.
*
* If this._isDesktop is not true, returns immediately without making
* any changes, since this data is not used, and it makes reading
* the logs confusing for manual verification of both ends of the call in
* the same browser, which is a case we care about.
* If this._sendTwoWayMediaTelemetry is not true, returns immediately
* without making any changes, since this data is not used, and it makes
* reading the logs confusing for manual verification of both ends of the
* call in the same browser, which is a case we care about.
*
* @param start start time in milliseconds, as returned by
* performance.now()
* @private
*/
_setTwoWayMediaStartTime: function(start) {
if (!this._isDesktop) {
if (!this._sendTwoWayMediaTelemetry) {
return;
}
@ -590,7 +597,7 @@ loop.OTSdkDriver = (function() {
// Now record the fact, and check if we've got all media yet.
this._publishedLocalStream = true;
if (this._checkAllStreamsConnected()) {
this._setTwoWayMediaStartTime(performance.now);
this._setTwoWayMediaStartTime(performance.now());
this.dispatcher.dispatch(new sharedActions.MediaConnected());
}
}
@ -676,15 +683,14 @@ loop.OTSdkDriver = (function() {
/**
* Note connection length if it's valid (the startTime has been initialized
* and is not later than endTime) and not yet already noted. If
* this._isDesktop is not true, we're assumed to be running in the
* standalone client and return immediately.
* this._sendTwoWayMediaTelemetry is not true, we return immediately.
*
* @param {number} startTime in milliseconds
* @param {number} endTime in milliseconds
* @private
*/
_noteConnectionLengthIfNeeded: function(startTime, endTime) {
if (!this._isDesktop) {
if (!this._sendTwoWayMediaTelemetry) {
return;
}

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

@ -1,6 +1,12 @@
#!/bin/sh
# Run from topsrcdir, no args
if [ "$1" == "--help" ]; then
echo "Usage: ./run-all-loop-tests.sh [options]"
echo " --skip-e10s Skips the e10s tests"
exit 0;
fi
set -e
# Main tests
@ -13,10 +19,17 @@ set -e
# prompting is in browser_devices_get_user_media_about_urls.js. It's possible
# to mess this up with CSP handling, and probably other changes, too.
./mach mochitest \
browser/components/loop/test/mochitest \
browser/modules/test/browser_UITour_loop.js \
TESTS="
browser/components/loop/test/mochitest
browser/modules/test/browser_UITour_loop.js
browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
"
./mach mochitest $TESTS
if [ "$1" != "--skip-e10s" ]; then
./mach mochitest --e10s $TESTS
fi
# This is currently disabled because the test itself is busted. Once bug
# 1062821 is landed, we should see if things work again, and then re-enable it.

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

@ -651,7 +651,8 @@ describe("loop.panel", function() {
React.createElement(loop.panel.RoomList, {
store: roomStore,
dispatcher: dispatcher,
userDisplayName: fakeEmail
userDisplayName: fakeEmail,
mozLoop: fakeMozLoop
}));
}
@ -708,6 +709,49 @@ describe("loop.panel", function() {
var buttonNode = view.getDOMNode().querySelector("button[disabled]");
expect(buttonNode).to.not.equal(null);
});
it("should show context information when a URL is available",
function() {
navigator.mozLoop.getLoopPref = function() {
return true;
}
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.ContextInfo, {
mozLoop: navigator.mozLoop
})
);
view.setState({
previews: [""],
description: "fake description",
url: "https://www.example.com"
});
var contextEnabledCheckbox = view.getDOMNode().querySelector(".context-enabled");
expect(contextEnabledCheckbox).to.not.equal(null);
});
it("should not show context information when a URL is unavailable",
function() {
navigator.mozLoop.getLoopPref = function() {
return true;
}
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.ContextInfo, {
mozLoop: navigator.mozLoop
})
);
view.setState({
previews: [""],
description: "fake description",
url: ""
});
var contextInfo = view.getDOMNode();
expect(contextInfo).to.equal(null);
});
});
describe('loop.panel.ToSView', function() {

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

@ -541,6 +541,28 @@ describe("loop.store.ActiveRoomStore", function () {
actionData);
});
it("should pass 'sendTwoWayMediaTelemetry' as true to connectSession if " +
"store._isDesktop is true", function() {
store._isDesktop = true;
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
sinon.assert.calledOnce(fakeSdkDriver.connectSession);
sinon.assert.calledWithMatch(fakeSdkDriver.connectSession,
sinon.match.has("sendTwoWayMediaTelemetry", true));
});
it("should pass 'sendTwoWayTelemetry' as false to connectionSession if " +
"store._isDesktop is false", function() {
store._isDesktop = false;
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
sinon.assert.calledOnce(fakeSdkDriver.connectSession);
sinon.assert.calledWithMatch(fakeSdkDriver.connectSession,
sinon.match.has("sendTwoWayMediaTelemetry", false));
});
it("should call mozLoop.addConversationContext", function() {
var actionData = new sharedActions.JoinedRoom(fakeJoinedData);

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

@ -252,7 +252,8 @@ describe("loop.store.ConversationStore", function () {
sinon.assert.calledWithExactly(sdkDriver.connectSession, {
apiKey: "fakeKey",
sessionId: "321456",
sessionToken: "341256"
sessionToken: "341256",
sendTwoWayMediaTelemetry: true
});
});
@ -580,7 +581,7 @@ describe("loop.store.ConversationStore", function () {
expect(store.getStoreState("callState")).eql(CALL_STATES.ONGOING);
});
it("should connect the session", function() {
it("should connect the session with sendTwoWayMediaTelemetry set as falsy", function() {
store.setStoreState(fakeSessionData);
store.acceptCall(
@ -590,7 +591,8 @@ describe("loop.store.ConversationStore", function () {
sinon.assert.calledWithExactly(sdkDriver.connectSession, {
apiKey: "fakeKey",
sessionId: "321456",
sessionToken: "341256"
sessionToken: "341256",
sendTwoWayMediaTelemetry: undefined
});
});

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

@ -104,14 +104,6 @@ describe("loop.OTSdkDriver", function () {
new loop.OTSdkDriver({dispatcher: dispatcher});
}).to.Throw(/sdk/);
});
it("should set the two-way media start time to 'uninitialized'", function() {
var driver = new loop.OTSdkDriver(
{sdk: sdk, dispatcher: dispatcher, mozLoop: mozLoop, isDesktop: true});
expect(driver._getTwoWayMediaStartTime()).to.
eql(driver.CONNECTION_START_TIME_UNINITIALIZED);
});
});
describe("#setupStreamElements", function() {
@ -352,6 +344,15 @@ describe("loop.OTSdkDriver", function () {
sinon.assert.calledWith(session.connect, "1234567890", "1357924680");
});
it("should set the two-way media start time to 'uninitialized' " +
"when sessionData.sendTwoWayMediaTelemetry is true'", function() {
driver.connectSession(_.extend(sessionData,
{sendTwoWayMediaTelemetry: true}));
expect(driver._getTwoWayMediaStartTime()).to.
eql(driver.CONNECTION_START_TIME_UNINITIALIZED);
});
describe("On connection complete", function() {
it("should publish the stream if the publisher is ready", function() {
driver._publisherReady = true;
@ -398,6 +399,7 @@ describe("loop.OTSdkDriver", function () {
driver.session = session;
var startTime = 1;
var endTime = 3;
driver._sendTwoWayMediaTelemetry = true;
driver._setTwoWayMediaStartTime(startTime);
sandbox.stub(performance, "now").returns(endTime);
sandbox.stub(driver, "_noteConnectionLengthIfNeeded");
@ -411,6 +413,7 @@ describe("loop.OTSdkDriver", function () {
it("should reset the two-way media connection start time", function() {
driver.session = session;
var startTime = 1;
driver._sendTwoWayMediaTelemetry = true;
driver._setTwoWayMediaStartTime(startTime);
sandbox.stub(performance, "now");
sandbox.stub(driver, "_noteConnectionLengthIfNeeded");
@ -426,6 +429,7 @@ describe("loop.OTSdkDriver", function () {
var startTimeMS;
beforeEach(function() {
startTimeMS = 1;
driver._sendTwoWayMediaTelemetry = true;
driver._setTwoWayMediaStartTime(startTimeMS);
});
@ -483,11 +487,11 @@ describe("loop.OTSdkDriver", function () {
mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M);
});
it("should not call mozLoop.noteConnectionLength if driver._isDesktop " +
"is false",
it("should not call mozLoop.noteConnectionLength if" +
" driver._sendTwoWayMediaTelemetry is false",
function() {
var endTimeMS = 10 * 60 * 1000;
driver._isDesktop = false;
driver._sendTwoWayMediaTelemetry = false;
driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
@ -616,6 +620,7 @@ describe("loop.OTSdkDriver", function () {
driver.session = session;
var startTime = 1;
var endTime = 3;
driver._sendTwoWayMediaTelemetry = true;
driver._setTwoWayMediaStartTime(startTime);
sandbox.stub(performance, "now").returns(endTime);
sandbox.stub(driver, "_noteConnectionLengthIfNeeded");
@ -660,6 +665,7 @@ describe("loop.OTSdkDriver", function () {
driver.session = session;
var startTime = 1;
var endTime = 3;
driver._sendTwoWayMediaTelemetry = true;
driver._setTwoWayMediaStartTime(startTime);
sandbox.stub(performance, "now").returns(endTime);
sandbox.stub(driver, "_noteConnectionLengthIfNeeded");
@ -747,7 +753,8 @@ describe("loop.OTSdkDriver", function () {
});
it("should store the start time when both streams are up and" +
" driver._isDesktop is true", function() {
" driver._sendTwoWayMediaTelemetry is true", function() {
driver._sendTwoWayMediaTelemetry = true;
driver._publishedLocalStream = true;
var startTime = 1;
sandbox.stub(performance, "now").returns(startTime);

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

@ -115,6 +115,7 @@ navigator.mozLoop = {
// Ensure we skip FTE completely.
case "gettingStarted.seen":
case "contacts.gravatars.promo":
case "contextInConverations.enabled":
return true;
case "contacts.gravatars.show":
return false;
@ -127,6 +128,13 @@ navigator.mozLoop = {
return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
"0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
},
getSelectedTabMetadata: function(callback) {
callback({
previews: ["chrome://branding/content/about-logo.png"],
description: "sample webpage description",
url: "https://www.example.com"
});
},
contacts: {
getAll: function(callback) {
callback(null, [].concat(fakeContacts));

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

@ -56,6 +56,28 @@
function noop(){}
// We save the visibility change listeners so that we can fake an event
// to the panel once we've loaded all the views.
var visibilityListeners = [];
var rootObject = window;
rootObject.document.addEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
visibilityListeners.push(func);
}
window.addEventListener(eventName, func);
};
rootObject.document.removeEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
var index = visibilityListeners.indexOf(func);
visibilityListeners.splice(index, 1);
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -757,6 +779,10 @@
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(React.createElement(App, null), document.getElementById("main"));
for (var listener of visibilityListeners) {
listener({target: {hidden: false}});
}
} catch(err) {
console.error(err);
uncaughtError = err;

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

@ -56,6 +56,28 @@
function noop(){}
// We save the visibility change listeners so that we can fake an event
// to the panel once we've loaded all the views.
var visibilityListeners = [];
var rootObject = window;
rootObject.document.addEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
visibilityListeners.push(func);
}
window.addEventListener(eventName, func);
};
rootObject.document.removeEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
var index = visibilityListeners.indexOf(func);
visibilityListeners.splice(index, 1);
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -757,6 +779,10 @@
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(<App />, document.getElementById("main"));
for (var listener of visibilityListeners) {
listener({target: {hidden: false}});
}
} catch(err) {
console.error(err);
uncaughtError = err;

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

@ -19,6 +19,12 @@ Cu.import("resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SQLiteStore",
"resource:///modules/readinglist/SQLiteStore.jsm");
// We use Sync's "Utils" module for the device name, which is unfortunate,
// but let's give it a better name here.
XPCOMUtils.defineLazyGetter(this, "SyncUtils", function() {
const {Utils} = Cu.import("resource://services-sync/util.js", {});
return Utils;
});
{ // Prevent the parent log setup from leaking into the global scope.
let parentLog = Log.repository.getLogger("readinglist");
@ -36,16 +42,21 @@ let log = Log.repository.getLogger("readinglist.api");
// Each ReadingListItem has a _record property, an object containing the raw
// data from the server and local store. These are the names of the properties
// in that object.
//
// Not important, but FYI: The order that these are listed in follows the order
// that the server doc lists the fields in the article data model, more or less:
// http://readinglist.readthedocs.org/en/latest/model.html
const ITEM_RECORD_PROPERTIES = `
guid
lastModified
serverLastModified
url
preview
title
resolvedURL
resolvedTitle
excerpt
preview
status
archived
deleted
favorite
isArticle
wordCount
@ -56,6 +67,7 @@ const ITEM_RECORD_PROPERTIES = `
markedReadBy
markedReadOn
readPosition
syncStatus
`.trim().split(/\s+/);
// Article objects that are passed to ReadingList.addItem may contain
@ -69,6 +81,37 @@ const ITEM_DISREGARDED_PROPERTIES = `
length
`.trim().split(/\s+/);
// Each local item has a syncStatus indicating the state of the item in relation
// to the sync server. See also Sync.jsm.
const SYNC_STATUS_SYNCED = 0;
const SYNC_STATUS_NEW = 1;
const SYNC_STATUS_CHANGED_STATUS = 2;
const SYNC_STATUS_CHANGED_MATERIAL = 3;
const SYNC_STATUS_DELETED = 4;
// These options are passed as the "control" options to store methods and filter
// out all records in the store with syncStatus SYNC_STATUS_DELETED.
const STORE_OPTIONS_IGNORE_DELETED = {
syncStatus: [
SYNC_STATUS_SYNCED,
SYNC_STATUS_NEW,
SYNC_STATUS_CHANGED_STATUS,
SYNC_STATUS_CHANGED_MATERIAL,
],
};
// Changes to the following item properties are considered "status," or
// "status-only," changes, in relation to the sync server. Changes to other
// properties are considered "material" changes. See also Sync.jsm.
const SYNC_STATUS_PROPERTIES_STATUS = `
favorite
markedReadBy
markedReadOn
readPosition
unread
`.trim().split(/\s+/);
/**
* A reading list contains ReadingListItems.
*
@ -131,6 +174,18 @@ ReadingListImpl.prototype = {
ItemRecordProperties: ITEM_RECORD_PROPERTIES,
SyncStatus: {
SYNCED: SYNC_STATUS_SYNCED,
NEW: SYNC_STATUS_NEW,
CHANGED_STATUS: SYNC_STATUS_CHANGED_STATUS,
CHANGED_MATERIAL: SYNC_STATUS_CHANGED_MATERIAL,
DELETED: SYNC_STATUS_DELETED,
},
SyncStatusProperties: {
STATUS: SYNC_STATUS_PROPERTIES_STATUS,
},
/**
* Yields the number of items in the list.
*
@ -140,7 +195,7 @@ ReadingListImpl.prototype = {
* with an Error on error.
*/
count: Task.async(function* (...optsList) {
return (yield this._store.count(...optsList));
return (yield this._store.count(optsList, STORE_OPTIONS_IGNORE_DELETED));
}),
/**
@ -151,7 +206,7 @@ ReadingListImpl.prototype = {
* whether the URL is in the list or not.
*/
hasItemForURL: Task.async(function* (url) {
url = normalizeURI(url).spec;
url = normalizeURI(url);
// This is used on every tab switch and page load of the current tab, so we
// want it to be quick and avoid a DB query whenever possible.
@ -189,6 +244,26 @@ ReadingListImpl.prototype = {
* an Error on error.
*/
forEachItem: Task.async(function* (callback, ...optsList) {
yield this._forEachItem(callback, optsList, STORE_OPTIONS_IGNORE_DELETED);
}),
/**
* Like forEachItem, but enumerates only previously synced items that are
* marked as being locally deleted.
*/
forEachSyncedDeletedItem: Task.async(function* (callback, ...optsList) {
yield this._forEachItem(callback, optsList, {
syncStatus: SYNC_STATUS_DELETED,
});
}),
/**
* See forEachItem.
*
* @param storeOptions An options object passed to the store as the "control"
* options.
*/
_forEachItem: Task.async(function* (callback, optsList, storeOptions) {
let promiseChain = Promise.resolve();
yield this._store.forEachItem(record => {
promiseChain = promiseChain.then(() => {
@ -201,7 +276,7 @@ ReadingListImpl.prototype = {
return undefined;
});
});
}, ...optsList);
}, optsList, storeOptions);
yield promiseChain;
}),
@ -236,10 +311,23 @@ ReadingListImpl.prototype = {
*/
addItem: Task.async(function* (record) {
record = normalizeRecord(record);
record.addedOn = Date.now();
if (Services.prefs.prefHasUserValue("services.sync.client.name")) {
record.addedBy = Services.prefs.getCharPref("services.sync.client.name");
if (!record.url) {
throw new Error("The item must have a url");
}
if (!("addedOn" in record)) {
record.addedOn = Date.now();
}
if (!("addedBy" in record)) {
try {
record.addedBy = Services.prefs.getCharPref("services.sync.client.name");
} catch (ex) {
record.addedBy = SyncUtils.getDefaultDeviceName();
}
}
if (!("syncStatus" in record)) {
record.syncStatus = SYNC_STATUS_NEW;
}
yield this._store.addItem(record);
this._invalidateIterators();
let item = this._itemFromRecord(record);
@ -264,6 +352,9 @@ ReadingListImpl.prototype = {
* Error on error.
*/
updateItem: Task.async(function* (item) {
if (!item._record.url) {
throw new Error("The item must have a url");
}
this._ensureItemBelongsToList(item);
yield this._store.updateItem(item._record);
this._invalidateIterators();
@ -282,7 +373,26 @@ ReadingListImpl.prototype = {
*/
deleteItem: Task.async(function* (item) {
this._ensureItemBelongsToList(item);
yield this._store.deleteItemByURL(item.url);
// If the item is new and therefore hasn't been synced yet, delete it from
// the store. Otherwise mark it as deleted but don't actually delete it so
// that its status can be synced.
if (item._record.syncStatus == SYNC_STATUS_NEW) {
yield this._store.deleteItemByURL(item.url);
}
else {
// To prevent data leakage, only keep the record fields needed to sync
// the deleted status: guid and syncStatus.
let newRecord = {};
for (let prop of ITEM_RECORD_PROPERTIES) {
newRecord[prop] = null;
}
newRecord.guid = item._record.guid;
newRecord.syncStatus = SYNC_STATUS_DELETED;
item._record = newRecord;
yield this._store.updateItemByGUID(item._record);
}
item.list = null;
this._itemsByNormalizedURL.delete(item.url);
this._invalidateIterators();
@ -309,7 +419,7 @@ ReadingListImpl.prototype = {
* @return The first matching item, or null if there are no matching items.
*/
itemForURL: Task.async(function* (uri) {
let url = normalizeURI(uri).spec;
let url = normalizeURI(uri);
return (yield this.item({ url: url }, { resolvedURL: url }));
}),
@ -508,7 +618,7 @@ ReadingListItem.prototype = {
* @type string
*/
get url() {
return this._record.url;
return this._record.url || undefined;
},
/**
@ -529,7 +639,7 @@ ReadingListItem.prototype = {
* @type string
*/
get resolvedURL() {
return this._record.resolvedURL;
return this._record.resolvedURL || undefined;
},
set resolvedURL(val) {
this._updateRecord({ resolvedURL: val });
@ -554,7 +664,7 @@ ReadingListItem.prototype = {
* @type string
*/
get title() {
return this._record.title;
return this._record.title || undefined;
},
set title(val) {
this._updateRecord({ title: val });
@ -565,7 +675,7 @@ ReadingListItem.prototype = {
* @type string
*/
get resolvedTitle() {
return this._record.resolvedTitle;
return this._record.resolvedTitle || undefined;
},
set resolvedTitle(val) {
this._updateRecord({ resolvedTitle: val });
@ -576,21 +686,21 @@ ReadingListItem.prototype = {
* @type string
*/
get excerpt() {
return this._record.excerpt;
return this._record.excerpt || undefined;
},
set excerpt(val) {
this._updateRecord({ excerpt: val });
},
/**
* The item's status.
* @type integer
* The item's archived status.
* @type boolean
*/
get status() {
return this._record.status;
get archived() {
return !!this._record.archived;
},
set status(val) {
this._updateRecord({ status: val });
set archived(val) {
this._updateRecord({ archived: !!val });
},
/**
@ -620,7 +730,7 @@ ReadingListItem.prototype = {
* @type integer
*/
get wordCount() {
return this._record.wordCount;
return this._record.wordCount || undefined;
},
set wordCount(val) {
this._updateRecord({ wordCount: val });
@ -668,7 +778,7 @@ ReadingListItem.prototype = {
* @type string
*/
get markedReadBy() {
return this._record.markedReadBy;
return this._record.markedReadBy || undefined;
},
set markedReadBy(val) {
this._updateRecord({ markedReadBy: val });
@ -692,7 +802,7 @@ ReadingListItem.prototype = {
* @param integer
*/
get readPosition() {
return this._record.readPosition;
return this._record.readPosition || undefined;
},
set readPosition(val) {
this._updateRecord({ readPosition: val });
@ -703,7 +813,7 @@ ReadingListItem.prototype = {
* @type string
*/
get preview() {
return this._record.preview;
return this._record.preview || undefined;
},
/**
@ -730,6 +840,11 @@ ReadingListItem.prototype = {
* not normalized, but everywhere else, records are always normalized unless
* otherwise stated. The setter normalizes the passed-in value, so it will
* throw an error if the value is not a valid record.
*
* This object should reflect the item's representation in the local store, so
* when calling the setter, be careful that it doesn't drift away from the
* store's record. If you set it, you should also call updateItem() around
* the same time.
*/
get _record() {
return this.__record;
@ -746,6 +861,18 @@ ReadingListItem.prototype = {
*/
_updateRecord(partialRecord) {
let record = this._record;
// The syncStatus flag can change from SYNCED to either CHANGED_STATUS or
// CHANGED_MATERIAL, or from CHANGED_STATUS to CHANGED_MATERIAL.
if (record.syncStatus == SYNC_STATUS_SYNCED ||
record.syncStatus == SYNC_STATUS_CHANGED_STATUS) {
let allStatusChanges = Object.keys(partialRecord).every(prop => {
return SYNC_STATUS_PROPERTIES_STATUS.indexOf(prop) >= 0;
});
record.syncStatus = allStatusChanges ? SYNC_STATUS_CHANGED_STATUS :
SYNC_STATUS_CHANGED_MATERIAL;
}
for (let prop in partialRecord) {
record[prop] = partialRecord[prop];
}
@ -864,17 +991,20 @@ ReadingListItemIterator.prototype = {
function normalizeRecord(nonNormalizedRecord) {
let record = {};
for (let prop in nonNormalizedRecord) {
if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) {
if (ITEM_DISREGARDED_PROPERTIES.indexOf(prop) >= 0) {
continue;
}
if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
if (ITEM_RECORD_PROPERTIES.indexOf(prop) < 0) {
throw new Error("Unrecognized item property: " + prop);
}
switch (prop) {
case "url":
case "resolvedURL":
if (nonNormalizedRecord[prop]) {
record[prop] = normalizeURI(nonNormalizedRecord[prop]).spec;
record[prop] = normalizeURI(nonNormalizedRecord[prop]);
}
else {
record[prop] = nonNormalizedRecord[prop];
}
break;
default:
@ -890,17 +1020,22 @@ function normalizeRecord(nonNormalizedRecord) {
* or compare against.
*
* @param {nsIURI/String} uri - URI to normalize.
* @returns {nsIURI} Cloned and normalized version of the input URI.
* @returns {String} String spec of a cloned and normalized version of the
* input URI.
*/
function normalizeURI(uri) {
if (typeof uri == "string") {
uri = Services.io.newURI(uri, "", null);
try {
uri = Services.io.newURI(uri, "", null);
} catch (ex) {
return uri;
}
}
uri = uri.cloneIgnoringRef();
try {
uri.userPass = "";
} catch (ex) {} // Not all nsURI impls (eg, nsSimpleURI) support .userPass
return uri;
return uri.spec;
};
function hash(str) {
@ -944,7 +1079,7 @@ function getMetadataFromBrowser(browser) {
Object.defineProperty(this, "ReadingList", {
get() {
if (!this._singleton) {
let store = new SQLiteStore("reading-list-temp2.sqlite");
let store = new SQLiteStore("reading-list.sqlite");
this._singleton = new ReadingListImpl(store);
}
return this._singleton;

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

@ -35,13 +35,16 @@ this.SQLiteStore.prototype = {
/**
* Yields the number of items in the store that match the given options.
*
* @param optsList A variable number of options objects that control the
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return Promise<number> The number of matching items in the store.
* Rejected with an Error on error.
*/
count: Task.async(function* (...optsList) {
let [sql, args] = sqlFromOptions(optsList);
count: Task.async(function* (userOptsList=[], controlOpts={}) {
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
let count = 0;
let conn = yield this._connectionPromise;
yield conn.executeCached(`
@ -55,13 +58,16 @@ this.SQLiteStore.prototype = {
*
* @param callback Called for each item in the enumeration. It's passed a
* single object, an item.
* @param optsList A variable number of options objects that control the
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return Promise<null> Resolved when the enumeration completes. Rejected
* with an Error on error.
*/
forEachItem: Task.async(function* (callback, ...optsList) {
let [sql, args] = sqlFromOptions(optsList);
forEachItem: Task.async(function* (callback, userOptsList=[], controlOpts={}) {
let [sql, args] = sqlWhereFromOptions(userOptsList, controlOpts);
let colNames = ReadingList.ItemRecordProperties;
let conn = yield this._connectionPromise;
yield conn.executeCached(`
@ -99,18 +105,23 @@ this.SQLiteStore.prototype = {
* Error on error.
*/
updateItem: Task.async(function* (item) {
let assignments = [];
for (let propName in item) {
assignments.push(`${propName} = :${propName}`);
}
let conn = yield this._connectionPromise;
yield conn.executeCached(`
UPDATE items SET ${assignments} WHERE url = :url;
`, item);
yield this._updateItem(item, "url");
}),
/**
* Deletes an item from the store.
* Same as updateItem, but the item is keyed off of its `guid` instead of its
* `url`.
*
* @param item The item to update. It must have a `guid`.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
updateItemByGUID: Task.async(function* (item) {
yield this._updateItem(item, "guid");
}),
/**
* Deletes an item from the store by its URL.
*
* @param url The URL string of the item to delete.
* @return Promise<null> Resolved when the store is updated. Rejected with an
@ -123,6 +134,20 @@ this.SQLiteStore.prototype = {
`, { url: url });
}),
/**
* Deletes an item from the store by its GUID.
*
* @param guid The GUID string of the item to delete.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
deleteItemByGUID: Task.async(function* (guid) {
let conn = yield this._connectionPromise;
yield conn.executeCached(`
DELETE FROM items WHERE guid = :guid;
`, { guid: guid });
}),
/**
* Call this when you're done with the store. Don't use it afterward.
*/
@ -161,6 +186,30 @@ this.SQLiteStore.prototype = {
}
}),
/**
* Updates the properties of an item that's already present in the store. See
* ReadingList.prototype.updateItem.
*
* @param item The item to update. It must have the property named by
* keyProp.
* @param keyProp The item is keyed off of this property.
* @return Promise<null> Resolved when the store is updated. Rejected with an
* Error on error.
*/
_updateItem: Task.async(function* (item, keyProp) {
let assignments = [];
for (let propName in item) {
assignments.push(`${propName} = :${propName}`);
}
let conn = yield this._connectionPromise;
if (!item[keyProp]) {
throw new Error("Item must have " + keyProp);
}
yield conn.executeCached(`
UPDATE items SET ${assignments} WHERE ${keyProp} = :${keyProp};
`, item);
}),
// Promise<Sqlite.OpenedConnection>
_connectionPromise: null,
@ -184,17 +233,23 @@ this.SQLiteStore.prototype = {
yield conn.execute(`
PRAGMA journal_size_limit = 524288;
`);
// Not important, but FYI: The order that these columns are listed in
// follows the order that the server doc lists the fields in the article
// data model, more or less:
// http://readinglist.readthedocs.org/en/latest/model.html
yield conn.execute(`
CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guid TEXT UNIQUE,
url TEXT NOT NULL UNIQUE,
resolvedURL TEXT UNIQUE,
lastModified INTEGER,
serverLastModified INTEGER,
url TEXT UNIQUE,
preview TEXT,
title TEXT,
resolvedURL TEXT UNIQUE,
resolvedTitle TEXT,
excerpt TEXT,
status INTEGER,
archived BOOLEAN,
deleted BOOLEAN,
favorite BOOLEAN,
isArticle BOOLEAN,
wordCount INTEGER,
@ -205,7 +260,7 @@ this.SQLiteStore.prototype = {
markedReadBy TEXT,
markedReadOn INTEGER,
readPosition INTEGER,
preview TEXT
syncStatus INTEGER
);
`);
yield conn.execute(`
@ -236,20 +291,24 @@ function itemFromRow(row) {
* Returns the back part of a SELECT statement generated from the given list of
* options.
*
* @param optsList See Options Objects in ReadingList.jsm.
* @param userOptsList A variable number of options objects that control the
* items that are matched. See Options Objects in ReadingList.jsm.
* @param controlOpts A single options object. Use this to filter out items
* that don't match it -- in other words, to override the user options.
* See Options Objects in ReadingList.jsm.
* @return An array [sql, args]. sql is a string of SQL. args is an object
* that contains arguments for all the parameters in sql.
*/
function sqlFromOptions(optsList) {
// We modify the options objects, which were passed in by the store client, so
// clone them first.
optsList = Cu.cloneInto(optsList, {}, { cloneFunctions: false });
function sqlWhereFromOptions(userOptsList, controlOpts) {
// We modify the options objects in userOptsList, which were passed in by the
// store client, so clone them first.
userOptsList = Cu.cloneInto(userOptsList, {}, { cloneFunctions: false });
let sort;
let sortDir;
let limit;
let offset;
for (let opts of optsList) {
for (let opts of userOptsList) {
if ("sort" in opts) {
sort = opts.sort;
delete opts.sort;
@ -284,21 +343,44 @@ function sqlFromOptions(optsList) {
}
let args = {};
let mainExprs = [];
function uniqueParamName(name) {
if (name in args) {
for (let i = 1; ; i++) {
let newName = `${name}_${i}`;
if (!(newName in args)) {
return newName;
}
}
}
return name;
let controlSQLExpr = sqlExpressionFromOptions([controlOpts], args);
if (controlSQLExpr) {
mainExprs.push(`(${controlSQLExpr})`);
}
// Build a WHERE clause for the remaining properties. Assume they all refer
// to columns. (If they don't, the SQL query will fail.)
let userSQLExpr = sqlExpressionFromOptions(userOptsList, args);
if (userSQLExpr) {
mainExprs.push(`(${userSQLExpr})`);
}
if (mainExprs.length) {
let conjunction = mainExprs.join(" AND ");
fragments.unshift(`WHERE ${conjunction}`);
}
let sql = fragments.join(" ");
return [sql, args];
}
/**
* Returns a SQL expression generated from the given options list. Each options
* object in the list generates a subexpression, and all the subexpressions are
* OR'ed together to produce the final top-level expression. (e.g., an optsList
* with three options objects would generate an expression like "(guid = :guid
* OR (title = :title AND unread = :unread) OR resolvedURL = :resolvedURL)".)
*
* All the properties of the options objects are assumed to refer to columns in
* the database. If they don't, your SQL query will fail.
*
* @param optsList See Options Objects in ReadingList.jsm.
* @param args An object that will hold the SQL parameters. It will be
* modified.
* @return A string of SQL. Also, args will contain arguments for all the
* parameters in the SQL.
*/
function sqlExpressionFromOptions(optsList, args) {
let disjunctions = [];
for (let opts of optsList) {
let conjunctions = [];
@ -310,14 +392,14 @@ function sqlFromOptions(optsList) {
let array = opts[key];
let params = [];
for (let i = 0; i < array.length; i++) {
let paramName = uniqueParamName(key);
let paramName = uniqueParamName(args, key);
params.push(`:${paramName}`);
args[paramName] = array[i];
}
conjunctions.push(`${key} IN (${params})`);
}
else {
let paramName = uniqueParamName(key);
let paramName = uniqueParamName(args, key);
conjunctions.push(`${key} = :${paramName}`);
args[paramName] = opts[key];
}
@ -328,11 +410,26 @@ function sqlFromOptions(optsList) {
}
}
let disjunction = disjunctions.join(" OR ");
if (disjunction) {
let where = `WHERE ${disjunction}`;
fragments = [where].concat(fragments);
}
let sql = fragments.join(" ");
return [sql, args];
return disjunction;
}
/**
* Returns a version of the given name such that it doesn't conflict with the
* name of any property in args. e.g., if name is "foo" but args already has
* properties named "foo", "foo1", and "foo2", then "foo3" is returned.
*
* @param args An object.
* @param name The name you want to use.
* @return A unique version of the given name.
*/
function uniqueParamName(args, name) {
if (name in args) {
for (let i = 1; ; i++) {
let newName = `${name}_${i}`;
if (!(newName in args)) {
return newName;
}
}
}
return name;
}

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

@ -8,6 +8,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import('resource://gre/modules/Task.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'LogManager',
@ -24,7 +25,14 @@ XPCOMUtils.defineLazyModuleGetter(this, 'setTimeout',
XPCOMUtils.defineLazyModuleGetter(this, 'clearTimeout',
'resource://gre/modules/Timer.jsm');
Cu.import('resource://gre/modules/Task.jsm');
// The main readinglist module.
XPCOMUtils.defineLazyModuleGetter(this, 'ReadingList',
'resource:///modules/readinglist/ReadingList.jsm');
// The "engine"
XPCOMUtils.defineLazyModuleGetter(this, 'Sync',
'resource:///modules/readinglist/Sync.jsm');
this.EXPORTED_SYMBOLS = ["ReadingListScheduler"];
@ -35,8 +43,6 @@ const OBSERVERS = [
"network:offline-status-changed",
// FxA notifications also cause us to check if we should sync.
"fxaccounts:onverified",
// When something notices a local change to an item.
"readinglist:item-changed",
// some notifications the engine might send if we have been requested to backoff.
"readinglist:backoff-requested",
// request to sync now
@ -44,13 +50,6 @@ const OBSERVERS = [
];
///////// A temp object until we get our "engine"
let engine = {
ERROR_AUTHENTICATION: "authentication error",
sync: Task.async(function* () {
}),
}
let prefs = new Preferences("readinglist.scheduler.");
// A helper to manage our interval values.
@ -62,23 +61,29 @@ let intervals = {
},
// How long after startup do we do an initial sync?
get initial() this._fixupIntervalPref("initial", 20), // 20 seconds.
get initial() this._fixupIntervalPref("initial", 10), // 10 seconds.
// Every interval after the first.
get schedule() this._fixupIntervalPref("schedule", 2 * 60 * 60), // 2 hours
// After we've been told an item has changed
get dirty() this._fixupIntervalPref("dirty", 2 * 60), // 2 mins
// After an error
get retry() this._fixupIntervalPref("retry", 2 * 60), // 2 mins
};
// This is the implementation, but it's not exposed directly.
function InternalScheduler() {
function InternalScheduler(readingList = null) {
// oh, I don't know what logs yet - let's guess!
let logs = ["readinglist", "FirefoxAccounts", "browserwindow.syncui"];
let logs = [
"browserwindow.syncui",
"FirefoxAccounts",
"readinglist.api",
"readinglist.serverclient",
"readinglist.sync",
];
this._logManager = new LogManager("readinglist.", logs, "readinglist");
this.log = Log.repository.getLogger("readinglist.scheduler");
this.log.info("readinglist scheduler created.")
this.state = this.STATE_OK;
this.readingList = readingList || ReadingList; // hook point for tests.
// don't this.init() here, but instead at the module level - tests want to
// add hooks before it is called.
@ -98,7 +103,7 @@ InternalScheduler.prototype = {
// rejects.
_timerRunning: false,
// Our sync engine - XXX - maybe just a callback?
_engine: engine,
_engine: Sync,
// Our state variable and constants.
state: null,
@ -108,6 +113,7 @@ InternalScheduler.prototype = {
init() {
this.log.info("scheduler initialzing");
this._setupRLListener();
this._observe = this.observe.bind(this);
for (let notification of OBSERVERS) {
Services.obs.addObserver(this._observe, notification, false);
@ -116,6 +122,26 @@ InternalScheduler.prototype = {
this._setupTimer();
},
_setupRLListener() {
let maybeSync = () => {
if (this._timerRunning) {
// If a sync is currently running it is possible it will miss the change
// just made, so tell the timer the next sync should be 1 ms after
// it completes (we don't use zero as that has special meaning re backoffs)
this._maybeReschedule(1);
} else {
// Do the sync now.
this._syncNow();
}
};
let listener = {
onItemAdded: maybeSync,
onItemUpdated: maybeSync,
onItemDeleted: maybeSync,
}
this.readingList.addListener(listener);
},
// Note: only called by tests.
finalize() {
this.log.info("scheduler finalizing");
@ -141,9 +167,6 @@ InternalScheduler.prototype = {
this._maybeReschedule(0);
break;
}
case "readinglist:local:dirty":
this._maybeReschedule(intervals.dirty);
break;
case "readinglist:user-sync":
this._syncNow();
break;
@ -234,8 +257,8 @@ InternalScheduler.prototype = {
}
// If there is something currently scheduled before the requested delay,
// keep the existing value (eg, if we have a timer firing in 1 second, and
// get a "dirty" notification that says we should sync in 2 seconds, we
// keep the 1 second value)
// get a notification that says we should sync in 2 seconds, we keep the 1
// second value)
this._nextScheduledSync = Math.min(this._nextScheduledSync, now + delay);
// But we still need to honor a backoff.
this._nextScheduledSync = Math.max(this._nextScheduledSync, this._backoffUntil);
@ -252,7 +275,7 @@ InternalScheduler.prototype = {
// we are running does the right thing.
this._nextScheduledSync = 0;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
this._engine.sync().then(() => {
this._engine.start().then(() => {
this.log.info("Sync completed successfully");
// Write a pref in the same format used to services/sync to indicate
// the last success.
@ -292,6 +315,11 @@ InternalScheduler.prototype = {
// already running, and rescheduling the timer.
// To call this, just send a "readinglist:user-sync" notification.
_syncNow() {
if (!prefs.get("enabled")) {
this.log.info("syncNow() but syncing is disabled - ignoring");
return;
}
if (this._timerRunning) {
this.log.info("syncNow() but a sync is already in progress - ignoring");
return;
@ -326,14 +354,14 @@ let ReadingListScheduler = {
// These functions are exposed purely for tests, which manage to grab them
// via a BackstagePass.
function createTestableScheduler() {
function createTestableScheduler(readingList) {
// kill the "real" scheduler as we don't want it listening to notifications etc.
if (internalScheduler) {
internalScheduler.finalize();
internalScheduler = null;
}
// No .init() call - that's up to the tests after hooking.
return new InternalScheduler();
return new InternalScheduler(readingList);
}
// mochitests want the internal state of the real scheduler for various things.

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

@ -0,0 +1,556 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = [
"Sync",
];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
"resource:///modules/readinglist/ReadingList.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ServerClient",
"resource:///modules/readinglist/ServerClient.jsm");
// The Last-Modified header of server responses is stored here.
const SERVER_LAST_MODIFIED_HEADER_PREF = "readinglist.sync.serverLastModified";
// Maps local record properties to server record properties.
const SERVER_PROPERTIES_BY_LOCAL_PROPERTIES = {
guid: "id",
serverLastModified: "last_modified",
url: "url",
preview: "preview",
title: "title",
resolvedURL: "resolved_url",
resolvedTitle: "resolved_title",
excerpt: "excerpt",
archived: "archived",
deleted: "deleted",
favorite: "favorite",
isArticle: "is_article",
wordCount: "word_count",
unread: "unread",
addedBy: "added_by",
addedOn: "added_on",
storedOn: "stored_on",
markedReadBy: "marked_read_by",
markedReadOn: "marked_read_on",
readPosition: "read_position",
};
// Local record properties that can be uploaded in new items.
const NEW_RECORD_PROPERTIES = `
url
title
resolvedURL
resolvedTitle
excerpt
favorite
isArticle
wordCount
unread
addedBy
addedOn
markedReadBy
markedReadOn
readPosition
preview
`.trim().split(/\s+/);
// Local record properties that can be uploaded in changed items.
const MUTABLE_RECORD_PROPERTIES = `
title
resolvedURL
resolvedTitle
excerpt
favorite
isArticle
wordCount
unread
markedReadBy
markedReadOn
readPosition
preview
`.trim().split(/\s+/);
let log = Log.repository.getLogger("readinglist.sync");
/**
* An object that syncs reading list state with a server. To sync, make a new
* SyncImpl object and then call start() on it.
*
* @param readingList The ReadingList to sync.
*/
function SyncImpl(readingList) {
this.list = readingList;
this._client = new ServerClient();
}
/**
* This implementation uses the sync algorithm described here:
* https://github.com/mozilla-services/readinglist/wiki/Client-phases
* The "phases" mentioned in the methods below refer to the phases in that
* document.
*/
SyncImpl.prototype = {
/**
* Starts sync, if it's not already started.
*
* @return Promise<null> this.promise, i.e., a promise that will be resolved
* when sync completes, rejected on error.
*/
start() {
if (!this.promise) {
this.promise = Task.spawn(function* () {
yield this._start();
delete this.promise;
}.bind(this));
}
return this.promise;
},
/**
* A Promise<null> that will be non-null when sync is ongoing. Resolved when
* sync completes, rejected on error.
*/
promise: null,
/**
* See the document linked above that describes the sync algorithm.
*/
_start: Task.async(function* () {
log.info("Starting sync");
yield this._uploadStatusChanges();
yield this._uploadNewItems();
yield this._uploadDeletedItems();
yield this._downloadModifiedItems();
// TODO: "Repeat [this phase] until no conflicts occur," says the doc.
yield this._uploadMaterialChanges();
log.info("Sync done");
}),
/**
* Phase 1 part 1
*
* Uploads not-new items with status-only changes. By design, status-only
* changes will never conflict with what's on the server.
*/
_uploadStatusChanges: Task.async(function* () {
log.debug("Phase 1 part 1: Uploading status changes");
yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_STATUS,
ReadingList.SyncStatusProperties.STATUS);
}),
/**
* There are two phases for uploading changed not-new items: one for items
* with status-only changes, one for items with material changes. The two
* work similarly mechanically, and this method is a helper for both.
*
* @param syncStatus Local items matching this sync status will be uploaded.
* @param localProperties An array of local record property names. The
* uploaded item records will include only these properties.
*/
_uploadChanges: Task.async(function* (syncStatus, localProperties) {
// Get local items that match the given syncStatus.
let requests = [];
yield this.list.forEachItem(localItem => {
requests.push({
path: "/articles/" + localItem.guid,
body: serverRecordFromLocalItem(localItem, localProperties),
});
}, { syncStatus: syncStatus });
if (!requests.length) {
log.debug("No local changes to upload");
return;
}
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "PATCH",
},
requests: requests,
},
headers: {},
};
if (this._serverLastModifiedHeader) {
request.headers["If-Unmodified-Since"] = this._serverLastModifiedHeader;
}
let batchResponse = yield this._sendRequest(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading changes", batchResponse);
return;
}
// Update local items based on the response.
for (let response of batchResponse.body.responses) {
if (response.status == 404) {
// item deleted
yield this._deleteItemForGUID(response.body.id);
continue;
}
if (response.status == 412 || response.status == 409) {
// 412 Precondition failed: The item was modified since the last sync.
// 409 Conflict: A change violated a uniqueness constraint.
// In either case, mark the item as having material changes, and
// reconcile and upload it in the material-changes phase.
// TODO
continue;
}
if (response.status != 200) {
this._handleUnexpectedResponse("uploading a change", response);
continue;
}
let item = yield this._itemForGUID(response.body.id);
yield this._updateItemWithServerRecord(item, response.body);
}
}),
/**
* Phase 1 part 2
*
* Uploads new items.
*/
_uploadNewItems: Task.async(function* () {
log.debug("Phase 1 part 2: Uploading new items");
// Get new local items.
let requests = [];
yield this.list.forEachItem(localItem => {
requests.push({
body: serverRecordFromLocalItem(localItem, NEW_RECORD_PROPERTIES),
});
}, { syncStatus: ReadingList.SyncStatus.NEW });
if (!requests.length) {
log.debug("No new local items to upload");
return;
}
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "POST",
path: "/articles",
},
requests: requests,
},
headers: {},
};
if (this._serverLastModifiedHeader) {
request.headers["If-Unmodified-Since"] = this._serverLastModifiedHeader;
}
let batchResponse = yield this._sendRequest(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading new items", batchResponse);
return;
}
// Update local items based on the response.
for (let response of batchResponse.body.responses) {
if (response.status == 303) {
// "See Other": An item with the URL already exists. Mark the item as
// having material changes, and reconcile and upload it in the
// material-changes phase.
// TODO
continue;
}
// Note that the server seems to return a 200 if an identical item already
// exists, but we shouldn't be uploading identical items in this phase in
// normal usage, so treat 200 as an unexpected response.
if (response.status != 201) {
this._handleUnexpectedResponse("uploading a new item", response);
continue;
}
let item = yield this.list.itemForURL(response.body.url);
yield this._updateItemWithServerRecord(item, response.body);
}
}),
/**
* Phase 1 part 3
*
* Uploads deleted synced items.
*/
_uploadDeletedItems: Task.async(function* () {
log.debug("Phase 1 part 3: Uploading deleted items");
// Get deleted synced local items.
let requests = [];
yield this.list.forEachSyncedDeletedItem(localItem => {
requests.push({
path: "/articles/" + localItem.guid,
});
});
if (!requests.length) {
log.debug("No local deleted synced items to upload");
return;
}
// Send the request.
let request = {
method: "POST",
path: "/batch",
body: {
defaults: {
method: "DELETE",
},
requests: requests,
},
headers: {},
};
if (this._serverLastModifiedHeader) {
request.headers["If-Unmodified-Since"] = this._serverLastModifiedHeader;
}
let batchResponse = yield this._sendRequest(request);
if (batchResponse.status != 200) {
this._handleUnexpectedResponse("uploading deleted items", batchResponse);
return;
}
// Delete local items based on the response.
for (let response of batchResponse.body.responses) {
if (response.status == 412) {
// "Precondition failed": The item was modified since the last sync.
// Mark the item as having material changes, and reconcile and upload it
// in the material-changes phase.
// TODO
continue;
}
// A 404 means the item was already deleted on the server, which is OK.
// We still need to make sure it's deleted locally, though.
if (response.status != 200 && response.status != 404) {
this._handleUnexpectedResponse("uploading a deleted item", response);
continue;
}
yield this._deleteItemForGUID(response.body.id);
}
}),
/**
* Phase 2
*
* Downloads items that were modified since the last sync.
*/
_downloadModifiedItems: Task.async(function* () {
log.debug("Phase 2: Downloading modified items");
// Get modified items from the server.
let path = "/articles";
if (this._serverLastModifiedHeader) {
path += "?_since=" + this._serverLastModifiedHeader;
}
let request = {
method: "GET",
path: path,
headers: {},
};
if (this._serverLastModifiedHeader) {
request.headers["If-Modified-Since"] = this._serverLastModifiedHeader;
}
let response = yield this._sendRequest(request);
if (response.status == 304) {
// not modified
log.debug("No server changes");
return;
}
if (response.status != 200) {
this._handleUnexpectedResponse("downloading modified items", response);
return;
}
// Update local items based on the response.
for (let serverRecord of response.body.items) {
let localItem = yield this._itemForGUID(serverRecord.id);
if (localItem) {
if (localItem.serverLastModified == serverRecord.last_modified) {
// We just uploaded this item in the new-items phase.
continue;
}
// The local item may have materially changed. In that case, don't
// overwrite the local changes with the server record. Instead, mark
// the item as having material changes and reconcile and upload it in
// the material-changes phase.
// TODO
if (serverRecord.deleted) {
yield this._deleteItemForGUID(serverRecord.id);
continue;
}
yield this._updateItemWithServerRecord(localItem, serverRecord);
continue;
}
// new item
yield this.list.addItem(localRecordFromServerRecord(serverRecord));
}
}),
/**
* Phase 3 (material changes)
*
* Uploads not-new items with material changes.
*/
_uploadMaterialChanges: Task.async(function* () {
log.debug("Phase 3: Uploading material changes");
yield this._uploadChanges(ReadingList.SyncStatus.CHANGED_MATERIAL,
MUTABLE_RECORD_PROPERTIES);
}),
/**
* Gets the local ReadingListItem with the given GUID.
*
* @param guid The item's GUID.
* @return The matching ReadingListItem.
*/
_itemForGUID: Task.async(function* (guid) {
return (yield this.list.item({ guid: guid }));
}),
/**
* Updates the given local ReadingListItem with the given server record. The
* local item's sync status is updated to reflect the fact that the item has
* been synced and is up to date.
*
* @param item A local ReadingListItem.
* @param serverRecord A server record representing the item.
*/
_updateItemWithServerRecord: Task.async(function* (localItem, serverRecord) {
if (!localItem) {
throw new Error("Item should exist");
}
localItem._record = localRecordFromServerRecord(serverRecord);
yield this.list.updateItem(localItem);
}),
/**
* Truly deletes the local ReadingListItem with the given GUID.
*
* @param guid The item's GUID.
*/
_deleteItemForGUID: Task.async(function* (guid) {
let item = yield this._itemForGUID(guid);
if (item) {
// If item is non-null, then it hasn't been deleted locally. Therefore
// it's important to delete it through its list so that the list and its
// consumers are notified properly. Set the syncStatus to NEW so that the
// list truly deletes the item.
item._record.syncStatus = ReadingList.SyncStatus.NEW;
yield this.list.deleteItem(item);
return;
}
// If item is null, then it may not actually exist locally, or it may have
// been synced and then deleted so that it's marked as being deleted. In
// that case, try to delete it directly from the store. As far as the list
// is concerned, the item has already been deleted.
log.debug("Item not present in list, deleting it by GUID instead");
this.list._store.deleteItemByGUID(guid);
}),
/**
* Sends a request to the server.
*
* @param req The request object: { method, path, body, headers }.
* @return Promise<response> Resolved with the server's response object:
* { status, body, headers }.
*/
_sendRequest: Task.async(function* (req) {
log.debug("Sending request", req);
let response = yield this._client.request(req);
log.debug("Received response", response);
// Response header names are lowercase.
if (response.headers && "last-modified" in response.headers) {
this._serverLastModifiedHeader = response.headers["last-modified"];
}
return response;
}),
_handleUnexpectedResponse(contextMsgFragment, response) {
log.warn(`Unexpected response ${contextMsgFragment}`, response);
},
// TODO: Wipe this pref when user logs out.
get _serverLastModifiedHeader() {
if (!("__serverLastModifiedHeader" in this)) {
this.__serverLastModifiedHeader =
Preferences.get(SERVER_LAST_MODIFIED_HEADER_PREF, undefined);
}
return this.__serverLastModifiedHeader;
},
set _serverLastModifiedHeader(val) {
this.__serverLastModifiedHeader = val;
Preferences.set(SERVER_LAST_MODIFIED_HEADER_PREF, val);
},
};
/**
* Translates a local ReadingListItem into a server record.
*
* @param localItem The local ReadingListItem.
* @param localProperties An array of local item property names. Only these
* properties will be included in the server record.
* @return The server record.
*/
function serverRecordFromLocalItem(localItem, localProperties) {
let serverRecord = {};
for (let localProp of localProperties) {
let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
if (localProp in localItem._record) {
serverRecord[serverProp] = localItem._record[localProp];
}
}
return serverRecord;
}
/**
* Translates a server record into a local record. The returned local record's
* syncStatus will reflect the fact that the local record is up-to-date synced.
*
* @param serverRecord The server record.
* @return The local record.
*/
function localRecordFromServerRecord(serverRecord) {
let localRecord = {
// Mark the record as being up-to-date synced.
syncStatus: ReadingList.SyncStatus.SYNCED,
};
for (let localProp in SERVER_PROPERTIES_BY_LOCAL_PROPERTIES) {
let serverProp = SERVER_PROPERTIES_BY_LOCAL_PROPERTIES[localProp];
if (serverProp in serverRecord) {
localRecord[localProp] = serverRecord[serverProp];
}
}
return localRecord;
}
Object.defineProperty(this, "Sync", {
get() {
if (!this._singleton) {
this._singleton = new SyncImpl(ReadingList);
}
return this._singleton;
},
});

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

@ -9,6 +9,7 @@ EXTRA_JS_MODULES.readinglist += [
'Scheduler.jsm',
'ServerClient.jsm',
'SQLiteStore.jsm',
'Sync.jsm',
]
TESTING_JS_MODULES += [

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

@ -301,6 +301,11 @@ let RLSidebar = {
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
let currentUrl = mainWindow.gBrowser.currentURI.spec;
if (currentUrl.startsWith("about:reader"))
url = "about:reader?url=" + encodeURIComponent(url);
mainWindow.openUILink(url, event);
},
@ -402,6 +407,10 @@ let RLSidebar = {
// TODO: Refactor this so we pass a direction to a generic method.
// See autocomplete.xml's getNextIndex
event.preventDefault();
if (!this.numItems) {
return;
}
let index = this.selectedIndex + 1;
if (index >= this.numItems) {
index = 0;
@ -412,6 +421,9 @@ let RLSidebar = {
} else if (event.keyCode == KeyEvent.DOM_VK_UP) {
event.preventDefault();
if (!this.numItems) {
return;
}
let index = this.selectedIndex - 1;
if (index < 0) {
index = this.numItems - 1;
@ -422,7 +434,7 @@ let RLSidebar = {
} else if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
let selectedItem = this.selectedItem;
if (selectedItem) {
this.activeItem = this.selectedItem;
this.activeItem = selectedItem;
this.openActiveItem(event);
}
}

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

@ -38,7 +38,6 @@ add_task(function* prepare() {
title: `title ${i}`,
excerpt: `excerpt ${i}`,
unread: 0,
lastModified: Date.now(),
favorite: 0,
isArticle: 1,
storedOn: Date.now(),
@ -137,7 +136,26 @@ add_task(function* constraints() {
catch (e) {
err = e;
}
checkError(err);
Assert.ok(err);
Assert.ok(err instanceof Cu.getGlobalForObject(ReadingList).Error, err);
Assert.equal(err.message, "The item must have a url");
// update an item with no url
item = (yield gList.item({ guid: gItems[0].guid }));
Assert.ok(item);
let oldURL = item._record.url;
item._record.url = null;
err = null;
try {
yield gList.updateItem(item);
}
catch (e) {
err = e;
}
item._record.url = oldURL;
Assert.ok(err);
Assert.ok(err instanceof Cu.getGlobalForObject(ReadingList).Error, err);
Assert.equal(err.message, "The item must have a url");
// add an item with a bogus property
item = kindOfClone(gItems[0]);
@ -269,6 +287,19 @@ add_task(function* forEachItem() {
checkItems(items, [gItems[0], gItems[1]]);
});
add_task(function* forEachSyncedDeletedItem() {
let deletedItem = yield gList.addItem({
guid: "forEachSyncedDeletedItem",
url: "http://example.com/forEachSyncedDeletedItem",
});
deletedItem._record.syncStatus = gList.SyncStatus.SYNCED;
yield gList.deleteItem(deletedItem);
let items = [];
yield gList.forEachSyncedDeletedItem(item => items.push(item));
Assert.equal(items.length, 1);
Assert.equal(items[0].guid, deletedItem.guid);
});
add_task(function* forEachItem_promises() {
// promises resolved immediately
let items = [];
@ -540,36 +571,22 @@ add_task(function* item_setRecord() {
let item = (yield iter.items(1))[0];
Assert.ok(item);
// Set item._record without an updateItem. After fetching the item again, its
// title should be the old title.
let oldTitle = item.title;
let newTitle = "item_setRecord title 1";
Assert.notEqual(oldTitle, newTitle);
item._record.title = newTitle;
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
});
let sameItem = (yield iter.items(1))[0];
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, oldTitle);
// Set item._record followed by an updateItem. After fetching the item again,
// its title should be the new title.
newTitle = "item_setRecord title 2";
let newTitle = "item_setRecord title 1";
item._record.title = newTitle;
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
});
sameItem = (yield iter.items(1))[0];
let sameItem = (yield iter.items(1))[0];
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, newTitle);
// Set item.title directly and call updateItem. After fetching the item
// again, its title should be the new title.
newTitle = "item_setRecord title 3";
newTitle = "item_setRecord title 2";
item.title = newTitle;
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
@ -678,11 +695,9 @@ add_task(function* deleteItem() {
function checkItems(actualItems, expectedItems) {
Assert.equal(actualItems.length, expectedItems.length);
for (let i = 0; i < expectedItems.length; i++) {
for (let prop in expectedItems[i]) {
if (prop != "list") {
Assert.ok(prop in actualItems[i]._record, prop);
Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
}
for (let prop in expectedItems[i]._record) {
Assert.ok(prop in actualItems[i]._record, prop);
Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
}
}
}

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

@ -161,100 +161,88 @@ add_task(function* constraints() {
yield gStore.deleteItemByURL(url1);
yield gStore.deleteItemByURL(url2);
let items = [];
yield gStore.forEachItem(i => items.push(i), { url: [url1, url2] });
yield gStore.forEachItem(i => items.push(i), [{ url: [url1, url2] }]);
Assert.equal(items.length, 0);
// add a new item with no url, which is not allowed
item = kindOfClone(gItems[0]);
delete item.url;
err = null;
try {
yield gStore.addItem(item);
}
catch (e) {
err = e;
}
checkError(err, "NOT NULL constraint failed: items.url");
});
add_task(function* count() {
let count = yield gStore.count();
Assert.equal(count, gItems.length);
count = yield gStore.count({
count = yield gStore.count([{
guid: gItems[0].guid,
});
}]);
Assert.equal(count, 1);
});
add_task(function* forEachItem() {
// all items
let items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
sort: "guid",
});
}]);
checkItems(items, gItems);
// first item
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
limit: 1,
sort: "guid",
});
}]);
checkItems(items, gItems.slice(0, 1));
// last item
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
limit: 1,
sort: "guid",
descending: true,
});
}]);
checkItems(items, gItems.slice(gItems.length - 1, gItems.length));
// match on a scalar property
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems[0].guid,
});
}]);
checkItems(items, gItems.slice(0, 1));
// match on an array
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
sort: "guid",
});
}]);
checkItems(items, gItems);
// match on AND'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
title: gItems[0].title,
sort: "guid",
});
}]);
checkItems(items, [gItems[0]]);
// match on OR'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems[1].guid,
sort: "guid",
}, {
guid: gItems[0].guid,
});
}]);
checkItems(items, [gItems[0], gItems[1]]);
// match on AND'ed and OR'ed properties
items = [];
yield gStore.forEachItem(item => items.push(item), {
yield gStore.forEachItem(item => items.push(item), [{
guid: gItems.map(i => i.guid),
title: gItems[1].title,
sort: "guid",
}, {
guid: gItems[0].guid,
});
}]);
checkItems(items, [gItems[0], gItems[1]]);
});
@ -263,9 +251,21 @@ add_task(function* updateItem() {
gItems[0].title = newTitle;
yield gStore.updateItem(gItems[0]);
let item;
yield gStore.forEachItem(i => item = i, {
yield gStore.forEachItem(i => item = i, [{
guid: gItems[0].guid,
});
}]);
Assert.ok(item);
Assert.equal(item.title, gItems[0].title);
});
add_task(function* updateItemByGUID() {
let newTitle = "updateItemByGUID";
gItems[0].title = newTitle;
yield gStore.updateItemByGUID(gItems[0]);
let item;
yield gStore.forEachItem(i => item = i, [{
guid: gItems[0].guid,
}]);
Assert.ok(item);
Assert.equal(item.title, gItems[0].title);
});
@ -276,27 +276,30 @@ add_task(function* deleteItemByURL() {
yield gStore.deleteItemByURL(gItems[0].url);
Assert.equal((yield gStore.count()), gItems.length - 1);
let items = [];
yield gStore.forEachItem(i => items.push(i), {
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
});
}]);
checkItems(items, gItems.slice(1));
// delete second item
yield gStore.deleteItemByURL(gItems[1].url);
Assert.equal((yield gStore.count()), gItems.length - 2);
items = [];
yield gStore.forEachItem(i => items.push(i), {
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
});
}]);
checkItems(items, gItems.slice(2));
});
// This test deletes items so it should probably run last.
add_task(function* deleteItemByGUID() {
// delete third item
yield gStore.deleteItemByURL(gItems[2].url);
yield gStore.deleteItemByGUID(gItems[2].guid);
Assert.equal((yield gStore.count()), gItems.length - 3);
items = [];
yield gStore.forEachItem(i => items.push(i), {
let items = [];
yield gStore.forEachItem(i => items.push(i), [{
sort: "guid",
});
}]);
checkItems(items, gItems.slice(3));
});

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

@ -0,0 +1,330 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gProfildDirFile = do_get_profile();
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource:///modules/readinglist/Sync.jsm");
let { localRecordFromServerRecord } =
Cu.import("resource:///modules/readinglist/Sync.jsm", {});
let gList;
let gSync;
let gClient;
let gLocalItems = [];
function run_test() {
run_next_test();
}
add_task(function* prepare() {
gSync = Sync;
gList = Sync.list;
let dbFile = gProfildDirFile.clone();
dbFile.append(gSync.list._store.pathRelativeToProfileDir);
do_register_cleanup(function* () {
// Wait for the list's store to close its connection to the database.
yield gList.destroy();
if (dbFile.exists()) {
dbFile.remove(true);
}
});
gClient = new MockClient();
gSync._client = gClient;
let dumpAppender = new Log.DumpAppender();
dumpAppender.level = Log.Level.All;
let logNames = [
"readinglist.sync",
];
for (let name of logNames) {
let log = Log.repository.getLogger(name);
log.level = Log.Level.All;
log.addAppender(dumpAppender);
}
});
add_task(function* uploadNewItems() {
// Add some local items.
for (let i = 0; i < 3; i++) {
let record = {
url: `http://example.com/${i}`,
title: `title ${i}`,
addedBy: "device name",
};
gLocalItems.push(yield gList.addItem(record));
}
Assert.ok(!("resolvedURL" in gLocalItems[0]._record));
yield gSync.start();
// The syncer should update local items with the items in the server response.
// e.g., the item didn't have a resolvedURL before sync, but after sync it
// should.
Assert.ok("resolvedURL" in gLocalItems[0]._record);
checkItems(gClient.items, gLocalItems);
});
add_task(function* uploadStatusChanges() {
// Change an item's unread from true to false.
Assert.ok(gLocalItems[0].unread === true);
gLocalItems[0].unread = false;
yield gList.updateItem(gLocalItems[0]);
yield gSync.start();
Assert.ok(gLocalItems[0].unread === false);
checkItems(gClient.items, gLocalItems);
});
add_task(function* downloadChanges() {
// Change an item on the server.
let newTitle = "downloadChanges new title";
let response = yield gClient.request({
method: "PATCH",
path: "/articles/1",
body: {
title: newTitle,
},
});
Assert.equal(response.status, 200);
// Add a new item on the server.
let newRecord = {
url: "http://example.com/downloadChanges-new-item",
title: "downloadChanges 2",
added_by: "device name",
};
response = yield gClient.request({
method: "POST",
path: "/articles",
body: newRecord,
});
Assert.equal(response.status, 201);
// Delete an item on the server.
response = yield gClient.request({
method: "DELETE",
path: "/articles/2",
});
Assert.equal(response.status, 200);
yield gSync.start();
// Refresh the list of local items. The changed item should be changed
// locally, the deleted item should be deleted locally, and the new item
// should appear in the list.
gLocalItems = (yield gList.iterator({ sort: "guid" }).
items(gLocalItems.length));
Assert.equal(gLocalItems[1].title, newTitle);
Assert.equal(gLocalItems[2].url, newRecord.url);
checkItems(gClient.items, gLocalItems);
});
function MockClient() {
this._items = [];
this._nextItemID = 0;
this._nextLastModifiedToken = 0;
}
MockClient.prototype = {
request(req) {
let response = this._routeRequest(req);
return new Promise(resolve => {
// Resolve the promise asyncly, just as if this were a real server, so
// that we don't somehow end up depending on sync behavior.
setTimeout(() => {
resolve(response);
}, 0);
});
},
get items() {
return this._items.slice().sort((item1, item2) => {
return item2.id < item1.id;
});
},
itemByID(id) {
return this._items.find(item => item.id == id);
},
itemByURL(url) {
return this._items.find(item => item.url == url);
},
_items: null,
_nextItemID: null,
_nextLastModifiedToken: null,
_routeRequest(req) {
for (let prop in this) {
let match = (new RegExp("^" + prop + "$")).exec(req.path);
if (match) {
let handler = this[prop];
let method = req.method.toLowerCase();
if (!(method in handler)) {
throw new Error(`Handler ${prop} does not support method ${method}`);
}
let response = handler[method].call(this, req.body, match);
// Make sure the response really is JSON'able (1) as a kind of sanity
// check, (2) to convert any non-primitives (e.g., new String()) into
// primitives, and (3) because that's what the real server returns.
response = JSON.parse(JSON.stringify(response));
return response;
}
}
throw new Error(`Unrecognized path: ${req.path}`);
},
// route handlers
"/articles": {
get(body) {
return new MockResponse(200, {
// No URL params supported right now.
items: this.items,
});
},
post(body) {
let existingItem = this.itemByURL(body.url);
if (existingItem) {
// The real server seems to return a 200 if the items are identical.
if (areSameItems(existingItem, body)) {
return new MockResponse(200);
}
// 303 see other
return new MockResponse(303, {
id: existingItem.id,
});
}
body.id = new String(this._nextItemID++);
let defaultProps = {
last_modified: this._nextLastModifiedToken,
preview: "",
resolved_url: body.url,
resolved_title: body.title,
excerpt: "",
archived: 0,
deleted: 0,
favorite: false,
is_article: true,
word_count: null,
unread: true,
added_on: null,
stored_on: this._nextLastModifiedToken,
marked_read_by: null,
marked_read_on: null,
read_position: null,
};
for (let prop in defaultProps) {
if (!(prop in body) || body[prop] === null) {
body[prop] = defaultProps[prop];
}
}
this._nextLastModifiedToken++;
this._items.push(body);
// 201 created
return new MockResponse(201, body);
},
},
"/articles/([^/]+)": {
get(body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
return new MockResponse(200, item);
},
patch(body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
for (let prop in body) {
item[prop] = body[prop];
}
item.last_modified = this._nextLastModifiedToken++;
return new MockResponse(200, item);
},
delete(body, routeMatch) {
let id = routeMatch[1];
let item = this.itemByID(id);
if (!item) {
return new MockResponse(404);
}
item.deleted = true;
return new MockResponse(200);
},
},
"/batch": {
post(body) {
let responses = [];
let defaults = body.defaults || {};
for (let request of body.requests) {
for (let prop in defaults) {
if (!(prop in request)) {
request[prop] = defaults[prop];
}
}
responses.push(this._routeRequest(request));
}
return new MockResponse(200, {
defaults: defaults,
responses: responses,
});
},
},
};
function MockResponse(status, body, headers={}) {
this.status = status;
this.body = body;
this.headers = headers;
}
function areSameItems(item1, item2) {
for (let prop in item1) {
if (!(prop in item2) || item1[prop] != item2[prop]) {
return false;
}
}
for (let prop in item2) {
if (!(prop in item1) || item1[prop] != item2[prop]) {
return false;
}
}
return true;
}
function checkItems(serverRecords, localItems) {
serverRecords = serverRecords.map(r => localRecordFromServerRecord(r));
serverRecords = serverRecords.filter(r => !r.deleted);
Assert.equal(serverRecords.length, localItems.length);
for (let i = 0; i < serverRecords.length; i++) {
for (let prop in localItems[i]._record) {
Assert.ok(prop in serverRecords[i], prop);
Assert.equal(serverRecords[i][prop], localItems[i]._record[prop]);
}
}
}

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

@ -26,6 +26,17 @@ function promiseObserver(topic) {
});
}
function ReadingListMock() {
this.listener = null;
}
ReadingListMock.prototype = {
addListener(listener) {
ok(!this.listener, "mock only expects 1 listener");
this.listener = listener;
},
}
function createScheduler(options) {
// avoid typos in the test and other footguns in the options.
let allowedOptions = ["expectedDelay", "expectNewTimer", "syncFunction"];
@ -34,10 +45,11 @@ function createScheduler(options) {
throw new Error("Invalid option " + key);
}
}
let scheduler = createTestableScheduler();
let rlMock = new ReadingListMock();
let scheduler = createTestableScheduler(rlMock);
// make our hooks
let syncFunction = options.syncFunction || Promise.resolve;
scheduler._engine.sync = syncFunction;
scheduler._engine.start = syncFunction;
// we expect _setTimeout to be called *twice* - first is the initial sync,
// and there's no need to test the delay used for that. options.expectedDelay
// is to check the *subsequent* timer.
@ -90,6 +102,27 @@ add_task(function* testSuccess() {
scheduler.finalize();
});
// Test that if we get a reading list notification while we are syncing we
// immediately start a new one when it complets.
add_task(function* testImmediateResyncWhenChangedDuringSync() {
// promises which resolve once we've got all the expected notifications.
let allNotifications = [
promiseObserver("readinglist:sync:start"),
promiseObserver("readinglist:sync:finish"),
];
prefs.set("schedule", 100);
// New delay should be "immediate".
let scheduler = createScheduler({
expectedDelay: 0,
syncFunction: () => {
// we are now syncing - pretend the readinglist has an item change
scheduler.readingList.listener.onItemAdded();
return Promise.resolve();
}});
yield Promise.all(allNotifications);
scheduler.finalize();
});
add_task(function* testOffline() {
let scheduler = createScheduler({expectNewTimer: false});
Services.io.offline = true;

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

@ -6,3 +6,4 @@ firefox-appdir = browser
[test_ServerClient.js]
[test_scheduler.js]
[test_SQLiteStore.js]
[test_Sync.js]

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

@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-common/rest.js");
Cu.import("resource://gre/modules/Http.jsm");
// The maximum amount of net data allowed per request on Bing's API.
const MAX_REQUEST_DATA = 5000; // Documentation says 10000 but anywhere
@ -129,13 +129,13 @@ this.BingTranslator.prototype = {
* returned by the public `translate()` method when there's no pending.
* request left.
*
* @param aError [optional] The RESTRequest that failed.
* @param aError [optional] The XHR object of the request that failed.
*/
_chunkFailed: function(aError) {
if (aError instanceof RESTRequest &&
[400, 401].indexOf(aError.response.status) != -1) {
let body = aError.response.body;
if (body.contains("TranslateApiException") &&
if (aError instanceof Ci.nsIXMLHttpRequest &&
[400, 401].indexOf(aError.status) != -1) {
let body = aError.responseText;
if (body && body.contains("TranslateApiException") &&
(body.contains("balance") || body.contains("active state")))
this._serviceUnavailable = true;
}
@ -178,13 +178,9 @@ this.BingTranslator.prototype = {
* @returns boolean True if parsing of this chunk was successful.
*/
_parseChunkResult: function(bingRequest) {
let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Ci.nsIDOMParser);
let results;
try {
let doc = domParser.parseFromString(bingRequest.networkRequest
.response.body, "text/xml");
let doc = bingRequest.networkRequest.responseXML;
results = doc.querySelectorAll("TranslatedText");
} catch (e) {
return false;
@ -291,15 +287,18 @@ BingRequest.prototype = {
*/
fireRequest: function() {
return Task.spawn(function *(){
// Prepare authentication.
let token = yield BingTokenManager.getToken();
let auth = "Bearer " + token;
let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
"browser.translation.bing.translateArrayURL",
false);
let request = new RESTRequest(url);
request.setHeader("Content-type", "text/xml");
request.setHeader("Authorization", auth);
// Prepare URL.
let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
"browser.translation.bing.translateArrayURL");
// Prepare request headers.
let headers = [["Content-type", "text/xml"], ["Authorization", auth]];
// Prepare the request body.
let requestString =
'<TranslateArrayRequest>' +
'<AppId/>' +
@ -319,16 +318,24 @@ BingRequest.prototype = {
'<To>' + this.targetLanguage + '</To>' +
'</TranslateArrayRequest>';
let utf8 = CommonUtils.encodeUTF8(requestString);
// Set up request options.
let deferred = Promise.defer();
request.post(utf8, function(err) {
if (request.error || !request.response.success)
deferred.reject(request);
let options = {
onLoad: (function(responseText, xhr) {
deferred.resolve(this);
}).bind(this),
onError: function(e, responseText, xhr) {
deferred.reject(xhr);
},
postData: requestString,
headers: headers
};
deferred.resolve(this);
}.bind(this));
// Fire the request.
let request = httpRequest(url, options);
// Override the response MIME type.
request.overrideMimeType("text/xml");
this.networkRequest = request;
return deferred.promise;
}.bind(this));
@ -373,45 +380,46 @@ let BingTokenManager = {
*/
_getNewToken: function() {
let url = getUrlParam("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13",
"browser.translation.bing.authURL",
false);
let request = new RESTRequest(url);
request.setHeader("Content-type", "application/x-www-form-urlencoded");
"browser.translation.bing.authURL");
let params = [
"grant_type=client_credentials",
"scope=" + encodeURIComponent("http://api.microsofttranslator.com"),
"client_id=" +
getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
"client_secret=" +
getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
["grant_type", "client_credentials"],
["scope", "http://api.microsofttranslator.com"],
["client_id",
getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride")],
["client_secret",
getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")]
];
let deferred = Promise.defer();
this._pendingRequest = deferred.promise;
request.post(params.join("&"), function(err) {
BingTokenManager._pendingRequest = null;
let options = {
onLoad: function(responseText, xhr) {
BingTokenManager._pendingRequest = null;
try {
let json = JSON.parse(responseText);
if (err) {
deferred.reject(err);
}
if (json.error) {
deferred.reject(json.error);
return;
}
try {
let json = JSON.parse(this.response.body);
if (json.error) {
deferred.reject(json.error);
return;
let token = json.access_token;
let expires_in = json.expires_in;
BingTokenManager._currentToken = token;
BingTokenManager._currentExpiryTime = new Date(Date.now() + expires_in * 1000);
deferred.resolve(token);
} catch (e) {
deferred.reject(e);
}
let token = json.access_token;
let expires_in = json.expires_in;
BingTokenManager._currentToken = token;
BingTokenManager._currentExpiryTime = new Date(Date.now() + expires_in * 1000);
deferred.resolve(token);
} catch (e) {
},
onError: function(e, responseText, xhr) {
BingTokenManager._pendingRequest = null;
deferred.reject(e);
}
});
},
postData: params
};
this._pendingRequest = deferred.promise;
let request = httpRequest(url, options);
return deferred.promise;
}
@ -433,10 +441,9 @@ function escapeXML(aStr) {
* Fetch an auth token (clientID or client secret), which may be overridden by
* a pref if it's set.
*/
function getUrlParam(paramValue, prefName, encode = true) {
function getUrlParam(paramValue, prefName) {
if (Services.prefs.getPrefType(prefName))
paramValue = Services.prefs.getCharPref(prefName);
paramValue = Services.urlFormatter.formatURL(paramValue);
return encode ? encodeURIComponent(paramValue) : paramValue;
return paramValue;
}

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

@ -151,7 +151,7 @@ function reallyHandleRequest(req, res) {
let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
log("contentType: " + contentType);
if (contentType == "text/xml") {
if (contentType.startsWith("text/xml")) {
try {
// For all these requests the client needs to supply the correct
// authentication headers.

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

@ -166,7 +166,7 @@ ul.children + .tag-line::before {
margin-right: 0;
}
.tag-state.flash-out {
.flash-out {
transition: background .5s;
}

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

@ -27,6 +27,7 @@ const promise = require("resource://gre/modules/Promise.jsm").Promise;
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const EventEmitter = require("devtools/toolkit/event-emitter");
const Heritage = require("sdk/core/heritage");
const {setTimeout, clearTimeout, setInterval, clearInterval} = require("sdk/timers");
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
@ -174,7 +175,7 @@ MarkupView.prototype = {
let docEl = this.doc.documentElement;
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
clearInterval(this._scrollInterval);
}
// Auto-scroll when the mouse approaches top/bottom edge
@ -189,7 +190,7 @@ MarkupView.prototype = {
// Here, we use minus because the value of speed - 15 is always negative
// and it makes the speed relative to the distance between mouse and edge
// the closer to the edge, the faster
this._scrollInterval = this.win.setInterval(() => {
this._scrollInterval = setInterval(() => {
docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
}, 0);
}
@ -199,7 +200,7 @@ MarkupView.prototype = {
let speed = map(distanceFromTop, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
this._scrollInterval = this.win.setInterval(() => {
this._scrollInterval = setInterval(() => {
docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
}, 0);
}
@ -256,7 +257,7 @@ MarkupView.prototype = {
this.indicateDragTarget(null);
}
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
clearInterval(this._scrollInterval);
}
},
@ -281,7 +282,7 @@ MarkupView.prototype = {
_onMouseLeave: function() {
if (this._scrollInterval) {
this.win.clearInterval(this._scrollInterval);
clearInterval(this._scrollInterval);
}
if (this.isDragging) return;
@ -320,13 +321,13 @@ MarkupView.prototype = {
let win = this._frame.contentWindow;
if (this._briefBoxModelTimer) {
win.clearTimeout(this._briefBoxModelTimer);
clearTimeout(this._briefBoxModelTimer);
this._briefBoxModelTimer = null;
}
this._showBoxModel(nodeFront);
this._briefBoxModelTimer = this._frame.contentWindow.setTimeout(() => {
this._briefBoxModelTimer = setTimeout(() => {
this._hideBoxModel();
}, NEW_SELECTION_HIGHLIGHTER_TIMER);
},
@ -780,11 +781,16 @@ MarkupView.prototype = {
let addedOrEditedContainers = new Set();
let removedContainers = new Set();
for (let {type, target, added, removed} of aMutations) {
for (let {type, target, added, removed, newValue} of aMutations) {
let container = this.getContainer(target);
if (container) {
if (type === "attributes" || type === "characterData") {
if (type === "characterData") {
addedOrEditedContainers.add(container);
} else if (type === "attributes" && newValue === null) {
// Removed attributes should flash the entire node.
// New or changed attributes will flash the attribute itself
// in ElementEditor.flashAttribute.
addedOrEditedContainers.add(container);
} else if (type === "childList") {
// If there has been removals, flash the parent
@ -1519,9 +1525,9 @@ MarkupView.prototype = {
}
let win = this._frame.contentWindow;
this._previewBar.classList.add("hide");
win.clearTimeout(this._resizePreviewTimeout);
clearTimeout(this._resizePreviewTimeout);
win.setTimeout(() => {
setTimeout(() => {
this._updatePreview();
this._previewBar.classList.remove("hide");
}, 1000);
@ -1818,7 +1824,7 @@ MarkupContainer.prototype = {
// Start dragging the container after a delay.
this.markup._dragStartEl = target;
this.win.setTimeout(() => {
setTimeout(() => {
// Make sure the mouse is still down and on target.
if (!this._isMouseDown || this.markup._dragStartEl !== target ||
this.node.isPseudoElement || this.node.isAnonymous ||
@ -1883,47 +1889,17 @@ MarkupContainer.prototype = {
flashMutation: function() {
if (!this.selected) {
let contentWin = this.win;
this.flashed = true;
flashElementOn(this.tagState, this.editor.elt);
if (this._flashMutationTimer) {
contentWin.clearTimeout(this._flashMutationTimer);
clearTimeout(this._flashMutationTimer);
this._flashMutationTimer = null;
}
this._flashMutationTimer = contentWin.setTimeout(() => {
this.flashed = false;
this._flashMutationTimer = setTimeout(() => {
flashElementOff(this.tagState, this.editor.elt);
}, this.markup.CONTAINER_FLASHING_DURATION);
}
},
set flashed(aValue) {
if (aValue) {
// Make sure the animation class is not here
this.tagState.classList.remove("flash-out");
// Change the background
this.tagState.classList.add("theme-bg-contrast");
// Change the text color
this.editor.elt.classList.add("theme-fg-contrast");
[].forEach.call(
this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.add("theme-fg-contrast")
);
} else {
// Add the animation class to smoothly remove the background
this.tagState.classList.add("flash-out");
// Remove the background
this.tagState.classList.remove("theme-bg-contrast");
// Remove the text color
this.editor.elt.classList.remove("theme-fg-contrast");
[].forEach.call(
this.editor.elt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.remove("theme-fg-contrast")
);
}
},
_hovered: false,
/**
@ -2350,6 +2326,7 @@ function ElementEditor(aContainer, aNode) {
this.doc = this.markup.doc;
this.attrs = {};
this.animationTimers = {};
// The templates will fill the following properties
this.elt = null;
@ -2407,9 +2384,23 @@ function ElementEditor(aContainer, aNode) {
this.eventNode.style.display = this.node.hasEventListeners ? "inline-block" : "none";
this.update();
this.initialized = true;
}
ElementEditor.prototype = {
flashAttribute: function(attrName) {
if (this.animationTimers[attrName]) {
clearTimeout(this.animationTimers[attrName]);
}
flashElementOn(this.getAttributeElement(attrName));
this.animationTimers[attrName] = setTimeout(() => {
flashElementOff(this.getAttributeElement(attrName));
}, this.markup.CONTAINER_FLASHING_DURATION);
},
/**
* Update the state of the editor from the node.
*/
@ -2424,9 +2415,9 @@ ElementEditor.prototype = {
let el = this.attrs[attr.name];
let valueChanged = el && el.querySelector(".attr-value").innerHTML !== attr.value;
let isEditing = el && el.querySelector(".editable").inplaceEditor;
let needToCreateAttributeEditor = el && (!valueChanged || isEditing);
let canSimplyShowEditor = el && (!valueChanged || isEditing);
if (needToCreateAttributeEditor) {
if (canSimplyShowEditor) {
// Element already exists and doesn't need to be recreated.
// Just show it (it's hidden by default due to the template).
attrsToRemove.delete(el);
@ -2436,6 +2427,13 @@ ElementEditor.prototype = {
// has changed.
let attribute = this._createAttribute(attr);
attribute.style.removeProperty("display");
// Temporarily flash the attribute to highlight the change.
// But not if this is the first time the editor instance has
// been created.
if (this.initialized) {
this.flashAttribute(attr.name);
}
}
}
@ -2708,7 +2706,12 @@ ElementEditor.prototype = {
});
},
destroy: function() {}
destroy: function() {
for (let key in this.animationTimers) {
clearTimeout(this.animationTimers[key]);
}
this.animationTimers = null;
}
};
function nodeDocument(node) {
@ -2762,6 +2765,62 @@ function parseAttributeValues(attr, doc) {
return attributes.reverse();
}
/**
* Apply a 'flashed' background and foreground color to elements. Intended
* to be used with flashElementOff as a way of drawing attention to an element.
*
* @param {Node} backgroundElt
* The element to set the highlighted background color on.
* @param {Node} foregroundElt
* The element to set the matching foreground color on.
* Optional. This will equal backgroundElt if not set.
*/
function flashElementOn(backgroundElt, foregroundElt=backgroundElt) {
if (!backgroundElt || !foregroundElt) {
return;
}
// Make sure the animation class is not here
backgroundElt.classList.remove("flash-out");
// Change the background
backgroundElt.classList.add("theme-bg-contrast");
foregroundElt.classList.add("theme-fg-contrast");
[].forEach.call(
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.add("theme-fg-contrast")
);
}
/**
* Remove a 'flashed' background and foreground color to elements.
* See flashElementOn.
*
* @param {Node} backgroundElt
* The element to reomve the highlighted background color on.
* @param {Node} foregroundElt
* The element to remove the matching foreground color on.
* Optional. This will equal backgroundElt if not set.
*/
function flashElementOff(backgroundElt, foregroundElt=backgroundElt) {
if (!backgroundElt || !foregroundElt) {
return;
}
// Add the animation class to smoothly remove the background
backgroundElt.classList.add("flash-out");
// Remove the background
backgroundElt.classList.remove("theme-bg-contrast");
foregroundElt.classList.remove("theme-fg-contrast");
[].forEach.call(
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.remove("theme-fg-contrast")
);
}
/**
* Map a number from one range to another.
*/

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

@ -13,6 +13,8 @@ const TEST_URL = TEST_URL_ROOT + "doc_markup_flashing.html";
// Each item is an object:
// - desc: a description of the test step, for better logging
// - mutate: a function that should make changes to the content DOM
// - attribute: if set, the test will expect the corresponding attribute to flash
// instead of the whole node
// - flashedNode: [optional] the css selector of the node that is expected to
// flash in the markup-view as a result of the mutation.
// If missing, the rootNode (".list") will be expected to flash
@ -36,15 +38,28 @@ const TEST_DATA = [{
},
flashedNode: ".list .item:last-child"
}, {
desc: "Adding an attribute should flash the node",
desc: "Adding an attribute should flash the attribute",
attribute: "test-name",
mutate: (doc, rootNode) => {
rootNode.setAttribute("name-" + Date.now(), "value-" + Date.now());
rootNode.setAttribute("test-name", "value-" + Date.now());
}
}, {
desc: "Editing an attribute should flash the node",
desc: "Editing an attribute should flash the attribute",
attribute: "class",
mutate: (doc, rootNode) => {
rootNode.setAttribute("class", "list value-" + Date.now());
}
}, {
desc: "Multiple changes to an attribute should flash the attribute",
attribute: "class",
mutate: (doc, rootNode) => {
rootNode.removeAttribute("class");
rootNode.setAttribute("class", "list value-" + Date.now());
rootNode.setAttribute("class", "list value-" + Date.now());
rootNode.removeAttribute("class");
rootNode.setAttribute("class", "list value-" + Date.now());
rootNode.setAttribute("class", "list value-" + Date.now());
}
}, {
desc: "Removing an attribute should flash the node",
mutate: (doc, rootNode) => {
@ -66,7 +81,7 @@ add_task(function*() {
info("Selecting the last element of the root node before starting");
yield selectNode(".list .item:nth-child(2)", inspector);
for (let {mutate, flashedNode, desc} of TEST_DATA) {
for (let {mutate, flashedNode, desc, attribute} of TEST_DATA) {
info("Starting test: " + desc);
info("Mutating the DOM and listening for markupmutation event");
@ -80,7 +95,12 @@ add_task(function*() {
if (flashedNode) {
flashingNodeFront = yield getNodeFront(flashedNode, inspector);
}
yield assertNodeFlashing(flashingNodeFront, inspector);
if (attribute) {
yield assertAttributeFlashing(flashingNodeFront, attribute, inspector);
} else {
yield assertNodeFlashing(flashingNodeFront, inspector);
}
// Making sure the inspector has finished updating before moving on
yield updated;
@ -95,6 +115,20 @@ function* assertNodeFlashing(nodeFront, inspector) {
// Clear the mutation flashing timeout now that we checked the node was flashing
let markup = inspector.markup;
markup._frame.contentWindow.clearTimeout(container._flashMutationTimer);
clearTimeout(container._flashMutationTimer);
container._flashMutationTimer = null;
container.tagState.classList.remove("theme-bg-contrast");
}
function* assertAttributeFlashing(nodeFront, attribute, inspector) {
let container = getContainerForNodeFront(nodeFront, inspector);
ok(container, "Markup container for node found");
ok(container.editor.attrs[attribute], "Attribute exists on editor");
let attributeElement = container.editor.getAttributeElement(attribute);
ok(attributeElement.classList.contains("theme-bg-contrast"),
"Element for " + attribute + " attribute is flashing");
attributeElement.classList.remove("theme-bg-contrast");
}

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

@ -14,26 +14,37 @@ add_task(function*() {
yield inspector.markup.expandAll();
info("Selecting the test node");
let node = content.document.querySelector("#retag-me");
let child = content.document.querySelector("#retag-me-2");
yield selectNode("#retag-me", inspector);
info("Getting the markup-container for the test node");
let container = yield getContainerForSelector("#retag-me", inspector);
is(node.tagName, "DIV", "We've got #retag-me element, it's a DIV");
ok(container.expanded, "It is expanded");
is(child.parentNode, node, "Child #retag-me-2 is inside #retag-me");
ok(container.expanded, "The container is expanded");
info("Changing the tagname");
let parentInfo = yield getNodeInfo("#retag-me");
is(parentInfo.tagName.toLowerCase(), "div",
"We've got #retag-me element, it's a DIV");
is(parentInfo.numChildren, 1, "#retag-me has one child");
let childInfo = yield getNodeInfo("#retag-me > *");
is(childInfo.attributes[0].value, "retag-me-2",
"#retag-me's only child is #retag-me-2");
info("Changing #retag-me's tagname in the markup-view");
let mutated = inspector.once("markupmutation");
let tagEditor = container.editor.tag;
setEditableFieldValue(tagEditor, "p", inspector);
yield mutated;
info("Checking that the tagname change was done");
node = content.document.querySelector("#retag-me");
info("Checking that the markup-container exists and is correct");
container = yield getContainerForSelector("#retag-me", inspector);
is(node.tagName, "P", "We've got #retag-me, it should now be a P");
ok(container.expanded, "It is still expanded");
ok(container.selected, "It is still selected");
is(child.parentNode, node, "Child #retag-me-2 is still inside #retag-me");
ok(container.expanded, "The container is still expanded");
ok(container.selected, "The container is still selected");
info("Checking that the tagname change was done");
parentInfo = yield getNodeInfo("#retag-me");
is(parentInfo.tagName.toLowerCase(), "p",
"The #retag-me element is now a P");
is(parentInfo.numChildren, 1, "#retag-me still has one child");
childInfo = yield getNodeInfo("#retag-me > *");
is(childInfo.attributes[0].value, "retag-me-2",
"#retag-me's only child is #retag-me-2");
});

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

@ -9,6 +9,7 @@ let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let promise = devtools.require("resource://gre/modules/Promise.jsm").Promise;
let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
let clipboard = devtools.require("sdk/clipboard");
let {setTimeout, clearTimeout} = devtools.require("sdk/timers");
// All test are asynchronous
waitForExplicitFinish();
@ -47,6 +48,7 @@ registerCleanupFunction(function*() {
const TEST_URL_ROOT = "http://mochi.test:8888/browser/browser/devtools/markupview/test/";
const CHROME_BASE = "chrome://mochitests/content/browser/browser/devtools/markupview/test/";
const COMMON_FRAME_SCRIPT_URL = "chrome://browser/content/devtools/frame-script-utils.js";
/**
* Add a new test tab in the browser and load the given url.
@ -65,6 +67,9 @@ function addTab(url) {
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
let linkedBrowser = tab.linkedBrowser;
info("Loading the helper frame script " + COMMON_FRAME_SCRIPT_URL);
linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
linkedBrowser.addEventListener("load", function onload() {
linkedBrowser.removeEventListener("load", onload, true);
info("URL '" + url + "' loading complete");
@ -123,6 +128,50 @@ function openInspector() {
return def.promise;
}
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
* @param {String} name The message name
* @return {Promise} A promise that resolves to the response data when the
* message has been received
*/
function waitForContentMessage(name) {
info("Expecting message " + name + " from content");
let mm = gBrowser.selectedBrowser.messageManager;
let def = promise.defer();
mm.addMessageListener(name, function onMessage(msg) {
mm.removeMessageListener(name, onMessage);
def.resolve(msg.data);
});
return def.promise;
}
/**
* Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome).
* @param {String} name The message name. Should be one of the messages defined
* in doc_frame_script.js
* @param {Object} data Optional data to send along
* @param {Object} objects Optional CPOW objects to send along
* @param {Boolean} expectResponse If set to false, don't wait for a response
* with the same name from the content script. Defaults to true.
* @return {Promise} Resolves to the response data if a response is expected,
* immediately resolves otherwise
*/
function executeInContent(name, data={}, objects={}, expectResponse=true) {
info("Sending message " + name + " to content");
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage(name, data, objects);
if (expectResponse) {
return waitForContentMessage(name);
} else {
return promise.resolve();
}
}
/**
* Simple DOM node accesor function that takes either a node or a string css
* selector as argument and returns the corresponding node
@ -151,6 +200,15 @@ function getNodeFront(selector, {walker}) {
return walker.querySelector(walker.rootNode, selector);
}
/**
* Get information about a DOM element, identified by its selector.
* @param {String} selector.
* @return {Promise} a promise that resolves to the element's information.
*/
function getNodeInfo(selector) {
return executeInContent("devtools:test:getDomElementInfo", {selector});
}
/**
* Highlight a node and set the inspector's current selection to the node or
* the first match of the given css selector.

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

@ -133,6 +133,36 @@ addMessageListener("devtools:test:setStyle", function(msg) {
sendAsyncMessage("devtools:test:setStyle");
});
/**
* Get information about a DOM element, identified by a selector.
* @param {Object} data
* - {String} selector The CSS selector to get the node (can be a "super"
* selector).
* @return {Object} data Null if selector didn't match any node, otherwise:
* - {String} tagName.
* - {String} namespaceURI.
* - {Number} numChildren The number of children in the element.
* - {Array} attributes An array of {name, value, namespaceURI} objects.
*/
addMessageListener("devtools:test:getDomElementInfo", function(msg) {
let {selector} = msg.data;
let node = superQuerySelector(selector);
let info = null;
if (node) {
info = {
tagName: node.tagName,
namespaceURI: node.namespaceURI,
numChildren: node.children.length,
attributes: [...node.attributes].map(({name, value, namespaceURI}) => {
return {name, value, namespaceURI};
})
};
}
sendAsyncMessage("devtools:test:getDomElementInfo", info);
});
/**
* Set a given attribute value on a node.
* @param {Object} data

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

@ -193,7 +193,7 @@
</popupset>
<notificationbox flex="1" id="notificationbox">
<hbox flex="1">
<hbox flex="1" id="deck-panels">
<vbox id="project-listing-panel" class="project-listing" flex="1">
<div id="project-listing-wrapper">
<iframe id="project-listing-panel-details" flex="1" src="project-listing.xhtml"/>

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

@ -324,13 +324,15 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
/* Toolbox */
#notificationbox[toolboxfullscreen] > .devtools-horizontal-splitter,
#notificationbox[toolboxfullscreen] > #deck,
#notificationbox[toolboxfullscreen] > #deck > iframe {
#notificationbox[toolboxfullscreen] > .devtools-horizontal-splitter {
min-height: 0;
max-height: 0;
}
#notificationbox[toolboxfullscreen] > #deck-panels {
display: none;
}
#notificationbox[toolboxfullscreen] > #toolbox {
-moz-box-flex: 1;
}

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

@ -19,7 +19,7 @@ contextMenuSearch.accesskey=S
bookmarkAllTabsDefault=[Folder Name]
xpinstallPromptWarning=%S prevented this site (%S) from asking you to install software on your computer.
xpinstallPromptMessage=%S prevented this site from asking you to install software on your computer.
xpinstallPromptAllowButton=Allow
# Accessibility Note:
# Be sure you do not choose an accesskey that is used elsewhere in the active context (e.g. main menu bar, submenu of the warning popup button)
@ -30,15 +30,27 @@ xpinstallDisabledMessage=Software installation is currently disabled. Click Enab
xpinstallDisabledButton=Enable
xpinstallDisabledButton.accesskey=n
# LOCALIZATION NOTE (addonDownloading, addonDownloadCancelled, addonDownloadRestart):
# LOCALIZATION NOTE (addonDownloadingAndVerifying):
# Semicolon-separated list of plural forms. See:
# http://developer.mozilla.org/en/docs/Localization_and_Plurals
# Also see https://bugzilla.mozilla.org/show_bug.cgi?id=570012 for mockups
addonDownloading=Add-on downloading;Add-ons downloading
addonDownloadCancelled=Add-on download cancelled.;Add-on downloads cancelled.
addonDownloadRestart=Restart Download;Restart Downloads
addonDownloadRestart.accessKey=R
addonDownloadCancelTooltip=Cancel
addonDownloadingAndVerifying=Downloading and verifying add-on…;Downloading and verifying #1 add-ons…
addonDownloadVerifying=Verifying
addonInstall.cancelButton.label=Cancel
addonInstall.cancelButton.accesskey=C
addonInstall.acceptButton.label=Install
addonInstall.acceptButton.accesskey=I
# LOCALIZATION NOTE (addonConfirmInstallMessage):
# Semicolon-separated list of plural forms. See:
# http://developer.mozilla.org/en/docs/Localization_and_Plurals
# #1 is brandShortName
# #2 is the number of add-ons being installed
addonConfirmInstall.message=This site would like to install an add-on in #1:;This site would like to install #2 add-ons in #1:
# LOCALIZATION NOTE (addonConfirmInstall.author):
# %S is the add-on author's name
addonConfirmInstall.author=by %S
addonwatch.slow=%1$S might be making %2$S run slowly
addonwatch.disable.label=Disable %S

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

@ -331,3 +331,6 @@ infobar_button_gotit_label=Got it!
infobar_button_gotit_accesskey=G
infobar_menuitem_dontshowagain_label=Don't show this again
infobar_menuitem_dontshowagain_accesskey=D
# Context in conversation strings
context_offer_label=Let's talk about this page

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

@ -1132,11 +1132,6 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
margin-top: 5px;
}
/* Notification popup */
#notification-popup {
min-width: 280px;
}
.popup-notification-icon {
width: 64px;
height: 64px;
@ -1149,15 +1144,29 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
.popup-notification-icon[popupid="xpinstall-disabled"],
.popup-notification-icon[popupid="addon-progress"],
.popup-notification-icon[popupid="addon-install-cancelled"],
.popup-notification-icon[popupid="addon-install-blocked"],
.popup-notification-icon[popupid="addon-install-failed"],
.popup-notification-icon[popupid="addon-install-confirmation"],
.popup-notification-icon[popupid="addon-install-complete"] {
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
width: 32px;
height: 32px;
}
.popup-notification-description[popupid="addon-progress"],
.popup-notification-description[popupid="addon-install-confirmation"] {
width: 27em;
max-width: 27em;
}
.popup-progress-meter {
margin-top: .5em;
}
.addon-install-confirmation-name {
font-weight: bold;
}
.popup-notification-icon[popupid="click-to-play-plugins"] {
list-style-image: url(chrome://mozapps/skin/plugins/pluginBlocked-64.png);
}
@ -1166,29 +1175,6 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
list-style-image: url(chrome://browser/skin/notification-64.png);
}
.addon-progress-description {
width: 350px;
max-width: 350px;
}
.popup-progress-label,
.popup-progress-meter {
-moz-margin-start: 0;
-moz-margin-end: 0;
}
.popup-progress-cancel {
-moz-appearance: none;
background: transparent;
border: none;
padding: 0;
margin: 0;
-moz-margin-start: 5px;
min-height: 0;
min-width: 0;
list-style-image: url("moz-icon://stock/gtk-cancel?size=menu");
}
.popup-notification-icon[popupid="indexedDB-permissions-prompt"],
.popup-notification-icon[popupid*="offline-app-requested"],
.popup-notification-icon[popupid="offline-app-usage"] {

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

@ -4,10 +4,22 @@
%include ../../shared/readinglist/sidebar.inc.css
html {
border: 1px solid ThreeDShadow;
background-color: -moz-Field;
color: -moz-FieldText;
box-sizing: border-box;
}
.item {
-moz-padding-end: 0;
}
.item.active {
background-color: -moz-cellhighlight;
color: -moz-cellhighlighttext;
}
.item-title {
margin: 1px 0 0;
}

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

@ -4150,22 +4150,19 @@ notification[value="loop-sharing-notification"] .messageImage {
.popup-notification-icon[popupid="xpinstall-disabled"],
.popup-notification-icon[popupid="addon-progress"],
.popup-notification-icon[popupid="addon-install-cancelled"],
.popup-notification-icon[popupid="addon-install-blocked"],
.popup-notification-icon[popupid="addon-install-failed"],
.popup-notification-icon[popupid="addon-install-confirmation"],
.popup-notification-icon[popupid="addon-install-complete"] {
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
width: 32px;
height: 32px;
}
.popup-notification-icon[popupid="click-to-play-plugins"] {
list-style-image: url(chrome://mozapps/skin/plugins/pluginBlocked-64.png);
}
.addon-progress-description {
width: 350px;
max-width: 350px;
.popup-notification-description[popupid="addon-progress"],
.popup-notification-description[popupid="addon-install-confirmation"] {
width: 27em;
max-width: 27em;
}
.popup-progress-label,
@ -4174,24 +4171,18 @@ notification[value="loop-sharing-notification"] .messageImage {
-moz-margin-end: 0;
}
.popup-progress-cancel {
-moz-appearance: none;
min-height: 16px;
min-width: 16px;
max-height: 16px;
max-width: 16px;
padding: 0;
margin: 0 1px 0 1px;
list-style-image: url(chrome://mozapps/skin/downloads/buttons.png);
-moz-image-region: rect(0px, 16px, 16px, 0px);
.popup-progress-meter,
#addon-install-confirmation-content {
margin-top: 1em;
}
.popup-progress-cancel:hover {
-moz-image-region: rect(0px, 32px, 16px, 16px);
.addon-install-confirmation-name {
font-weight: bold;
-moz-margin-start: 0 !important; /* override default label margin to match description margin */
}
.popup-progress-cancel:active {
-moz-image-region: rect(0px, 48px, 16px, 32px);
.popup-notification-icon[popupid="click-to-play-plugins"] {
list-style-image: url(chrome://mozapps/skin/plugins/pluginBlocked-64.png);
}
.popup-notification-icon[popupid="indexedDB-permissions-prompt"],

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

@ -4,6 +4,10 @@
%include ../../shared/readinglist/sidebar.inc.css
html {
border-top: 1px solid #bdbdbd;
}
.item-title {
margin: 4px 0 0;
}

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

@ -10,7 +10,6 @@
body {
margin: 0;
font: message-box;
background: #F8F7F8;
color: #333333;
-moz-user-select: none;
overflow: hidden;

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

@ -2158,15 +2158,30 @@ toolbarbutton.bookmark-item[dragover="true"][open="true"] {
.popup-notification-icon[popupid="xpinstall-disabled"],
.popup-notification-icon[popupid="addon-progress"],
.popup-notification-icon[popupid="addon-install-cancelled"],
.popup-notification-icon[popupid="addon-install-blocked"],
.popup-notification-icon[popupid="addon-install-failed"],
.popup-notification-icon[popupid="addon-install-confirmation"],
.popup-notification-icon[popupid="addon-install-complete"] {
list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
width: 32px;
height: 32px;
}
.popup-notification-description[popupid="addon-progress"],
.popup-notification-description[popupid="addon-install-confirmation"] {
width: 27em;
max-width: 27em;
}
.popup-progress-meter,
#addon-install-confirmation-content {
margin-top: 1em;
}
.addon-install-confirmation-name {
font-weight: bold;
}
.popup-notification-icon[popupid="click-to-play-plugins"] {
list-style-image: url(chrome://mozapps/skin/plugins/pluginBlocked-64.png);
}
@ -2175,37 +2190,6 @@ toolbarbutton.bookmark-item[dragover="true"][open="true"] {
list-style-image: url(chrome://browser/skin/notification-64.png);
}
.addon-progress-description {
width: 350px;
max-width: 350px;
}
.popup-progress-label,
.popup-progress-meter {
-moz-margin-start: 0;
-moz-margin-end: 0;
}
.popup-progress-cancel {
-moz-appearance: none;
background: transparent;
border: none;
padding: 0;
margin: 0;
min-height: 0;
min-width: 0;
list-style-image: url(chrome://mozapps/skin/downloads/downloadButtons.png);
-moz-image-region: rect(0px, 32px, 16px, 16px);
}
.popup-progress-cancel:hover {
-moz-image-region: rect(16px, 32px, 32px, 16px);
}
.popup-progress-cancel:active {
-moz-image-region: rect(32px, 32px, 48px, 16px);
}
.popup-notification-icon[popupid="indexedDB-permissions-prompt"],
.popup-notification-icon[popupid*="offline-app-requested"],
.popup-notification-icon[popupid="offline-app-usage"] {

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

@ -4,6 +4,10 @@
%include ../../shared/readinglist/sidebar.inc.css
html {
background-color: #EEF3FA;
}
.item {
-moz-padding-end: 0;
}

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

@ -94,7 +94,6 @@ skip-if = e10s # Bug ?????? - event handler checks event.target is the content d
[browser_bug92473.js]
skip-if = e10s # Bug ?????? - event handler checks event.target is the content document and test e10s-utils doesn't do that.
[browser_uriFixupIntegration.js]
skip-if = e10s
[browser_loadDisallowInherit.js]
skip-if = e10s
[browser_loadURI.js]

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

@ -1,80 +1,51 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const Cc = Components.classes;
const Ci = Components.interfaces;
"use strict";
const kSearchEngineID = "browser_urifixup_search_engine";
const kSearchEngineURL = "http://example.com/?search={searchTerms}";
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
kSearchEngineURL);
let oldDefaultEngine = Services.search.defaultEngine;
Services.search.defaultEngine = Services.search.getEngineByName(kSearchEngineID);
add_task(function* setup() {
// Add a new fake search engine.
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
kSearchEngineURL);
let tab;
let searchParams;
let oldDefaultEngine = Services.search.defaultEngine;
Services.search.defaultEngine = Services.search.getEngineByName(kSearchEngineID);
function checkURL() {
let escapedParams = encodeURIComponent(searchParams).replace("%20", "+");
let expectedURL = kSearchEngineURL.replace("{searchTerms}", escapedParams);
is(tab.linkedBrowser.currentURI.spec, expectedURL,
"New tab should have loaded with expected url.");
}
// Remove the fake engine when done.
registerCleanupFunction(() => {
if (oldDefaultEngine) {
Services.search.defaultEngine = oldDefaultEngine;
}
function addPageShowListener(aFunc) {
gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() {
gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false);
aFunc();
});
}
function locationBarEnter(aCallback) {
executeSoon(function() {
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
addPageShowListener(aCallback);
});
}
let urlbarInput = [
"foo bar",
"brokenprotocol:somethingelse"
];
function test() {
waitForExplicitFinish();
nextTest();
}
function nextTest() {
searchParams = urlbarInput.pop();
tab = gBrowser.selectedTab = gBrowser.addTab();
gURLBar.value = searchParams;
locationBarEnter(function() {
checkURL();
gBrowser.removeTab(tab);
tab = null;
if (urlbarInput.length) {
nextTest();
} else {
finish();
let engine = Services.search.getEngineByName(kSearchEngineID);
if (engine) {
Services.search.removeEngine(engine);
}
});
}
registerCleanupFunction(function () {
if (tab) {
gBrowser.removeTab(tab);
}
if (oldDefaultEngine) {
Services.search.defaultEngine = oldDefaultEngine;
}
let engine = Services.search.getEngineByName(kSearchEngineID);
if (engine) {
Services.search.removeEngine(engine);
}
});
add_task(function* test() {
for (let searchParams of ["foo bar", "brokenprotocol:somethingelse"]) {
// Add a new blank tab.
gBrowser.selectedTab = gBrowser.addTab("about:blank");
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
// Enter search terms and start a search.
gURLBar.value = searchParams;
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
// Check that we arrived at the correct URL.
let escapedParams = encodeURIComponent(searchParams).replace("%20", "+");
let expectedURL = kSearchEngineURL.replace("{searchTerms}", escapedParams);
is(gBrowser.selectedBrowser.currentURI.spec, expectedURL,
"New tab should have loaded with expected url.");
// Cleanup.
gBrowser.removeCurrentTab();
}
});

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

@ -1852,20 +1852,11 @@ RadioInterface.prototype = {
handleUnsolicitedWorkerMessage: function(message) {
let connHandler = gDataConnectionManager.getConnectionHandler(this.clientId);
switch (message.rilMessageType) {
case "audioStateChanged":
gTelephonyService.notifyAudioStateChanged(this.clientId, message.state);
break;
case "callRing":
gTelephonyService.notifyCallRing();
break;
case "callStateChange":
gTelephonyService.notifyCallStateChanged(this.clientId, message.call);
break;
case "callDisconnected":
gTelephonyService.notifyCallDisconnected(this.clientId, message.call);
break;
case "conferenceCallStateChanged":
gTelephonyService.notifyConferenceCallStateChanged(message.state);
case "currentCalls":
gTelephonyService.notifyCurrentCalls(this.clientId, message.calls);
break;
case "cdmaCallWaiting":
gTelephonyService.notifyCdmaCallWaiting(this.clientId,
@ -1873,7 +1864,7 @@ RadioInterface.prototype = {
break;
case "suppSvcNotification":
gTelephonyService.notifySupplementaryService(this.clientId,
message.callIndex,
message.number,
message.notification);
break;
case "ussdreceived":

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

@ -464,11 +464,6 @@ this.CELL_INFO_TYPE_CDMA = 2;
this.CELL_INFO_TYPE_LTE = 3;
this.CELL_INFO_TYPE_WCDMA = 4;
// Order matters.
this.AUDIO_STATE_NO_CALL = 0;
this.AUDIO_STATE_INCOMING = 1;
this.AUDIO_STATE_IN_CALL = 2;
this.CALL_STATE_UNKNOWN = -1;
this.CALL_STATE_ACTIVE = 0;
this.CALL_STATE_HOLDING = 1;

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

@ -223,7 +223,6 @@ function RilObject(aContext) {
this.context = aContext;
this.telephonyRequestQueue = new TelephonyRequestQueue(this);
this.currentCalls = {};
this.currentConferenceState = CALL_STATE_UNKNOWN;
this._pendingSentSmsMap = {};
this.pendingNetworkType = {};
@ -232,8 +231,6 @@ function RilObject(aContext) {
// Init properties that are only initialized once.
this.v5Legacy = RILQUIRKS_V5_LEGACY;
this.pendingMO = null;
}
RilObject.prototype = {
context: null,
@ -244,11 +241,6 @@ RilObject.prototype = {
version: null,
v5Legacy: null,
/**
* Valid calls.
*/
currentCalls: null,
/**
* Call state of current conference group.
*/
@ -354,11 +346,11 @@ RilObject.prototype = {
*/
this.basebandVersion = null;
// Clean up this.currentCalls: rild might have restarted.
for each (let currentCall in this.currentCalls) {
delete this.currentCalls[currentCall.callIndex];
this._handleDisconnectedCall(currentCall);
}
// Clean up currentCalls: rild might have restarted.
this.sendChromeMessage({
rilMessageType: "currentCalls",
calls: {}
});
// Don't clean up this._pendingSentSmsMap
// because on rild restart: we may continue with the pending segments.
@ -401,12 +393,6 @@ RilObject.prototype = {
};
this.mergedCellBroadcastConfig = null;
/**
* A successful dialing request.
* { options: options of the corresponding dialing request }
*/
this.pendingMO = null;
/**
* True when the request to report SMS Memory Status is pending.
*/
@ -1437,14 +1423,6 @@ RilObject.prototype = {
this.sendExitEmergencyCbModeRequest(options);
},
/**
* Cache the request for making an emergency call when radio is off. The
* request shall include two types of callback functions. 'callback' is
* called when radio is ready, and 'onerror' is called when turning radio
* on fails.
*/
cachedDialRequest : null,
/**
* Dial a non-emergency number.
*
@ -1458,46 +1436,19 @@ RilObject.prototype = {
* Integer doing something XXX TODO
*/
dial: function(options) {
let onerror = (function onerror(options, errorMsg) {
options.success = false;
options.errorMsg = errorMsg;
this.sendChromeMessage(options);
}).bind(this, options);
let isRadioOff = (this.radioState === GECKO_RADIOSTATE_DISABLED);
if (options.isEmergency) {
options.request = RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL ?
REQUEST_DIAL_EMERGENCY_CALL : REQUEST_DIAL;
if (isRadioOff) {
if (DEBUG) {
this.context.debug("Automatically enable radio for an emergency call.");
}
this.cachedDialRequest = {
callback: this.dialInternal.bind(this, options),
onerror: onerror
};
this.setRadioEnabled({enabled: true});
return;
}
this.dialInternal(options);
} else {
options.request = REQUEST_DIAL;
// Exit emergency callback mode when user dial a non-emergency call.
if (this._isInEmergencyCbMode) {
this.exitEmergencyCbMode();
}
options.request = REQUEST_DIAL;
this.dialInternal(options);
}
},
dialInternal: function(options) {
this.telephonyRequestQueue.push(options.request, () => {
let Buf = this.context.Buf;
Buf.newParcel(options.request, options);
@ -1532,37 +1483,16 @@ RilObject.prototype = {
* Call index (1-based) as reported by REQUEST_GET_CURRENT_CALLS.
*/
hangUpCall: function(options) {
let call = this.currentCalls[options.callIndex];
if (!call) {
// |hangUpCall()| is used to remove a call from the current call list,
// so we consider it as an successful case when hanging up a call that
// doesn't exist in the current call list.
options.success = true;
this.sendChromeMessage(options);
return;
}
call.hangUpLocal = true;
if (call.state === CALL_STATE_HOLDING) {
this.hangUpBackground(options);
} else {
this.telephonyRequestQueue.push(REQUEST_HANGUP, () => {
let Buf = this.context.Buf;
Buf.newParcel(REQUEST_HANGUP, options);
Buf.writeInt32(1);
Buf.writeInt32(options.callIndex);
Buf.sendParcel();
});
}
this.telephonyRequestQueue.push(REQUEST_HANGUP, () => {
let Buf = this.context.Buf;
Buf.newParcel(REQUEST_HANGUP, options);
Buf.writeInt32(1);
Buf.writeInt32(options.callIndex);
Buf.sendParcel();
});
},
hangUpForeground: function(options) {
for each (let currentCall in this.currentCalls) {
if (currentCall.state == CALL_STATE_ACTIVE) {
currentCall.hangUpLocal = true;
}
}
this.telephonyRequestQueue.push(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, () => {
this.context.Buf.simpleRequest(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND,
options);
@ -1570,28 +1500,6 @@ RilObject.prototype = {
},
hangUpBackground: function(options) {
let waitingCalls = [];
let heldCalls = [];
for each (let currentCall in this.currentCalls) {
switch (currentCall.state) {
case CALL_STATE_WAITING:
waitingCalls.push(currentCall);
break;
case CALL_STATE_HOLDING:
heldCalls.push(currentCall);
break;
}
}
// When both a held and a waiting call exist, the request shall apply to
// the waiting call.
if (waitingCalls.length) {
waitingCalls.forEach(call => call.hangUpLocal = true);
} else {
heldCalls.forEach(call => call.hangUpLocal = true);
}
this.telephonyRequestQueue.push(REQUEST_HANGUP_WAITING_OR_BACKGROUND, () => {
this.context.Buf.simpleRequest(REQUEST_HANGUP_WAITING_OR_BACKGROUND,
options);
@ -1611,88 +1519,10 @@ RilObject.prototype = {
});
},
/**
* Answer an incoming/waiting call.
*
* @param callIndex
* Call index of the call to answer.
*/
answerCall: function(options) {
let call = this.currentCalls[options.callIndex];
if (!call) {
options.success = false;
options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
this.sendChromeMessage(options);
return;
}
// Check for races. Since we dispatched the incoming/waiting call
// notification the incoming/waiting call may have changed. The main
// thread thinks that it is answering the call with the given index,
// so only answer if that is still incoming/waiting.
switch (call.state) {
case CALL_STATE_INCOMING:
this.telephonyRequestQueue.push(REQUEST_ANSWER, () => {
this.context.Buf.simpleRequest(REQUEST_ANSWER, options);
});
break;
case CALL_STATE_WAITING:
// Answer the waiting (second) call, and hold the first call.
this.switchActiveCall(options);
break;
default:
if (DEBUG) this.context.debug("AnswerCall: Invalid call state");
options.success = false;
options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
this.sendChromeMessage(options);
}
},
/**
* Reject an incoming/waiting call.
*
* @param callIndex
* Call index of the call to reject.
*/
rejectCall: function(options) {
let call = this.currentCalls[options.callIndex];
if (!call) {
// |hangUpCall()| is used to remove an imcoming call from the current
// call list, so we consider it as an successful case when rejecting
// a call that doesn't exist in the current call list.
options.success = true;
this.sendChromeMessage(options);
return;
}
call.hangUpLocal = true;
if (this._isCdma) {
// AT+CHLD=0 means "release held or UDUB."
this.hangUpBackground(options);
return;
}
// Check for races. Since we dispatched the incoming/waiting call
// notification the incoming/waiting call may have changed. The main
// thread thinks that it is rejecting the call with the given index,
// so only reject if that is still incoming/waiting.
switch (call.state) {
case CALL_STATE_INCOMING:
this.udub(options);
break;
case CALL_STATE_WAITING:
// Reject the waiting (second) call, and remain the first call.
this.hangUpBackground(options);
break;
default:
if (DEBUG) this.context.debug("RejectCall: Invalid call state");
options.success = false;
options.errorMsg = GECKO_ERROR_GENERIC_FAILURE;
this.sendChromeMessage(options);
}
this.telephonyRequestQueue.push(REQUEST_ANSWER, () => {
this.context.Buf.simpleRequest(REQUEST_ANSWER, options);
});
},
conferenceCall: function(options) {
@ -1714,9 +1544,9 @@ RilObject.prototype = {
/**
* Get current calls.
*/
getCurrentCalls: function() {
getCurrentCalls: function(options) {
this.telephonyRequestQueue.push(REQUEST_GET_CURRENT_CALLS, () => {
this.context.Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS);
this.context.Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS, options);
});
},
@ -2139,9 +1969,8 @@ RilObject.prototype = {
/**
* Get failure casue code for the most recently failed PDP context.
*/
getFailCauseCode: function(callback) {
this.context.Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE,
{callback: callback});
getFailCause: function(options) {
this.context.Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE, options);
},
sendMMI: function(options) {
@ -3494,12 +3323,6 @@ RilObject.prototype = {
info.rilMessageType = "signalstrengthchange";
this._sendNetworkInfoMessage(NETWORK_INFO_SIGNAL, info);
if (this.cachedDialRequest && info.voice.signalStrength) {
// Radio is ready for making the cached emergency call.
this.cachedDialRequest.callback();
this.cachedDialRequest = null;
}
},
/**
@ -3661,244 +3484,6 @@ RilObject.prototype = {
}
},
/**
* Classify new calls into three groups: (removed, remained, added).
*/
_classifyCalls: function(newCalls) {
newCalls = newCalls || {};
let removedCalls = [];
let remainedCalls = [];
let addedCalls = [];
for each (let currentCall in this.currentCalls) {
let newCall = newCalls[currentCall.callIndex];
if (!newCall) {
removedCalls.push(currentCall);
} else {
remainedCalls.push(newCall);
delete newCalls[currentCall.callIndex];
}
}
// Go through any remaining calls that are new to us.
for each (let newCall in newCalls) {
if (newCall.isVoice) {
addedCalls.push(newCall);
}
}
return [removedCalls, remainedCalls, addedCalls];
},
/**
* Check the calls in addedCalls and assign an appropriate one to pendingMO.
* Also update the |isEmergency| on that call.
*/
_assignPendingMO: function(addedCalls) {
let options = this.pendingMO.options;
this.pendingMO = null;
for (let call of addedCalls) {
if (call.state !== CALL_STATE_INCOMING) {
call.isEmergency = options.isEmergency;
options.success = true;
options.callIndex = call.callIndex;
this.sendChromeMessage(options);
return;
}
}
// The call doesn't exist.
options.success = false;
options.errorMsg = GECKO_CALL_ERROR_UNSPECIFIED;
this.sendChromeMessage(options);
},
/**
* Check the currentCalls and identify the conference group.
* Return the conference state and the group as a set.
*/
_detectConference: function() {
// There are some difficuties to identify the conference by |.isMpty| so we
// don't rely on this flag.
// - |.isMpty| becomes false when the conference call is put on hold.
// - |.isMpty| may remain true when other participants left the conference.
// All the calls in the conference should have the same state and it is
// either ACTIVE or HOLDING. That means, if we find a group of call with
// the same state and its size is larger than 2, it must be a conference.
let activeCalls = new Set();
let holdingCalls = new Set();
for each (let call in this.currentCalls) {
if (call.state === CALL_STATE_ACTIVE) {
activeCalls.add(call);
} else if (call.state === CALL_STATE_HOLDING) {
holdingCalls.add(call);
}
}
if (activeCalls.size >= 2) {
return [CALL_STATE_ACTIVE, activeCalls];
} else if (holdingCalls.size >= 2) {
return [CALL_STATE_HOLDING, holdingCalls];
}
return [CALL_STATE_UNKNOWN, new Set()];
},
/**
* Helpers for processing call state changes.
*/
_processCalls: function(newCalls, failCause) {
if (DEBUG) this.context.debug("_processCalls: " + JSON.stringify(newCalls) +
" failCause: " + failCause);
// Let's get the failCause first if there are removed calls. Otherwise, we
// need to trigger another async request when removing call and it cause
// the order of callDisconnected and conferenceCallStateChanged
// unpredictable.
if (failCause === undefined) {
for each (let currentCall in this.currentCalls) {
if (!newCalls[currentCall.callIndex] && !currentCall.hangUpLocal) {
this.getFailCauseCode((function(newCalls, failCause) {
this._processCalls(newCalls, failCause);
}).bind(this, newCalls));
return;
}
}
}
let [removedCalls, remainedCalls, addedCalls] =
this._classifyCalls(newCalls);
// Handle removed calls.
// Only remove it from the map here. Notify callDisconnected later.
for (let call of removedCalls) {
delete this.currentCalls[call.callIndex];
call.failCause = call.hangUpLocal ? GECKO_CALL_ERROR_NORMAL_CALL_CLEARING
: failCause;
}
let changedCalls = new Set();
// Handle remained calls.
for (let newCall of remainedCalls) {
let oldCall = this.currentCalls[newCall.callIndex];
if (oldCall.state == newCall.state) {
continue;
}
if (oldCall.state == CALL_STATE_WAITING &&
newCall.state == CALL_STATE_INCOMING) {
// Update the call internally but we don't notify chrome since these two
// states are viewed as the same one there.
oldCall.state = newCall.state;
continue;
}
if (!oldCall.started && newCall.state == CALL_STATE_ACTIVE) {
oldCall.started = new Date().getTime();
}
oldCall.state = newCall.state;
oldCall.number =
this._formatInternationalNumber(newCall.number, newCall.toa);
changedCalls.add(oldCall);
}
// Handle pendingMO.
if (this.pendingMO) {
this._assignPendingMO(addedCalls);
}
// Handle added calls.
for (let call of addedCalls) {
this._addVoiceCall(call);
changedCalls.add(call);
}
// Detect conference and update isConference flag.
let [newConferenceState, conference] = this._detectConference();
for each (let call in this.currentCalls) {
let isConference = conference.has(call);
if (call.isConference != isConference) {
call.isConference = isConference;
changedCalls.add(call);
}
}
// Update audio state. We have to send this message before callStateChange
// and callDisconnected to make sure that the audio state is ready first.
this.sendChromeMessage({
rilMessageType: "audioStateChanged",
state: this._detectAudioState()
});
// Notify call disconnected.
for (let call of removedCalls) {
this._handleDisconnectedCall(call);
}
// Notify call state change.
for (let call of changedCalls) {
this._handleChangedCallState(call);
}
// Notify conference state change.
if (this.currentConferenceState != newConferenceState) {
this.currentConferenceState = newConferenceState;
let message = {rilMessageType: "conferenceCallStateChanged",
state: newConferenceState};
this.sendChromeMessage(message);
}
},
_detectAudioState: function() {
let callNum = Object.keys(this.currentCalls).length;
if (!callNum) {
return AUDIO_STATE_NO_CALL;
}
let firstIndex = Object.keys(this.currentCalls)[0];
if (callNum == 1 &&
this.currentCalls[firstIndex].state == CALL_STATE_INCOMING) {
return AUDIO_STATE_INCOMING;
}
return AUDIO_STATE_IN_CALL;
},
// Format international numbers appropriately.
_formatInternationalNumber: function(number, toa) {
if (number && toa == TOA_INTERNATIONAL && number[0] != "+") {
number = "+" + number;
}
return number;
},
_addVoiceCall: function(newCall) {
newCall.number = this._formatInternationalNumber(newCall.number, newCall.toa);
newCall.isOutgoing = !newCall.isMT;
newCall.isConference = false;
this.currentCalls[newCall.callIndex] = newCall;
},
_handleChangedCallState: function(changedCall) {
let message = {rilMessageType: "callStateChange",
call: changedCall};
this.sendChromeMessage(message);
},
_handleDisconnectedCall: function(disconnectedCall) {
let message = {rilMessageType: "callDisconnected",
call: disconnectedCall};
this.sendChromeMessage(message);
},
_setDataCallGeckoState: function(datacall) {
switch (datacall.active) {
case DATACALL_INACTIVE:
@ -3925,7 +3510,6 @@ RilObject.prototype = {
}
let notification = null;
let callIndex = -1;
switch (info.code) {
case SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD:
@ -3937,26 +3521,9 @@ RilObject.prototype = {
return;
}
// Get the target call object for this notification.
let currentCallIndexes = Object.keys(this.currentCalls);
if (currentCallIndexes.length === 1) {
// Only one call exists. This should be the target.
callIndex = currentCallIndexes[0];
} else {
// Find the call in |currentCalls| by the given number.
if (info.number) {
for each (let currentCall in this.currentCalls) {
if (currentCall.number == info.number) {
callIndex = currentCall.callIndex;
break;
}
}
}
}
let message = {rilMessageType: "suppSvcNotification",
notification: notification,
callIndex: callIndex};
number: info.number, // could be empty.
notification: notification};
this.sendChromeMessage(message);
},
@ -4887,19 +4454,6 @@ RilObject.prototype = {
method.call(this, message);
},
/**
* Get a list of current voice calls.
*/
enumerateCalls: function(options) {
if (DEBUG) this.context.debug("Sending all current calls");
let calls = [];
for each (let call in this.currentCalls) {
calls.push(call);
}
options.calls = calls;
this.sendChromeMessage(options);
},
/**
* Process STK Proactive Command.
*/
@ -5054,10 +4608,13 @@ RilObject.prototype[REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE] =
};
RilObject.prototype[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CALLS(length, options) {
// Retry getCurrentCalls several times when error occurs.
if (options.rilRequestError &&
this._getCurrentCallsRetryCount < GET_CURRENT_CALLS_RETRY_MAX) {
this._getCurrentCallsRetryCount++;
this.getCurrentCalls();
if (options.rilRequestError) {
if (this._getCurrentCallsRetryCount < GET_CURRENT_CALLS_RETRY_MAX) {
this._getCurrentCallsRetryCount++;
this.getCurrentCalls(options);
} else {
this.sendDefaultResponse(options);
}
return;
}
@ -5107,20 +4664,17 @@ RilObject.prototype[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CA
};
}
calls[call.callIndex] = call;
if (call.isVoice) {
calls[call.callIndex] = call;
}
}
this._processCalls(calls);
options.calls = calls;
options.rilMessageType = options.rilMessageType || "currentCalls";
this.sendChromeMessage(options);
};
RilObject.prototype[REQUEST_DIAL] = function REQUEST_DIAL(length, options) {
if (options.rilRequestError === 0) {
this.pendingMO = {options: options};
} else {
this.getFailCauseCode((function(options, failCause) {
options.success = false;
options.errorMsg = failCause;
this.sendChromeMessage(options);
}).bind(this, options));
}
this.sendDefaultResponse(options);
};
RilObject.prototype[REQUEST_DIAL_EMERGENCY_CALL] = function REQUEST_DIAL_EMERGENCY_CALL(length, options) {
RilObject.prototype[REQUEST_DIAL].call(this, length, options);
@ -5158,21 +4712,22 @@ RilObject.prototype[REQUEST_UDUB] = function REQUEST_UDUB(length, options) {
this.sendDefaultResponse(options);
};
RilObject.prototype[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) {
let Buf = this.context.Buf;
let num = length ? Buf.readInt32() : 0;
let failCause = null;
// Treat it as CALL_FAIL_ERROR_UNSPECIFIED if the request failed.
let failCause = CALL_FAIL_ERROR_UNSPECIFIED;
if (num) {
let causeNum = Buf.readInt32();
// To make _processCalls work as design, failCause couldn't be "undefined."
// See Bug 1112550 for details.
failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[causeNum] || null;
}
if (DEBUG) this.context.debug("Last call fail cause: " + failCause);
if (options.rilRequestError === 0) {
let Buf = this.context.Buf;
let num = length ? Buf.readInt32() : 0;
if (options.callback) {
options.callback(failCause);
if (num) {
let causeNum = Buf.readInt32();
failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[causeNum] || failCause;
}
if (DEBUG) this.context.debug("Last call fail cause: " + failCause);
}
options.failCause = failCause;
this.sendChromeMessage(options);
};
RilObject.prototype[REQUEST_SIGNAL_STRENGTH] = function REQUEST_SIGNAL_STRENGTH(length, options) {
this._receivedNetworkInfo(NETWORK_INFO_SIGNAL);
@ -5218,15 +4773,6 @@ RilObject.prototype[REQUEST_VOICE_REGISTRATION_STATE] = function REQUEST_VOICE_R
if (DEBUG) this.context.debug("voice registration state: " + state);
this._processVoiceRegistrationState(state);
if (this.cachedDialRequest &&
(this.voiceRegistrationState.emergencyCallsOnly ||
this.voiceRegistrationState.connected) &&
this.voiceRegistrationState.radioTech != NETWORK_CREG_TECH_UNKNOWN) {
// Radio is ready for making the cached emergency call.
this.cachedDialRequest.callback();
this.cachedDialRequest = null;
}
};
RilObject.prototype[REQUEST_DATA_REGISTRATION_STATE] = function REQUEST_DATA_REGISTRATION_STATE(length, options) {
this._receivedNetworkInfo(NETWORK_INFO_DATA_REGISTRATION_STATE);
@ -5250,19 +4796,7 @@ RilObject.prototype[REQUEST_OPERATOR] = function REQUEST_OPERATOR(length, option
this._processOperator(operatorData);
};
RilObject.prototype[REQUEST_RADIO_POWER] = function REQUEST_RADIO_POWER(length, options) {
if (options.rilMessageType == null) {
// The request was made by ril_worker itself.
if (options.rilRequestError) {
if (this.cachedDialRequest && options.enabled) {
// Turning on radio fails. Notify the error of making an emergency call.
this.cachedDialRequest.onerror(GECKO_ERROR_RADIO_NOT_AVAILABLE);
this.cachedDialRequest = null;
}
}
return;
}
this.sendChromeMessage(options);
this.sendDefaultResponse(options);
};
RilObject.prototype[REQUEST_DTMF] = null;
RilObject.prototype[REQUEST_SEND_SMS] = function REQUEST_SEND_SMS(length, options) {

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

@ -41,12 +41,8 @@ add_test(function test_change_call_barring_password() {
add_test(function test_check_change_call_barring_password_result() {
let barringPasswordOptions;
let worker = newWorker({
postMessage: function(message) {
equal(barringPasswordOptions.pin, PIN);
equal(barringPasswordOptions.newPin, NEW_PIN);
}
});
let workerHelper = newInterceptWorker();
let worker = workerHelper.worker;
let context = worker.ContextPool._contexts[0];
context.RIL.changeCallBarringPassword =
@ -55,9 +51,13 @@ add_test(function test_check_change_call_barring_password_result() {
context.RIL[REQUEST_CHANGE_BARRING_PASSWORD](0, {
rilRequestError: ERROR_SUCCESS
});
}
};
context.RIL.changeCallBarringPassword({pin: PIN, newPin: NEW_PIN});
let postedMessage = workerHelper.postedMessage;
equal(barringPasswordOptions.pin, PIN);
equal(barringPasswordOptions.newPin, NEW_PIN);
run_next_test();
});

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

@ -45,16 +45,17 @@ add_test(function test_notification() {
1: new Call(1, '11111')
};
function testNotification(calls, code, number, resultNotification,
resultCallIndex) {
function testNotification(calls, code, number, resultNotification) {
let testInfo = {calls: calls, code: code, number: number,
resultNotification: resultNotification,
resultCallIndex: resultCallIndex};
resultNotification: resultNotification};
do_print('Test case info: ' + JSON.stringify(testInfo));
// Set current calls.
context.RIL._processCalls(calls);
context.RIL.sendChromeMessage({
rilMessageType: "currentCalls",
calls: calls
});
let notificationInfo = {
notificationType: 1, // MT
@ -68,33 +69,36 @@ add_test(function test_notification() {
let postedMessage = workerHelper.postedMessage;
equal(postedMessage.rilMessageType, 'suppSvcNotification');
equal(postedMessage.number, number);
equal(postedMessage.notification, resultNotification);
equal(postedMessage.callIndex, resultCallIndex);
// Clear all existed calls.
context.RIL._processCalls({});
context.RIL.sendChromeMessage({
rilMessageType: "currentCalls",
calls: {}
});
}
testNotification(oneCall, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, null,
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD, 0);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD);
testNotification(oneCall, SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED, null,
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED, 0);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED);
testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, null,
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD, -1);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD);
testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED, null,
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED, -1);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED);
testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '00000',
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD, 0);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD);
testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '11111',
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD, 1);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD);
testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '22222',
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD, -1);
GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD);
run_next_test();
});

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

@ -165,6 +165,45 @@ TelephonyCallInfo.prototype = {
isMergeable: true
};
function Call(aClientId, aCallIndex) {
this.clientId = aClientId;
this.callIndex = aCallIndex;
}
Call.prototype = {
clientId: 0,
callIndex: 0,
state: nsITelephonyService.CALL_STATE_UNKNOWN,
number: "",
numberPresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,
name: "",
namePresentation: nsITelephonyService.CALL_PRESENTATION_ALLOWED,
isOutgoing: true,
isEmergency: false,
isConference: false,
isSwitchable: true,
isMergeable: true,
started: null
};
function MobileConnectionListener() {}
MobileConnectionListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionListener]),
// nsIMobileConnectionListener
notifyVoiceChanged: function() {},
notifyDataChanged: function() {},
notifyDataError: function(message) {},
notifyCFStateChanged: function(action, reason, number, timeSeconds, serviceClass) {},
notifyEmergencyCbModeChanged: function(active, timeoutMs) {},
notifyOtaStatusChanged: function(status) {},
notifyRadioStateChanged: function() {},
notifyClirModeChanged: function(mode) {},
notifyLastKnownNetworkChanged: function() {},
notifyLastKnownHomeNetworkChanged: function() {},
notifyNetworkSelectionModeChanged: function() {}
};
function TelephonyService() {
this._numClients = gRadioInterfaceLayer.numRadioInterfaces;
this._listeners = [];
@ -173,7 +212,7 @@ function TelephonyService() {
this._cachedDialRequest = null;
this._currentCalls = {};
this._currentConferenceState = nsITelephonyService.CALL_STATE_UNKNOWN;
this._audioStates = {};
this._audioStates = [];
this._cdmaCallWaitingNumber = null;
@ -186,8 +225,9 @@ function TelephonyService() {
Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
for (let i = 0; i < this._numClients; ++i) {
this._audioStates[i] = nsITelephonyAudioService.PHONE_STATE_NORMAL;
this._currentCalls[i] = {};
this._enumerateCallsForClient(i);
this._audioStates[i] = RIL.AUDIO_STATE_NO_CALL;
}
}
TelephonyService.prototype = {
@ -271,7 +311,8 @@ TelephonyService.prototype = {
},
_isRadioOn: function(aClientId) {
return gGonkMobileConnectionService.getItemByServiceId(aClientId).radioState === nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED;
let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
return connection.radioState === nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED;
},
// An array of nsITelephonyListener instances.
@ -293,18 +334,39 @@ TelephonyService.prototype = {
}
},
_updateAudioState: function(aAudioState) {
switch (aAudioState) {
case RIL.AUDIO_STATE_NO_CALL:
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_NORMAL);
break;
case RIL.AUDIO_STATE_INCOMING:
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_RINGTONE);
break;
case RIL.AUDIO_STATE_IN_CALL:
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_IN_CALL);
break;
_computeAudioStateForClient: function(aClientId) {
let indexes = Object.keys(this._currentCalls[aClientId]);
if (!indexes.length) {
return nsITelephonyAudioService.PHONE_STATE_NORMAL;
}
let firstCall = this._currentCalls[aClientId][indexes[0]];
if (indexes.length === 1 &&
firstCall.state === nsITelephonyService.CALL_STATE_INCOMING) {
return nsITelephonyAudioService.PHONE_STATE_RINGTONE;
}
return nsITelephonyAudioService.PHONE_STATE_IN_CALL;
},
_updateAudioState: function(aClientId) {
this._audioStates[aClientId] = this._computeAudioStateForClient(aClientId);
if (this._audioStates.some(state => state === nsITelephonyAudioService.PHONE_STATE_IN_CALL)) {
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_IN_CALL);
} else if (this._audioStates.some(state => state === nsITelephonyAudioService.PHONE_STATE_RINGTONE)) {
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_RINGTONE);
} else {
gAudioService.setPhoneState(nsITelephonyAudioService.PHONE_STATE_NORMAL);
}
},
_formatInternationalNumber: function(aNumber, aToa) {
if (aNumber && aToa == RIL.TOA_INTERNATIONAL && aNumber[0] != "+") {
return "+" + aNumber;
}
return aNumber;
},
_convertRILCallState: function(aState) {
@ -367,17 +429,17 @@ TelephonyService.prototype = {
_enumerateCallsForClient: function(aClientId) {
if (DEBUG) debug("Enumeration of calls for client " + aClientId);
this._sendToRilWorker(aClientId, "enumerateCalls", null, response => {
if (!this._currentCalls[aClientId]) {
this._currentCalls[aClientId] = {};
this._sendToRilWorker(aClientId, "getCurrentCalls", null, response => {
if (response.errorMsg) {
return;
}
for (let call of response.calls) {
call.clientId = aClientId;
call.state = this._convertRILCallState(call.state);
call.isSwitchable = true;
call.isMergeable = true;
this._currentCalls[aClientId][call.callIndex] = call;
// Clear all.
this._currentCalls[aClientId] = {};
for (let i in response.calls) {
let call = this._currentCalls[aClientId][i] = new Call(aClientId, i);
this._updateCallFromRil(call, response.calls[i]);
}
});
},
@ -482,16 +544,16 @@ TelephonyService.prototype = {
},
/**
* Get arbitrary one of active call.
* Is there an active call?
*/
_getOneActiveCall: function(aClientId) {
_isActive: function(aClientId) {
for (let index in this._currentCalls[aClientId]) {
let call = this._currentCalls[aClientId][index];
if (call.state === nsITelephonyService.CALL_STATE_CONNECTED) {
return call;
return true;
}
}
return null;
return false;
},
/**
@ -569,29 +631,29 @@ TelephonyService.prototype = {
// Handling of supplementary services within a call as 3GPP TS 22.030 6.5.5
_dialInCallMMI: function(aClientId, aNumber, aCallback) {
let mmiCallback = response => {
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
if (!response.success) {
aCallback.notifyDialMMIError(RIL.MMI_ERROR_KS_ERROR);
} else {
aCallback.notifyDialMMISuccess(RIL.MMI_SM_KS_CALL_CONTROL);
}
let mmiCallback = {
notifyError: () => aCallback.notifyDialMMIError(RIL.MMI_ERROR_KS_ERROR),
notifySuccess: () => aCallback.notifyDialMMISuccess(RIL.MMI_SM_KS_CALL_CONTROL)
};
if (aNumber === "0") {
this._sendToRilWorker(aClientId, "hangUpBackground", null, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this._hangUpBackground(aClientId, mmiCallback);
} else if (aNumber === "1") {
this._sendToRilWorker(aClientId, "hangUpForeground", null, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this._hangUpForeground(aClientId, mmiCallback);
} else if (aNumber[0] === "1" && aNumber.length === 2) {
this._sendToRilWorker(aClientId, "hangUpCall",
{ callIndex: parseInt(aNumber[1]) }, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this.hangUpCall(aClientId, parseInt(aNumber[1]), mmiCallback);
} else if (aNumber === "2") {
this._sendToRilWorker(aClientId, "switchActiveCall", null, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this._switchActiveCall(aClientId, mmiCallback);
} else if (aNumber[0] === "2" && aNumber.length === 2) {
this._sendToRilWorker(aClientId, "separateCall",
{ callIndex: parseInt(aNumber[1]) }, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this._separateCallGsm(aClientId, parseInt(aNumber[1]), mmiCallback);
} else if (aNumber === "3") {
this._sendToRilWorker(aClientId, "conferenceCall", null, mmiCallback);
aCallback.notifyDialMMI(RIL.MMI_KS_SC_CALL);
this._conferenceCallGsm(aClientId, mmiCallback);
} else {
this._dialCall(aClientId, aNumber, undefined, aCallback);
}
@ -644,6 +706,32 @@ TelephonyService.prototype = {
aCallback.notifyError(DIAL_ERROR_INVALID_STATE_ERROR);
return;
}
// Radio is off. Turn it on first.
if (!this._isRadioOn(aClientId)) {
let connection = gGonkMobileConnectionService.getItemByServiceId(aClientId);
let listener = new MobileConnectionListener();
listener.notifyRadioStateChanged = () => {
if (this._isRadioOn(aClientId)) {
this._dialCall(aClientId, aNumber, undefined, aCallback);
connection.unregisterListener(listener);
}
};
connection.registerListener(listener);
// Turn on radio.
connection.setRadioEnabled(true, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileConnectionCallback]),
notifySuccess: () => {},
notifyError: aErrorMsg => {
connection.unregisterListener(listener);
aCallback.notifyError(DIAL_ERROR_RADIO_NOT_AVAILABLE);
}
});
return;
}
}
let options = {
@ -653,8 +741,7 @@ TelephonyService.prototype = {
};
// No active call. Dial it out directly.
let activeCall = this._getOneActiveCall(aClientId);
if (!activeCall) {
if (!this._isActive(aClientId)) {
this._sendDialCallRequest(aClientId, options, aCallback);
return;
}
@ -674,7 +761,7 @@ TelephonyService.prototype = {
return;
}
let autoHoldCallback = {
this._switchActiveCall(aClientId, {
QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyCallback]),
notifySuccess: () => {
@ -689,13 +776,7 @@ TelephonyService.prototype = {
if (DEBUG) debug("Error: Fail to automatically hold the active call.");
aCallback.notifyError(aErrorMsg);
}
};
if (activeCall.isConference) {
this.holdConference(aClientId, autoHoldCallback);
} else {
this.holdCall(aClientId, activeCall.callIndex, autoHoldCallback);
}
});
},
_dialCdmaThreeWayCall: function(aClientId, aNumber, aCallback) {
@ -710,30 +791,30 @@ TelephonyService.prototype = {
aCallback.notifyDialCallSuccess(aClientId, CDMA_SECOND_CALL_INDEX,
aNumber);
let childCall = {
callIndex: CDMA_SECOND_CALL_INDEX,
state: RIL.CALL_STATE_DIALING,
number: aNumber,
isOutgoing: true,
isEmergency: false,
isConference: false,
isSwitchable: false,
isMergeable: true,
parentId: CDMA_FIRST_CALL_INDEX
};
let childCall = this._currentCalls[aClientId][CDMA_SECOND_CALL_INDEX] =
new Call(aClientId, CDMA_SECOND_CALL_INDEX);
childCall.parentId = CDMA_FIRST_CALL_INDEX;
childCall.state = nsITelephonyService.CALL_STATE_DIALING;
childCall.number = aNumber;
childCall.isOutgoing = true;
childCall.isEmergency = gDialNumberUtils.isEmergency(aNumber);
childCall.isConference = false;
childCall.isSwitchable = false;
childCall.isMergeable = true;
// Manual update call state according to the request response.
this.notifyCallStateChanged(aClientId, childCall);
this._handleCallStateChanged(aClientId, childCall);
childCall.state = RIL.CALL_STATE_ACTIVE;
this.notifyCallStateChanged(aClientId, childCall);
childCall.state = nsITelephonyService.CALL_STATE_CONNECTED;
this._handleCallStateChanged(aClientId, childCall);
let parentCall = this._currentCalls[aClientId][childCall.parentId];
parentCall.childId = CDMA_SECOND_CALL_INDEX;
parentCall.state = RIL.CALL_STATE_HOLDING;
parentCall.state = nsITelephonyService.CALL_STATE_HELD;
parentCall.isSwitchable = false;
parentCall.isMergeable = true;
this.notifyCallStateChanged(aClientId, parentCall);
this._handleCallStateChanged(aClientId, parentCall);
});
},
@ -744,12 +825,15 @@ TelephonyService.prototype = {
this._isDialing = false;
if (!response.success) {
aCallback.notifyError(response.errorMsg);
return;
this._sendToRilWorker(aClientId, "getFailCause", null, response => {
aCallback.notifyError(response.failCause);
});
} else {
this._ongoingDial = {
clientId: aClientId,
callback: aCallback
};
}
aCallback.notifyDialCallSuccess(aClientId, response.callIndex,
response.number);
});
},
@ -896,6 +980,76 @@ TelephonyService.prototype = {
}
},
_getCallsWithState: function(aClientId, aState) {
let calls = [];
for (let i in this._currentCalls[aClientId]) {
let call = this._currentCalls[aClientId][i];
if (call.state === aState) {
calls.push(call);
}
}
return calls;
},
/**
* Update call information from RIL.
*
* @return Boolean to indicate whether the data is changed.
*/
_updateCallFromRil: function(aCall, aRilCall) {
aRilCall.state = this._convertRILCallState(aRilCall.state);
aRilCall.number = this._formatInternationalNumber(aRilCall.number,
aRilCall.toa);
if (!aCall.started &&
aCall.state == nsITelephonyService.CALL_STATE_CONNECTED) {
aCall.started = new Date().getTime();
}
let change = false;
const key = ["state", "number", "numberPresentation", "name",
"namePresentation"];
for (let k of key) {
if (aCall[k] != aRilCall[k]) {
aCall[k] = aRilCall[k];
change = true;
}
}
aCall.isOutgoing = !aRilCall.isMT;
aCall.isEmergency = gDialNumberUtils.isEmergency(aCall.number);
return change;
},
/**
* Identify the conference group.
* Return the conference state and a array of calls in group.
*
* TODO: handle multi-sim case.
*/
_detectConference: function(aClientId) {
// There are some difficuties to identify the conference by |.isMpty| from RIL
// so we don't rely on this flag.
// - |.isMpty| becomes false when the conference call is put on hold.
// - |.isMpty| may remain true when other participants left the conference.
// All the calls in the conference should have the same state and it is
// either CONNECTED or HELD. That means, if we find a group of call with
// the same state and its size is larger than 2, it must be a conference.
let connectedCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_CONNECTED);
let heldCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_HELD);
if (connectedCalls.length >= 2) {
return [nsITelephonyService.CALL_STATE_CONNECTED, connectedCalls];
} else if (heldCalls.length >= 2) {
return [nsITelephonyService.CALL_STATE_HELD, heldCalls];
}
return [nsITelephonyService.CALL_STATE_UNKNOWN, null];
},
sendTones: function(aClientId, aDtmfChars, aPauseDuration, aToneDuration,
aCallback) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@ -935,12 +1089,63 @@ TelephonyService.prototype = {
},
answerCall: function(aClientId, aCallIndex, aCallback) {
this._sendToRilWorker(aClientId, "answerCall", { callIndex: aCallIndex },
this._defaultCallbackHandler.bind(this, aCallback));
let call = this._currentCalls[aClientId][aCallIndex];
if (!call || call.state != nsITelephonyService.CALL_STATE_INCOMING) {
aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
return;
}
let callNum = Object.keys(this._currentCalls[aClientId]).length;
if (callNum !== 1) {
this._switchActiveCall(aClientId, aCallback);
} else {
this._sendToRilWorker(aClientId, "answerCall", null,
this._defaultCallbackHandler.bind(this, aCallback));
}
},
rejectCall: function(aClientId, aCallIndex, aCallback) {
this._sendToRilWorker(aClientId, "rejectCall", { callIndex: aCallIndex },
if (this._isCdmaClient(aClientId)) {
this._hangUpBackground(aClientId, aCallback);
return;
}
let call = this._currentCalls[aClientId][aCallIndex];
if (!call || call.state != nsITelephonyService.CALL_STATE_INCOMING) {
aCallback.notifyError(RIL.GECKO_ERROR_GENERIC_FAILURE);
return;
}
let callNum = Object.keys(this._currentCalls[aClientId]).length;
if (callNum !== 1) {
this._hangUpBackground(aClientId, aCallback);
} else {
this._sendToRilWorker(aClientId, "udub", null,
this._defaultCallbackHandler.bind(this, aCallback));
}
},
_hangUpForeground: function(aClientId, aCallback) {
let calls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_CONNECTED);
calls.forEach(call => call.hangUpLocal = true);
this._sendToRilWorker(aClientId, "hangUpForeground", null,
this._defaultCallbackHandler.bind(this, aCallback));
},
_hangUpBackground: function(aClientId, aCallback) {
// When both a held and a waiting call exist, the request shall apply to
// the waiting call.
let waitingCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_INCOMING);
let heldCalls = this._getCallsWithState(aClientId, nsITelephonyService.CALL_STATE_HELD);
if (waitingCalls.length) {
waitingCalls.forEach(call => call.hangUpLocal = true);
} else {
heldCalls.forEach(call => call.hangUpLocal = true);
}
this._sendToRilWorker(aClientId, "hangUpBackground", null,
this._defaultCallbackHandler.bind(this, aCallback));
},
@ -949,6 +1154,13 @@ TelephonyService.prototype = {
// the parent call, we send 'parentId' to RIL.
aCallIndex = this._currentCalls[aClientId][aCallIndex].parentId || aCallIndex;
let call = this._currentCalls[aClientId][aCallIndex];
if (call.state === nsITelephonyService.CALL_STATE_HELD) {
this._hangUpBackground(aClientId, aCallback);
return;
}
call.hangUpLocal = true;
this._sendToRilWorker(aClientId, "hangUpCall", { callIndex: aCallIndex },
this._defaultCallbackHandler.bind(this, aCallback));
},
@ -974,6 +1186,10 @@ TelephonyService.prototype = {
return;
}
this._switchActiveCall(aClientId, aCallback);
},
_switchActiveCall: function(aClientId, aCallback) {
this._sendToRilWorker(aClientId, "switchActiveCall", null,
this._defaultCallbackHandler.bind(this, aCallback));
},
@ -1033,11 +1249,11 @@ TelephonyService.prototype = {
for (let index in this._currentCalls[aClientId]) {
let call = this._currentCalls[aClientId][index];
call.state = RIL.CALL_STATE_ACTIVE;
call.state = nsITelephonyService.CALL_STATE_CONNECTED;
call.isConference = true;
this.notifyCallStateChanged(aClientId, call);
this._handleCallStateChanged(aClientId, call);
}
this.notifyConferenceCallStateChanged(RIL.CALL_STATE_ACTIVE);
this._handleConferenceCallStateChanged(nsITelephonyService.CALL_STATE_CONNECTED);
aCallback.notifySuccess();
});
@ -1085,7 +1301,7 @@ TelephonyService.prototype = {
}
let childCall = this._currentCalls[aClientId][CDMA_SECOND_CALL_INDEX];
this.notifyCallDisconnected(aClientId, childCall);
this._handleCallDisconnected(aClientId, childCall);
aCallback.notifySuccess();
});
@ -1128,8 +1344,7 @@ TelephonyService.prototype = {
return;
}
this._sendToRilWorker(aClientId, "switchActiveCall", null,
this._defaultCallbackHandler.bind(this, aCallback));
this._switchActiveCall(aClientId, aCallback);
},
holdConference: function(aClientId, aCallback) {
@ -1170,26 +1385,13 @@ TelephonyService.prototype = {
* nsIGonkTelephonyService interface.
*/
notifyAudioStateChanged: function(aClientId, aState) {
this._audioStates[aClientId] = aState;
let audioState = aState;
for (let i = 0; i < this._numClients; ++i) {
audioState = Math.max(audioState, this._audioStates[i]);
}
this._updateAudioState(audioState);
},
/**
* Handle call disconnects by updating our current state and the audio system.
*/
notifyCallDisconnected: function(aClientId, aCall) {
_handleCallDisconnected: function(aClientId, aCall) {
if (DEBUG) debug("handleCallDisconnected: " + JSON.stringify(aCall));
aCall.clientId = aClientId;
aCall.state = nsITelephonyService.CALL_STATE_DISCONNECTED;
aCall.isEmergency = gDialNumberUtils.isEmergency(aCall.number);
let duration = ("started" in aCall && typeof aCall.started == "number") ?
new Date().getTime() - aCall.started : 0;
@ -1209,7 +1411,7 @@ TelephonyService.prototype = {
if (childId) {
// Child cannot live without parent.
let childCall = this._currentCalls[aClientId][childId];
this.notifyCallDisconnected(aClientId, childCall);
this._handleCallDisconnected(aClientId, childCall);
} else {
let parentId = this._currentCalls[aClientId][aCall.callIndex].parentId;
if (parentId) {
@ -1224,12 +1426,12 @@ TelephonyService.prototype = {
parentCall.isSwitchable = true;
parentCall.isMergeable = true;
aCall.isConference = false;
this.notifyCallStateChanged(aClientId, parentCall, true);
this._handleCallStateChanged(aClientId, parentCall);
}
}
}
if (!aCall.failCause ||
if (aCall.hangUpLocal || !aCall.failCause ||
aCall.failCause === RIL.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING) {
let callInfo = new TelephonyCallInfo(aCall);
this._notifyAllListeners("callStateChanged", [callInfo]);
@ -1237,10 +1439,11 @@ TelephonyService.prototype = {
this._notifyAllListeners("notifyError",
[aClientId, aCall.callIndex, aCall.failCause]);
}
delete this._currentCalls[aClientId][aCall.callIndex];
if (manualConfStateChange) {
this.notifyConferenceCallStateChanged(RIL.CALL_STATE_UNKNOWN);
this._handleConferenceCallStateChanged(nsITelephonyService.CALL_STATE_UNKNOWN);
}
},
@ -1259,56 +1462,125 @@ TelephonyService.prototype = {
},
/**
* Handle call state changes by updating our current state and the audio
* system.
* Handle current calls reported from RIL.
*
* @param aCalls call from RIL, which contains:
* state, callIndex, toa, isMT, number, numberPresentation, name,
* namePresentation.
*/
notifyCallStateChanged: function(aClientId, aCall, aSkipStateConversion) {
if (DEBUG) debug("handleCallStateChange: " + JSON.stringify(aCall));
notifyCurrentCalls: function(aClientId, aCalls) {
// Check whether there is a removed call.
let hasRemovedCalls = () => {
let newIndexes = new Set(Object.keys(aCalls));
for (let i in this._currentCalls[aClientId]) {
if (!newIndexes.has(i)) {
return true;
}
}
return false;
};
if (!aSkipStateConversion) {
aCall.state = this._convertRILCallState(aCall.state);
// If there are removedCalls, we should fetch the failCause first.
if (!hasRemovedCalls()) {
this._handleCurrentCalls(aClientId, aCalls);
} else {
this._sendToRilWorker(aClientId, "getFailCause", null, response => {
this._handleCurrentCalls(aClientId, aCalls, response.failCause);
});
}
},
_handleCurrentCalls: function(aClientId, aCalls,
aFailCause = RIL.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING) {
if (DEBUG) debug("handleCurrentCalls: " + JSON.stringify(aCalls) +
", failCause: " + aFailCause);
let changedCalls = new Set();
let removedCalls = new Set();
let allIndexes = new Set([...Object.keys(this._currentCalls[aClientId]),
...Object.keys(aCalls)]);
for (let i of allIndexes) {
let call = this._currentCalls[aClientId][i];
let rilCall = aCalls[i];
// Determine the change of call.
if (call && !rilCall) { // removed.
removedCalls.add(call);
} else if (call && rilCall) { // changed.
if (this._updateCallFromRil(call, rilCall)) {
changedCalls.add(call);
}
} else { // !call && rilCall. added.
this._currentCalls[aClientId][i] = call = new Call(aClientId, i);
this._updateCallFromRil(call, rilCall);
changedCalls.add(call);
// Handle ongoingDial.
if (this._ongoingDial && this._ongoingDial.clientId === aClientId &&
call.state !== nsITelephonyService.CALL_STATE_INCOMING) {
this._ongoingDial.callback.notifyDialCallSuccess(aClientId, i,
call.number);
this._ongoingDial = null;
}
}
}
// For correct conference detection, we should mark removedCalls as
// DISCONNECTED first.
removedCalls.forEach(call => {
call.state = nsITelephonyService.CALL_STATE_DISCONNECTED;
call.failCause = aFailCause;
this._handleCallDisconnected(aClientId, call);
});
// Detect conference and update isConference flag.
let [newConferenceState, conferenceCalls] = this._detectConference(aClientId);
if (DEBUG) debug("Conference state: " + newConferenceState);
let conference = new Set(conferenceCalls);
for (let i in this._currentCalls[aClientId]) {
let call = this._currentCalls[aClientId][i];
let isConference = conference.has(call);
if (call.isConference != isConference) {
call.isConference = isConference;
changedCalls.add(call);
}
}
changedCalls.forEach(call => this._handleCallStateChanged(aClientId, call));
// Should handle conferenceCallStateChange after callStateChanged and
// callDisconnected.
if (newConferenceState != this._currentConferenceState) {
this._handleConferenceCallStateChanged(newConferenceState);
}
this._updateAudioState(aClientId);
// Handle cached dial request.
if (this._cachedDialRequest && !this._isActive(aClientId)) {
if (DEBUG) debug("All calls held. Perform the cached dial request.");
let request = this._cachedDialRequest;
this._sendDialCallRequest(request.clientId, request.options,
request.callback);
this._cachedDialRequest = null;
}
},
/**
* Handle call state changes.
*/
_handleCallStateChanged: function(aClientId, aCall) {
if (DEBUG) debug("handleCallStateChange: " + JSON.stringify(aCall));
if (aCall.state == nsITelephonyService.CALL_STATE_DIALING) {
gTelephonyMessenger.notifyNewCall();
}
aCall.clientId = aClientId;
function pick(arg, defaultValue) {
return typeof arg !== 'undefined' ? arg : defaultValue;
}
let call = this._currentCalls[aClientId][aCall.callIndex];
if (call) {
call.state = aCall.state;
call.number = aCall.number;
call.isConference = aCall.isConference;
call.isEmergency = gDialNumberUtils.isEmergency(aCall.number);
call.isSwitchable = pick(aCall.isSwitchable, call.isSwitchable);
call.isMergeable = pick(aCall.isMergeable, call.isMergeable);
} else {
call = aCall;
call.isEmergency = pick(aCall.isEmergency, gDialNumberUtils.isEmergency(aCall.number));
call.isSwitchable = pick(aCall.isSwitchable, true);
call.isMergeable = pick(aCall.isMergeable, true);
call.name = pick(aCall.name, "");
call.numberPresentaation = pick(aCall.numberPresentation, nsITelephonyService.CALL_PRESENTATION_ALLOWED);
call.namePresentaation = pick(aCall.namePresentation, nsITelephonyService.CALL_PRESENTATION_ALLOWED);
this._currentCalls[aClientId][aCall.callIndex] = call;
}
// Handle cached dial request.
if (this._cachedDialRequest && !this._getOneActiveCall(aClientId)) {
if (DEBUG) debug("All calls held. Perform the cached dial request.");
let request = this._cachedDialRequest;
this._sendDialCallRequest(request.clientId, request.options, request.callback);
this._cachedDialRequest = null;
}
let callInfo = new TelephonyCallInfo(call);
let callInfo = new TelephonyCallInfo(aCall);
this._notifyAllListeners("callStateChanged", [callInfo]);
},
@ -1321,7 +1593,7 @@ TelephonyService.prototype = {
if (call) {
// TODO: Bug 977503 - B2G RIL: [CDMA] update callNumber when a waiting
// call comes after a 3way call.
this.notifyCallDisconnected(aClientId, call);
this._handleCallDisconnected(aClientId, call);
}
this._cdmaCallWaitingNumber = aCall.number;
@ -1333,17 +1605,37 @@ TelephonyService.prototype = {
aCall.namePresentation]);
},
notifySupplementaryService: function(aClientId, aCallIndex, aNotification) {
notifySupplementaryService: function(aClientId, aNumber, aNotification) {
let notification = this._convertRILSuppSvcNotification(aNotification);
// Get the target call object for this notification.
let callIndex = -1;
let indexes = Object.keys(this.currentCalls);
if (indexes.length === 1) {
// Only one call exists. This should be the target.
callIndex = indexes[0];
} else {
// Find the call in |currentCalls| by the given number.
if (aNumber) {
for (let i in this._currentCalls) {
let call = this._currentCalls[aClientId][i];
if (call.number === aNumber) {
callIndex = i;
break;
}
}
}
}
this._notifyAllListeners("supplementaryServiceNotification",
[aClientId, aCallIndex, notification]);
[aClientId, callIndex, notification]);
},
notifyConferenceCallStateChanged: function(aState) {
_handleConferenceCallStateChanged: function(aState) {
if (DEBUG) debug("handleConferenceCallStateChanged: " + aState);
this._currentConferenceState = this._convertRILCallState(aState);
this._notifyAllListeners("conferenceCallStateChanged",
[this._currentConferenceState]);
this._currentConferenceState = aState;
this._notifyAllListeners("conferenceCallStateChanged", [aState]);
},
notifyUssdReceived: function(aClientId, aMessage, aSessionEnded) {

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

@ -10,25 +10,18 @@
"@mozilla.org/telephony/gonktelephonyservice;1"
%}
[scriptable, uuid(eab4b7b4-bf78-4c44-8182-ca305e70f971)]
[scriptable, uuid(d287e11a-0a65-4456-b481-c63d62afdb5d)]
interface nsIGonkTelephonyService : nsITelephonyService
{
void notifyAudioStateChanged(in unsigned long clientId, in short state);
void notifyCallDisconnected(in unsigned long clientId, in jsval call);
void notifyCallRing();
void notifyCallStateChanged(in unsigned long clientId, in jsval call,
[optional] in boolean skipStateConversion);
void notifyCurrentCalls(in unsigned long clientId, in jsval calls);
void notifyCdmaCallWaiting(in unsigned long clientId, in jsval waitingCall);
void notifySupplementaryService(in unsigned long clientId, in long callIndex,
void notifySupplementaryService(in unsigned long clientId, in AString number,
in AString notification);
void notifyConferenceCallStateChanged(in short state);
void notifyUssdReceived(in unsigned long clientId, in DOMString message,
in boolean sessionEnded);
};

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

@ -16,7 +16,7 @@ function testDialOutInvalidNumber() {
return telephony.dial(number).then(call => {
outCall = call;
ok(outCall);
is(outCall.id.number, number);
is(outCall.id.number, ""); // Emulator returns empty number for this call.
is(outCall.state, "dialing");
is(outCall, telephony.active);

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

@ -119,17 +119,7 @@ public class OverlayActionService extends Service {
OverlayToastHelper.showSuccessToast(getApplicationContext(), shareMethod.getSuccessMessage());
break;
case TRANSIENT_FAILURE:
// An OnClickListener to do this share again.
View.OnClickListener retryListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
handleShare(intent);
}
};
// Show a failure toast with a retry button.
OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage(), retryListener);
break;
// Fall-through
case PERMANENT_FAILURE:
// Show a failure toast without a retry button.
OverlayToastHelper.showFailureToast(getApplicationContext(), shareMethod.getFailureMessage());

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

@ -30,8 +30,8 @@ public class OverlayDialogButton extends LinearLayout {
private static final String LOGTAG = "GeckoOverlayDialogButton";
// The views making up this button.
private ImageView icon;
private TextView label;
private final ImageView icon;
private final TextView label;
// Label/icon used when enabled.
private String enabledLabel;
@ -49,26 +49,16 @@ public class OverlayDialogButton extends LinearLayout {
private boolean isEnabled = true;
public OverlayDialogButton(Context context) {
super(context);
init(context);
this(context, null);
}
public OverlayDialogButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public OverlayDialogButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setOrientation(HORIZONTAL);
setPadding(0, 0, 0, 0);
setBackgroundResource(R.drawable.overlay_share_button_background);
setOrientation(LinearLayout.HORIZONTAL);
LayoutInflater.from(context).inflate(R.layout.overlay_share_button, this);
icon = (ImageView) findViewById(R.id.overlaybtn_icon);
label = (TextView) findViewById(R.id.overlaybtn_label);
}

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

@ -5,10 +5,8 @@
package org.mozilla.gecko.overlays.ui;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@ -22,18 +20,14 @@ import org.mozilla.gecko.R;
* picture of some description to educate the user on how to use the feature) TODO: Bug 1048645.
*/
public class OverlayToastHelper {
/**
* Show a toast indicating a failure to share.
* @param context Context in which to inflate the toast.
* @param failureMessage String to display in the toast.
* @param isTransient Should a retry button be presented?
* @param retryListener Listener to fire when the retry button is pressed.
*/
public static void showFailureToast(Context context, String failureMessage, View.OnClickListener retryListener) {
showToast(context, failureMessage, false, retryListener);
}
public static void showFailureToast(Context context, String failureMessage) {
showFailureToast(context, failureMessage, null);
showToast(context, failureMessage, false);
}
/**
@ -41,10 +35,10 @@ public class OverlayToastHelper {
* @param successMessage Message to show in the toast.
*/
public static void showSuccessToast(Context context, String successMessage) {
showToast(context, successMessage, true, null);
showToast(context, successMessage, true);
}
private static void showToast(Context context, String message, boolean success, View.OnClickListener retryListener) {
private static void showToast(Context context, String message, boolean success) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.overlay_share_toast, null);
@ -52,23 +46,12 @@ public class OverlayToastHelper {
TextView text = (TextView) layout.findViewById(R.id.overlay_toast_message);
text.setText(message);
if (retryListener == null) {
// Hide the retry button.
layout.findViewById(R.id.overlay_toast_separator).setVisibility(View.GONE);
layout.findViewById(R.id.overlay_toast_retry_btn).setVisibility(View.GONE);
} else {
// Set up the button to perform a retry.
Button retryBtn = (Button) layout.findViewById(R.id.overlay_toast_retry_btn);
retryBtn.setOnClickListener(retryListener);
}
if (!success) {
// Hide the happy green tick.
text.setCompoundDrawables(null, null, null, null);
}
Toast toast = new Toast(context);
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.BOTTOM, 0, 0);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(layout);
toast.show();

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

@ -14,10 +14,12 @@ import org.mozilla.gecko.overlays.ui.SendTabList.State;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClientRecord> {
@ -39,7 +41,7 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
private AlertDialog dialog;
public SendTabDeviceListArrayAdapter(Context context, SendTabTargetSelectedListener aListener) {
super(context, R.layout.overlay_share_send_tab_item);
super(context, R.layout.overlay_share_send_tab_item, R.id.overlaybtn_label);
listener = aListener;
@ -88,15 +90,22 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
final Context context = getContext();
// Reuse View objects if they exist.
TextView row = (TextView) convertView;
OverlayDialogButton row = (OverlayDialogButton) convertView;
if (row == null) {
row = (TextView) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
row = (OverlayDialogButton) View.inflate(context, R.layout.overlay_share_send_tab_item, null);
}
// The first view in the list has a unique style.
if (position == 0) {
row.setBackgroundResource(R.drawable.overlay_share_button_background_first);
} else {
row.setBackgroundResource(R.drawable.overlay_share_button_background);
}
if (currentState != State.LIST) {
// If we're in a special "Button-like" state, use the override string and a generic icon.
row.setText(dummyRecordName);
row.setCompoundDrawablesWithIntrinsicBounds(R.drawable.overlay_send_tab_icon, 0, 0, 0);
final Drawable sendTabIcon = context.getResources().getDrawable(R.drawable.overlay_send_tab_icon);
row.setEnabledLabelAndIcon(dummyRecordName, sendTabIcon);
}
// If we're just a button to launch the dialog, set the listener and abort.
@ -114,8 +123,8 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
// The remaining states delegate to the SentTabTargetSelectedListener.
final ParcelableClientRecord clientRecord = getItem(position);
if (currentState == State.LIST) {
row.setText(clientRecord.name);
row.setCompoundDrawablesWithIntrinsicBounds(getImage(clientRecord), 0, 0, 0);
final Drawable clientIcon = context.getResources().getDrawable(getImage(clientRecord));
row.setEnabledLabelAndIcon(clientRecord.name, clientIcon);
final String listenerGUID = clientRecord.guid;

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

@ -71,6 +71,9 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
private OverlayDialogButton readingListButton;
private OverlayDialogButton bookmarkButton;
// The reading list drawable set from XML - we need this to reset state.
private Drawable readingListButtonDrawable;
private String url;
private String title;
@ -122,6 +125,16 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
clientrecords.length <= MAXIMUM_INLINE_DEVICES) {
// Show the list of devices in-line.
sendTabList.switchState(SendTabList.State.LIST);
// The first item in the list has a unique style. If there are no items
// in the list, the next button appears to be the first item in the list.
//
// Note: a more thorough implementation would add this
// (and other non-ListView buttons) into a custom ListView.
if (clientrecords == null || clientrecords.length == 0) {
readingListButton.setBackgroundResource(
R.drawable.overlay_share_button_background_first);
}
return;
}
@ -174,6 +187,8 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
bookmarkButton = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
readingListButton = (OverlayDialogButton) findViewById(R.id.overlay_share_reading_list_btn);
readingListButtonDrawable = readingListButton.getBackground();
final Resources resources = getResources();
final String bookmarkEnabledLabel = resources.getString(R.string.overlay_share_bookmark_btn_label);
final Drawable bookmarkEnabledIcon = resources.getDrawable(R.drawable.overlay_bookmark_icon);
@ -218,6 +233,7 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
// If the Activity is being reused, we need to reset the state. Ideally, we create a
// new instance for each call, but Android L breaks this (bug 1137928).
sendTabList.switchState(SendTabList.State.LOADING);
readingListButton.setBackgroundDrawable(readingListButtonDrawable);
// The URL is usually hiding somewhere in the extra text. Extract it.
final String extraText = ContextUtils.getStringExtra(intent, Intent.EXTRA_TEXT);
@ -263,15 +279,14 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
subtitleView.setMarqueeRepeatLimit(5);
subtitleView.setSelected(true);
final ImageView foxIcon = (ImageView) findViewById(R.id.share_overlay_icon);
final LinearLayout topBar = (LinearLayout) findViewById(R.id.share_overlay_top_bar);
final View titleView = findViewById(R.id.title);
if (state == State.DEVICES_ONLY) {
bookmarkButton.setVisibility(View.GONE);
readingListButton.setVisibility(View.GONE);
foxIcon.setOnClickListener(null);
topBar.setOnClickListener(null);
titleView.setOnClickListener(null);
subtitleView.setOnClickListener(null);
return;
}
@ -286,8 +301,8 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
}
};
foxIcon.setOnClickListener(launchBrowser);
topBar.setOnClickListener(launchBrowser);
titleView.setOnClickListener(launchBrowser);
subtitleView.setOnClickListener(launchBrowser);
final LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile());
setButtonState(url, browserDB);

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

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@color/text_color_overlaybtn_disabled" />
<item android:color="@color/text_color_overlaybtn" />
</selector>

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

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false" android:color="@color/disabled_grey" />
<item android:color="@color/text_and_tabs_tray_grey" />
</selector>

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

До

Ширина:  |  Высота:  |  Размер: 4.4 KiB

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

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

До

Ширина:  |  Высота:  |  Размер: 951 B

После

Ширина:  |  Высота:  |  Размер: 951 B

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

До

Ширина:  |  Высота:  |  Размер: 3.5 KiB

После

Ширина:  |  Высота:  |  Размер: 282 B

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

До

Ширина:  |  Высота:  |  Размер: 3.6 KiB

После

Ширина:  |  Высота:  |  Размер: 702 B

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

До

Ширина:  |  Высота:  |  Размер: 4.0 KiB

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

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

До

Ширина:  |  Высота:  |  Размер: 3.8 KiB

После

Ширина:  |  Высота:  |  Размер: 983 B

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

До

Ширина:  |  Высота:  |  Размер: 710 B

После

Ширина:  |  Высота:  |  Размер: 710 B

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

До

Ширина:  |  Высота:  |  Размер: 3.2 KiB

После

Ширина:  |  Высота:  |  Размер: 205 B

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

До

Ширина:  |  Высота:  |  Размер: 3.4 KiB

После

Ширина:  |  Высота:  |  Размер: 510 B

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

До

Ширина:  |  Высота:  |  Размер: 3.5 KiB

После

Ширина:  |  Высота:  |  Размер: 789 B

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

До

Ширина:  |  Высота:  |  Размер: 5.0 KiB

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

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

До

Ширина:  |  Высота:  |  Размер: 1.2 KiB

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше