Merge m-c to inbound. a=merge
|
@ -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.eventCallback = (aEvent) => {
|
||||
switch (aEvent) {
|
||||
case "removed":
|
||||
options.contentWindow = null;
|
||||
options.sourceURI = null;
|
||||
};
|
||||
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);
|
||||
}
|
||||
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 "addon-install-complete":
|
||||
var needsRestart = installInfo.installs.some(function(i) {
|
||||
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":
|
||||
this.updateReaderButton();
|
||||
break;
|
||||
|
||||
}
|
||||
},
|
||||
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,46 +709,16 @@ 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");
|
||||
|
||||
// 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");
|
||||
ok(!PopupNotifications.isPanelOpen, "Notification should be closed");
|
||||
|
||||
AddonManager.getAllInstalls(function(aInstalls) {
|
||||
is(aInstalls.length, 1, "Should be one pending install");
|
||||
aInstalls[0].cancel();
|
||||
is(aInstalls.length, 0, "Should be no pending install");
|
||||
|
||||
Services.perms.remove("example.com", "install");
|
||||
wait_for_notification_close(runNextTest);
|
||||
gBrowser.removeTab(gBrowser.selectedTab);
|
||||
runNextTest();
|
||||
});
|
||||
});
|
||||
|
||||
aWindow.document.documentElement.acceptDialog();
|
||||
});
|
||||
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* shift_left_click_test() {
|
||||
info("Running test: Shift left click");
|
||||
|
||||
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);
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
aWindow.close();
|
||||
|
||||
// Continue testing when the original window has focus again.
|
||||
whenWindowActivated(window, runNextTest);
|
||||
// Cleanup.
|
||||
yield promiseWindowClosed(win);
|
||||
});
|
||||
}, "http://example.com/");
|
||||
});
|
||||
Services.wm.addListener(listener);
|
||||
|
||||
info("Running test: Shift left click");
|
||||
triggerCommand(true, { shiftKey: true });
|
||||
}
|
||||
add_task(function* right_click_test() {
|
||||
info("Running test: Right click on go button");
|
||||
|
||||
function runNextTest() {
|
||||
let test = gTests.shift();
|
||||
if (!test) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Add a new tab.
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
|
||||
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
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);
|
||||
triggerCommand(true, {button: 2});
|
||||
|
||||
// Clean up
|
||||
while (gBrowser.tabs.length > 1)
|
||||
gBrowser.removeTab(gBrowser.selectedTab)
|
||||
runNextTest();
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
// Right click should do nothing (context menu will be shown).
|
||||
is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered");
|
||||
}
|
||||
},
|
||||
|
||||
{ desc: "Left click on go button",
|
||||
click: true,
|
||||
event: {},
|
||||
check: checkCurrent
|
||||
},
|
||||
// Cleanup.
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
{ desc: "Ctrl/Cmd left click on go button",
|
||||
click: true,
|
||||
event: { accelKey: true },
|
||||
check: checkNewTab
|
||||
},
|
||||
add_task(function* shift_accel_left_click_test() {
|
||||
info("Running test: Shift+Ctrl/Cmd left click on go button");
|
||||
|
||||
{ desc: "Shift+Ctrl/Cmd left click on go button",
|
||||
click: true,
|
||||
event: { accelKey: true, shiftKey: true },
|
||||
check: function(aTab) {
|
||||
// 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, aTab, "Focus did not change to the new tab");
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
{ desc: "Simple return keypress",
|
||||
event: {},
|
||||
check: checkCurrent
|
||||
},
|
||||
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}
|
||||
];
|
||||
|
||||
{ desc: "Alt+Return keypress in a blank tab",
|
||||
event: { altKey: true },
|
||||
check: checkCurrent
|
||||
},
|
||||
for (let test of tests) {
|
||||
info(`Running test: ${test.desc}`);
|
||||
|
||||
{ desc: "Alt+Return keypress in a dirty tab",
|
||||
event: { altKey: true },
|
||||
check: checkNewTab,
|
||||
startValue: START_VALUE
|
||||
},
|
||||
// Add a new tab.
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(test.url || "about:blank");
|
||||
yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||
|
||||
{ desc: "Ctrl/Cmd+Return keypress",
|
||||
event: { accelKey: true },
|
||||
check: checkCurrent
|
||||
// Trigger a load and check it occurs in the current tab.
|
||||
let tabSelectedPromise = promiseNewTabSelected();
|
||||
triggerCommand(test.click || false, test.event || {});
|
||||
yield tabSelectedPromise;
|
||||
|
||||
// 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");
|
||||
|
||||
// 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");
|
||||
function promiseLoadStarted() {
|
||||
return new Promise(resolve => {
|
||||
gBrowser.addTabsProgressListener({
|
||||
onStateChange(browser, webProgress, req, flags, status) {
|
||||
if (flags & Ci.nsIWebProgressListener.STATE_START) {
|
||||
gBrowser.removeTabsProgressListener(this);
|
||||
resolve();
|
||||
}
|
||||
|
||||
/* 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 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)
|
||||
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();
|
||||
}
|
||||
|
||||
whenWindowActivated(domwindow, maybeRunCallback);
|
||||
whenDelayedStartupFinished(domwindow, maybeRunCallback);
|
||||
Services.wm.removeListener(listener);
|
||||
whenDelayedStartupFinished(win, () => resolve(win));
|
||||
},
|
||||
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) {
|
||||
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();
|
||||
// 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, 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:spacer flex="1"/>
|
||||
<xul:hbox align="center">
|
||||
<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:button anonid="cancel" class="popup-progress-cancel" oncommand="document.getBindingParent(this).cancel()"/>
|
||||
</xul:hbox>
|
||||
<xul:label anonid="progresstext" class="popup-progress-label"/>
|
||||
<xul:label anonid="progresstext" class="popup-progress-label" flex="1" crop="end"/>
|
||||
<xul:spacer flex="1"/>
|
||||
<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,8 +1721,14 @@
|
|||
|
||||
if (downloadingCount == 0) {
|
||||
this.destroy();
|
||||
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);
|
||||
|
||||
// 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") {
|
||||
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);
|
||||
return disjunction;
|
||||
}
|
||||
|
||||
let sql = fragments.join(" ");
|
||||
return [sql, args];
|
||||
/**
|
||||
* 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,14 +695,12 @@ 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") {
|
||||
for (let prop in expectedItems[i]._record) {
|
||||
Assert.ok(prop in actualItems[i]._record, prop);
|
||||
Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkError(err) {
|
||||
Assert.ok(err);
|
||||
|
|
|
@ -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));
|
||||
|
||||
// delete third item
|
||||
yield gStore.deleteItemByURL(gItems[2].url);
|
||||
Assert.equal((yield gStore.count()), gItems.length - 3);
|
||||
items = [];
|
||||
yield gStore.forEachItem(i => items.push(i), {
|
||||
sort: "guid",
|
||||
});
|
||||
|
||||
// This test deletes items so it should probably run last.
|
||||
add_task(function* deleteItemByGUID() {
|
||||
// delete third item
|
||||
yield gStore.deleteItemByGUID(gItems[2].guid);
|
||||
Assert.equal((yield gStore.count()), gItems.length - 3);
|
||||
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));
|
||||
}).bind(this),
|
||||
onError: function(e, responseText, xhr) {
|
||||
deferred.reject(xhr);
|
||||
},
|
||||
postData: requestString,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
// 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,30 +380,22 @@ 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) {
|
||||
let options = {
|
||||
onLoad: function(responseText, xhr) {
|
||||
BingTokenManager._pendingRequest = null;
|
||||
|
||||
if (err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
try {
|
||||
let json = JSON.parse(this.response.body);
|
||||
let json = JSON.parse(responseText);
|
||||
|
||||
if (json.error) {
|
||||
deferred.reject(json.error);
|
||||
|
@ -411,7 +410,16 @@ let BingTokenManager = {
|
|||
} catch (e) {
|
||||
deferred.reject(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);
|
||||
}
|
||||
|
||||
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}";
|
||||
|
||||
add_task(function* setup() {
|
||||
// Add a new fake search engine.
|
||||
Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
|
||||
kSearchEngineURL);
|
||||
|
||||
let oldDefaultEngine = Services.search.defaultEngine;
|
||||
Services.search.defaultEngine = Services.search.getEngineByName(kSearchEngineID);
|
||||
|
||||
let tab;
|
||||
let searchParams;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerCleanupFunction(function () {
|
||||
if (tab) {
|
||||
gBrowser.removeTab(tab);
|
||||
}
|
||||
|
||||
// Remove the fake engine when done.
|
||||
registerCleanupFunction(() => {
|
||||
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,20 +1483,6 @@ 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);
|
||||
|
@ -1553,16 +1490,9 @@ RilObject.prototype = {
|
|||
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);
|
||||
}
|
||||
},
|
||||
|
||||
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) {
|
||||
if (options.rilRequestError) {
|
||||
if (this._getCurrentCallsRetryCount < GET_CURRENT_CALLS_RETRY_MAX) {
|
||||
this._getCurrentCallsRetryCount++;
|
||||
this.getCurrentCalls();
|
||||
this.getCurrentCalls(options);
|
||||
} else {
|
||||
this.sendDefaultResponse(options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -5107,20 +4664,17 @@ RilObject.prototype[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CA
|
|||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
// Treat it as CALL_FAIL_ERROR_UNSPECIFIED if the request failed.
|
||||
let failCause = CALL_FAIL_ERROR_UNSPECIFIED;
|
||||
|
||||
if (options.rilRequestError === 0) {
|
||||
let Buf = this.context.Buf;
|
||||
let num = length ? Buf.readInt32() : 0;
|
||||
let failCause = null;
|
||||
|
||||
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;
|
||||
failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[causeNum] || failCause;
|
||||
}
|
||||
if (DEBUG) this.context.debug("Last call fail cause: " + failCause);
|
||||
|
||||
if (options.callback) {
|
||||
options.callback(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 },
|
||||
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));
|
||||
|
||||
if (!aSkipStateConversion) {
|
||||
aCall.state = this._convertRILCallState(aCall.state);
|
||||
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 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 |