Merge m-c to inbound, a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2015-09-17 15:27:39 -07:00
commit 16a989a7b0
81 changed files with 1918 additions and 6394 deletions

View File

@ -537,6 +537,48 @@ SettingsListener.observe("theme.selected",
setPAC();
})();
#ifdef MOZ_B2G_RIL
XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
"resource://gre/modules/AppsUtils.jsm");
// ======================= Dogfooders FOTA ==========================
SettingsListener.observe('debug.performance_data.dogfooding', false,
isDogfooder => {
if (!isDogfooder) {
dump('AUS:Settings: Not a dogfooder!\n');
return;
}
if (!('mozTelephony' in navigator)) {
dump('AUS:Settings: There is no mozTelephony!\n');
return;
}
if (!('mozMobileConnections' in navigator)) {
dump('AUS:Settings: There is no mozMobileConnections!\n');
return;
}
let conn = navigator.mozMobileConnections[0];
conn.addEventListener('radiostatechange', function onradiostatechange() {
if (conn.radioState !== 'enabled') {
return;
}
conn.removeEventListener('radiostatechange', onradiostatechange);
navigator.mozTelephony.dial('*#06#').then(call => {
return call.result.then(res => {
if (res.success && res.statusMessage
&& (res.serviceCode === 'scImei')) {
Services.prefs.setCharPref("app.update.imei_hash",
AppsUtils.computeHash(res.statusMessage, "SHA512"));
}
});
});
});
});
#endif
// =================== Various simple mapping ======================
var settingsToObserve = {
'accessibility.screenreader_quicknav_modes': {

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -19,12 +19,12 @@
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>

View File

@ -17,9 +17,9 @@
</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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a1f9532e4157df2dc8d3e9c8100120f80117dcd4"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -19,12 +19,12 @@
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "aede8622d780ec71f766a3ecccbff74c04aaba4e",
"git_revision": "2082894c8e974b0c371e4dec298e0ad0f3ac56b1",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "dbd3a4ea9042cae987147f2d05f41d2a7ebaccbc",
"revision": "0e712c8d330e10908f99194a9638e62a07c5c483",
"repo_path": "integration/gaia-central"
}

View File

@ -17,9 +17,9 @@
</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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a1f9532e4157df2dc8d3e9c8100120f80117dcd4"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -15,11 +15,11 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>

View File

@ -6211,12 +6211,23 @@
else
aMenuitem.removeAttribute("selected");
function addEndImage() {
let endImage = document.createElement("image");
endImage.setAttribute("class", "alltabs-endimage");
let endImageContainer = document.createElement("hbox");
endImageContainer.setAttribute("align", "center");
endImageContainer.setAttribute("pack", "center");
endImageContainer.appendChild(endImage);
aMenuitem.appendChild(endImageContainer);
return endImage;
}
if (aMenuitem.firstChild)
aMenuitem.firstChild.remove();
if (aTab.hasAttribute("muted"))
aMenuitem.setAttribute("endimage", "chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted");
addEndImage().setAttribute("muted", "true");
else if (aTab.hasAttribute("soundplaying"))
aMenuitem.setAttribute("endimage", "chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio");
else
aMenuitem.removeAttribute("endimage");
addEndImage().setAttribute("soundplaying", "true");
]]></body>
</method>
</implementation>

View File

@ -10,8 +10,6 @@
* ALL need to match an error in order for that error not to cause a test
* failure. */
const kWhitelist = [
// Cleopatra is imported as-is, see bug 1004421.
{sourceName: /cleopatra.*(tree|ui)\.css/i},
// CodeMirror is imported as-is, see bug 1004423.
{sourceName: /codemirror\.css/i},
// PDFjs is futureproofing its pseudoselectors, and those rules are dropped.
@ -24,6 +22,13 @@ const kWhitelist = [
// Loop standalone client CSS uses placeholder cross browser pseudo-element
{sourceName: /loop\/.*\.css/i,
errorMessage: /Unknown pseudo-class.*placeholder/i},
// Loop issues that crept in since this test was broken, see bug ...
{sourceName: /loop\/.*shared\/css\/conversation.css/i,
errorMessage: /Error in parsing value for 'display'/i},
{sourceName: /loop\/.*shared\/css\/common.css/i,
errorMessage: /Unknown property 'user-select'/i},
{sourceName: /loop\/.*css\/panel.css/i,
errorMessage: /Expected color but found 'none'/i},
// Highlighter CSS uses chrome-only pseudo-class, see bug 985597.
{sourceName: /highlighter\.css/i,
errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i},
@ -65,6 +70,21 @@ function once(target, name) {
});
}
function messageIsCSSError(msg, innerWindowID, outerWindowID) {
// Only care about CSS errors generated by our iframe:
if ((msg instanceof Ci.nsIScriptError) &&
msg.category.includes("CSS") &&
msg.innerWindowID === innerWindowID && msg.outerWindowID === outerWindowID) {
// Check if this error is whitelisted in kWhitelist
if (!ignoredError(msg)) {
ok(false, "Got error message for " + msg.sourceName + ": " + msg.errorMessage);
return true;
}
info("Ignored error for " + msg.sourceName + " because of filter.");
}
return false;
}
add_task(function checkAllTheCSS() {
let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
// This asynchronously produces a list of URLs (sadly, mostly sync on our
@ -86,32 +106,20 @@ add_task(function checkAllTheCSS() {
iframe.contentWindow.location = testFile;
yield iframeLoaded;
let doc = iframe.contentWindow.document;
// Listen for errors caused by the CSS:
let errorListener = {
observe: function(aMessage) {
if (!aMessage || !(aMessage instanceof Ci.nsIScriptError)) {
return;
}
// Only care about CSS errors generated by our iframe:
if (aMessage.category.includes("CSS") && aMessage.innerWindowID === 0 && aMessage.outerWindowID === 0) {
// Check if this error is whitelisted in kWhitelist
if (!ignoredError(aMessage)) {
ok(false, "Got error message for " + aMessage.sourceName + ": " + aMessage.errorMessage);
errors++;
} else {
info("Ignored error for " + aMessage.sourceName + " because of filter.");
}
}
}
};
let windowUtils = iframe.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let innerWindowID = windowUtils.currentInnerWindowID;
let outerWindowID = windowUtils.outerWindowID;
// We build a list of promises that get resolved when their respective
// files have loaded and produced no errors.
let allPromises = [];
let errors = 0;
// Register the error listener to keep track of errors.
Services.console.registerListener(errorListener);
// filter out either the devtools paths or the non-devtools paths:
let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
let devtoolsPathBits = ["webide", "devtools"];
uris = uris.filter(uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path)));
for (let uri of uris) {
let linkEl = doc.createElement("link");
linkEl.setAttribute("rel", "stylesheet");
@ -122,7 +130,8 @@ add_task(function checkAllTheCSS() {
linkEl.removeEventListener("error", onError);
};
let onError = (e) => {
promiseForThisSpec.reject({error: e, href: linkEl.getAttribute("href")});
ok(false, "Loading " + linkEl.getAttribute("href") + " threw an error!");
promiseForThisSpec.resolve();
linkEl.removeEventListener("load", onLoad);
linkEl.removeEventListener("error", onError);
};
@ -136,12 +145,14 @@ add_task(function checkAllTheCSS() {
// Wait for all the files to have actually loaded:
yield Promise.all(allPromises);
let messages = Services.console.getMessageArray();
// Count errors (the test output will list actual issues for us, as well
// as the ok(false) in the error listener)
is(errors, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
// as the ok(false) in messageIsCSSError.
let errors = messages.filter(m => messageIsCSSError(m, innerWindowID, outerWindowID));
is(errors.length, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
// Clean up to avoid leaks:
Services.console.unregisterListener(errorListener);
iframe.remove();
doc.head.innerHTML = '';
doc = null;

View File

@ -64,8 +64,12 @@ function iterateOverPath(path, extensions) {
} else if (extensions.some((extension) => entry.name.endsWith(extension))) {
let file = parentDir.clone();
file.append(entry.name);
let uriSpec = getURLForFile(file);
files.push(Services.io.newURI(uriSpec, null, null));
// the build system might leave dead symlinks hanging around, which are
// returned as part of the directory iterator, but don't actually exist:
if (file.exists()) {
let uriSpec = getURLForFile(file);
files.push(Services.io.newURI(uriSpec, null, null));
}
} else if (entry.name.endsWith(".ja") || entry.name.endsWith(".jar")) {
let file = parentDir.clone();
file.append(entry.name);

View File

@ -663,12 +663,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
case "userMadeSearchSuggestionsChoice":
case "suggest.searches":
this._cacheUserMadeSearchSuggestionsChoice();
// Make sure the urlbar is focused. It won't be, for example,
// if the user used an accesskey to make an opt-in choice.
// mIgnoreFocus prevents the text from being selected.
this.mIgnoreFocus = true;
this.focus();
this.mIgnoreFocus = false;
if (this._userMadeSearchSuggestionsChoice) {
this.popup.searchSuggestionsNotificationWasDismissed(
this._prefs.getBoolPref("suggest.searches")
@ -1189,6 +1183,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<method name="dismissSearchSuggestionsNotification">
<parameter name="enableSuggestions"/>
<body><![CDATA[
// Make sure the urlbar is focused. It won't be, for example, if the
// user used an accesskey to make an opt-in choice. mIgnoreFocus
// prevents the text from being selected.
this.input.mIgnoreFocus = true;
this.input.focus();
this.input.mIgnoreFocus = false;
Services.prefs.setBoolPref(
"browser.urlbar.suggest.searches", enableSuggestions
);

View File

@ -19,7 +19,6 @@
"chai": false,
"console": false,
"loop": true,
"MozActivity": false,
"mozRTCSessionDescription": false,
"OT": false,
"performance": false,

View File

@ -317,12 +317,9 @@ html[dir="rtl"] .contact-filter {
background-size: 14px 14px;
}
.icon-contact-video-call:hover {
background-color: #47b396;
}
.icon-contact-video-call:hover,
.icon-contact-video-call:active {
background-color: #3aa689;
background-color: #50E3C2;
}
.icon-vertical-ellipsis {

View File

@ -134,9 +134,6 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,

View File

@ -134,9 +134,6 @@ loop.conversation = (function(mozL10n) {
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,

View File

@ -7,363 +7,6 @@ loop.shared = loop.shared || {};
loop.shared.models = (function(l10n) {
"use strict";
/**
* Conversation model.
*/
var ConversationModel = Backbone.Model.extend({
defaults: {
connected: false, // Session connected flag
ongoing: false, // Ongoing call flag
callerId: undefined, // Loop caller id
loopToken: undefined, // Loop conversation token
sessionId: undefined, // OT session id
sessionToken: undefined, // OT session token
sessionType: undefined, // Hawk session type
apiKey: undefined, // OT api key
windowId: undefined, // The window id
callId: undefined, // The callId on the server
progressURL: undefined, // The websocket url to use for progress
websocketToken: undefined, // The token to use for websocket auth, this is
// stored as a hex string which is what the server
// requires.
callType: undefined, // The type of incoming call selected by
// other peer ("audio" or "audio-video")
selectedCallType: "audio-video", // The selected type for the call that was
// initiated ("audio" or "audio-video")
callToken: undefined, // Incoming call token.
callUrl: undefined, // Incoming call url
// Used for blocking a call url
subscribedStream: false, // Used to indicate that a stream has been
// subscribed to
publishedStream: false // Used to indicate that a stream has been
// published
},
/**
* SDK object.
* @type {OT}
*/
sdk: undefined,
/**
* SDK session object.
* @type {XXX}
*/
session: undefined,
/**
* Constructor.
*
* Options:
* - {OT} mozLoop: browser mozLoop service object.
*
* Required:
* - {OT} sdk: OT SDK object.
*
* @param {Object} attributes Attributes object.
* @param {Object} options Options object.
*/
initialize: function(attributes, options) {
options = options || {};
this.mozLoop = options.mozLoop;
if (!options.sdk) {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
// Set loop.debug.sdk to true in the browser, or standalone:
// localStorage.setItem("debug.sdk", true);
if (loop.shared.utils.getBoolPreference("debug.sdk")) {
this.sdk.setLogLevel(this.sdk.DEBUG);
}
},
/**
* Indicates an incoming conversation has been accepted.
*/
accepted: function() {
this.trigger("call:accepted");
},
/**
* Used to indicate that an outgoing call should start any necessary
* set-up.
*
* @param {String} selectedCallType Call type ("audio" or "audio-video")
*/
setupOutgoingCall: function(selectedCallType) {
if (selectedCallType) {
this.set("selectedCallType", selectedCallType);
}
this.trigger("call:outgoing:get-media-privs");
},
/**
* Used to indicate that media privileges have been accepted.
*/
gotMediaPrivs: function() {
this.trigger("call:outgoing:setup");
},
/**
* Starts an outgoing conversation.
*
* @param {Object} sessionData The session data received from the
* server for the outgoing call.
*/
outgoing: function(sessionData) {
this.setOutgoingSessionData(sessionData);
this.trigger("call:outgoing");
},
/**
* Checks that the session is ready.
*
* @return {Boolean}
*/
isSessionReady: function() {
return !!this.get("sessionId");
},
/**
* Sets session information.
* Session data received by creating an outgoing call.
*
* @param {Object} sessionData Conversation session information.
*/
setOutgoingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
apiKey: sessionData.apiKey,
callId: sessionData.callId,
progressURL: sessionData.progressURL,
websocketToken: sessionData.websocketToken.toString(16)
});
},
/**
* Sets session information about the incoming call.
*
* @param {Object} sessionData Conversation session information.
*/
setIncomingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
sessionId: sessionData.sessionId,
sessionToken: sessionData.sessionToken,
sessionType: sessionData.sessionType,
apiKey: sessionData.apiKey,
callId: sessionData.callId,
callerId: sessionData.callerId,
urlCreationDate: sessionData.urlCreationDate,
progressURL: sessionData.progressURL,
websocketToken: sessionData.websocketToken.toString(16),
callType: sessionData.callType || "audio-video",
callToken: sessionData.callToken,
callUrl: sessionData.callUrl
});
},
/**
* Starts a SDK session and subscribe to call events.
*/
startSession: function() {
if (!this.isSessionReady()) {
throw new Error("Can't start session as it's not ready");
}
this.set({
publishedStream: false,
subscribedStream: false
});
this.session = this.sdk.initSession(this.get("sessionId"));
this.listenTo(this.session, "streamCreated", this._streamCreated);
this.listenTo(this.session, "connectionDestroyed",
this._connectionDestroyed);
this.listenTo(this.session, "sessionDisconnected",
this._sessionDisconnected);
this.session.connect(this.get("apiKey"), this.get("sessionToken"),
this._onConnectCompletion.bind(this));
// We store the call credentials for debugging purposes.
if (this.mozLoop) {
this.mozLoop.addConversationContext(this.get("windowId"),
this.get("sessionId"),
this.get("callId"));
}
},
/**
* Ends current session.
*/
endSession: function() {
this.session.disconnect();
this.set({
publishedStream: false,
subscribedStream: false,
ongoing: false
}).once("session:ended", this.stopListening, this);
},
/**
* Helper function to determine if video stream is available for the
* incoming or outgoing call
*
* @param {string} callType Incoming or outgoing call
*/
hasVideoStream: function(callType) {
if (callType === "incoming") {
return this.get("callType") === "audio-video";
}
if (callType === "outgoing") {
return this.get("selectedCallType") === "audio-video";
}
return undefined;
},
/**
* Used to remove the scheme from a url.
*/
_removeScheme: function(url) {
if (!url) {
return "";
}
return url.replace(/^https?:\/\//, "");
},
/**
* Returns a conversation identifier for the incoming call view
*/
getCallIdentifier: function() {
return this.get("callerId") || this._removeScheme(this.get("callUrl"));
},
/**
* Publishes a local stream.
*
* @param {Publisher} publisher The publisher object to publish
* to the session.
*/
publish: function(publisher) {
this.session.publish(publisher);
this.set("publishedStream", true);
},
/**
* Subscribes to a remote stream.
*
* @param {Stream} stream The remote stream to subscribe to.
* @param {DOMElement} element The element to display the stream in.
* @param {Object} config The display properties to set on the stream as
* documented in:
* https://tokbox.com/opentok/libraries/client/js/reference/Session.html#subscribe
*/
subscribe: function(stream, element, config) {
this.session.subscribe(stream, element, config);
this.set("subscribedStream", true);
},
/**
* Returns true if a stream has been published and a stream has been
* subscribed to.
*/
streamsConnected: function() {
return this.get("publishedStream") && this.get("subscribedStream");
},
/**
* Handle a loop-server error, which has an optional `errno` property which
* is server error identifier.
*
* Triggers the following events:
*
* - `session:expired` for expired call urls
* - `session:error` for other generic errors
*
* @param {Error} err Error object.
*/
_handleServerError: function(err) {
switch (err.errno) {
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
// missing OR expired; we treat this information as if the url is always
// expired.
case 105:
this.trigger("session:expired", err);
break;
default:
this.trigger("session:error", err);
break;
}
},
/**
* Manages connection status
* triggers apropriate event for connection error/success
* http://tokbox.com/opentok/tutorials/connect-session/js/
* http://tokbox.com/opentok/tutorials/hello-world/js/
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
*
* @param {error|null} error
*/
_onConnectCompletion: function(error) {
if (error) {
this.trigger("session:connection-error", error);
this.endSession();
} else {
this.trigger("session:connected");
this.set("connected", true);
}
},
/**
* New created streams are available.
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
this.set("ongoing", true)
.trigger("session:stream-created", event);
},
/**
* Local user hung up.
* http://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
*
* @param {SessionDisconnectEvent} event
*/
_sessionDisconnected: function(event) {
if(event.reason === "networkDisconnected") {
this._signalEnd("session:network-disconnected", event);
} else {
this._signalEnd("session:ended", event);
}
},
_signalEnd: function(eventName, event) {
this.set("connected", false)
.set("ongoing", false)
.trigger(eventName, event);
},
/**
* Peer hung up. Disconnects local session.
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
*
* @param {ConnectionEvent} event
*/
_connectionDestroyed: function(event) {
if (event.reason === "networkDisconnected") {
this._signalEnd("session:network-disconnected", event);
} else {
this._signalEnd("session:peer-hungup", event);
}
this.endSession();
}
});
/**
* Notification model.
*/
@ -445,7 +88,6 @@ loop.shared.models = (function(l10n) {
});
return {
ConversationModel: ConversationModel,
NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel
};

View File

@ -485,232 +485,6 @@ loop.shared.views = (function(_, mozL10n) {
}
});
/**
* Conversation view.
*/
var ConversationView = React.createClass({displayName: "ConversationView",
mixins: [
Backbone.Events,
sharedMixins.AudioMixin,
sharedMixins.MediaSetupMixin
],
propTypes: {
audio: React.PropTypes.object,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
initiate: React.PropTypes.bool,
isDesktop: React.PropTypes.bool,
model: React.PropTypes.object.isRequired,
mozLoop: React.PropTypes.object,
sdk: React.PropTypes.object.isRequired,
video: React.PropTypes.object
},
getDefaultProps: function() {
return {
initiate: true,
isDesktop: false,
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
getInitialState: function() {
return {
video: this.props.video,
audio: this.props.audio
};
},
componentDidMount: function() {
if (this.props.initiate) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this.props.isDesktop &&
!window.MediaStreamTrack.getSources) {
// If there's no getSources function, the sdk defines its own and caches
// the result. So here we define the "normal" one which doesn't get cached, so
// we can change it later.
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}, {kind: "video"}]);
};
}
this.listenTo(this.props.sdk, "exception", this._handleSdkException);
this.listenTo(this.props.model, "session:connected",
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
}
},
componentWillUnmount: function() {
// Unregister all local event listeners
this.stopListening();
this.hangup();
},
hangup: function() {
this.stopPublishing();
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*
* XXX: for now we only support a single remote stream, hence a single DOM
* element.
*
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
var incoming = this.getDOMNode().querySelector(".remote");
this.props.model.subscribe(event.stream, incoming,
this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}));
},
/**
* Handles the SDK Exception event.
*
* https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
*
* @param {ExceptionEvent} event
*/
_handleSdkException: function(event) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this.publisher &&
event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
event.message === "GetUserMedia" &&
this.state.video.enabled) {
this.state.video.enabled = false;
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}]);
};
this.stopListening(this.publisher);
this.publisher.destroy();
this.startPublishing();
}
},
/**
* Publishes remote streams available once a session is connected.
*
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
*
* @param {SessionConnectEvent} event
*/
startPublishing: function(event) {
var outgoing = this.getDOMNode().querySelector(".local");
// XXX move this into its StreamingVideo component?
this.publisher = this.props.sdk.initPublisher(
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
// Suppress OT GuM custom dialog, see bug 1018875
this.listenTo(this.publisher, "accessDialogOpened accessDenied",
function(ev) {
ev.preventDefault();
});
this.listenTo(this.publisher, "streamCreated", function(ev) {
this.setState({
audio: {enabled: ev.stream.hasAudio},
video: {enabled: ev.stream.hasVideo}
});
});
this.listenTo(this.publisher, "streamDestroyed", function() {
this.setState({
audio: {enabled: false},
video: {enabled: false}
});
});
this.props.model.publish(this.publisher);
},
/**
* Toggles streaming status for a given stream type.
*
* @param {String} type Stream type ("audio" or "video").
* @param {Boolean} enabled Enabled stream flag.
*/
publishStream: function(type, enabled) {
if (type === "audio") {
this.publisher.publishAudio(enabled);
this.setState({audio: {enabled: enabled}});
} else {
this.publisher.publishVideo(enabled);
this.setState({video: {enabled: enabled}});
}
},
/**
* Unpublishes local stream.
*/
stopPublishing: function() {
if (this.publisher) {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
this.props.model.session.unpublish(this.publisher);
}
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
"local-stream-audio": !this.state.video.enabled
});
return (
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation in-call"},
React.createElement("div", {className: "media nested"},
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote focus-stream"},
React.createElement(ConversationToolbar, {
audio: this.state.audio,
dispatcher: this.props.dispatcher,
hangup: this.hangup,
mozLoop: this.props.mozLoop,
publishStream: this.publishStream,
video: this.state.video})
)
),
React.createElement("div", {className: localStreamClasses})
)
)
)
);
}
});
/**
* Notification view.
*/
@ -1322,7 +1096,6 @@ loop.shared.views = (function(_, mozL10n) {
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ContextUrlView: ContextUrlView,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaLayoutView: MediaLayoutView,

View File

@ -485,232 +485,6 @@ loop.shared.views = (function(_, mozL10n) {
}
});
/**
* Conversation view.
*/
var ConversationView = React.createClass({
mixins: [
Backbone.Events,
sharedMixins.AudioMixin,
sharedMixins.MediaSetupMixin
],
propTypes: {
audio: React.PropTypes.object,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
initiate: React.PropTypes.bool,
isDesktop: React.PropTypes.bool,
model: React.PropTypes.object.isRequired,
mozLoop: React.PropTypes.object,
sdk: React.PropTypes.object.isRequired,
video: React.PropTypes.object
},
getDefaultProps: function() {
return {
initiate: true,
isDesktop: false,
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
getInitialState: function() {
return {
video: this.props.video,
audio: this.props.audio
};
},
componentDidMount: function() {
if (this.props.initiate) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this.props.isDesktop &&
!window.MediaStreamTrack.getSources) {
// If there's no getSources function, the sdk defines its own and caches
// the result. So here we define the "normal" one which doesn't get cached, so
// we can change it later.
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}, {kind: "video"}]);
};
}
this.listenTo(this.props.sdk, "exception", this._handleSdkException);
this.listenTo(this.props.model, "session:connected",
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
}
},
componentWillUnmount: function() {
// Unregister all local event listeners
this.stopListening();
this.hangup();
},
hangup: function() {
this.stopPublishing();
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*
* XXX: for now we only support a single remote stream, hence a single DOM
* element.
*
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*
* @param {StreamEvent} event
*/
_streamCreated: function(event) {
var incoming = this.getDOMNode().querySelector(".remote");
this.props.model.subscribe(event.stream, incoming,
this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}));
},
/**
* Handles the SDK Exception event.
*
* https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
*
* @param {ExceptionEvent} event
*/
_handleSdkException: function(event) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this.publisher &&
event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
event.message === "GetUserMedia" &&
this.state.video.enabled) {
this.state.video.enabled = false;
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}]);
};
this.stopListening(this.publisher);
this.publisher.destroy();
this.startPublishing();
}
},
/**
* Publishes remote streams available once a session is connected.
*
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
*
* @param {SessionConnectEvent} event
*/
startPublishing: function(event) {
var outgoing = this.getDOMNode().querySelector(".local");
// XXX move this into its StreamingVideo component?
this.publisher = this.props.sdk.initPublisher(
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
// Suppress OT GuM custom dialog, see bug 1018875
this.listenTo(this.publisher, "accessDialogOpened accessDenied",
function(ev) {
ev.preventDefault();
});
this.listenTo(this.publisher, "streamCreated", function(ev) {
this.setState({
audio: {enabled: ev.stream.hasAudio},
video: {enabled: ev.stream.hasVideo}
});
});
this.listenTo(this.publisher, "streamDestroyed", function() {
this.setState({
audio: {enabled: false},
video: {enabled: false}
});
});
this.props.model.publish(this.publisher);
},
/**
* Toggles streaming status for a given stream type.
*
* @param {String} type Stream type ("audio" or "video").
* @param {Boolean} enabled Enabled stream flag.
*/
publishStream: function(type, enabled) {
if (type === "audio") {
this.publisher.publishAudio(enabled);
this.setState({audio: {enabled: enabled}});
} else {
this.publisher.publishVideo(enabled);
this.setState({video: {enabled: enabled}});
}
},
/**
* Unpublishes local stream.
*/
stopPublishing: function() {
if (this.publisher) {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
this.props.model.session.unpublish(this.publisher);
}
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
"local-stream-audio": !this.state.video.enabled
});
return (
<div className="video-layout-wrapper">
<div className="conversation in-call">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote focus-stream">
<ConversationToolbar
audio={this.state.audio}
dispatcher={this.props.dispatcher}
hangup={this.hangup}
mozLoop={this.props.mozLoop}
publishStream={this.publishStream}
video={this.state.video} />
</div>
</div>
<div className={localStreamClasses}></div>
</div>
</div>
</div>
);
}
});
/**
* Notification view.
*/
@ -1322,7 +1096,6 @@ loop.shared.views = (function(_, mozL10n) {
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ContextUrlView: ContextUrlView,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaLayoutView: MediaLayoutView,

View File

@ -120,16 +120,7 @@
// We don't use the SDK's CSS. This will prevent spurious 404 errors.
window.OTProperties.cssURL = "about:blank";
</script>
<script type="text/javascript" src="js/multiplexGum.js"></script>
<script type="text/javascript" src="shared/libs/sdk.js"></script>
<script>
// multiplexGum needs evaluation before sdk.js, but TBPlugin is not
// defined until after sdk.js has been evaluated. This updates the
// navigator object to reference TBPlugin if it was defined by sdk.js.
if (!navigator.originalGum) {
navigator.originalGum = (window.TBPlugin && window.TBPlugin.getUserMedia);
}
</script>
<script type="text/javascript" src="libs/l10n-gaia-02ca67948fe8.js"></script>
<script type="text/javascript" src="shared/libs/react-0.12.2.js"></script>
<script type="text/javascript" src="shared/libs/lodash-3.9.3.js"></script>
@ -139,7 +130,6 @@
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/crypto.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
@ -154,7 +144,6 @@
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/standaloneMetricsStore.js"></script>

View File

@ -1,149 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
/**
* Monkeypatch getUserMedia in a way that prevents additional camera and
* microphone prompts, at the cost of ignoring all constraints other than
* the first set passed in.
*
* The first call to navigator.getUserMedia (also now aliased to
* multiplexGum.getPermsAndCacheMedia to allow for explicit calling code)
* will cause the underlying gUM implementation to be called.
*
* While permission is pending, subsequent calls will result in the callbacks
* being queued. Once the call succeeds or fails, all queued success or
* failure callbacks will be invoked. Subsequent calls to either function will
* cause the success or failure callback to be invoked immediately.
*/
loop.standaloneMedia = (function() {
"use strict";
function patchSymbolIfExtant(objectName, propertyName, replacement) {
var object;
if (window[objectName]) {
object = window[objectName];
}
if (object && object[propertyName]) {
object[propertyName] = replacement;
}
}
// originalGum _must_ be on navigator; otherwise things blow up.
// For TBPlugin users, navigator.originalGum is set after the TB SDK is loaded.
navigator.originalGum = navigator.getUserMedia ||
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia;
function _MultiplexGum() {
this.reset();
}
_MultiplexGum.prototype = {
/**
* @see The docs at the top of this file for overall semantics,
* & http://developer.mozilla.org/en-US/docs/NavigatorUserMedia.getUserMedia
* for params, since this is intended to be purely a passthrough to gUM.
*/
getPermsAndCacheMedia: function(constraints, onSuccess, onError) {
function handleResult(callbacks, param) {
// Operate on a copy of the array in case any of the callbacks
// calls reset, which would cause an infinite-recursion.
this.userMedia.successCallbacks = [];
this.userMedia.errorCallbacks = [];
callbacks.forEach(function(cb) {
if (typeof cb == "function") {
cb(param);
}
});
}
function handleSuccess(localStream) {
this.userMedia.pending = false;
this.userMedia.localStream = localStream;
this.userMedia.error = null;
handleResult.call(this, this.userMedia.successCallbacks.slice(0), localStream);
}
function handleError(error) {
this.userMedia.pending = false;
this.userMedia.error = error;
handleResult.call(this, this.userMedia.errorCallbacks.slice(0), error);
this.error = null;
}
if (this.userMedia.localStream &&
this.userMedia.localStream.ended) {
this.userMedia.localStream = null;
}
this.userMedia.errorCallbacks.push(onError);
this.userMedia.successCallbacks.push(onSuccess);
if (this.userMedia.localStream) {
handleSuccess.call(this, this.userMedia.localStream);
return;
} else if (this.userMedia.error) {
handleError.call(this, this.userMedia.error);
return;
}
if (this.userMedia.pending) {
return;
}
this.userMedia.pending = true;
navigator.originalGum(constraints, handleSuccess.bind(this),
handleError.bind(this));
},
/**
* Reset the cached permissions, callbacks, and media to their default
* state and call any error callbacks to let any waiting callers know
* not to ever expect any more callbacks. We use "PERMISSION_DENIED",
* for lack of a better, more specific gUM code that callers are likely
* to be prepared to handle.
*/
reset: function() {
// When called from the ctor, userMedia is not created yet.
if (this.userMedia) {
this.userMedia.errorCallbacks.forEach(function(cb) {
if (typeof cb == "function") {
cb("PERMISSION_DENIED");
}
});
if (this.userMedia.localStream &&
typeof this.userMedia.localStream.stop == "function") {
this.userMedia.localStream.stop();
}
}
this.userMedia = {
error: null,
localStream: null,
pending: false,
errorCallbacks: [],
successCallbacks: []
};
}
};
var singletonMultiplexGum = new _MultiplexGum();
function myGetUserMedia() {
// This function is needed to pull in the instance
// of the singleton for tests to overwrite the used instance.
singletonMultiplexGum.getPermsAndCacheMedia.apply(singletonMultiplexGum, arguments);
}
patchSymbolIfExtant("navigator", "mozGetUserMedia", myGetUserMedia);
patchSymbolIfExtant("navigator", "webkitGetUserMedia", myGetUserMedia);
patchSymbolIfExtant("navigator", "getUserMedia", myGetUserMedia);
patchSymbolIfExtant("TBPlugin", "getUserMedia", myGetUserMedia);
return {
multiplexGum: singletonMultiplexGum,
_MultiplexGum: _MultiplexGum,
setSingleton: function(singleton) {
singletonMultiplexGum = singleton;
}
};
})();

View File

@ -6,7 +6,7 @@ var loop = loop || {};
loop.store = loop.store || {};
/**
* Manages the conversation window app controller view. Used to get
* Manages the standalone app controller view. Used to get
* the window data and store the window type.
*/
loop.store.StandaloneAppStore = (function() {
@ -31,14 +31,10 @@ loop.store.StandaloneAppStore = (function() {
if (!options.sdk) {
throw new Error("Missing option sdk");
}
if (!options.conversation) {
throw new Error("Missing option conversation");
}
this._dispatcher = options.dispatcher;
this._storeState = {};
this._sdk = options.sdk;
this._conversation = options.conversation;
this._dispatcher.register(this, [
"extractTokenInfo"
@ -132,10 +128,6 @@ loop.store.StandaloneAppStore = (function() {
}
// Else type is home.
if (token) {
this._conversation.set({loopToken: token});
}
this.setStoreState({
windowType: windowType,
isFirefox: sharedUtils.isFirefox(navigator.userAgent),

View File

@ -1,155 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.StandaloneClient = (function() {
"use strict";
// The expected properties to be returned from the POST /calls request.
var expectedCallsProperties = [ "sessionId", "sessionToken", "apiKey" ];
/**
* Loop server standalone client.
*
* @param {Object} settings Settings object.
*/
function StandaloneClient(settings) {
settings = settings || {};
if (!settings.baseServerUrl) {
throw new Error("missing required baseServerUrl");
}
this.settings = settings;
}
StandaloneClient.prototype = {
/**
* Validates a data object to confirm it has the specified properties.
*
* @param {Object} data The data object to verify
* @param {Array} properties The list of properties to verify within the object
* @return This returns either the specific property if only one
* property is specified, or it returns all properties
*/
_validate: function(data, properties) {
if (typeof data !== "object") {
throw new Error("Invalid data received from server");
}
properties.forEach(function (property) {
if (!data.hasOwnProperty(property)) {
throw new Error("Invalid data received from server - missing " +
property);
}
});
if (properties.length === 1) {
return data[properties[0]];
}
return data;
},
/**
* Generic handler for XHR failures.
*
* @param {Function} cb Callback(err)
* @param xhrReq
*/
_failureHandler: function(cb, xhrReq) {
var jsonErr = JSON.parse(xhrReq.responseText && xhrReq.responseText || "{}");
var message = "HTTP " + xhrReq.status + " " + xhrReq.statusText;
// Logging the technical error to the console
console.error("Server error", message, jsonErr);
// Create an error with server error `errno` code attached as a property
var err = new Error(message);
err.errno = jsonErr.errno;
cb(err);
},
/**
* Makes a request for url creation date for standalone UI
*
* @param {String} loopToken The loopToken representing the call
* @param {Function} cb Callback(err, callUrlInfo)
*
**/
requestCallUrlInfo: function(loopToken, cb) {
if (!loopToken) {
throw new Error("Missing required parameter loopToken");
}
if (!cb) {
throw new Error("Missing required callback function");
}
var url = this.settings.baseServerUrl + "/calls/" + loopToken;
var xhrReq = new XMLHttpRequest();
xhrReq.open("GET", url, true);
xhrReq.setRequestHeader("Content-type", "application/json");
xhrReq.onload = function() {
var request = xhrReq;
var responseJSON = JSON.parse(request.responseText || null);
if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
try {
cb(null, responseJSON);
} catch (err) {
console.error("Error requesting call info", err.message);
cb(err);
}
} else {
this._failureHandler(cb, request);
}
}.bind(this, xhrReq);
xhrReq.send();
},
/**
* Posts a call request to the server for a call represented by the
* loopToken. Will return the session data for the call.
*
* @param {String} loopToken The loopToken representing the call
* @param {String} callType The type of media in the call, e.g.
* "audio" or "audio-video"
* @param {Function} cb Callback(err, sessionData)
*/
requestCallInfo: function(loopToken, callType, cb) {
if (!loopToken) {
throw new Error("missing required parameter loopToken");
}
var url = this.settings.baseServerUrl + "/calls/" + loopToken;
var xhrReq = new XMLHttpRequest();
xhrReq.open("POST", url, true);
xhrReq.setRequestHeader("Content-type", "application/json");
xhrReq.onload = function() {
var request = xhrReq;
var responseJSON = JSON.parse(request.responseText || null);
if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
try {
cb(null, this._validate(responseJSON, expectedCallsProperties));
} catch (err) {
console.error("Error requesting call info", err.message);
cb(err);
}
} else {
this._failureHandler(cb, request);
}
}.bind(this, xhrReq);
xhrReq.send(JSON.stringify({callType: callType, channel: "standalone"}));
}
};
return StandaloneClient;
})();

View File

@ -16,14 +16,11 @@ loop.webapp = (function(_, OT, mozL10n) {
var sharedUtils = loop.shared.utils;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var multiplexGum = loop.standaloneMedia.multiplexGum;
/**
* Homepage view.
*/
var HomeView = React.createClass({displayName: "HomeView",
render: function() {
multiplexGum.reset();
return (
React.createElement("p", null, mozL10n.get("welcome", {clientShortname: mozL10n.get("clientShortname2")}))
);
@ -111,732 +108,6 @@ loop.webapp = (function(_, OT, mozL10n) {
}
});
/**
* Expired call URL view.
*/
var CallUrlExpiredView = React.createClass({displayName: "CallUrlExpiredView",
propTypes: {
isFirefox: React.PropTypes.bool.isRequired
},
render: function() {
return (
React.createElement("div", {className: "highlight-issue-box"},
React.createElement("div", {className: "info-panel"},
React.createElement("div", {className: "firefox-logo"}),
React.createElement("h1", null, mozL10n.get("call_url_unavailable_notification_heading")),
React.createElement("h4", null, mozL10n.get("call_url_unavailable_notification_message2"))
),
React.createElement(PromoteFirefoxView, {isFirefox: this.props.isFirefox})
)
);
}
});
var ConversationBranding = React.createClass({displayName: "ConversationBranding",
render: function() {
return (
React.createElement("h1", {className: "standalone-header-title"},
React.createElement("strong", null, mozL10n.get("clientShortname2"))
)
);
}
});
var ConversationHeader = React.createClass({displayName: "ConversationHeader",
propTypes: {
urlCreationDateString: React.PropTypes.string.isRequired
},
render: function() {
var cx = React.addons.classSet;
var conversationUrl = location.href;
var urlCreationDateClasses = cx({
"light-color-font": true,
"call-url-date": true, /* Used as a handler in the tests */
// Hidden until date is available.
"hide": !this.props.urlCreationDateString.length
});
var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
"call_url_creation_date": this.props.urlCreationDateString
});
return (
React.createElement("header", {className: "standalone-header header-box container-box"},
React.createElement(ConversationBranding, null),
React.createElement("div", {className: "loop-logo",
title: mozL10n.get("client_alttext",
{clientShortname: mozL10n.get("clientShortname2")})}),
React.createElement("h3", {className: "call-url"},
conversationUrl
),
React.createElement("h4", {className: urlCreationDateClasses},
callUrlCreationDateString
)
)
);
}
});
var ConversationFooter = React.createClass({displayName: "ConversationFooter",
render: function() {
return (
React.createElement("div", {className: "standalone-footer container-box"},
React.createElement("div", {className: "footer-logo",
title: mozL10n.get("vendor_alttext",
{vendorShortname: mozL10n.get("vendorShortname")})}),
React.createElement("div", {className: "footer-external-links"},
React.createElement("a", {href: loop.config.generalSupportUrl, target: "_blank"},
mozL10n.get("support_link")
)
)
)
);
}
});
/**
* A view for when conversations are pending, displays any messages
* and an option cancel button.
*/
var PendingConversationView = React.createClass({displayName: "PendingConversationView",
propTypes: {
callState: React.PropTypes.string.isRequired,
// If not supplied, the cancel button is not displayed.
cancelCallback: React.PropTypes.func
},
render: function() {
var cancelButtonClasses = React.addons.classSet({
btn: true,
"btn-large": true,
"btn-cancel": true,
hide: !this.props.cancelCallback
});
return (
React.createElement("div", {className: "container"},
React.createElement("div", {className: "container-box"},
React.createElement("header", {className: "pending-header header-box"},
React.createElement(ConversationBranding, null)
),
React.createElement("div", {id: "cameraPreview"}),
React.createElement("div", {id: "messages"}),
React.createElement("p", {className: "standalone-btn-label"},
this.props.callState
),
React.createElement("div", {className: "btn-pending-cancel-group btn-group"},
React.createElement("div", {className: "flex-padding-1"}),
React.createElement("button", {className: cancelButtonClasses,
onClick: this.props.cancelCallback},
React.createElement("span", {className: "standalone-call-btn-text"},
mozL10n.get("initiate_call_cancel_button")
)
),
React.createElement("div", {className: "flex-padding-1"})
)
),
React.createElement(ConversationFooter, null)
)
);
}
});
/**
* View displayed whilst the get user media prompt is being displayed. Indicates
* to the user to accept the prompt.
*/
var GumPromptConversationView = React.createClass({displayName: "GumPromptConversationView",
render: function() {
var callState = mozL10n.get("call_progress_getting_media_description", {
clientShortname: mozL10n.get("clientShortname2")
});
document.title = mozL10n.get("standalone_title_with_status", {
clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("call_progress_getting_media_title")
});
return React.createElement(PendingConversationView, {callState: callState});
}
});
/**
* View displayed waiting for a call to be connected. Updates the display
* once the websocket shows that the callee is being alerted.
*/
var WaitingConversationView = React.createClass({displayName: "WaitingConversationView",
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: "connecting"
};
},
propTypes: {
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
.isRequired
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringtone", {loop: true});
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
multiplexGum.reset();
this.props.websocket.cancel();
},
render: function() {
var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
var callState = mozL10n.get(callStateStringEntityName);
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get(callStateStringEntityName)});
return (
React.createElement(PendingConversationView, {
callState: callState,
cancelCallback: this._cancelOutgoingCall})
);
}
});
var InitiateCallButton = React.createClass({displayName: "InitiateCallButton",
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
caption: React.PropTypes.string.isRequired,
disabled: React.PropTypes.bool,
startCall: React.PropTypes.func.isRequired
},
getDefaultProps: function() {
return {disabled: false};
},
render: function() {
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showMenu
});
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.props.disabled
});
return (
React.createElement("div", {className: "standalone-btn-chevron-menu-group"},
React.createElement("div", {className: "btn-group-chevron"},
React.createElement("div", {className: "btn-group"},
React.createElement("button", {className: "btn btn-constrained btn-large btn-accept",
disabled: this.props.disabled,
onClick: this.props.startCall("audio-video"),
title: mozL10n.get("initiate_audio_video_call_tooltip2")},
React.createElement("span", {className: "standalone-call-btn-text"},
this.props.caption
),
React.createElement("span", {className: "standalone-call-btn-video-icon"})
),
React.createElement("div", {className: chevronClasses,
onClick: this.toggleDropdownMenu}
)
),
React.createElement("ul", {className: dropdownMenuClasses},
React.createElement("li", null,
React.createElement("button", {className: "start-audio-only-call",
disabled: this.props.disabled,
onClick: this.props.startCall("audio")},
mozL10n.get("initiate_audio_call_button2")
)
)
)
)
)
);
}
});
/**
* Initiate conversation view.
*/
var InitiateConversationView = React.createClass({displayName: "InitiateConversationView",
mixins: [Backbone.Events],
propTypes: {
callButtonLabel: React.PropTypes.string.isRequired,
client: React.PropTypes.object.isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
title: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
urlCreationDateString: "",
disableCallButton: false
};
},
componentDidMount: function() {
this.listenTo(this.props.conversation,
"session:error", this._onSessionError);
this.props.client.requestCallUrlInfo(
this.props.conversation.get("loopToken"),
this._setConversationTimestamp);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversation);
localStorage.setItem("has-seen-tos", "true");
},
_onSessionError: function(error, l10nProps) {
var errorL10n = error || "unable_retrieve_call_info";
this.props.notifications.errorL10n(errorL10n, l10nProps);
console.error(errorL10n);
},
/**
* Initiates the call.
* Takes in a call type parameter "audio" or "audio-video" and returns
* a function that initiates the call. React click handler requires a function
* to be called when that event happenes.
*
* @param {string} User call type choice "audio" or "audio-video"
*/
startCall: function(callType) {
return function() {
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this);
},
_setConversationTimestamp: function(err, callUrlInfo) {
if (err) {
this.props.notifications.errorL10n("unable_retrieve_call_info");
} else {
this.setState({
urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
});
}
},
render: function() {
var tosLinkName = mozL10n.get("terms_of_use_link_text");
var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": "<a target=_blank href='" +
loop.config.legalWebsiteUrl + "'>" +
tosLinkName + "</a>",
"privacy_notice_url": "<a target=_blank href='" +
loop.config.privacyWebsiteUrl + "'>" + privacyNoticeName + "</a>"
});
var tosClasses = React.addons.classSet({
"terms-service": true,
hide: (localStorage.getItem("has-seen-tos") === "true")
});
return (
React.createElement("div", {className: "container"},
React.createElement("div", {className: "container-box"},
React.createElement(ConversationHeader, {
urlCreationDateString: this.state.urlCreationDateString}),
React.createElement("p", {className: "standalone-btn-label"},
this.props.title
),
React.createElement("div", {id: "messages"}),
React.createElement("div", {className: "btn-group"},
React.createElement("div", {className: "flex-padding-1"}),
React.createElement(InitiateCallButton, {
caption: this.props.callButtonLabel,
disabled: this.state.disableCallButton,
startCall: this.startCall}
),
React.createElement("div", {className: "flex-padding-1"})
),
React.createElement("p", {className: tosClasses,
dangerouslySetInnerHTML: {__html: tosHTML}})
),
React.createElement(ConversationFooter, null)
)
);
}
});
/**
* Ended conversation view.
*/
var EndedConversationView = React.createClass({displayName: "EndedConversationView",
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired,
sdk: React.PropTypes.object.isRequired
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_conversation_ended")});
return (
React.createElement("div", {className: "ended-conversation"},
React.createElement(sharedViews.ConversationView, {
audio: {enabled: false, visible: false},
dispatcher: this.props.dispatcher,
initiate: false,
model: this.props.conversation,
sdk: this.props.sdk,
video: {enabled: false, visible: false}})
)
);
}
});
var StartConversationView = React.createClass({displayName: "StartConversationView",
render: function() {
document.title = mozL10n.get("clientShortname2");
return (
React.createElement(InitiateConversationView, React.__spread({},
this.props,
{callButtonLabel: mozL10n.get("initiate_audio_video_call_button2"),
title: mozL10n.get("initiate_call_button_label2")}))
);
}
});
var FailedConversationView = React.createClass({displayName: "FailedConversationView",
mixins: [sharedMixins.AudioMixin],
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_error")});
return (
React.createElement(InitiateConversationView, React.__spread({},
this.props,
{callButtonLabel: mozL10n.get("retry_call_button"),
title: mozL10n.get("call_failed_title")}))
);
}
});
/**
* This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this._endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
resetCallStatus: function() {
return function() {
this.setState({callStatus: "start"});
}.bind(this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "start": {
return (
React.createElement(StartConversationView, {
client: this.props.client,
conversation: this.props.conversation,
notifications: this.props.notifications})
);
}
case "failure": {
return (
React.createElement(FailedConversationView, {
client: this.props.client,
conversation: this.props.conversation,
notifications: this.props.notifications})
);
}
case "gumPrompt": {
return React.createElement(GumPromptConversationView, null);
}
case "pending": {
return React.createElement(WaitingConversationView, {websocket: this._websocket});
}
case "connected": {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_in_conversation")});
return (
React.createElement(sharedViews.ConversationView, {
dispatcher: this.props.dispatcher,
initiate: true,
model: this.props.conversation,
sdk: this.props.sdk,
video: {enabled: this.props.conversation.hasVideoStream("outgoing")}})
);
}
case "end": {
return (
React.createElement(EndedConversationView, {
conversation: this.props.conversation,
dispatcher: this.props.dispatcher,
onAfterFeedbackReceived: this.resetCallStatus(),
sdk: this.props.sdk})
);
}
case "expired": {
return (
React.createElement(CallUrlExpiredView, {isFirefox: this.props.isFirefox})
);
}
default: {
return React.createElement(HomeView, null);
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.error(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.props.notifications.warnL10n("peer_ended_conversation2");
this.setState({callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
this.props.notifications.warnL10n("network_disconnected");
this.setState({callStatus: "end"});
},
/**
* Starts the set up of a call, obtaining the required information from the
* server.
*/
setupOutgoingCall: function() {
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
} else {
var callType = this.props.conversation.get("selectedCallType");
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
callType, function(err, sessionData) {
if (err) {
switch (err.errno) {
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
// missing OR expired; we treat this information as if the url is always
// expired.
case 105:
this.setState({callStatus: "expired"});
break;
default:
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
break;
}
return;
}
this.props.conversation.outgoing(sessionData);
}.bind(this));
}
},
/**
* Asks the user for the media privileges, handling the result appropriately.
*/
getMediaPrivs: function() {
this.setState({callStatus: "gumPrompt"});
multiplexGum.getPermsAndCacheMedia({audio: true, video: true},
function(localStream) {
this.props.conversation.gotMediaPrivs();
}.bind(this),
function(errorCode) {
multiplexGum.reset();
this.setState({callStatus: "failure"});
}.bind(this)
);
},
/**
* Actually starts the call.
*/
startCall: function() {
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
return;
}
this._setupWebSocket();
this.setState({callStatus: "pending"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId")
});
this._websocket.promiseConnect().then(function() {
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
switch(progressData.state) {
case "connecting": {
// We just go straight to the connected view as the media gets set up.
this.setState({callStatus: "connected"});
break;
}
case "terminated": {
// At the moment, we show the same text regardless
// of the terminated reason.
this._handleCallTerminated(progressData.reason);
break;
}
}
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated (reject, busy,
* timeout, cancel, media-fail, user-unknown, closed)
*/
_handleCallTerminated: function(reason) {
multiplexGum.reset();
if (reason === WEBSOCKET_REASONS.CANCEL) {
this.setState({callStatus: "start"});
return;
}
// XXX later, we'll want to display more meaningfull messages (needs UX)
this.props.notifications.errorL10n("call_timeout_notification_text");
this.setState({callStatus: "failure"});
},
/**
* Handles ending a call by resetting the view to the start state.
*/
_endCall: function() {
multiplexGum.reset();
if (this.state.callStatus !== "failure") {
this.setState({callStatus: "end"});
}
}
});
/**
* Webapp Root View. This is the main, single, view that controls the display
* of the webapp page.
@ -849,12 +120,7 @@ loop.webapp = (function(_, OT, mozL10n) {
propTypes: {
activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired,
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired
},
@ -885,17 +151,6 @@ loop.webapp = (function(_, OT, mozL10n) {
case "unsupportedBrowser": {
return React.createElement(UnsupportedBrowserView, {isFirefox: this.state.isFirefox});
}
case "outgoing": {
return (
React.createElement(OutgoingConversationView, {
client: this.props.client,
conversation: this.props.conversation,
dispatcher: this.props.dispatcher,
isFirefox: this.state.isFirefox,
notifications: this.props.notifications,
sdk: this.props.sdk})
);
}
case "room": {
return (
React.createElement(loop.standaloneRoomViews.StandaloneRoomView, {
@ -924,14 +179,8 @@ loop.webapp = (function(_, OT, mozL10n) {
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
var sdkDriver = new loop.OTSdkDriver({
// For the standalone, always request data channels. If they aren't
// implemented on the client, there won't be a similar message to us, and
@ -941,9 +190,6 @@ loop.webapp = (function(_, OT, mozL10n) {
sdk: OT
});
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
@ -951,7 +197,6 @@ loop.webapp = (function(_, OT, mozL10n) {
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
@ -976,11 +221,7 @@ loop.webapp = (function(_, OT, mozL10n) {
React.render(React.createElement(WebappRootView, {
activeRoomStore: activeRoomStore,
client: client,
conversation: conversation,
dispatcher: dispatcher,
notifications: notifications,
sdk: OT,
standaloneAppStore: standaloneAppStore}), document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
@ -997,14 +238,6 @@ loop.webapp = (function(_, OT, mozL10n) {
}
return {
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
GumPromptConversationView: GumPromptConversationView,
WaitingConversationView: WaitingConversationView,
StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,

View File

@ -16,14 +16,11 @@ loop.webapp = (function(_, OT, mozL10n) {
var sharedUtils = loop.shared.utils;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var multiplexGum = loop.standaloneMedia.multiplexGum;
/**
* Homepage view.
*/
var HomeView = React.createClass({
render: function() {
multiplexGum.reset();
return (
<p>{mozL10n.get("welcome", {clientShortname: mozL10n.get("clientShortname2")})}</p>
);
@ -111,732 +108,6 @@ loop.webapp = (function(_, OT, mozL10n) {
}
});
/**
* Expired call URL view.
*/
var CallUrlExpiredView = React.createClass({
propTypes: {
isFirefox: React.PropTypes.bool.isRequired
},
render: function() {
return (
<div className="highlight-issue-box">
<div className="info-panel">
<div className="firefox-logo" />
<h1>{mozL10n.get("call_url_unavailable_notification_heading")}</h1>
<h4>{mozL10n.get("call_url_unavailable_notification_message2")}</h4>
</div>
<PromoteFirefoxView isFirefox={this.props.isFirefox}/>
</div>
);
}
});
var ConversationBranding = React.createClass({
render: function() {
return (
<h1 className="standalone-header-title">
<strong>{mozL10n.get("clientShortname2")}</strong>
</h1>
);
}
});
var ConversationHeader = React.createClass({
propTypes: {
urlCreationDateString: React.PropTypes.string.isRequired
},
render: function() {
var cx = React.addons.classSet;
var conversationUrl = location.href;
var urlCreationDateClasses = cx({
"light-color-font": true,
"call-url-date": true, /* Used as a handler in the tests */
// Hidden until date is available.
"hide": !this.props.urlCreationDateString.length
});
var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
"call_url_creation_date": this.props.urlCreationDateString
});
return (
<header className="standalone-header header-box container-box">
<ConversationBranding />
<div className="loop-logo"
title={mozL10n.get("client_alttext",
{clientShortname: mozL10n.get("clientShortname2")})}></div>
<h3 className="call-url">
{conversationUrl}
</h3>
<h4 className={urlCreationDateClasses}>
{callUrlCreationDateString}
</h4>
</header>
);
}
});
var ConversationFooter = React.createClass({
render: function() {
return (
<div className="standalone-footer container-box">
<div className="footer-logo"
title={mozL10n.get("vendor_alttext",
{vendorShortname: mozL10n.get("vendorShortname")})} />
<div className="footer-external-links">
<a href={loop.config.generalSupportUrl} target="_blank">
{mozL10n.get("support_link")}
</a>
</div>
</div>
);
}
});
/**
* A view for when conversations are pending, displays any messages
* and an option cancel button.
*/
var PendingConversationView = React.createClass({
propTypes: {
callState: React.PropTypes.string.isRequired,
// If not supplied, the cancel button is not displayed.
cancelCallback: React.PropTypes.func
},
render: function() {
var cancelButtonClasses = React.addons.classSet({
btn: true,
"btn-large": true,
"btn-cancel": true,
hide: !this.props.cancelCallback
});
return (
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
<ConversationBranding />
</header>
<div id="cameraPreview" />
<div id="messages" />
<p className="standalone-btn-label">
{this.props.callState}
</p>
<div className="btn-pending-cancel-group btn-group">
<div className="flex-padding-1" />
<button className={cancelButtonClasses}
onClick={this.props.cancelCallback} >
<span className="standalone-call-btn-text">
{mozL10n.get("initiate_call_cancel_button")}
</span>
</button>
<div className="flex-padding-1" />
</div>
</div>
<ConversationFooter />
</div>
);
}
});
/**
* View displayed whilst the get user media prompt is being displayed. Indicates
* to the user to accept the prompt.
*/
var GumPromptConversationView = React.createClass({
render: function() {
var callState = mozL10n.get("call_progress_getting_media_description", {
clientShortname: mozL10n.get("clientShortname2")
});
document.title = mozL10n.get("standalone_title_with_status", {
clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("call_progress_getting_media_title")
});
return <PendingConversationView callState={callState}/>;
}
});
/**
* View displayed waiting for a call to be connected. Updates the display
* once the websocket shows that the callee is being alerted.
*/
var WaitingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: "connecting"
};
},
propTypes: {
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
.isRequired
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringtone", {loop: true});
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
multiplexGum.reset();
this.props.websocket.cancel();
},
render: function() {
var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
var callState = mozL10n.get(callStateStringEntityName);
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get(callStateStringEntityName)});
return (
<PendingConversationView
callState={callState}
cancelCallback={this._cancelOutgoingCall} />
);
}
});
var InitiateCallButton = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
caption: React.PropTypes.string.isRequired,
disabled: React.PropTypes.bool,
startCall: React.PropTypes.func.isRequired
},
getDefaultProps: function() {
return {disabled: false};
},
render: function() {
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showMenu
});
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.props.disabled
});
return (
<div className="standalone-btn-chevron-menu-group">
<div className="btn-group-chevron">
<div className="btn-group">
<button className="btn btn-constrained btn-large btn-accept"
disabled={this.props.disabled}
onClick={this.props.startCall("audio-video")}
title={mozL10n.get("initiate_audio_video_call_tooltip2")}>
<span className="standalone-call-btn-text">
{this.props.caption}
</span>
<span className="standalone-call-btn-video-icon" />
</button>
<div className={chevronClasses}
onClick={this.toggleDropdownMenu}>
</div>
</div>
<ul className={dropdownMenuClasses}>
<li>
<button className="start-audio-only-call"
disabled={this.props.disabled}
onClick={this.props.startCall("audio")}>
{mozL10n.get("initiate_audio_call_button2")}
</button>
</li>
</ul>
</div>
</div>
);
}
});
/**
* Initiate conversation view.
*/
var InitiateConversationView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
callButtonLabel: React.PropTypes.string.isRequired,
client: React.PropTypes.object.isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
title: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
urlCreationDateString: "",
disableCallButton: false
};
},
componentDidMount: function() {
this.listenTo(this.props.conversation,
"session:error", this._onSessionError);
this.props.client.requestCallUrlInfo(
this.props.conversation.get("loopToken"),
this._setConversationTimestamp);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversation);
localStorage.setItem("has-seen-tos", "true");
},
_onSessionError: function(error, l10nProps) {
var errorL10n = error || "unable_retrieve_call_info";
this.props.notifications.errorL10n(errorL10n, l10nProps);
console.error(errorL10n);
},
/**
* Initiates the call.
* Takes in a call type parameter "audio" or "audio-video" and returns
* a function that initiates the call. React click handler requires a function
* to be called when that event happenes.
*
* @param {string} User call type choice "audio" or "audio-video"
*/
startCall: function(callType) {
return function() {
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this);
},
_setConversationTimestamp: function(err, callUrlInfo) {
if (err) {
this.props.notifications.errorL10n("unable_retrieve_call_info");
} else {
this.setState({
urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
});
}
},
render: function() {
var tosLinkName = mozL10n.get("terms_of_use_link_text");
var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": "<a target=_blank href='" +
loop.config.legalWebsiteUrl + "'>" +
tosLinkName + "</a>",
"privacy_notice_url": "<a target=_blank href='" +
loop.config.privacyWebsiteUrl + "'>" + privacyNoticeName + "</a>"
});
var tosClasses = React.addons.classSet({
"terms-service": true,
hide: (localStorage.getItem("has-seen-tos") === "true")
});
return (
<div className="container">
<div className="container-box">
<ConversationHeader
urlCreationDateString={this.state.urlCreationDateString} />
<p className="standalone-btn-label">
{this.props.title}
</p>
<div id="messages"></div>
<div className="btn-group">
<div className="flex-padding-1" />
<InitiateCallButton
caption={this.props.callButtonLabel}
disabled={this.state.disableCallButton}
startCall={this.startCall}
/>
<div className="flex-padding-1" />
</div>
<p className={tosClasses}
dangerouslySetInnerHTML={{__html: tosHTML}}></p>
</div>
<ConversationFooter />
</div>
);
}
});
/**
* Ended conversation view.
*/
var EndedConversationView = React.createClass({
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired,
sdk: React.PropTypes.object.isRequired
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_conversation_ended")});
return (
<div className="ended-conversation">
<sharedViews.ConversationView
audio={{enabled: false, visible: false}}
dispatcher={this.props.dispatcher}
initiate={false}
model={this.props.conversation}
sdk={this.props.sdk}
video={{enabled: false, visible: false}} />
</div>
);
}
});
var StartConversationView = React.createClass({
render: function() {
document.title = mozL10n.get("clientShortname2");
return (
<InitiateConversationView
{...this.props}
callButtonLabel={mozL10n.get("initiate_audio_video_call_button2")}
title={mozL10n.get("initiate_call_button_label2")} />
);
}
});
var FailedConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_error")});
return (
<InitiateConversationView
{...this.props}
callButtonLabel={mozL10n.get("retry_call_button")}
title={mozL10n.get("call_failed_title")} />
);
}
});
/**
* This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var OutgoingConversationView = React.createClass({
propTypes: {
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
sdk: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this._endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
resetCallStatus: function() {
return function() {
this.setState({callStatus: "start"});
}.bind(this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "start": {
return (
<StartConversationView
client={this.props.client}
conversation={this.props.conversation}
notifications={this.props.notifications} />
);
}
case "failure": {
return (
<FailedConversationView
client={this.props.client}
conversation={this.props.conversation}
notifications={this.props.notifications} />
);
}
case "gumPrompt": {
return <GumPromptConversationView />;
}
case "pending": {
return <WaitingConversationView websocket={this._websocket} />;
}
case "connected": {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_in_conversation")});
return (
<sharedViews.ConversationView
dispatcher={this.props.dispatcher}
initiate={true}
model={this.props.conversation}
sdk={this.props.sdk}
video={{enabled: this.props.conversation.hasVideoStream("outgoing")}} />
);
}
case "end": {
return (
<EndedConversationView
conversation={this.props.conversation}
dispatcher={this.props.dispatcher}
onAfterFeedbackReceived={this.resetCallStatus()}
sdk={this.props.sdk} />
);
}
case "expired": {
return (
<CallUrlExpiredView isFirefox={this.props.isFirefox}/>
);
}
default: {
return <HomeView />;
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.error(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.props.notifications.warnL10n("peer_ended_conversation2");
this.setState({callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
this.props.notifications.warnL10n("network_disconnected");
this.setState({callStatus: "end"});
},
/**
* Starts the set up of a call, obtaining the required information from the
* server.
*/
setupOutgoingCall: function() {
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
} else {
var callType = this.props.conversation.get("selectedCallType");
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
callType, function(err, sessionData) {
if (err) {
switch (err.errno) {
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
// missing OR expired; we treat this information as if the url is always
// expired.
case 105:
this.setState({callStatus: "expired"});
break;
default:
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
break;
}
return;
}
this.props.conversation.outgoing(sessionData);
}.bind(this));
}
},
/**
* Asks the user for the media privileges, handling the result appropriately.
*/
getMediaPrivs: function() {
this.setState({callStatus: "gumPrompt"});
multiplexGum.getPermsAndCacheMedia({audio: true, video: true},
function(localStream) {
this.props.conversation.gotMediaPrivs();
}.bind(this),
function(errorCode) {
multiplexGum.reset();
this.setState({callStatus: "failure"});
}.bind(this)
);
},
/**
* Actually starts the call.
*/
startCall: function() {
var loopToken = this.props.conversation.get("loopToken");
if (!loopToken) {
this.props.notifications.errorL10n("missing_conversation_info");
this.setState({callStatus: "failure"});
return;
}
this._setupWebSocket();
this.setState({callStatus: "pending"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId")
});
this._websocket.promiseConnect().then(function() {
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
switch(progressData.state) {
case "connecting": {
// We just go straight to the connected view as the media gets set up.
this.setState({callStatus: "connected"});
break;
}
case "terminated": {
// At the moment, we show the same text regardless
// of the terminated reason.
this._handleCallTerminated(progressData.reason);
break;
}
}
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated (reject, busy,
* timeout, cancel, media-fail, user-unknown, closed)
*/
_handleCallTerminated: function(reason) {
multiplexGum.reset();
if (reason === WEBSOCKET_REASONS.CANCEL) {
this.setState({callStatus: "start"});
return;
}
// XXX later, we'll want to display more meaningfull messages (needs UX)
this.props.notifications.errorL10n("call_timeout_notification_text");
this.setState({callStatus: "failure"});
},
/**
* Handles ending a call by resetting the view to the start state.
*/
_endCall: function() {
multiplexGum.reset();
if (this.state.callStatus !== "failure") {
this.setState({callStatus: "end"});
}
}
});
/**
* Webapp Root View. This is the main, single, view that controls the display
* of the webapp page.
@ -849,12 +120,7 @@ loop.webapp = (function(_, OT, mozL10n) {
propTypes: {
activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired,
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired
},
@ -885,17 +151,6 @@ loop.webapp = (function(_, OT, mozL10n) {
case "unsupportedBrowser": {
return <UnsupportedBrowserView isFirefox={this.state.isFirefox}/>;
}
case "outgoing": {
return (
<OutgoingConversationView
client={this.props.client}
conversation={this.props.conversation}
dispatcher={this.props.dispatcher}
isFirefox={this.state.isFirefox}
notifications={this.props.notifications}
sdk={this.props.sdk} />
);
}
case "room": {
return (
<loop.standaloneRoomViews.StandaloneRoomView
@ -924,14 +179,8 @@ loop.webapp = (function(_, OT, mozL10n) {
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
var sdkDriver = new loop.OTSdkDriver({
// For the standalone, always request data channels. If they aren't
// implemented on the client, there won't be a similar message to us, and
@ -941,9 +190,6 @@ loop.webapp = (function(_, OT, mozL10n) {
sdk: OT
});
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
@ -951,7 +197,6 @@ loop.webapp = (function(_, OT, mozL10n) {
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
@ -976,11 +221,7 @@ loop.webapp = (function(_, OT, mozL10n) {
React.render(<WebappRootView
activeRoomStore={activeRoomStore}
client={client}
conversation={conversation}
dispatcher={dispatcher}
notifications={notifications}
sdk={OT}
standaloneAppStore={standaloneAppStore} />, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
@ -997,14 +238,6 @@ loop.webapp = (function(_, OT, mozL10n) {
}
return {
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
GumPromptConversationView: GumPromptConversationView,
WaitingConversationView: WaitingConversationView,
StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,

View File

@ -1,11 +1,5 @@
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
restart_call=Rejoin
conversation_has_ended=Your conversation has ended.
call_timeout_notification_text=Your call did not go through.
missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly.
peer_ended_conversation2=The person you were calling has ended the conversation.
call_failed_title=Call failed.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
@ -20,33 +14,21 @@ unmute_local_video_button_title2=Enable video
active_screenshare_button_title=Stop sharing
inactive_screenshare_button_title=Share your screen
outgoing_call_title=Start conversation?
call_with_contact_title=Conversation with {{incomingCallIdentity}}
welcome=Welcome to the {{clientShortname}} web client.
incompatible_browser_heading=Oops!
incompatible_browser_message=Firefox Hello only works in browsers that support WebRTC
powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
use_latest_firefox=Please try this link in a WebRTC-enabled browser, such as {{firefoxBrandNameLink}}.
unsupported_platform_heading=Sorry!
unsupported_platform_message={{platform}} does not currently support {{clientShortname}}
unsupported_platform_ios=iOS
unsupported_platform_windows_phone=Windows Phone
unsupported_platform_blackberry=Blackberry
unsupported_platform_learn_more_link=Learn more about why your platform doesn't support {{clientShortname}}
connection_error_see_console_notification=Call failed; see console for details.
call_url_unavailable_notification_heading=Oops!
call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
get_firefox_button=Get {{brandShortname}}
initiate_call_button_label2=Ready to start your conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
initiate_call_cancel_button=Cancel
legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
terms_of_use_link_text=Terms of use
privacy_notice_link_text=Privacy notice
invite_header_text=Invite someone to join you.
self_view_hidden_message=Self-view hidden but still being sent; resize window \
to show
@ -60,33 +42,11 @@ clientShortname2=Firefox Hello
## should remain "Mozilla" for all locales.
vendorShortname=Mozilla
## LOCALIZATION NOTE(client_alttext): {{clientShortname}} will be replaced with the
## value of the clientShortname2 string above.
client_alttext={{clientShortname}} logo
vendor_alttext={{vendorShortname}} logo
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
call_progress_getting_media_title=Waiting for media…
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
## replaced by the brand name
first_time_experience_title={{clientShortname}} — Join the conversation
first_time_experience_button_label=Get Started
help_label=Help
tour_label=Tour
rooms_default_room_name_template=Conversation {{conversationLabel}}
## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
@ -115,18 +75,11 @@ room_information_failure_unsupported_browser=Your browser cannot access any info
# localized content.
rooms_read_while_wait_offer=Want something to read while you wait?
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
## replaced by the brand name and {{currentStatus}} will be replaced
## by the current call status (Connecting, Ringing, etc.)
standalone_title_with_status={{clientShortname}} — {{currentStatus}}
## LOCALIZATION_NOTE(standalone_title_with_room_name): {{roomName}} will be replaced
## by the name of the conversation and {{clientShortname}} will be
## replaced by the brand name.
standalone_title_with_room_name={{roomName}} — {{clientShortname}}
status_in_conversation=In conversation
status_conversation_ended=Conversation ended
status_error=Something went wrong
support_link=Get Help
# Text chat strings

View File

@ -87,9 +87,6 @@ describe("loop.conversation", function() {
sandbox.stub(React, "render");
sandbox.stub(document.mozL10n, "initialize");
sandbox.stub(loop.shared.models.ConversationModel.prototype,
"initialize");
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
sandbox.stub(loop.shared.utils,

View File

@ -34,9 +34,7 @@ module.exports = function(config) {
"content/shared/js/textChatView.js",
"content/shared/js/urlRegExps.js",
"content/shared/js/linkifiedTextView.js",
"standalone/content/js/multiplexGum.js",
"standalone/content/js/standaloneAppStore.js",
"standalone/content/js/standaloneClient.js",
"standalone/content/js/standaloneMozLoop.js",
"standalone/content/js/standaloneRoomViews.js",
"standalone/content/js/standaloneMetricsStore.js",

View File

@ -6,442 +6,18 @@ describe("loop.shared.models", function() {
"use strict";
var expect = chai.expect;
var l10n = navigator.mozL10n || document.mozL10n;
var sharedModels = loop.shared.models, sandbox, fakeXHR,
requests = [], fakeSDK, fakeMozLoop, fakeSession, fakeSessionData;
var l10n = navigator.mozL10n;
var sharedModels = loop.shared.models;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function(xhr) {
requests.push(xhr);
};
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
websocketToken: 123,
callToken: "callToken",
callUrl: "http://invalid/callToken",
callerId: "mrssmith"
};
fakeSession = _.extend({
connect: function () {},
endSession: sandbox.stub(),
set: sandbox.stub(),
disconnect: sandbox.spy(),
unpublish: sandbox.spy()
}, Backbone.Events);
fakeSDK = {
initPublisher: sandbox.spy(),
initSession: sandbox.stub().returns(fakeSession)
};
fakeMozLoop = {
addConversationContext: sinon.spy()
};
});
afterEach(function() {
sandbox.restore();
});
describe("ConversationModel", function() {
describe("#initialize", function() {
it("should require a sdk option", function() {
expect(function() {
new sharedModels.ConversationModel({}, {});
}).to.Throw(Error, /missing required sdk/);
});
});
describe("constructed", function() {
var conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {
sdk: fakeSDK,
mozLoop: fakeMozLoop
});
conversation.set("loopToken", "fakeToken");
});
describe("#accepted", function() {
it("should trigger a `call:accepted` event", function(done) {
conversation.once("call:accepted", function() {
done();
});
conversation.accepted();
});
});
describe("#setupOutgoingCall", function() {
it("should set the a custom selected call type", function() {
conversation.setupOutgoingCall("audio");
expect(conversation.get("selectedCallType")).eql("audio");
});
it("should respect the default selected call type when none is passed",
function() {
conversation.setupOutgoingCall();
expect(conversation.get("selectedCallType")).eql("audio-video");
});
it("should trigger a `call:outgoing:get-media-privs` event", function(done) {
conversation.once("call:outgoing:get-media-privs", function() {
done();
});
conversation.setupOutgoingCall();
});
});
describe("#gotMediaPrivs", function() {
it("should trigger a `call:outgoing:setup` event", function(done) {
conversation.once("call:outgoing:setup", function() {
done();
});
conversation.gotMediaPrivs();
});
});
describe("#outgoing", function() {
beforeEach(function() {
sandbox.stub(conversation, "endSession");
sandbox.stub(conversation, "setOutgoingSessionData");
sandbox.stub(conversation, "setIncomingSessionData");
});
it("should save the outgoing sessionData", function() {
conversation.outgoing(fakeSessionData);
sinon.assert.calledOnce(conversation.setOutgoingSessionData);
});
it("should trigger a `call:outgoing` event", function(done) {
conversation.once("call:outgoing", function() {
done();
});
conversation.outgoing();
});
});
describe("#setSessionData", function() {
it("should update outgoing conversation session information",
function() {
conversation.setOutgoingSessionData(fakeSessionData);
expect(conversation.get("sessionId")).eql("sessionId");
expect(conversation.get("sessionToken")).eql("sessionToken");
expect(conversation.get("apiKey")).eql("apiKey");
});
it("should update incoming conversation session information",
function() {
conversation.setIncomingSessionData(fakeSessionData);
expect(conversation.get("sessionId")).eql("sessionId");
expect(conversation.get("sessionToken")).eql("sessionToken");
expect(conversation.get("apiKey")).eql("apiKey");
expect(conversation.get("callType")).eql("callType");
expect(conversation.get("callToken")).eql("callToken");
});
});
describe("#startSession", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK,
mozLoop: fakeMozLoop
});
model.set({
publishedStream: true,
subscribedStream: true
});
model.startSession();
});
it("should start a session", function() {
sinon.assert.calledOnce(fakeSDK.initSession);
});
it("should reset the stream flags", function() {
expect(model.get("publishedStream")).eql(false);
expect(model.get("subscribedStream")).eql(false);
});
it("should call addConversationContext", function() {
fakeMozLoop.addConversationContext = sandbox.stub();
model.set({
windowId: "28",
sessionId: "321456",
callId: "142536"
});
model.startSession();
sinon.assert.calledOnce(fakeMozLoop.addConversationContext);
sinon.assert.calledWithExactly(fakeMozLoop.addConversationContext,
"28", "321456", "142536");
});
it("should call connect", function() {
fakeSession.connect = sandbox.stub();
model.startSession();
sinon.assert.calledOnce(fakeSession.connect);
sinon.assert.calledWithExactly(fakeSession.connect,
sinon.match.string, sinon.match.string,
sinon.match.func);
});
it("should set connected to true when no error is called back",
function() {
fakeSession.connect = function(key, token, cb) {
cb(null);
};
sandbox.stub(model, "set");
model.startSession();
sinon.assert.calledWith(model.set, "connected", true);
});
it("should trigger session:connected when no error is called back",
function() {
fakeSession.connect = function(key, token, cb) {
cb(null);
};
sandbox.stub(model, "trigger");
model.startSession();
sinon.assert.calledWithExactly(model.trigger, "session:connected");
});
describe("Session events", function() {
it("should trigger a fail event when an error is called back",
function() {
fakeSession.connect = function(key, token, cb) {
cb({
error: true
});
};
sandbox.stub(model, "endSession");
model.startSession();
sinon.assert.calledOnce(model.endSession);
sinon.assert.calledWithExactly(model.endSession);
});
it("should trigger session:connection-error event when an error is" +
" called back", function() {
fakeSession.connect = function(key, token, cb) {
cb({
error: true
});
};
sandbox.stub(model, "trigger");
model.startSession();
sinon.assert.called(model.trigger);
sinon.assert.calledWithExactly(model.trigger,
"session:connection-error", sinon.match.object);
});
it("should set the connected attr to true on connection completed",
function() {
fakeSession.connect = function(key, token, cb) {
cb();
};
model.startSession();
expect(model.get("connected")).eql(true);
});
it("should trigger a session:ended event on sessionDisconnected",
function(done) {
model.once("session:ended", function(){ done(); });
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
});
it("should trigger network-disconnected on networkDisconnect reason",
function(done) {
model.once("session:network-disconnected", function() {
done();
});
var fakeEvent = {
connectionId: 42,
reason: "networkDisconnected"
};
fakeSession.trigger("sessionDisconnected", fakeEvent);
});
it("should set the connected attribute to false on sessionDisconnected",
function() {
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
expect(model.get("connected")).eql(false);
});
it("should set the ongoing attribute to false on sessionDisconnected",
function() {
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
expect(model.get("ongoing")).eql(false);
});
describe("connectionDestroyed event received", function() {
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
it("should trigger a session:peer-hungup model event",
function(done) {
model.once("session:peer-hungup", function(event) {
expect(event.connection.connectionId).eql(42);
done();
});
fakeSession.trigger("connectionDestroyed", fakeEvent);
});
it("should terminate the session", function() {
sandbox.stub(model, "endSession");
fakeSession.trigger("connectionDestroyed", fakeEvent);
sinon.assert.calledOnce(model.endSession);
});
});
});
});
describe("#endSession", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
model.set("ongoing", true);
model.startSession();
});
it("should disconnect current session", function() {
model.endSession();
sinon.assert.calledOnce(fakeSession.disconnect);
});
it("should set the connected attribute to false", function() {
model.endSession();
expect(model.get("connected")).eql(false);
});
it("should set the ongoing attribute to false", function() {
model.endSession();
expect(model.get("ongoing")).eql(false);
});
it("should set the streams to unpublished", function() {
model.set({
publishedStream: true,
subscribedStream: true
});
model.endSession();
expect(model.get("publishedStream")).eql(false);
expect(model.get("subscribedStream")).eql(false);
});
it("should stop listening to session events once the session is " +
"actually disconnected", function() {
sandbox.stub(model, "stopListening");
model.endSession();
model.trigger("session:ended");
sinon.assert.calledOnce(model.stopListening);
});
});
describe("#hasVideoStream", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
model.startSession();
});
it("should return true for incoming callType", function() {
model.set("callType", "audio-video");
expect(model.hasVideoStream("incoming")).to.eql(true);
});
it("should return true for outgoing callType", function() {
model.set("selectedCallType", "audio-video");
expect(model.hasVideoStream("outgoing")).to.eql(true);
});
});
describe("#getCallIdentifier", function() {
var model;
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
model.startSession();
});
it("should return the callerId", function() {
expect(model.getCallIdentifier()).eql("mrssmith");
});
it("should return the shorted callUrl if the callerId does not exist",
function() {
model.set({callerId: ""});
expect(model.getCallIdentifier()).eql("invalid/callToken");
});
it("should return an empty string if neither callerId nor callUrl exist",
function() {
model.set({
callerId: undefined,
callUrl: undefined
});
expect(model.getCallIdentifier()).eql("");
});
});
});
});
describe("NotificationCollection", function() {
var collection, notifData, testNotif;
@ -502,6 +78,5 @@ describe("loop.shared.models", function() {
expect(collection.at(0).get("message")).eql("translated:fakeId:fakeProp");
});
});
});
});

View File

@ -574,341 +574,6 @@ describe("loop.shared.views", function() {
});
});
describe("ConversationView", function() {
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model, fakeAudio;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
mozLoop: {}
}, props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.ConversationView, props));
}
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
fakeSession = _.extend({
connection: {connectionId: 42},
connect: sandbox.spy(),
disconnect: sandbox.spy(),
publish: sandbox.spy(),
unpublish: sandbox.spy(),
subscribe: sandbox.spy()
}, Backbone.Events);
fakePublisher = _.extend({
publishAudio: sandbox.spy(),
publishVideo: sandbox.spy()
}, Backbone.Events);
fakeSDK = {
initPublisher: sandbox.stub().returns(fakePublisher),
initSession: sandbox.stub().returns(fakeSession),
on: sandbox.stub()
};
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK
});
});
describe("#componentDidMount", function() {
it("should start a session by default", function() {
sandbox.stub(model, "startSession");
mountTestComponent({
sdk: fakeSDK,
model: model,
video: {enabled: true}
});
sinon.assert.calledOnce(model.startSession);
});
// Test loop.shared.utils.findParentNode.
// Added here to take advantage of having markup.
it("should find '.video-layout-wrapper'", function() {
var view = mountTestComponent({
initiate: false,
sdk: fakeSDK,
model: model,
video: {enabled: true}
});
var menu = view.getDOMNode().querySelector(".btn-hangup-entry");
var result = loop.shared.utils.findParentNode(menu,
"video-layout-wrapper");
expect(result.classList.contains("video-layout-wrapper")).to.eql(true);
});
it("shouldn't start a session if initiate is false", function() {
sandbox.stub(model, "startSession");
mountTestComponent({
initiate: false,
sdk: fakeSDK,
model: model,
video: {enabled: true}
});
sinon.assert.notCalled(model.startSession);
});
});
describe("constructed", function() {
var comp;
beforeEach(function() {
comp = mountTestComponent({
sdk: fakeSDK,
model: model,
video: {enabled: false}
});
});
describe("#hangup", function() {
beforeEach(function() {
comp.startPublishing();
});
it("should disconnect the session", function() {
sandbox.stub(model, "endSession");
comp.hangup();
sinon.assert.calledOnce(model.endSession);
});
it("should stop publishing local streams", function() {
comp.hangup();
sinon.assert.calledOnce(fakeSession.unpublish);
});
});
describe("#startPublishing", function() {
it("should publish local stream", function() {
comp.startPublishing();
sinon.assert.calledOnce(fakeSDK.initPublisher);
sinon.assert.calledOnce(fakeSession.publish);
});
// XXX This test would need reworking, but the code should be going
// away after the obsolences of call urls (currently bug 1170150).
it("should start listening to OT publisher accessDialogOpened and " +
" accessDenied events");
});
describe("#stopPublishing", function() {
beforeEach(function() {
sandbox.stub(fakePublisher, "off");
comp.startPublishing();
});
it("should stop publish local stream", function() {
comp.stopPublishing();
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unsubscribe from publisher events",
function() {
comp.stopPublishing();
// Note: Backbone.Events#stopListening calls off() on passed object.
sinon.assert.calledOnce(fakePublisher.off);
});
});
describe("#publishStream", function() {
var component;
beforeEach(function() {
component = mountTestComponent({
sdk: fakeSDK,
model: model,
video: {enabled: false}
});
component.startPublishing();
});
it("should start streaming local audio", function() {
component.publishStream("audio", true);
sinon.assert.calledOnce(fakePublisher.publishAudio);
sinon.assert.calledWithExactly(fakePublisher.publishAudio, true);
});
it("should stop streaming local audio", function() {
component.publishStream("audio", false);
sinon.assert.calledOnce(fakePublisher.publishAudio);
sinon.assert.calledWithExactly(fakePublisher.publishAudio, false);
});
it("should start streaming local video", function() {
component.publishStream("video", true);
sinon.assert.calledOnce(fakePublisher.publishVideo);
sinon.assert.calledWithExactly(fakePublisher.publishVideo, true);
});
it("should stop streaming local video", function() {
component.publishStream("video", false);
sinon.assert.calledOnce(fakePublisher.publishVideo);
sinon.assert.calledWithExactly(fakePublisher.publishVideo, false);
});
});
describe("Model events", function() {
describe("for standalone", function() {
beforeEach(function() {
// In standalone, navigator.mozLoop does not exists
if (navigator.hasOwnProperty("mozLoop")) {
sandbox.stub(navigator, "mozLoop", undefined);
}
});
it("should play a connected sound, once, on session:connected",
function() {
var url = "shared/sounds/connected.ogg";
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
model.trigger("session:connected");
fakeAudioXHR.onload();
sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(fakeAudioXHR.open, "GET", url, true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true);
});
});
describe("for desktop", function() {
var origMozLoop;
beforeEach(function() {
origMozLoop = navigator.mozLoop;
navigator.mozLoop = {
getAudioBlob: sinon.spy(function(name, callback) {
var data = new ArrayBuffer(10);
callback(null, new Blob([data], {type: "audio/ogg"}));
})
};
});
afterEach(function() {
navigator.mozLoop = origMozLoop;
});
it("should play a connected sound, once, on session:connected",
function() {
var url = "chrome://browser/content/loop/shared/sounds/connected.ogg";
model.trigger("session:connected");
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"connected", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true);
});
});
describe("for both (standalone and desktop)", function() {
beforeEach(function() {
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
});
it("should start streaming on session:connected", function() {
model.trigger("session:connected");
sinon.assert.calledOnce(fakeSDK.initPublisher);
});
it("should publish remote stream on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
model.trigger("session:stream-created", {stream: s1});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s1);
});
it("should unpublish local stream on session:ended", function() {
comp.startPublishing();
model.trigger("session:ended");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:peer-hungup", function() {
comp.startPublishing();
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:network-disconnected",
function() {
comp.startPublishing();
model.trigger("session:network-disconnected");
sinon.assert.calledOnce(fakeSession.unpublish);
});
});
});
describe("Publisher events", function() {
beforeEach(function() {
comp.startPublishing();
});
it("should set audio state on streamCreated", function() {
fakePublisher.trigger("streamCreated", {stream: {hasAudio: true}});
expect(comp.state.audio.enabled).eql(true);
fakePublisher.trigger("streamCreated", {stream: {hasAudio: false}});
expect(comp.state.audio.enabled).eql(false);
});
it("should set video state on streamCreated", function() {
fakePublisher.trigger("streamCreated", {stream: {hasVideo: true}});
expect(comp.state.video.enabled).eql(true);
fakePublisher.trigger("streamCreated", {stream: {hasVideo: false}});
expect(comp.state.video.enabled).eql(false);
});
it("should set media state on streamDestroyed", function() {
fakePublisher.trigger("streamDestroyed");
expect(comp.state.audio.enabled).eql(false);
expect(comp.state.video.enabled).eql(false);
});
});
});
});
describe("NotificationListView", function() {
var coll, view, testNotif;

View File

@ -45,7 +45,6 @@
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
@ -58,21 +57,17 @@
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->
<script src="standalone_client_test.js"></script>
<script src="standaloneAppStore_test.js"></script>
<script src="standaloneMozLoop_test.js"></script>
<script src="standaloneRoomViews_test.js"></script>
<script src="standaloneMetricsStore_test.js"></script>
<script src="webapp_test.js"></script>
<script src="multiplexGum_test.js"></script>
<script>
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {

View File

@ -1,363 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.standaloneMedia._MultiplexGum", function() {
"use strict";
var expect = chai.expect;
var defaultGum =
navigator.getUserMedia ||
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
(window.TBPlugin && window.TBPlugin.getUserMedia);
var sandbox;
var multiplexGum;
beforeEach(function() {
sandbox = sinon.sandbox.create();
multiplexGum = new loop.standaloneMedia._MultiplexGum();
loop.standaloneMedia.setSingleton(multiplexGum);
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("pending should default to false", function() {
expect(multiplexGum.userMedia.pending).to.equal(false);
});
});
describe("default getUserMedia", function() {
it("should call getPermsAndCacheMedia", function() {
var fakeOptions = {audio: true, video: true};
var successCB = function() {};
var errorCB = function() {};
sandbox.stub(navigator, "originalGum");
sandbox.stub(loop.standaloneMedia._MultiplexGum.prototype,
"getPermsAndCacheMedia");
multiplexGum = new loop.standaloneMedia._MultiplexGum();
defaultGum(fakeOptions, successCB, errorCB);
sinon.assert.calledOnce(multiplexGum.getPermsAndCacheMedia);
sinon.assert.calledWithExactly(multiplexGum.getPermsAndCacheMedia,
fakeOptions, successCB, errorCB);
});
});
describe("#getPermsAndCacheMedia", function() {
beforeEach(function() {
sandbox.stub(navigator, "originalGum");
});
it("should change pending to true", function() {
multiplexGum.getPermsAndCacheMedia();
expect(multiplexGum.userMedia.pending).to.equal(true);
});
it("should call originalGum", function() {
multiplexGum.getPermsAndCacheMedia();
sinon.assert.calledOnce(navigator.originalGum);
});
it("should reset the pending state when the error callback is called",
function(done) {
var fakeError = new Error();
navigator.originalGum.callsArgWith(2, fakeError);
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
expect(multiplexGum.userMedia.pending).to.equal(false);
done();
});
});
it("should reset the pending state when the success callback is called",
function(done) {
var fakeLocalStream = {};
navigator.originalGum.callsArgWith(1, fakeLocalStream);
multiplexGum.getPermsAndCacheMedia(null,
function onSuccess(localStream) {
expect(multiplexGum.userMedia.pending).to.equal(false);
done();
}, null);
});
it("should call the error callback when originalGum calls back an error",
function(done) {
var fakeError = new Error();
navigator.originalGum.callsArgWith(2, fakeError);
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
expect(error).to.eql(fakeError);
done();
});
});
it("should propagate the success callback when originalGum succeeds",
function(done) {
var fakeLocalStream = {};
navigator.originalGum.callsArgWith(1, fakeLocalStream);
multiplexGum.getPermsAndCacheMedia(null,
function onSuccess(localStream) {
expect(localStream).to.eql(fakeLocalStream);
done();
}, null);
});
it("should call the success callback when the stream is cached",
function(done) {
var fakeLocalStream = {};
multiplexGum.userMedia.localStream = fakeLocalStream;
sinon.assert.notCalled(navigator.originalGum);
multiplexGum.getPermsAndCacheMedia(null,
function onSuccess(localStream) {
expect(localStream).to.eql(fakeLocalStream);
done();
}, null);
});
it("should call the error callback when an error is cached",
function(done) {
var fakeError = new Error();
multiplexGum.userMedia.error = fakeError;
sinon.assert.notCalled(navigator.originalGum);
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
expect(error).to.eql(fakeError);
done();
});
});
it("should clear the error when success is called back", function(done) {
var fakeError = new Error();
var fakeLocalStream = {};
multiplexGum.userMedia.localStream = fakeLocalStream;
multiplexGum.userMedia.error = fakeError;
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
expect(multiplexGum.userMedia.error).to.not.eql(fakeError);
expect(localStream).to.eql(fakeLocalStream);
done();
}, null);
});
it("should call all success callbacks when success is achieved",
function(done) {
var fakeLocalStream = {};
var calls = 0;
// Async is needed so that the callbacks can be queued up.
navigator.originalGum.callsArgWithAsync(1, fakeLocalStream);
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
calls += 1;
expect(localStream).to.eql(fakeLocalStream);
}, null);
expect(multiplexGum.userMedia).to.have.property("pending", true);
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
calls += 10;
expect(localStream).to.eql(fakeLocalStream);
expect(calls).to.equal(11);
done();
}, null);
});
it("should call all error callbacks when error is encountered",
function(done) {
var fakeError = new Error();
var calls = 0;
// Async is needed so that the callbacks can be queued up.
navigator.originalGum.callsArgWithAsync(2, fakeError);
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
calls += 1;
expect(error).to.eql(fakeError);
});
expect(multiplexGum.userMedia).to.have.property("pending", true);
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
calls += 10;
expect(error).to.eql(fakeError);
expect(calls).to.eql(11);
done();
});
});
it("should not call a getPermsAndCacheMedia success callback at the time" +
" of gUM success callback fires",
function() {
var fakeLocalStream = {};
multiplexGum.userMedia.localStream = fakeLocalStream;
navigator.originalGum.callsArgWith(1, fakeLocalStream);
var calledOnce = false;
var promiseCalledOnce = new Promise(function(resolve, reject) {
multiplexGum.getPermsAndCacheMedia(null,
function gPACMSuccess(localStream) {
expect(localStream).to.eql(fakeLocalStream);
expect(multiplexGum.userMedia).to.have.property("pending", false);
expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
if (calledOnce) {
sinon.assert.fail("original callback was called twice");
}
calledOnce = true;
resolve();
}, function() {
sinon.assert.fail("error callback should not have fired");
reject();
});
});
return promiseCalledOnce.then(function() {
defaultGum(null, function gUMSuccess(localStream2) {
expect(localStream2).to.eql(fakeLocalStream);
expect(multiplexGum.userMedia).to.have.property("pending", false);
expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
});
});
});
it("should not call a getPermsAndCacheMedia error callback when the " +
" gUM error callback fires",
function() {
var fakeError = "monkeys ate the stream";
multiplexGum.userMedia.error = fakeError;
navigator.originalGum.callsArgWith(2, fakeError);
var calledOnce = false;
var promiseCalledOnce = new Promise(function(resolve, reject) {
multiplexGum.getPermsAndCacheMedia(null, function() {
sinon.assert.fail("success callback should not have fired");
reject();
}, function gPACMError(errString) {
expect(errString).to.eql(fakeError);
expect(multiplexGum.userMedia).to.have.property("pending", false);
if (calledOnce) {
sinon.assert.fail("original error callback was called twice");
}
calledOnce = true;
resolve();
});
});
return promiseCalledOnce.then(function() {
defaultGum(null, function() {},
function gUMError(errString) {
expect(errString).to.eql(fakeError);
expect(multiplexGum.userMedia).to.have.property("pending", false);
});
});
});
it("should call the success callback with a new stream, " +
" when a new stream is available",
function(done) {
var endedStream = {ended: true};
var newStream = {};
multiplexGum.userMedia.localStream = endedStream;
navigator.originalGum.callsArgWith(1, newStream);
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
expect(localStream).to.eql(newStream);
done();
}, null);
});
});
describe("#reset", function () {
it("should reset all userMedia state to default", function() {
// If userMedia is defined, then it needs to have all of
// the properties that multipleGum will depend on. It is
// easier to simply delete the object than to setup a fake
// state of the object.
delete multiplexGum.userMedia;
multiplexGum.reset();
expect(multiplexGum.userMedia).to.deep.equal({
error: null,
localStream: null,
pending: false,
errorCallbacks: [],
successCallbacks: []
});
});
it("should call all queued error callbacks with 'PERMISSION_DENIED'",
function(done) {
sandbox.stub(navigator, "originalGum");
multiplexGum.getPermsAndCacheMedia(null, function(localStream) {
sinon.assert.fail(
"The success callback shouldn't be called due to reset");
}, function(error) {
expect(error).to.equal("PERMISSION_DENIED");
done();
});
multiplexGum.reset();
});
it("should call MST.stop() on the stream tracks", function() {
var stopStub = sandbox.stub();
multiplexGum.userMedia.localStream = {stop: stopStub};
multiplexGum.reset();
sinon.assert.calledOnce(stopStub);
});
it("should not call MST.stop() on the stream tracks if .stop() doesn't exist",
function() {
multiplexGum.userMedia.localStream = {};
try {
multiplexGum.reset();
} catch (ex) {
sinon.assert.fail(
"reset shouldn't throw when a stream doesn't implement stop(): "
+ ex);
}
});
it("should not get stuck in recursion if the error callback calls 'reset'",
function() {
sandbox.stub(navigator, "originalGum");
navigator.originalGum.callsArgWith(2, "PERMISSION_DENIED");
var calledOnce = false;
multiplexGum.getPermsAndCacheMedia(null, null, function() {
if (calledOnce) {
sinon.assert.fail("reset should only be called once");
}
calledOnce = true;
multiplexGum.reset.bind(multiplexGum)();
});
});
it("should not get stuck in recursion if the success callback calls 'reset'",
function() {
sandbox.stub(navigator, "originalGum");
navigator.originalGum.callsArgWith(1, {});
var calledOnce = false;
multiplexGum.getPermsAndCacheMedia(null, function() {
calledOnce = true;
multiplexGum.reset.bind(multiplexGum)();
}, function() {
if (calledOnce) {
sinon.assert.fail("reset should only be called once");
}
calledOnce = true;
});
});
});
});

View File

@ -22,9 +22,8 @@ describe("loop.store.StandaloneAppStore", function () {
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.StandaloneAppStore({
sdk: {},
helper: {},
conversation: {}
sdk: {}
});
}).to.Throw(/dispatcher/);
});
@ -33,25 +32,14 @@ describe("loop.store.StandaloneAppStore", function () {
expect(function() {
new loop.store.StandaloneAppStore({
dispatcher: dispatcher,
helper: {},
conversation: {}
});
}).to.Throw(/sdk/);
});
it("should throw an error if conversation is missing", function() {
expect(function() {
new loop.store.StandaloneAppStore({
dispatcher: dispatcher,
sdk: {},
helper: {}
});
}).to.Throw(/conversation/);
}).to.Throw(/sdk/);
});
});
describe("#extractTokenInfo", function() {
var store, fakeGetWindowData, fakeSdk, fakeConversation, helper;
var store, fakeGetWindowData, fakeSdk, helper;
beforeEach(function() {
fakeGetWindowData = {
@ -66,17 +54,12 @@ describe("loop.store.StandaloneAppStore", function () {
checkSystemRequirements: sinon.stub().returns(true)
};
fakeConversation = {
set: sinon.spy()
};
sandbox.stub(dispatcher, "dispatch");
store = new loop.store.StandaloneAppStore({
dispatcher: dispatcher,
sdk: fakeSdk,
helper: helper,
conversation: fakeConversation
sdk: fakeSdk
});
});
@ -153,32 +136,6 @@ describe("loop.store.StandaloneAppStore", function () {
expect(store.getStoreState().windowType).eql("home");
});
it("should set the loopToken on the conversation for call paths",
function() {
fakeGetWindowData.windowPath = "/c/fakecalltoken";
store.extractTokenInfo(
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
sinon.assert.calledOnce(fakeConversation.set);
sinon.assert.calledWithExactly(fakeConversation.set, {
loopToken: "fakecalltoken"
});
});
it("should set the loopToken on the conversation for room paths",
function() {
fakeGetWindowData.windowPath = "/c/fakeroomtoken";
store.extractTokenInfo(
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
sinon.assert.calledOnce(fakeConversation.set);
sinon.assert.calledWithExactly(fakeConversation.set, {
loopToken: "fakeroomtoken"
});
});
it("should dispatch a FetchServerData action for call paths",
function() {
fakeGetWindowData.windowPath = "/c/fakecalltoken";

View File

@ -1,179 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.StandaloneClient", function() {
"use strict";
var expect = chai.expect;
var sandbox,
fakeXHR,
requests = [],
callback,
fakeToken;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
callback = sinon.spy();
fakeToken = "fakeTokenText";
});
afterEach(function() {
sandbox.restore();
});
describe("loop.StandaloneClient", function() {
describe("#constructor", function() {
it("should require a baseServerUrl setting", function() {
expect(function() {
new loop.StandaloneClient();
}).to.Throw(Error, /required/);
});
});
describe("#requestCallUrlInfo", function() {
var client, fakeServerErrorDescription;
beforeEach(function() {
client = new loop.StandaloneClient(
{baseServerUrl: "http://fake.api"}
);
});
describe("should make the requests to the server", function() {
it("should throw if loopToken is missing", function() {
expect(client.requestCallUrlInfo).to
.throw(/Missing required parameter loopToken/);
});
it("should make a GET request for the call url creation date", function() {
client.requestCallUrlInfo("fakeCallUrlToken", function() {});
expect(requests).to.have.length.of(1);
expect(requests[0].url)
.to.eql("http://fake.api/calls/fakeCallUrlToken");
expect(requests[0].method).to.eql("GET");
});
it("should call the callback with (null, serverResponse)", function() {
var successCallback = sandbox.spy(function() {});
var serverResponse = {
calleeFriendlyName: "Andrei",
urlCreationDate: 0
};
client.requestCallUrlInfo("fakeCallUrlToken", successCallback);
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(serverResponse));
sinon.assert.calledWithExactly(successCallback,
null,
serverResponse);
});
it("should log the error if the requests fails", function() {
sinon.stub(console, "error");
var serverResponse = {error: true};
var error = JSON.stringify(serverResponse);
client.requestCallUrlInfo("fakeCallUrlToken", sandbox.stub());
requests[0].respond(404, {"Content-Type": "application/json"},
error);
sinon.assert.calledOnce(console.error);
sinon.assert.calledWithExactly(console.error, "Server error",
"HTTP 404 Not Found", serverResponse);
});
});
});
describe("requestCallInfo", function() {
var client, fakeServerErrorDescription;
beforeEach(function() {
client = new loop.StandaloneClient(
{baseServerUrl: "http://fake.api"}
);
fakeServerErrorDescription = {
code: 401,
errno: 101,
error: "error",
message: "invalid token",
info: "error info"
};
});
it("should prevent launching a conversation when token is missing",
function() {
expect(function() {
client.requestCallInfo();
}).to.Throw(Error, /missing.*[Tt]oken/);
});
it("should post data for the given call", function() {
client.requestCallInfo("fake", "audio", callback);
expect(requests).to.have.length.of(1);
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
expect(requests[0].method).to.be.equal("POST");
expect(requests[0].requestBody).to.be.equal('{"callType":"audio","channel":"standalone"}');
});
it("should receive call data for the given call", function() {
client.requestCallInfo("fake", "audio-video", callback);
var sessionData = {
sessionId: "one",
sessionToken: "two",
apiKey: "three"
};
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(sessionData));
sinon.assert.calledWithExactly(callback, null, sessionData);
});
it("should send an error when the request fails", function() {
client.requestCallInfo("fake", "audio", callback);
requests[0].respond(401, {"Content-Type": "application/json"},
JSON.stringify(fakeServerErrorDescription));
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP 401 Unauthorized/.test(err.message);
}));
});
it("should attach the server error description object to the error " +
"passed to the callback",
function() {
client.requestCallInfo("fake", "audio", callback);
requests[0].respond(401, {"Content-Type": "application/json"},
JSON.stringify(fakeServerErrorDescription));
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return err.errno === fakeServerErrorDescription.errno;
}));
});
it("should send an error if the data is not valid", function() {
client.requestCallInfo("fake", "audio", callback);
requests[0].respond(200, {"Content-Type": "application/json"},
'{"bad": "one"}');
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /Invalid data received/.test(err.message);
}));
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,6 @@
<div id="results"></div>
<script src="fake-mozLoop.js"></script>
<script src="fake-l10n.js"></script>
<script src="../content/js/multiplexGum.js"></script>
<script src="../content/shared/libs/react-0.12.2.js"></script>
<script src="../content/shared/libs/lodash-3.9.3.js"></script>
<script src="../content/shared/libs/backbone-1.2.1.js"></script>
@ -53,7 +52,6 @@
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../standalone/content/js/multiplexGum.js"></script>
<script src="../standalone/content/js/webapp.js"></script>
<script src="../standalone/content/js/standaloneRoomViews.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>

View File

@ -331,7 +331,7 @@ menuitem.bookmark-item {
}
/* Stock icons for the menu bar items */
menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip):not([endimage]) {
menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
-moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic");
}
@ -1636,6 +1636,14 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
box-shadow: inset -5px 0 ThreeDShadow;
}
.alltabs-endimage[muted] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
}
.alltabs-endimage[soundplaying] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
}
/* Sidebar */
#sidebar-throbber[loading="true"] {
list-style-image: url("chrome://global/skin/icons/loading_16.png");

View File

@ -2958,6 +2958,14 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
box-shadow: inset -5px 0 ThreeDShadow;
}
.alltabs-endimage[muted] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
}
.alltabs-endimage[soundplaying] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
}
/* Bookmarks toolbar */
#PlacesToolbarDropIndicator {
list-style-image: url(chrome://browser/skin/places/toolbarDropMarker.png);

View File

@ -3,7 +3,7 @@
* 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/. */
@import url("floating-scrollbars.css");
@import url("chrome://browser/skin/devtools/floating-scrollbars.css");
scrollbar thumb {
background-color: rgba(170,170,170,0.2) !important;

View File

@ -732,7 +732,7 @@ toolbar[brighttext] .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
padding: var(--toolbarbutton-vertical-inner-padding) 6px;
border: 1px solid transparent;
border-radius: 1px;
transition-property: background-color, border-color;
transition-property: background-color, border-color, box-shadow;
transition-duration: 150ms;
}
@ -773,22 +773,6 @@ toolbaritem[cui-areatype="toolbar"] > :-moz-any(@nestedButtons@) > .toolbarbutto
--toolbarbutton-checkedhover-backgroundcolor: rgba(90%,90%,90%,.4);
}
.findbar-button > .toolbarbutton-text,
#nav-bar .toolbarbutton-1 > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1 > .toolbarbutton-text,
#nav-bar .toolbarbutton-1 > .toolbarbutton-badge-stack,
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
background-color: hsla(210,32%,93%,0);
background-origin: padding-box;
border-radius: 2px;
border-color: hsla(210,54%,20%,0) hsla(210,54%,20%,0) hsla(210,54%,20%,0);
box-shadow: 0 1px hsla(0,0%,100%,0) inset,
0 1px hsla(210,54%,20%,0),
0 0 2px hsla(210,54%,20%,0);
transition-property: background-color, border-color, box-shadow;
transition-duration: 150ms;
}
}
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon:-moz-locale-dir(ltr),
@ -893,26 +877,6 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
box-shadow: var(--toolbarbutton-hover-boxshadow);
}
@media (-moz-os-version: windows-xp),
(-moz-os-version: windows-vista),
(-moz-os-version: windows-win7) {
/* < Win8 */
#nav-bar .toolbarbutton-1:not(:hover):not(:active):not([open]) > .toolbarbutton-menubutton-dropmarker::before,
#nav-bar .toolbaritem-combined-buttons > .toolbarbutton-1:-moz-any(:not(:hover):not([open]),[disabled]) + .toolbarbutton-1:-moz-any(:not(:hover):not([open]),[disabled])::before {
height: 18px;
background-size: 1px 18px;
}
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled]):not([open]):not(:active):hover > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1:not([buttonover]):not([open]):not(:active):hover > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon,
@conditionalForwardWithUrlbar@ > #forward-button:not([open]):not(:active):not([disabled]):hover > .toolbarbutton-icon {
border-color: hsla(210,54%,20%,.3) hsla(210,54%,20%,.35) hsla(210,54%,20%,.4);
background-color: hsla(210,48%,96%,.75);
box-shadow: 0 0 1px hsla(210,54%,20%,.03),
0 0 2px hsla(210,54%,20%,.1);
}
}
.findbar-button:not([disabled=true]):-moz-any([checked="true"],:hover:active) > .toolbarbutton-text,
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled=true]):-moz-any(:hover:active, [open]) > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1[open] > .toolbarbutton-menubutton-dropmarker:not([disabled=true]) > .dropmarker-icon,
@ -925,28 +889,9 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
transition-duration: 10ms;
}
@media (-moz-os-version: windows-xp),
(-moz-os-version: windows-vista),
(-moz-os-version: windows-win7) {
/* < Win8 */
.findbar-button:not([disabled=true]):-moz-any([checked="true"],:hover:active) > .toolbarbutton-text,
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled=true]):-moz-any(:hover:active, [open]) > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1[open] > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon,
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-text,
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-badge-stack {
text-shadow: none;
transition: none;
}
#nav-bar .toolbarbutton-1:-moz-any(:hover,[open]) > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon {
-moz-border-start-color: hsla(210,54%,20%,.35);
}
#nav-bar .toolbarbutton-1[checked]:not(:active):hover > .toolbarbutton-icon {
background-color: var(--toolbarbutton-checkedhover-backgroundcolor);
transition: background-color .4s;
}
#nav-bar .toolbarbutton-1[checked]:not(:active):hover > .toolbarbutton-icon {
background-color: var(--toolbarbutton-checkedhover-backgroundcolor);
transition: background-color .4s;
}
#TabsToolbar .toolbarbutton-1,
@ -2245,6 +2190,14 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
box-shadow: inset -5px 0 ThreeDShadow;
}
.alltabs-endimage[muted] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
}
.alltabs-endimage[soundplaying] {
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
}
toolbarbutton.chevron {
list-style-image: url("chrome://global/skin/toolbar/chevron.gif") !important;
}

View File

@ -8717,6 +8717,10 @@ AC_SUBST(MOZ_CHILD_PROCESS_BUNDLE)
# "Profile" field, which controls profile location.
# - MOZ_APP_ID: When set, used for application.ini's "ID" field, and
# crash reporter server url.
# - MOZ_APP_ANDROID_VERSION_CODE: On android, "android:versionCode" for
# the main application is set to the value of this variable. If not
# set, it falls back to a Mozilla-specific value derived from the
# build ID.
# - MOZ_PROFILE_MIGRATOR: When set, enables profile migrator.
if test -z "$MOZ_APP_NAME"; then
@ -8760,6 +8764,7 @@ AC_SUBST(MOZ_APP_BASENAME)
AC_SUBST(MOZ_APP_VENDOR)
AC_SUBST(MOZ_APP_PROFILE)
AC_SUBST(MOZ_APP_ID)
AC_SUBST(MOZ_APP_ANDROID_VERSION_CODE)
AC_SUBST(MAR_CHANNEL_ID)
AC_SUBST(ACCEPTED_MAR_CHANNEL_IDS)
AC_SUBST(MOZ_PROFILE_MIGRATOR)

View File

@ -757,8 +757,8 @@ this.AppsUtils = {
return deferred.promise;
},
// Returns the MD5 hash of a string.
computeHash: function(aString) {
// Returns the hash of a string, with MD5 as a default hashing function.
computeHash: function(aString, aAlgorithm = "MD5") {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
@ -768,7 +768,7 @@ this.AppsUtils = {
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.MD5);
hasher.initWithString(aAlgorithm);
hasher.update(data, data.length);
// We're passing false to get the binary hash and not base64.
let hash = hasher.finish(false);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,346 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h
#define mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h
#include "BluetoothDaemonHelpers.h"
#include "BluetoothInterface.h"
#include "mozilla/ipc/DaemonRunnables.h"
BEGIN_BLUETOOTH_NAMESPACE
using mozilla::ipc::DaemonSocketPDU;
using mozilla::ipc::DaemonSocketPDUHeader;
using mozilla::ipc::DaemonSocketResultHandler;
class BluetoothDaemonCoreModule
{
public:
enum {
SERVICE_ID = 0x01
};
enum {
OPCODE_ERROR = 0x00,
OPCODE_ENABLE = 0x01,
OPCODE_DISABLE = 0x02,
OPCODE_GET_ADAPTER_PROPERTIES = 0x03,
OPCODE_GET_ADAPTER_PROPERTY = 0x04,
OPCODE_SET_ADAPTER_PROPERTY = 0x05,
OPCODE_GET_REMOTE_DEVICE_PROPERTIES = 0x06,
OPCODE_GET_REMOTE_DEVICE_PROPERTY = 0x07,
OPCODE_SET_REMOTE_DEVICE_PROPERTY = 0x08,
OPCODE_GET_REMOTE_SERVICE_RECORD = 0x09,
OPCODE_GET_REMOTE_SERVICES = 0x0a,
OPCODE_START_DISCOVERY = 0x0b,
OPCODE_CANCEL_DISCOVERY = 0x0c,
OPCODE_CREATE_BOND = 0x0d,
OPCODE_REMOVE_BOND = 0x0e,
OPCODE_CANCEL_BOND = 0x0f,
OPCODE_PIN_REPLY = 0x10,
OPCODE_SSP_REPLY = 0x11,
OPCODE_DUT_MODE_CONFIGURE = 0x12,
OPCODE_DUT_MODE_SEND = 0x13,
OPCODE_LE_TEST_MODE = 0x14,
OPCODE_ADAPTER_STATE_CHANGED_NTF = 0x81,
OPCODE_ADAPTER_PROPERTIES_NTF = 0x82,
OPCODE_REMOTE_DEVICE_PROPERTIES_NTF = 0x83,
OPCODE_DEVICE_FOUND_NTF = 0x84,
OPCODE_DISCOVERY_STATE_CHANGED_NTF = 0x85,
OPCODE_PIN_REQUEST_NTF = 0x86,
OPCODE_SSP_REQUEST_NTF = 0x87,
OPCODE_BOND_STATE_CHANGED_NTF = 0x88,
OPCODE_ACL_STATE_CHANGED_NTF = 0x89,
OPCODE_DUT_MODE_RECV_NTF = 0x8a,
OPCODE_LE_TEST_MODE_NTF = 0x8b
};
static const int MAX_NUM_CLIENTS;
virtual nsresult Send(DaemonSocketPDU* aPDU,
DaemonSocketResultHandler* aRes) = 0;
void SetNotificationHandler(
BluetoothNotificationHandler* aNotificationHandler);
BluetoothNotificationHandler* GetNotificationHandler();
//
// Commands
//
nsresult EnableCmd(BluetoothResultHandler* aRes);
nsresult DisableCmd(BluetoothResultHandler* aRes);
nsresult GetAdapterPropertiesCmd(BluetoothResultHandler* aRes);
nsresult GetAdapterPropertyCmd(const nsAString& aName,
BluetoothResultHandler* aRes);
nsresult SetAdapterPropertyCmd(const BluetoothNamedValue& aProperty,
BluetoothResultHandler* aRes);
nsresult GetRemoteDevicePropertiesCmd(const nsAString& aRemoteAddr,
BluetoothResultHandler* aRes);
nsresult GetRemoteDevicePropertyCmd(const nsAString& aRemoteAddr,
const nsAString& aName,
BluetoothResultHandler* aRes);
nsresult SetRemoteDevicePropertyCmd(const nsAString& aRemoteAddr,
const BluetoothNamedValue& aProperty,
BluetoothResultHandler* aRes);
nsresult GetRemoteServiceRecordCmd(const nsAString& aRemoteAddr,
const uint8_t aUuid[16],
BluetoothResultHandler* aRes);
nsresult GetRemoteServicesCmd(const nsAString& aRemoteAddr,
BluetoothResultHandler* aRes);
nsresult StartDiscoveryCmd(BluetoothResultHandler* aRes);
nsresult CancelDiscoveryCmd(BluetoothResultHandler* aRes);
nsresult CreateBondCmd(const nsAString& aBdAddr,
BluetoothTransport aTransport,
BluetoothResultHandler* aRes);
nsresult RemoveBondCmd(const nsAString& aBdAddr,
BluetoothResultHandler* aRes);
nsresult CancelBondCmd(const nsAString& aBdAddr,
BluetoothResultHandler* aRes);
nsresult PinReplyCmd(const nsAString& aBdAddr, bool aAccept,
const nsAString& aPinCode,
BluetoothResultHandler* aRes);
nsresult SspReplyCmd(const nsAString& aBdAddr, BluetoothSspVariant aVariant,
bool aAccept, uint32_t aPasskey,
BluetoothResultHandler* aRes);
nsresult DutModeConfigureCmd(bool aEnable, BluetoothResultHandler* aRes);
nsresult DutModeSendCmd(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
BluetoothResultHandler* aRes);
nsresult LeTestModeCmd(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
BluetoothResultHandler* aRes);
protected:
void HandleSvc(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
private:
//
// Responses
//
typedef mozilla::ipc::DaemonResultRunnable0<
BluetoothResultHandler, void>
ResultRunnable;
typedef mozilla::ipc::DaemonResultRunnable1<
BluetoothResultHandler, void, BluetoothStatus, BluetoothStatus>
ErrorRunnable;
void ErrorRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void EnableRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void DisableRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void GetAdapterPropertiesRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void GetAdapterPropertyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void SetAdapterPropertyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void GetRemoteDevicePropertiesRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void
GetRemoteDevicePropertyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void SetRemoteDevicePropertyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void GetRemoteServiceRecordRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void GetRemoteServicesRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void StartDiscoveryRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void CancelDiscoveryRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void CreateBondRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void RemoveBondRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void CancelBondRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void PinReplyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void SspReplyRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void DutModeConfigureRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void DutModeSendRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void LeTestModeRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU,
BluetoothResultHandler* aRes);
void HandleRsp(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
//
// Notifications
//
class NotificationHandlerWrapper;
typedef mozilla::ipc::DaemonNotificationRunnable1<
NotificationHandlerWrapper, void, bool>
AdapterStateChangedNotification;
typedef mozilla::ipc::DaemonNotificationRunnable3<
NotificationHandlerWrapper, void, BluetoothStatus, int,
nsAutoArrayPtr<BluetoothProperty>, BluetoothStatus, int,
const BluetoothProperty*>
AdapterPropertiesNotification;
typedef mozilla::ipc::DaemonNotificationRunnable4<
NotificationHandlerWrapper, void, BluetoothStatus, nsString, int,
nsAutoArrayPtr<BluetoothProperty>, BluetoothStatus, const nsAString&,
int, const BluetoothProperty*>
RemoteDevicePropertiesNotification;
typedef mozilla::ipc::DaemonNotificationRunnable2<
NotificationHandlerWrapper, void, int, nsAutoArrayPtr<BluetoothProperty>,
int, const BluetoothProperty*>
DeviceFoundNotification;
typedef mozilla::ipc::DaemonNotificationRunnable1<
NotificationHandlerWrapper, void, bool>
DiscoveryStateChangedNotification;
typedef mozilla::ipc::DaemonNotificationRunnable3<
NotificationHandlerWrapper, void, nsString, nsString, uint32_t,
const nsAString&, const nsAString&>
PinRequestNotification;
typedef mozilla::ipc::DaemonNotificationRunnable5<
NotificationHandlerWrapper, void, nsString, nsString, uint32_t,
BluetoothSspVariant, uint32_t, const nsAString&, const nsAString&>
SspRequestNotification;
typedef mozilla::ipc::DaemonNotificationRunnable3<
NotificationHandlerWrapper, void, BluetoothStatus, nsString,
BluetoothBondState, BluetoothStatus, const nsAString&>
BondStateChangedNotification;
typedef mozilla::ipc::DaemonNotificationRunnable3<
NotificationHandlerWrapper, void, BluetoothStatus, nsString, bool,
BluetoothStatus, const nsAString&>
AclStateChangedNotification;
typedef mozilla::ipc::DaemonNotificationRunnable3<
NotificationHandlerWrapper, void, uint16_t, nsAutoArrayPtr<uint8_t>,
uint8_t, uint16_t, const uint8_t*>
DutModeRecvNotification;
typedef mozilla::ipc::DaemonNotificationRunnable2<
NotificationHandlerWrapper, void, BluetoothStatus, uint16_t>
LeTestModeNotification;
class AclStateChangedInitOp;
class AdapterPropertiesInitOp;
class BondStateChangedInitOp;
class DeviceFoundInitOp;
class DutModeRecvInitOp;
class PinRequestInitOp;
class RemoteDevicePropertiesInitOp;
class SspRequestInitOp;
void AdapterStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void AdapterPropertiesNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void RemoteDevicePropertiesNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void DeviceFoundNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void DiscoveryStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void PinRequestNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void SspRequestNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void BondStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void AclStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void DutModeRecvNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void LeTestModeNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU);
void HandleNtf(const DaemonSocketPDUHeader& aHeader,
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
static BluetoothNotificationHandler* sNotificationHandler;
};
END_BLUETOOTH_NAMESPACE
#endif // mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,9 @@ BluetoothDaemonSocketModule::ListenCmd(BluetoothSocketType aType,
{
MOZ_ASSERT(NS_IsMainThread());
nsAutoPtr<DaemonSocketPDU> pdu(new DaemonSocketPDU(0x02, 0x01, 0));
nsAutoPtr<DaemonSocketPDU> pdu(
new DaemonSocketPDU(SERVICE_ID, OPCODE_LISTEN,
0));
nsresult rv = PackPDU(
aType,
@ -61,7 +63,9 @@ BluetoothDaemonSocketModule::ConnectCmd(const nsAString& aBdAddr,
{
MOZ_ASSERT(NS_IsMainThread());
nsAutoPtr<DaemonSocketPDU> pdu(new DaemonSocketPDU(0x02, 0x02, 0));
nsAutoPtr<DaemonSocketPDU> pdu(
new DaemonSocketPDU(SERVICE_ID, OPCODE_CONNECT,
0));
nsresult rv = PackPDU(
PackConversion<nsAString, BluetoothAddress>(aBdAddr),
@ -166,9 +170,9 @@ BluetoothDaemonSocketModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
const DaemonSocketPDUHeader&,
DaemonSocketPDU&,
BluetoothSocketResultHandler*) = {
[0x00] = &BluetoothDaemonSocketModule::ErrorRsp,
[0x01] = &BluetoothDaemonSocketModule::ListenRsp,
[0x02] = &BluetoothDaemonSocketModule::ConnectRsp
[OPCODE_ERROR] = &BluetoothDaemonSocketModule::ErrorRsp,
[OPCODE_LISTEN] = &BluetoothDaemonSocketModule::ListenRsp,
[OPCODE_CONNECT] = &BluetoothDaemonSocketModule::ConnectRsp
};
if (NS_WARN_IF(MOZ_ARRAY_LENGTH(HandleRsp) <= aHeader.mOpcode) ||

View File

@ -20,6 +20,16 @@ using mozilla::ipc::DaemonSocketResultHandler;
class BluetoothDaemonSocketModule
{
public:
enum {
SERVICE_ID = 0x02
};
enum {
OPCODE_ERROR = 0x00,
OPCODE_LISTEN = 0x01,
OPCODE_CONNECT = 0x02
};
static const int MAX_NUM_CLIENTS;
virtual nsresult Send(DaemonSocketPDU* aPDU,

View File

@ -74,6 +74,7 @@ if CONFIG['MOZ_B2G_BT']:
'bluedroid/BluetoothAvrcpManager.cpp',
'bluedroid/BluetoothDaemonA2dpInterface.cpp',
'bluedroid/BluetoothDaemonAvrcpInterface.cpp',
'bluedroid/BluetoothDaemonCoreInterface.cpp',
'bluedroid/BluetoothDaemonGattInterface.cpp',
'bluedroid/BluetoothDaemonHandsfreeInterface.cpp',
'bluedroid/BluetoothDaemonHelpers.cpp',

View File

@ -72,7 +72,8 @@ var CopyPasteAssistent = {
collapsed: e.collapsed,
caretVisible: e.caretVisible,
selectionVisible: e.selectionVisible,
selectionEditable: e.selectionEditable
selectionEditable: e.selectionEditable,
selectedTextContent: e.selectedTextContent
};
// Get correct geometry information if we have nested iframe.

View File

@ -459,6 +459,8 @@ BrowserElementParent.prototype = {
// - caretVisible: Indicate the caret visiibility.
// - selectionVisible: Indicate current selection is visible or not.
// - selectionEditable: Indicate current selection is editable or not.
// - selectedTextContent: Contains current selected text content, which is
// equivalent to the string returned by Selection.toString().
_handleCaretStateChanged: function(data) {
let evt = this._createEvent('caretstatechanged', data.json,
/* cancelable = */ false);

View File

@ -20,6 +20,7 @@ dictionary CaretStateChangedEventInit : EventInit {
boolean caretVisible = false;
boolean selectionVisible = false;
boolean selectionEditable = false;
DOMString selectedTextContent = "";
};
[Constructor(DOMString type, optional CaretStateChangedEventInit eventInit),
@ -31,4 +32,5 @@ interface CaretStateChangedEvent : Event {
readonly attribute boolean caretVisible;
readonly attribute boolean selectionVisible;
readonly attribute boolean selectionEditable;
readonly attribute DOMString selectedTextContent;
};

View File

@ -13,7 +13,7 @@
#ifdef MOZ_NUWA_PROCESS
#include "ipc/Nuwa.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/PContent.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/PNuwa.h"
#include "mozilla/hal_sandbox/PHal.h"
#if defined(DEBUG) || defined(ENABLE_TESTS)
@ -144,7 +144,7 @@ ProcessLink::Open(mozilla::ipc::Transport* aTransport, MessageLoop *aIOLoop, Sid
}
#ifdef MOZ_NUWA_PROCESS
if (IsNuwaProcess() &&
if (IsNuwaProcess() && NS_IsMainThread() &&
Preferences::GetBool("dom.ipc.processPrelaunch.testMode")) {
// The pref value is turned on in a deadlock test against the Nuwa
// process. The sleep here makes it easy to trigger the deadlock

View File

@ -953,6 +953,7 @@ AccessibleCaretManager::DispatchCaretStateChangedEvent(CaretChangedReason aReaso
init.mCollapsed = sel->IsCollapsed();
init.mCaretVisible = mFirstCaret->IsLogicallyVisible() ||
mSecondCaret->IsLogicallyVisible();
sel->Stringify(init.mSelectedTextContent);
nsRefPtr<CaretStateChangedEvent> event =
CaretStateChangedEvent::Constructor(doc, NS_LITERAL_STRING("mozcaretstatechanged"), init);

View File

@ -2751,7 +2751,15 @@ public class BrowserApp extends GeckoApp
// prevents this issue.
fm.executePendingTransactions();
fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
Fragment f = fm.findFragmentById(R.id.search_container);
// checking if fragment is already present
if (f != null) {
fm.beginTransaction().show(f).commitAllowingStateLoss();
} else {
// add fragment if not already present
fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
}
mBrowserSearch.setUserVisibleHint(true);
// We want to adjust the window size when the keyboard appears to bring the
@ -2777,7 +2785,7 @@ public class BrowserApp extends GeckoApp
mBrowserSearchContainer.setVisibility(View.INVISIBLE);
getSupportFragmentManager().beginTransaction()
.remove(mBrowserSearch).commitAllowingStateLoss();
.hide(mBrowserSearch).commitAllowingStateLoss();
mBrowserSearch.setUserVisibleHint(false);
getWindow().setBackgroundDrawable(null);

View File

@ -4,13 +4,19 @@
MOZ_APP_BUILDID=$(shell cat $(DEPTH)/config/buildid)
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
--verbose \
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
$(MOZ_APP_BUILDID))
# Set the appropriate version code, based on the existance of the
# MOZ_APP_ANDROID_VERSION_CODE variable.
ifdef MOZ_APP_ANDROID_VERSION_CODE
ANDROID_VERSION_CODE:=$(MOZ_APP_ANDROID_VERSION_CODE)
else
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
--verbose \
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
$(MOZ_APP_BUILDID))
endif
DEFINES += \
-DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \

View File

@ -21,7 +21,7 @@ import java.util.EnumSet;
public class RestrictedWelcomePanel extends FirstrunPanel {
public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/kids";
private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/controlledaccess";
private HomePager.OnUrlOpenListener onUrlOpenListener;

View File

@ -41,6 +41,7 @@ import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.ThreadUtils;
@ -94,6 +95,7 @@ public class FxAccountStatusFragment
protected PreferenceCategory accountCategory;
protected Preference profilePreference;
protected Preference emailPreference;
protected Preference manageAccountPreference;
protected Preference authServerPreference;
protected Preference needsPasswordPreference;
@ -169,6 +171,10 @@ public class FxAccountStatusFragment
} else {
accountCategory.removePreference(profilePreference);
}
manageAccountPreference = ensureFindPreference("manage_account");
if (AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
accountCategory.removePreference(manageAccountPreference);
}
authServerPreference = ensureFindPreference("auth_server");
needsPasswordPreference = ensureFindPreference("needs_credentials");
@ -197,6 +203,7 @@ public class FxAccountStatusFragment
} else {
emailPreference.setOnPreferenceClickListener(this);
}
manageAccountPreference.setOnPreferenceClickListener(this);
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
@ -234,6 +241,12 @@ public class FxAccountStatusFragment
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference == manageAccountPreference) {
// There's no native equivalent, so no need to re-direct through an Intent filter.
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
return true;
}
if (preference == needsPasswordPreference) {
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
final Bundle extras = getExtrasForAccount();

View File

@ -723,7 +723,7 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!ENTITY bookmarks_support "Firefox: Support">
<!--LOCALIZATION NOTE (bookmarks_marketplace):link title for https://marketplace.firefox.com -->
<!ENTITY bookmarks_marketplace "Firefox Marketplace">
<!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/kids -->
<!ENTITY bookmarks_restricted_support "Firefox Help and Support for a simplified kid-friendly version of Firefox">
<!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/controlledaccess -->
<!ENTITY bookmarks_restricted_support2 "Firefox Help and Support for restricted profiles on Android tablets">
<!-- LOCALIZATION NOTE (bookmarks_restricted_webmaker):link title for https://webmaker.org -->
<!ENTITY bookmarks_restricted_webmaker "Learn the Web: Mozilla Webmaker">

View File

@ -197,6 +197,7 @@
<!ENTITY fxaccount_status_header2 'Firefox Account'>
<!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
<!ENTITY fxaccount_status_manage_account 'Manage account'>
<!ENTITY fxaccount_status_auth_server 'Account server'>
<!ENTITY fxaccount_status_sync_now 'Sync now'>
<!ENTITY fxaccount_status_syncing2 'Syncing…'>

View File

@ -16,6 +16,11 @@
android:key="email"
android:persistent="false"
android:title="@string/fxaccount_email_hint" />
<Preference
android:editable="false"
android:key="manage_account"
android:persistent="false"
android:title="@string/fxaccount_status_manage_account" />
<Preference
android:editable="false"
android:key="auth_server"

View File

@ -472,8 +472,8 @@
<string name="bookmarkdefaults_title_restricted_webmaker">&bookmarks_restricted_webmaker;</string>
<string name="bookmarkdefaults_url_restricted_webmaker">https://webmaker.org/</string>
<string name="bookmarkdefaults_title_restricted_support">&bookmarks_restricted_support;</string>
<string name="bookmarkdefaults_url_restricted_support">https://support.mozilla.org/kb/kids</string>
<string name="bookmarkdefaults_title_restricted_support">&bookmarks_restricted_support2;</string>
<string name="bookmarkdefaults_url_restricted_support">https://support.mozilla.org/kb/controlledaccess</string>
<!-- Site identity popup -->
<string name="identity_connection_secure">&identity_connection_secure;</string>

View File

@ -519,6 +519,10 @@ var BrowserApp = {
Services.prefs.setIntPref("extensions.enabledScopes", 1);
Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
Services.prefs.setBoolPref("xpinstall.enabled", false);
} else if (ParentalControls.parentalControlsEnabled) {
Services.prefs.clearUserPref("extensions.enabledScopes");
Services.prefs.clearUserPref("extensions.autoDisableScopes");
Services.prefs.setBoolPref("xpinstall.enabled", true);
}
try {

View File

@ -186,6 +186,7 @@
<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
<string name="fxaccount_status_header">&fxaccount_status_header2;</string>
<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
<string name="fxaccount_status_manage_account">&fxaccount_status_manage_account;</string>
<string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string>
<string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string>
<string name="fxaccount_status_syncing">&fxaccount_status_syncing2;</string>
@ -240,4 +241,4 @@
<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string>
<!-- Log Personal information -->
<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string>
<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string>

View File

@ -75,8 +75,8 @@ browser.suggestedsites.restricted.list.0=restricted_fxsupport
browser.suggestedsites.restricted.list.1=webmaker
browser.suggestedsites.restricted.list.2=restricted_mozilla
browser.suggestedsites.restricted_fxsupport.title=Firefox Help and Support for a simplified kid-friendly version of Firefox
browser.suggestedsites.restricted_fxsupport.url=https://support.mozilla.org/kb/kids
browser.suggestedsites.restricted_fxsupport.title=Firefox Help and Support for restricted profiles on Android tablets
browser.suggestedsites.restricted_fxsupport.url=https://support.mozilla.org/kb/controlledaccess
browser.suggestedsites.restricted_fxsupport.bgcolor=#f37c00
browser.suggestedsites.webmaker.title=Learn the Web: Mozilla Webmaker

View File

@ -196,7 +196,7 @@ def remove_caches_from_task(task):
"""
whitelist = [
"tc-vcs",
"tc-vcs-public-source",
"tc-vcs-public-sources",
"tooltool-cache",
]
try:

View File

@ -1176,7 +1176,7 @@ File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
let promise = Scheduler.post("writeAtomic",
[Type.path.toMsg(path),
Type.void_t.in_ptr.toMsg(buffer),
options], [options, buffer]);
options], [options, buffer, path]);
TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
return promise;
};

View File

@ -7261,7 +7261,7 @@
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 13,
"description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:reserved, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)"
"description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:Windows 10, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)"
},
"DEVTOOLS_OS_IS_64_BITS_PER_USER": {
"expires_in_version": "never",

View File

@ -16,6 +16,10 @@ Cu.import("resource://gre/modules/TelemetryArchive.jsm");
Cu.import("resource://gre/modules/TelemetryUtils.jsm");
Cu.import("resource://gre/modules/TelemetryLog.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
const Telemetry = Services.telemetry;
const bundle = Services.strings.createBundle(
@ -228,8 +232,17 @@ var Settings = {
let elements = document.getElementsByClassName("change-data-choices-link");
for (let el of elements) {
el.addEventListener("click", function() {
let mainWindow = getMainWindowWithPreferencesPane();
mainWindow.openAdvancedPreferences("dataChoicesTab");
if (AppConstants.platform == "android") {
Cu.import("resource://gre/modules/Messaging.jsm");
Messaging.sendRequest({
type: "Settings:Show",
resource: "preferences_vendor",
});
} else {
// Show the data choices preferences on desktop.
let mainWindow = getMainWindowWithPreferencesPane();
mainWindow.openAdvancedPreferences("dataChoicesTab");
}
}, false);
}
},

View File

@ -217,25 +217,13 @@
<xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
</xul:hbox>
<xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
<children/>
<xul:hbox class="menu-accel-container" anonid="accel">
<xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
</xul:hbox>
</content>
</binding>
<binding id="menuitem-iconic-both" extends="chrome://global/content/bindings/menu.xml#menuitem">
<content>
<xul:hbox class="menu-iconic-left" align="center" pack="center"
xbl:inherits="selected,_moz-menuactive,disabled,checked">
<xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
</xul:hbox>
<xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
<xul:hbox class="menu-iconic-right" align="center" pack="center">
<xul:image class="menu-iconic-icon" xbl:inherits="src=endimage"/>
</xul:hbox>
</content>
</binding>
<binding id="menuitem-iconic-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
<content>
<xul:hbox class="menu-iconic-left" align="center" pack="center"

View File

@ -388,10 +388,6 @@ menuitem.menuitem-non-iconic {
-moz-binding: url("chrome://global/content/bindings/menu.xml#menubutton-item");
}
menuitem.menuitem-iconic[endimage] {
-moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic-both");
}
menucaption {
-moz-binding: url("chrome://global/content/bindings/menu.xml#menucaption");
}

View File

@ -282,7 +282,9 @@ function getOSCPU() {
if (oscpu.includes("Linux")) {
return 6;
}
if (oscpu.includes("NT 10.")) {
return 7;
}
// Other OS.
return 12;
}

View File

@ -31,6 +31,7 @@ const PREF_APP_UPDATE_CERT_ERRORS = "app.update.cert.errors";
const PREF_APP_UPDATE_CERT_MAXERRORS = "app.update.cert.maxErrors";
const PREF_APP_UPDATE_CERT_REQUIREBUILTIN = "app.update.cert.requireBuiltIn";
const PREF_APP_UPDATE_CUSTOM = "app.update.custom";
const PREF_APP_UPDATE_IMEI_HASH = "app.update.imei_hash";
const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
const PREF_APP_UPDATE_INCOMPATIBLE_MODE = "app.update.incompatible.mode";
@ -3546,6 +3547,8 @@ Checker.prototype = {
}
url = url.replace(/%B2G_VERSION%/g,
getPref("getCharPref", PREF_APP_B2G_VERSION, null));
url = url.replace(/%IMEI%/g,
getPref("getCharPref", PREF_APP_UPDATE_IMEI_HASH, "default"));
}
if (force) {

View File

@ -77,12 +77,6 @@ menuitem[src] > .menu-iconic-left > .menu-iconic-icon {
width: 16px;
}
menuitem[endimage] > .menu-iconic-right > .menu-iconic-icon {
-moz-margin-start: 2px;
-moz-margin-end: 0;
width: 16px;
}
/* ..... menu arrow box ..... */
.menu-right,

View File

@ -13,7 +13,7 @@ menulist {
}
menulist:not([popuponly="true"]) {
min-height: 20px !important;
min-height: 20px;
}
.menulist-label-box {

View File

@ -974,7 +974,7 @@ button.button-link {
color: #0095dd;
cursor: pointer;
min-width: 0;
height: 20px;
min-height: 20px;
margin: 0 6px;
}

View File

@ -181,7 +181,7 @@ html|button {
xul|colorpicker[type="button"],
xul|menulist {
-moz-appearance: none;
height: 30px;
min-height: 30px;
color: var(--in-content-text-color);
border: 1px solid var(--in-content-box-border-color);
-moz-border-top-colors: none !important;

View File

@ -103,18 +103,11 @@ menucaption > .menu-iconic-text {
menu.menu-iconic > .menu-iconic-left,
menuitem.menuitem-iconic > .menu-iconic-left,
.splitmenu-menuitem[iconic="true"] > .menu-iconic-left,
menuitem[endimage] > .menu-iconic-right {
.splitmenu-menuitem[iconic="true"] > .menu-iconic-left {
-moz-appearance: menuimage;
padding-top: 2px;
}
menuitem[endimage] > .menu-iconic-right > .menu-iconic-icon {
-moz-margin-start: 2px;
-moz-margin-end: 5px;
width: 16px;
}
/* ..... menu arrow box ..... */
.menu-right {