This commit is contained in:
Phil Ringnalda 2014-09-13 10:12:15 -07:00
Родитель 1a91d40956 b624d936aa
Коммит f3779e27ad
170 изменённых файлов: 5067 добавлений и 2389 удалений

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

@ -11,12 +11,15 @@
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
DIRS += ["source/modules/system"]
EXTRA_JS_MODULES.sdk += [
'source/app-extension/bootstrap.js',
]
EXTRA_JS_MODULES.sdk.system += [
'source/modules/system/Startup.js',
'source/modules/system/XulApp.js',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
EXTRA_JS_MODULES.commonjs.method.test += [
'source/lib/method/test/browser.js',

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

@ -14,74 +14,25 @@ module.metadata = {
require('chrome') // Otherwise CFX will complain about Components
require('toolkit/loader') // Otherwise CFX will stip out loader.js
require('sdk/addon/runner') // Otherwise CFX will stip out addon/runner.js
require('sdk/system/xul-app') // Otherwise CFX will stip out sdk/system/xul-app
*/
const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu } = Components;
const {
incompatibility
} = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}).XulApp;
// `loadSandbox` is exposed by bootstrap.js
const loaderURI = module.uri.replace("sdk/loader/cuddlefish.js",
"toolkit/loader.js");
const xulappURI = module.uri.replace("loader/cuddlefish.js",
"system/xul-app.js");
// We need to keep a reference to the sandbox in order to unload it in
// bootstrap.js
const loaderSandbox = loadSandbox(loaderURI);
const loaderModule = loaderSandbox.exports;
const xulappSandbox = loadSandbox(xulappURI);
const xulappModule = xulappSandbox.exports;
const { override, load } = loaderModule;
/**
* Ensure the current application satisfied the requirements specified in the
* module given. If not, an exception related to the incompatibility is
* returned; `null` otherwise.
*
* @param {Object} module
* The module to check
* @returns {Error}
*/
function incompatibility(module) {
let { metadata, id } = module;
// if metadata or engines are not specified we assume compatibility is not
// an issue.
if (!metadata || !("engines" in metadata))
return null;
let { engines } = metadata;
if (engines === null || typeof(engines) !== "object")
return new Error("Malformed engines' property in metadata");
let applications = Object.keys(engines);
let versionRange;
applications.forEach(function(name) {
if (xulappModule.is(name)) {
versionRange = engines[name];
// Continue iteration. We want to ensure the module doesn't
// contain a typo in the applications' name or some unknown
// application - `is` function throws an exception in that case.
}
});
if (typeof(versionRange) === "string") {
if (xulappModule.satisfiesVersion(versionRange))
return null;
return new Error("Unsupported Application version: The module " + id +
" currently supports only version " + versionRange + " of " +
xulappModule.name + ".");
}
return new Error("Unsupported Application: The module " + id +
" currently supports only " + applications.join(", ") + ".")
}
function CuddlefishLoader(options) {
let { manifest } = options;
@ -90,8 +41,7 @@ function CuddlefishLoader(options) {
// cache to avoid subsequent loads via `require`.
modules: override({
'toolkit/loader': loaderModule,
'sdk/loader/cuddlefish': exports,
'sdk/system/xul-app': xulappModule
'sdk/loader/cuddlefish': exports
}, options.modules),
resolve: function resolve(id, requirer) {
let entry = requirer && requirer in manifest && manifest[requirer];

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

@ -42,6 +42,9 @@ const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {});
const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm");
const {
incompatibility
} = Cu.import("resource://gre/modules/sdk/system/XulApp.js", {}).XulApp;
// Define some shortcuts.
const bind = Function.call.bind(Function.bind);
@ -349,6 +352,12 @@ const load = iced(function load(loader, module) {
});
}
let (error = incompatibility(module)) {
if (error) {
throw error;
}
}
if (module.exports && typeof(module.exports) === 'object')
freeze(module.exports);

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

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["Startup"];
this.EXPORTED_SYMBOLS = ["Startup"];
const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
@ -20,10 +20,10 @@ const NAME2TOPIC = {
'Thunderbird': 'mail-startup-done'
};
var Startup = {
var exports = {
initialized: !appStartupSrv.startingUp
};
var exports = Startup;
this.Startup = exports;
let gOnceInitializedDeferred = defer();
exports.onceInitialized = gOnceInitializedDeferred.promise;

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

@ -3,15 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["XulApp"];
this.EXPORTED_SYMBOLS = ["XulApp"];
var { classes: Cc, interfaces: Ci } = Components;
var exports = {};
var XulApp = exports;
this.XulApp = exports;
var appInfo = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo);
var appInfo = Cc["@mozilla.org/xre/app-info;1"]
.getService(Ci.nsIXULAppInfo);
var vc = Cc["@mozilla.org/xpcom/version-comparator;1"]
.getService(Ci.nsIVersionComparator);
@ -183,3 +184,51 @@ function satisfiesVersion(version, versionRange) {
});
}
exports.satisfiesVersion = satisfiesVersion;
/**
* Ensure the current application satisfied the requirements specified in the
* module given. If not, an exception related to the incompatibility is
* returned; `null` otherwise.
*
* @param {Object} module
* The module to check
* @returns {Error}
*/
function incompatibility(module) {
let { metadata, id } = module;
// if metadata or engines are not specified we assume compatibility is not
// an issue.
if (!metadata || !("engines" in metadata))
return null;
let { engines } = metadata;
if (engines === null || typeof(engines) !== "object")
return new Error("Malformed engines' property in metadata");
let applications = Object.keys(engines);
let versionRange;
applications.forEach(function(name) {
if (is(name)) {
versionRange = engines[name];
// Continue iteration. We want to ensure the module doesn't
// contain a typo in the applications' name or some unknown
// application - `is` function throws an exception in that case.
}
});
if (typeof(versionRange) === "string") {
if (satisfiesVersion(versionRange))
return null;
return new Error("Unsupported Application version: The module " + id +
" currently supports only version " + versionRange + " of " +
name + ".");
}
return new Error("Unsupported Application: The module " + id +
" currently supports only " + applications.join(", ") + ".")
}
exports.incompatibility = incompatibility;

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

@ -1,10 +0,0 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_JS_MODULES.sdk.system += [
'Startup.js',
'XulApp.js',
]

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

@ -1,10 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { Loader, Require, unload, override } = require('sdk/loader/cuddlefish');
const app = require('sdk/system/xul-app');
const packaging = require('@loader/options');
exports['test loader'] = function(assert) {
@ -44,4 +44,19 @@ exports['test loader'] = function(assert) {
'loader.unload() must call listeners in LIFO order.');
};
require('test').run(exports);
exports['test loader on unsupported modules'] = function(assert) {
let loader = Loader({});
let err = "";
assert.throws(() => {
if (!app.is('Firefox')) {
require('./fixtures/loader/unsupported/firefox');
}
else {
require('./fixtures/loader/unsupported/fennec');
}
}, /^Unsupported Application/, "throws Unsupported Application");
unload(loader);
};
require('sdk/test').run(exports);

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

@ -1,7 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
let {
@ -11,9 +10,10 @@ let { readURI } = require('sdk/net/url');
let root = module.uri.substr(0, module.uri.lastIndexOf('/'))
// The following adds Debugger constructor to the global namespace.
const { Cu } = require('chrome');
const app = require('sdk/system/xul-app');
const { addDebuggerToGlobal } = Cu.import('resource://gre/modules/jsdebugger.jsm', {});
addDebuggerToGlobal(this);
@ -331,7 +331,7 @@ exports['test console global by default'] = function (assert) {
let uri = root + '/fixtures/loader/globals/';
let loader = Loader({ paths: { '': uri }});
let program = main(loader, 'main');
assert.ok(typeof program.console === 'object', 'global `console` exists');
assert.ok(typeof program.console.log === 'function', 'global `console.log` exists');
@ -374,4 +374,19 @@ exports["test require#resolve"] = function(assert) {
assert.equal(root + "toolkit/loader.js", require.resolve("toolkit/loader"), "correct resolution of sdk module");
};
require('test').run(exports);
exports['test loader on unsupported modules'] = function(assert) {
let loader = Loader({});
let err = "";
assert.throws(() => {
if (!app.is('Firefox')) {
require('./fixtures/loader/unsupported/firefox');
}
else {
require('./fixtures/loader/unsupported/fennec');
}
}, /^Unsupported Application/, "throws Unsupported Application");
unload(loader);
};
require('sdk/test').run(exports);

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

@ -325,6 +325,9 @@ setUpdateTrackingId();
(function setupAccessibility() {
let accessibilityScope = {};
SettingsListener.observe("accessibility.screenreader", false, function(value) {
if (!value) {
return;
}
if (!('AccessFu' in accessibilityScope)) {
Cu.import('resource://gre/modules/accessibility/AccessFu.jsm',
accessibilityScope);

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
@ -127,7 +127,7 @@
<!-- Stock Android things -->
<project name="platform/external/icu4c" path="external/icu4c" revision="2bb01561780583cc37bc667f0ea79f48a122d8a2"/>
<!-- dolphin specific things -->
<project name="device/sprd" path="device/sprd" revision="ebb1ce6af72efe15c6919e2ceb9ee805ce2e5960"/>
<project name="device/sprd" path="device/sprd" revision="0351ccd65808a2486e0fefb99674ca7a64c2c6dc"/>
<project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="4e58336019b5cbcfd134caf55b142236cf986618"/>
<project name="platform/frameworks/av" path="frameworks/av" revision="facca8d3e35431b66f85a4eb42bc6c5b24bd04da"/>
<project name="platform/hardware/akm" path="hardware/akm" revision="6d3be412647b0eab0adff8a2768736cf4eb68039"/>
@ -137,7 +137,7 @@
<project name="platform/system/core" path="system/core" revision="53d584d4a4b4316e4de9ee5f210d662f89b44e7e"/>
<project name="u-boot" path="u-boot" revision="2d7a801a3e002078f885e8085fad374a564682e5"/>
<project name="vendor/sprd/gps" path="vendor/sprd/gps" revision="7feb3df0e150053e0143ef525f6e082bda320aea"/>
<project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="cbc0a8e207a21bfaa96e07971ac1f380d9a677cf"/>
<project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="69c8c336794666b010e34b2f501d89118513c546"/>
<project name="vendor/sprd/partner" path="vendor/sprd/partner" revision="8649c7145972251af11b0639997edfecabfc7c2e"/>
<project name="vendor/sprd/proprietories" path="vendor/sprd/proprietories" revision="d2466593022f7078aaaf69026adf3367c2adb7bb"/>
</manifest>

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

@ -19,13 +19,13 @@
<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="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

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

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>

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

@ -19,13 +19,13 @@
<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="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
<project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>

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

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="fe92ddd450e03b38edb2d465de7897971d68ac68">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
@ -23,7 +23,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
<project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>

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

@ -17,10 +17,10 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->
@ -123,7 +123,7 @@
<project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="e8a318f7690092e639ba88891606f4183e846d3f"/>
<project name="device/qcom/common" path="device/qcom/common" revision="878804e0becfe5635bb8ccbf2671333d546c6fb6"/>
<project name="device-flame" path="device/t2m/flame" remote="b2g" revision="55ba09d8edffe7daffd954986b913319fd97890f"/>
<project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="ebb14165369f5edc3f335d5bde6eef8439073589"/>
<project name="codeaurora_kernel_msm" path="kernel" remote="b2g" revision="49417cfc622074daa3c76b345a199f6731375800"/>
<project name="kernel_lk" path="bootable/bootloader/lk" remote="b2g" revision="9eb619d2efdf4bd121587d8296f5c10481f750b8"/>
<project name="platform_bootable_recovery" path="bootable/recovery" remote="b2g" revision="e81502511cda303c803e63f049574634bc96f9f2"/>
<project name="platform/external/bluetooth/bluedroid" path="external/bluetooth/bluedroid" revision="81c4a859d75d413ad688067829d21b7ba9205f81"/>

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

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "51bb0dde2b9800784dc6b4688eb8108aa18de765",
"revision": "90c5e3b6bc763bd6a40aa5671801ff6852ad951d",
"repo_path": "/integration/gaia-central"
}

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

@ -17,12 +17,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="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
<project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>

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

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

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

@ -17,10 +17,10 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
<!-- Stock Android things -->

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

@ -17,12 +17,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="b72909030e214175144342f7e5df7e88a2b52fd4"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="e5da0e462e51cf7f56963e87deb845f87a3a1cf4"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="6969df171e5295f855f12d12db0382048e6892e7"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="7c22462206967693ab96b6af1627ba6925f5723f"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="e29a2effcf580682728fcbab5608bcf82aad48b0"/>
<project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
<!-- Stock Android things -->
<project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>

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

@ -1,3 +1,6 @@
# 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/.
DIST_SUBDIR = 'browser'
export('DIST_SUBDIR')

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

@ -651,7 +651,7 @@
; [Default Preferences]
; All the pref files must be part of base to prevent migration bugs
#ifdef MOZ_MULET
@BINPATH@/defaults/pref/b2g.js
@BINPATH@/browser/@PREF_DIR@/b2g.js
#else
@BINPATH@/@PREF_DIR@/b2g.js
#endif

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

@ -1482,10 +1482,6 @@ pref("devtools.browserconsole.filter.secwarn", true);
// Text size in the Web Console. Use 0 for the system default size.
pref("devtools.webconsole.fontSize", 0);
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);
// Persistent logging: |true| if you want the Web Console to keep all of the
// logged messages after reloading the page, |false| if you want the output to
// be cleared each time page navigation happens.
@ -1616,6 +1612,7 @@ pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");

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

@ -4134,7 +4134,7 @@ function nsBrowserAccess() { }
nsBrowserAccess.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
_openURIInNewTab: function(aURI, aOpener, aIsExternal) {
_openURIInNewTab: function(aURI, aOpener, aIsExternal, aEnsureNonRemote=false) {
let win, needToFocusWin;
// try the current window. if we're in a popup, fall back on the most recent browser window
@ -4166,6 +4166,15 @@ nsBrowserAccess.prototype = {
inBackground: loadInBackground});
let browser = win.gBrowser.getBrowserForTab(tab);
// It's possible that we've been asked to open a new non-remote
// browser in a window that defaults to having remote browsers -
// this can happen if we're opening the new tab due to a window.open
// or _blank anchor in a non-remote browser. If so, we have to force
// the newly opened browser to also not be remote.
if (win.gMultiProcessBrowser && aEnsureNonRemote) {
win.gBrowser.updateBrowserRemoteness(browser, false);
}
if (needToFocusWin || (!loadInBackground && aIsExternal))
win.focus();
@ -4173,6 +4182,14 @@ nsBrowserAccess.prototype = {
},
openURI: function (aURI, aOpener, aWhere, aContext) {
// This function should only ever be called if we're opening a URI
// from a non-remote browser window (via nsContentTreeOwner).
if (aOpener && Cu.isCrossProcessWrapper(aOpener)) {
Cu.reportError("nsBrowserAccess.openURI was passed a CPOW for aOpener. " +
"openURI should only ever be called from non-remote browsers.");
throw Cr.NS_ERROR_FAILURE;
}
var newWindow = null;
var isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
@ -4198,7 +4215,7 @@ nsBrowserAccess.prototype = {
newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null);
break;
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB :
let browser = this._openURIInNewTab(aURI, aOpener, isExternal);
let browser = this._openURIInNewTab(aURI, aOpener, isExternal, true);
if (browser)
newWindow = browser.contentWindow;
break;

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

@ -357,7 +357,7 @@ skip-if = e10s # Bug ?????? - test directly manipulates content
[browser_parsable_css.js]
skip-if = e10s
[browser_parsable_script.js]
skip-if = debug || asan # Times out on debug/asan, and we are less picky about our JS there
skip-if = asan # Disabled because it takes a long time (see test for more information)
[browser_pinnedTabs.js]
[browser_plainTextLinks.js]
@ -483,3 +483,5 @@ skip-if = e10s
skip-if = e10s # Bug ?????? - test directly manipulates content (content.document.getElementById)
[browser_bug1045809.js]
skip-if = e10s
[browser_bug1047603.js]
skip-if = os == "linux" # Bug 1066856 - waiting for OMTC to be enabled by default on Linux.

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

@ -0,0 +1,139 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
const OPEN_LOCATION_PREF = "browser.link.open_newwindow";
const NON_REMOTE_PAGE = "about:crashes";
const SIMPLE_PAGE_HTML = `
<a href="about:home" target="_blank" id="testAnchor">Open a window</a>
`;
function frame_script() {
addMessageListener("test:click", (message) => {
let element = content.document.getElementById("testAnchor");
element.click();
});
sendAsyncMessage("test:ready");
}
/**
* Returns a Promise that resolves once the frame_script is loaded
* in the browser, and has seen the DOMContentLoaded event.
*/
function waitForFrameScriptReady(mm) {
return new Promise((resolve, reject) => {
mm.addMessageListener("test:ready", function onTestReady() {
mm.removeMessageListener("test:ready", onTestReady);
resolve();
});
});
}
/**
* Takes some browser in some window, and forces that browser
* to become non-remote, and then navigates it to a page that
* we're not supposed to be displaying remotely. Returns a
* Promise that resolves when the browser is no longer remote.
*/
function prepareNonRemoteBrowser(aWindow, browser) {
aWindow.gBrowser.updateBrowserRemoteness(browser, false);
browser.loadURI(NON_REMOTE_PAGE);
return new Promise((resolve, reject) => {
waitForCondition(() => !browser.isRemoteBrowser, () => {
resolve();
}, "Waiting for browser to become non-remote");
})
}
registerCleanupFunction(() => {
Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
});
/**
* Test that if we open a new tab from a link in a non-remote
* browser in an e10s window, that the new tab's browser is also
* not remote. Also tests with a private browsing window.
*/
add_task(function* test_new_tab() {
let normalWindow = yield promiseOpenAndLoadWindow({
remote: true
}, true);
let privateWindow = yield promiseOpenAndLoadWindow({
remote: true,
private: true,
}, true);
for (let testWindow of [normalWindow, privateWindow]) {
let testBrowser = testWindow.gBrowser.selectedBrowser;
yield prepareNonRemoteBrowser(testWindow, testBrowser);
// Get our framescript ready
let mm = testBrowser.messageManager;
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
let readyPromise = waitForFrameScriptReady(mm);
yield readyPromise;
// Inject our test HTML into our non-remote tab.
testBrowser.contentDocument.body.innerHTML = SIMPLE_PAGE_HTML;
// Click on the link in the browser, and wait for the new tab.
mm.sendAsyncMessage("test:click");
let tabOpenEvent = yield waitForNewTab(testWindow.gBrowser);
let newTab = tabOpenEvent.target;
ok(!newTab.linkedBrowser.isRemoteBrowser,
"The opened browser should not be remote.");
testWindow.gBrowser.removeTab(newTab);
}
normalWindow.close();
privateWindow.close();
});
/**
* Test that if we open a new window from a link in a non-remote
* browser in an e10s window, that the new window is not an e10s
* window. Also tests with a private browsing window.
*/
add_task(function* test_new_window() {
let normalWindow = yield promiseOpenAndLoadWindow({
remote: true
}, true);
let privateWindow = yield promiseOpenAndLoadWindow({
remote: true,
private: true,
}, true);
// Fiddle with the prefs so that we open target="_blank" links
// in new windows instead of new tabs.
Services.prefs.setIntPref(OPEN_LOCATION_PREF,
Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW);
for (let testWindow of [normalWindow, privateWindow]) {
let testBrowser = testWindow.gBrowser.selectedBrowser;
yield prepareNonRemoteBrowser(testWindow, testBrowser);
// Get our framescript ready
let mm = testBrowser.messageManager;
mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
let readyPromise = waitForFrameScriptReady(mm);
yield readyPromise;
// Inject our test HTML into our non-remote window.
testBrowser.contentDocument.body.innerHTML = SIMPLE_PAGE_HTML;
// Click on the link in the browser, and wait for the new window.
let windowOpenPromise = promiseTopicObserved("browser-delayed-startup-finished");
mm.sendAsyncMessage("test:click");
let [newWindow] = yield windowOpenPromise;
ok(!newWindow.gMultiProcessBrowser,
"The opened window should not be an e10s window.");
newWindow.close();
}
normalWindow.close();
privateWindow.close();
Services.prefs.clearUserPref(OPEN_LOCATION_PREF);
});

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

@ -60,18 +60,58 @@ function parsePromise(uri) {
}
add_task(function* checkAllTheJS() {
let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
// This asynchronously produces a list of URLs (sadly, mostly sync on our
// test infrastructure because it runs against jarfiles there, and
// our zipreader APIs are all sync)
let uris = yield generateURIsFromDirTree(appDir, [".js", ".jsm"]);
// In debug builds, even on a fast machine, collecting the file list may take
// more than 30 seconds, and parsing all files may take four more minutes.
// For this reason, this test must be explictly requested in debug builds by
// using the "--setpref parse=<filter>" argument to mach. You can specify:
// - A case-sensitive substring of the file name to test (slow).
// - A single absolute URI printed out by a previous run (fast).
// - An empty string to run the test on all files (slowest).
let parseRequested = Services.prefs.prefHasUserValue("parse");
let parseValue = parseRequested && Services.prefs.getCharPref("parse");
if (SpecialPowers.isDebugBuild) {
if (!parseRequested) {
ok(true, "Test disabled on debug build. To run, execute: ./mach" +
" mochitest-browser --setpref parse=<case_sensitive_filter>" +
" browser/base/content/test/general/browser_parsable_script.js");
return;
}
// Request a 10 minutes timeout (30 seconds * 20) for debug builds.
requestLongerTimeout(20);
}
let uris;
// If an absolute URI is specified on the command line, use it immediately.
if (parseValue && parseValue.contains(":")) {
uris = [NetUtil.newURI(parseValue)];
} else {
let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
// This asynchronously produces a list of URLs (sadly, mostly sync on our
// test infrastructure because it runs against jarfiles there, and
// our zipreader APIs are all sync)
let startTimeMs = Date.now();
info("Collecting URIs");
uris = yield generateURIsFromDirTree(appDir, [".js", ".jsm"]);
info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
// Apply the filter specified on the command line, if any.
if (parseValue) {
uris = uris.filter(uri => {
if (uri.spec.contains(parseValue)) {
return true;
}
info("Not checking filtered out " + uri.spec);
return false;
});
}
}
// We create an array of promises so we can parallelize all our parsing
// and file loading activity:
let allPromises = [];
for (let uri of uris) {
if (uriIsWhiteListed(uri)) {
info("Not checking " + uri.spec);
info("Not checking whitelisted " + uri.spec);
continue;
}
allPromises.push(parsePromise(uri.spec));

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

@ -665,3 +665,7 @@ function assertWebRTCIndicatorStatus(expected) {
}
}
}
function waitForNewTab(aTabBrowser) {
return promiseWaitForEvent(aTabBrowser.tabContainer, "TabOpen");
}

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

@ -33,3 +33,7 @@ pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 5);

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

@ -30,3 +30,7 @@ pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 5);

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

@ -30,3 +30,7 @@ pref("browser.search.param.yahoo-fr-ja", "mozff");
pref("browser.search.param.ms-pc-metro", "MOZW");
pref("browser.search.param.yahoo-fr-metro", "mozilla_metro_search");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);

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

@ -29,3 +29,7 @@ pref("browser.search.param.yahoo-fr-ja", "mozff");
#ifdef MOZ_METRO
pref("browser.search.param.yahoo-fr-metro", "");
#endif
// Number of usages of the web console or scratchpad.
// If this is less than 5, then pasting code into the web console or scratchpad is disabled
pref("devtools.selfxss.count", 0);

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

@ -30,11 +30,16 @@ function openAboutAccountsFromMenuPanel(entryPoint) {
ok(syncButton, "The Sync button was added to the Panel Menu");
let deferred = Promise.defer();
let handler = () => {
gBrowser.selectedTab.removeEventListener("load", handler, true);
let handler = (e) => {
if (e.originalTarget != gBrowser.selectedTab.linkedBrowser.contentDocument ||
e.target.location.href == "about:blank") {
info("Skipping spurious 'load' event for " + e.target.location.href);
return;
}
gBrowser.selectedTab.linkedBrowser.removeEventListener("load", handler, true);
deferred.resolve();
}
gBrowser.selectedTab.addEventListener("load", handler, true);
gBrowser.selectedTab.linkedBrowser.addEventListener("load", handler, true);
syncButton.click();
yield deferred.promise;

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

@ -41,6 +41,28 @@ const cloneErrorObject = function(error, targetWindow) {
return obj;
};
/**
* Makes an object or value available to an unprivileged target window.
*
* Primitives are returned as they are, while objects are cloned into the
* specified target. Error objects are also handled correctly.
*
* @param {any} value Value or object to copy
* @param {nsIDOMWindow} targetWindow The content window to copy to
*/
const cloneValueInto = function(value, targetWindow) {
if (!value || typeof value != "object") {
return value;
}
// Inspect for an error this way, because the Error object is special.
if (value.constructor.name == "Error") {
return cloneErrorObject(value, targetWindow);
}
return Cu.cloneInto(value, targetWindow);
};
/**
* Inject any API containing _only_ function properties into the given window.
*
@ -56,17 +78,7 @@ const injectObjectAPI = function(api, targetWindow) {
injectedAPI[func] = function(...params) {
let callback = params.pop();
api[func](...params, function(...results) {
results = results.map(result => {
if (result && typeof result == "object") {
// Inspect for an error this way, because the Error object is special.
if (result.constructor.name == "Error") {
return cloneErrorObject(result.message)
}
return Cu.cloneInto(result, targetWindow);
}
return result;
});
callback(...results);
callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
});
};
});
@ -203,11 +215,11 @@ function injectLoopAPI(targetWindow) {
value: function(callback) {
// We translate from a promise to a callback, as we can't pass promises from
// Promise.jsm across the priv versus unpriv boundary.
return MozLoopService.register().then(() => {
MozLoopService.register().then(() => {
callback(null);
}, err => {
callback(err);
});
callback(cloneValueInto(err, targetWindow));
}).catch(Cu.reportError);
}
},
@ -357,11 +369,20 @@ function injectLoopAPI(targetWindow) {
value: function(path, method, payloadObj, callback) {
// XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST
// XXX Should really return a DOM promise here.
return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
callback(null, response.body);
}, (error) => {
callback(Cu.cloneInto(error, targetWindow));
});
}, hawkError => {
// The hawkError.error property, while usually a string representing
// an HTTP response status message, may also incorrectly be a native
// error object that will cause the cloning function to fail.
callback(Cu.cloneInto({
error: (hawkError.error && typeof hawkError.error == "string")
? hawkError.error : "Unexpected exception",
message: hawkError.message,
code: hawkError.code,
errno: hawkError.errno,
}, targetWindow));
}).catch(Cu.reportError);
}
},

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

@ -11,9 +11,7 @@ var loop = loop || {};
loop.conversation = (function(OT, mozL10n) {
"use strict";
var sharedViews = loop.shared.views,
// aliasing translation function as __ for concision
__ = mozL10n.get;
var sharedViews = loop.shared.views;
/**
* App router.
@ -24,11 +22,15 @@ loop.conversation = (function(OT, mozL10n) {
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
propTypes: {
model: React.PropTypes.object.isRequired
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getInitialProps: function() {
return {showDeclineMenu: false};
getDefaultProps: function() {
return {
showDeclineMenu: false,
video: true
};
},
getInitialState: function() {
@ -79,6 +81,37 @@ loop.conversation = (function(OT, mozL10n) {
this.setState({showDeclineMenu: false});
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var btnClassAccept = "btn btn-accept";
@ -91,7 +124,7 @@ loop.conversation = (function(OT, mozL10n) {
});
return (
React.DOM.div({className: conversationPanelClass},
React.DOM.h2(null, __("incoming_call_title2")),
React.DOM.h2(null, mozL10n.get("incoming_call_title2")),
React.DOM.div({className: "btn-group incoming-call-action-group"},
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}),
@ -102,7 +135,7 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.button({className: btnClassDecline,
onClick: this._handleDecline},
__("incoming_call_cancel_button")
mozL10n.get("incoming_call_cancel_button")
),
React.DOM.div({className: "btn-chevron",
onClick: this._toggleDeclineMenu}
@ -111,7 +144,7 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.ul({className: dropdownMenuClassesDecline},
React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock},
__("incoming_call_cancel_and_block_button")
mozL10n.get("incoming_call_cancel_and_block_button")
)
)
@ -120,22 +153,7 @@ loop.conversation = (function(OT, mozL10n) {
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}),
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: btnClassAccept,
onClick: this._handleAccept("audio-video")},
React.DOM.span({className: "fx-embedded-answer-btn-text"},
__("incoming_call_accept_button")
),
React.DOM.span({className: "fx-embedded-btn-icon-video"}
)
),
React.DOM.div({className: "call-audio-only",
onClick: this._handleAccept("audio"),
title: __("incoming_call_accept_audio_only_tooltip")}
)
)
),
AcceptCallButton({mode: this._answerModeProps()}),
React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"})
@ -146,6 +164,41 @@ loop.conversation = (function(OT, mozL10n) {
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
propTypes: {
mode: React.PropTypes.object.isRequired,
},
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-accept",
onClick: mode.primary.handler,
title: mozL10n.get(mode.primary.tooltip)},
React.DOM.span({className: "fx-embedded-answer-btn-text"},
mozL10n.get("incoming_call_accept_button")
),
React.DOM.span({className: mode.primary.className})
),
React.DOM.div({className: mode.secondary.className,
onClick: mode.secondary.handler,
title: mozL10n.get(mode.secondary.tooltip)}
)
)
)
/* jshint ignore:end */
);
}
});
/**
* Conversation router.
*
@ -225,7 +278,7 @@ loop.conversation = (function(OT, mozL10n) {
this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
video: this._conversation.hasVideoStream("incoming")
}));
}.bind(this), function() {
this._handleSessionError();

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

@ -11,9 +11,7 @@ var loop = loop || {};
loop.conversation = (function(OT, mozL10n) {
"use strict";
var sharedViews = loop.shared.views,
// aliasing translation function as __ for concision
__ = mozL10n.get;
var sharedViews = loop.shared.views;
/**
* App router.
@ -24,11 +22,15 @@ loop.conversation = (function(OT, mozL10n) {
var IncomingCallView = React.createClass({
propTypes: {
model: React.PropTypes.object.isRequired
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getInitialProps: function() {
return {showDeclineMenu: false};
getDefaultProps: function() {
return {
showDeclineMenu: false,
video: true
};
},
getInitialState: function() {
@ -79,6 +81,37 @@ loop.conversation = (function(OT, mozL10n) {
this.setState({showDeclineMenu: false});
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var btnClassAccept = "btn btn-accept";
@ -91,7 +124,7 @@ loop.conversation = (function(OT, mozL10n) {
});
return (
<div className={conversationPanelClass}>
<h2>{__("incoming_call_title2")}</h2>
<h2>{mozL10n.get("incoming_call_title2")}</h2>
<div className="btn-group incoming-call-action-group">
<div className="fx-embedded-incoming-call-button-spacer"></div>
@ -102,7 +135,7 @@ loop.conversation = (function(OT, mozL10n) {
<button className={btnClassDecline}
onClick={this._handleDecline}>
{__("incoming_call_cancel_button")}
{mozL10n.get("incoming_call_cancel_button")}
</button>
<div className="btn-chevron"
onClick={this._toggleDeclineMenu}>
@ -111,7 +144,7 @@ loop.conversation = (function(OT, mozL10n) {
<ul className={dropdownMenuClassesDecline}>
<li className="btn-block" onClick={this._handleDeclineBlock}>
{__("incoming_call_cancel_and_block_button")}
{mozL10n.get("incoming_call_cancel_and_block_button")}
</li>
</ul>
@ -120,22 +153,7 @@ loop.conversation = (function(OT, mozL10n) {
<div className="fx-embedded-incoming-call-button-spacer"></div>
<div className="btn-chevron-menu-group">
<div className="btn-group">
<button className={btnClassAccept}
onClick={this._handleAccept("audio-video")}>
<span className="fx-embedded-answer-btn-text">
{__("incoming_call_accept_button")}
</span>
<span className="fx-embedded-btn-icon-video">
</span>
</button>
<div className="call-audio-only"
onClick={this._handleAccept("audio")}
title={__("incoming_call_accept_audio_only_tooltip")} >
</div>
</div>
</div>
<AcceptCallButton mode={this._answerModeProps()} />
<div className="fx-embedded-incoming-call-button-spacer"></div>
@ -146,6 +164,41 @@ loop.conversation = (function(OT, mozL10n) {
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({
propTypes: {
mode: React.PropTypes.object.isRequired,
},
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
<div className="btn-chevron-menu-group">
<div className="btn-group">
<button className="btn btn-accept"
onClick={mode.primary.handler}
title={mozL10n.get(mode.primary.tooltip)}>
<span className="fx-embedded-answer-btn-text">
{mozL10n.get("incoming_call_accept_button")}
</span>
<span className={mode.primary.className}></span>
</button>
<div className={mode.secondary.className}
onClick={mode.secondary.handler}
title={mozL10n.get(mode.secondary.tooltip)}>
</div>
</div>
</div>
/* jshint ignore:end */
);
}
});
/**
* Conversation router.
*
@ -225,7 +278,7 @@ loop.conversation = (function(OT, mozL10n) {
this._websocket.promiseConnect().then(function() {
this.loadReactComponent(loop.conversation.IncomingCallView({
model: this._conversation,
video: {enabled: this._conversation.hasVideoStream("incoming")}
video: this._conversation.hasVideoStream("incoming")
}));
}.bind(this), function() {
this._handleSessionError();

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

@ -135,6 +135,7 @@ p {
background-color: #f0ad4e;
}
.btn-cancel,
.btn-error,
.btn-hangup,
.btn-error + .btn-chevron {
@ -142,6 +143,7 @@ p {
border: 1px solid #d74345;
}
.btn-cancel:hover,
.btn-error:hover,
.btn-hangup:hover,
.btn-error + .btn-chevron:hover {
@ -149,6 +151,7 @@ p {
border: 1px solid #c53436;
}
.btn-cancel:active,
.btn-error:active,
.btn-hangup:active,
.btn-error + .btn-chevron:active {
@ -222,7 +225,7 @@ p {
/* Alerts */
.alert {
background: #eee;
padding: .2em 1em;
padding: .4em 1em;
margin-bottom: 1em;
border-bottom: 2px solid #E9E9E9;
}
@ -232,17 +235,12 @@ p {
margin: 0;
}
.alert.alert-error {
display: flex;
align-content: center;
padding: 5px;
font-size: 10px;
justify-content: center;
color: #FFF;
.alert-error {
background: repeating-linear-gradient(-45deg, #D74345, #D74345 10px, #D94B4D 10px, #D94B4D 20px) repeat scroll 0% 0% transparent;
color: #fff;
}
.alert.alert-warning {
.alert-warning {
background: #fcf8e3;
border: 1px solid #fbeed5;
}

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

@ -83,16 +83,54 @@
max-width: 80%;
}
.fx-embedded-btn-icon-video {
.fx-embedded-btn-icon-video,
.fx-embedded-btn-icon-audio {
display: inline-block;
vertical-align: top;
width: .8rem;
height: .8rem;
background-image: url("../img/video-inverse-14x14.png");
background-repeat: no-repeat;
cursor: pointer;
}
.fx-embedded-btn-icon-video,
.fx-embedded-btn-video-small {
background-image: url("../img/video-inverse-14x14.png");
}
.fx-embedded-btn-icon-audio,
.fx-embedded-btn-audio-small {
background-image: url("../img/audio-inverse-14x14.png");
}
.fx-embedded-btn-audio-small,
.fx-embedded-btn-video-small {
width: 26px;
height: 26px;
border-left: 1px solid rgba(255,255,255,.4);
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
background-color: #74BF43;
background-position: center;
background-size: 1rem;
background-repeat: no-repeat;
cursor: pointer;
}
.fx-embedded-btn-video-small:hover,
.fx-embedded-btn-audio-small:hover {
background-color: #6cb23e;
}
@media (min-resolution: 2dppx) {
.fx-embedded-btn-audio-small {
background-image: url("../img/audio-inverse-14x14@2x.png");
}
.fx-embedded-btn-video-small {
background-image: url("../img/video-inverse-14x14@2x.png");
}
}
.standalone .btn-hangup {
width: auto;
font-size: 12px;
@ -233,30 +271,6 @@
flex: 1;
}
.call-audio-only {
width: 26px;
height: 26px;
border-left: 1px solid rgba(255,255,255,.4);
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
background-color: #74BF43;
background-image: url("../img/audio-inverse-14x14.png");
background-size: 1rem;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
.call-audio-only:hover {
background-color: #6cb23e;
}
@media (min-resolution: 2dppx) {
.call-audio-only {
background-image: url("../img/audio-inverse-14x14@2x.png");
}
}
/* Expired call url page */
.expired-url-info {

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

@ -51,18 +51,6 @@ loop.shared.models = (function(l10n) {
*/
session: undefined,
/**
* Pending call timeout value.
* @type {Number}
*/
pendingCallTimeout: undefined,
/**
* Pending call timer.
* @type {Number}
*/
_pendingCallTimer: undefined,
/**
* Constructor.
*
@ -71,10 +59,6 @@ loop.shared.models = (function(l10n) {
* Required:
* - {OT} sdk: OT SDK object.
*
* Optional:
* - {Number} pendingCallTimeout: Pending call timeout in milliseconds
* (default: 20000).
*
* @param {Object} attributes Attributes object.
* @param {Object} options Options object.
*/
@ -84,10 +68,12 @@ loop.shared.models = (function(l10n) {
throw new Error("missing required sdk");
}
this.sdk = options.sdk;
this.pendingCallTimeout = options.pendingCallTimeout || 20000;
// Ensure that any pending call timer is cleared on disconnect/error
this.on("session:ended session:error", this._clearPendingCallTimer, this);
// 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);
}
},
/**
@ -112,20 +98,6 @@ loop.shared.models = (function(l10n) {
* server for the outgoing call.
*/
outgoing: function(sessionData) {
this._clearPendingCallTimer();
// Outgoing call has never reached destination, closing - see bug 1020448
function handleOutgoingCallTimeout() {
/*jshint validthis:true */
if (!this.get("ongoing")) {
this.trigger("timeout").endSession();
}
}
// Setup pending call timeout.
this._pendingCallTimer = setTimeout(
handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
this.setOutgoingSessionData(sessionData);
this.trigger("call:outgoing");
},
@ -278,15 +250,6 @@ loop.shared.models = (function(l10n) {
}
},
/**
* Clears current pending call timer, if any.
*/
_clearPendingCallTimer: function() {
if (this._pendingCallTimer) {
clearTimeout(this._pendingCallTimer);
}
},
/**
* Manages connection status
* triggers apropriate event for connection error/success

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

@ -29,7 +29,25 @@ loop.shared.utils = (function() {
return platform;
}
/**
* Used for getting a boolean preference. It will either use the browser preferences
* (if navigator.mozLoop is defined) or try to get them from localStorage.
*
* @param {String} prefName The name of the preference. Note that mozLoop adds
* 'loop.' to the start of the string.
*
* @return The value of the preference, or false if not available.
*/
function getBoolPreference(prefName) {
if (navigator.mozLoop) {
return !!navigator.mozLoop.getLoopBoolPref(prefName);
}
return !!localStorage.getItem(prefName);
}
return {
getTargetPlatform: getTargetPlatform
getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference
};
})();

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

@ -36,11 +36,10 @@ loop.CallConnectionWebSocket = (function() {
throw new Error("No websocketToken in options");
}
// Save the debug pref now, to avoid getting it each time.
if (navigator.mozLoop) {
this._debugWebSocket =
navigator.mozLoop.getLoopBoolPref("debug.websocket");
}
// Set loop.debug.sdk to true in the browser, or standalone:
// localStorage.setItem("debug.websocket", true);
this._debugWebSocket =
loop.shared.utils.getBoolPreference("debug.websocket");
_.extend(this, Backbone.Events);
};
@ -148,6 +147,18 @@ loop.CallConnectionWebSocket = (function() {
});
},
/**
* Notifies the server that the outgoing call is cancelled by the
* user.
*/
cancel: function() {
this._send({
messageType: "action",
event: "terminate",
reason: "cancel"
});
},
/**
* Sends data on the websocket.
*
@ -206,6 +217,7 @@ loop.CallConnectionWebSocket = (function() {
this._completeConnection();
break;
case "progress":
this.trigger("progress:" + msg.state);
this.trigger("progress", msg);
break;
}

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

@ -223,11 +223,13 @@
}
.OT_closeButton {
top: 15px;
right: 15px;
position: absolute;
font-size: 18px;
color: #999999;
cursor: pointer;
font-size: 32px;
line-height: 30px;
position: absolute;
right: 15px;
top: 0;
}
.OT_dialog-messages {
@ -266,6 +268,10 @@
color: #ffffff;
}
.OT_dialog-hidden {
display: none;
}
.OT_dialog-single-button {
position: absolute;
bottom: 41px;
@ -275,6 +281,22 @@
width: 193px;
}
.OT_dialog-single-button-wide {
bottom: 35px;
height: 140px;
left: 5px;
position: absolute;
right: 0;
}
.OT_dialog-single-button-with-title {
margin: 0 auto;
padding-left: 30px;
padding-right: 30px;
width: 270px;
}
.OT_dialog-button-pair {
position: absolute;
bottom: 45px;
@ -301,11 +323,20 @@
font-weight: 300;
text-align: center;
margin-bottom: 15px;
font-size: 12px;
font-size: 14px;
line-height: 150%;
color: #A4A4A4;
color: #999999;
}
.OT_dialog-button-title label {
color: #999999;
}
.OT_dialog-button-title a,
.OT_dialog-button-title a:link,
.OT_dialog-button-title a:active {
color: #02A1DE;
}
.OT_dialog-button-title strong {
color: #ffffff;
@ -318,16 +349,19 @@
display: block;
line-height: 50px;
height: 47px;
background-color: #29A4DA;
background-color: #1CA3DC;
text-align: center;
font-size: 16pt;
cursor: pointer;
}
.OT_dialog-button.OT_dialog-button-disabled {
background-color: #444444;
color: #999999;
cursor: not-allowed;
/* IE 8 */
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
opacity: 0.5;
}
.OT_dialog-button.OT_dialog-button-large {
@ -335,6 +369,16 @@
height: 58px;
}
.OT_dialog-button.OT_dialog-button-small {
background-color: #444444;
color: #999999;
font-size: 12pt;
height: 40px;
line-height: 40px;
margin: 20px auto 0 auto;
width: 86px;
}
.OT_dialog-progress-bar {
border: 1px solid #4E4E4E;
height: 8px;
@ -518,6 +562,11 @@
background: rgba(0, 0, 0, 0.4);
}
.OT_publisher .OT_edge-bar-item,
.OT_subscriber .OT_edge-bar-item {
z-index: 1; /* required to get audio level meter underneath */
}
/* The publisher/subscriber name panel/archiving status bar */
.OT_publisher .OT_name,
.OT_subscriber .OT_name {
@ -900,6 +949,8 @@
.OT_publisher .OT_edge-bar-item.OT_mode-on,
.OT_subscriber .OT_edge-bar-item.OT_mode-on,
.OT_publisher .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber .OT_edge-bar-item.OT_mode-auto.OT_mode-on-hold,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-auto,
.OT_subscriber:hover .OT_edge-bar-item.OT_mode-auto,
.OT_publisher:hover .OT_edge-bar-item.OT_mode-mini-auto,
@ -932,8 +983,10 @@
}
.OT_publisher .OT_opentok.OT_mode-on,
.OT_publisher .OT_opentok.OT_mode-auto.OT_mode-on-hold,
.OT_publisher:hover .OT_opentok.OT_mode-auto,
.OT_subscriber .OT_opentok.OT_mode-on,
.OT_subscriber .OT_opentok.OT_mode-auto.OT_mode-on-hold,
.OT_subscriber:hover .OT_opentok.OT_mode-auto {
top: 8px;
}
@ -981,15 +1034,98 @@
.OT_video-poster {
width: 100%;
height: 100%;
background-position: 50% 50%;
display: none;
opacity: .25;
background-size: auto 76%;
background-repeat: no-repeat;
background-position: center bottom;
background-image: url(../images/rtc/audioonly-silhouette.svg);
}
.OT_audio-level-meter {
position: absolute;
width: 25%;
max-width: 224px;
min-width: 21px;
top: 0;
right: 0;
overflow: hidden;
}
.OT_audio-level-meter:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__bar {
position: absolute;
width: 192%; /* meter value can overflow of 8% */
height: 192%;
top: -96% /* half of the size */;
right: -96%;
border-radius: 50%;
background-color: rgba(0, 0, 0, .8);
}
.OT_audio-level-meter__audio-only-img {
position: absolute;
top: 22%;
right: 15%;
width: 40%;
opacity: .7;
background: url(../images/rtc/audioonly-headset.svg) no-repeat center;
}
.OT_audio-level-meter__audio-only-img:before {
/* makes the height of the container equals its width */
content: '';
display: block;
padding-top: 100%;
}
.OT_audio-level-meter__value {
position: absolute;
border-radius: 50%;
background-image: radial-gradient(circle, rgba(151,206,0,1) 0%, rgba(151,206,0,0) 100%);
}
.OT_audio-level-meter {
display: none;
}
.OT_publisher .OT_video-poster {
background-image: url(../images/rtc/audioonly-publisher.png);
.OT_audio-level-meter.OT_mode-on,
.OT_audio-only .OT_audio-level-meter.OT_mode-auto {
display: block;
}
.OT_subscriber .OT_video-poster {
background-image: url(../images/rtc/audioonly-subscriber.png);
.OT_video-disabled-indicator {
opacity: 1;
border: none;
display: none;
position: absolute;
background-color: transparent;
background-repeat: no-repeat;
background-position:bottom right;
top: 0;
left: 0;
bottom: 3px;
right: 3px;
}
.OT_video-disabled {
background-image: url(../images/rtc/video-disabled.png);
}
.OT_video-disabled-warning {
background-image: url(../images/rtc/video-disabled-warning.png);
}
.OT_video-disabled-indicator.OT_active {
display: block;
}

3398
browser/components/loop/content/shared/libs/sdk.js Normal file → Executable file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -3,7 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
LOOP_PENDING_CALL_TIMEOUT := $(shell echo $${LOOP_PENDING_CALL_TIMEOUT-20000})
NODE_LOCAL_BIN=./node_modules/.bin
install:
@ -53,4 +52,3 @@ config:
@echo "var loop = loop || {};" > content/config.js
@echo "loop.config = loop.config || {};" >> content/config.js
@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
@echo "loop.config.pendingCallTimeout = `echo $(LOOP_PENDING_CALL_TIMEOUT)`;" >> content/config.js

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

@ -21,9 +21,12 @@ body,
.standalone-header {
border-radius: 4px;
background: #fff;
padding: 1rem 5rem;
border: 1px solid #E7E7E7;
box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.03);
}
.header-box {
padding: 1rem 5rem;
margin-top: 2rem;
}
@ -103,7 +106,7 @@ body,
}
.standalone-header-title,
.standalone-call-btn-label {
.standalone-btn-label {
font-weight: lighter;
}
@ -112,7 +115,7 @@ body,
line-height: 2.2rem;
}
.standalone-call-btn-label {
.standalone-btn-label {
font-size: 1.2rem;
}
@ -179,6 +182,10 @@ body,
}
}
.btn-pending-cancel-group > .btn-cancel {
flex: 2 1 auto;
}
.btn-large {
/* Dimensions from spec
* https://people.mozilla.org/~dhenein/labs/loop-link-spec/#call-start */

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

@ -120,6 +120,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
render: function() {
return (
React.DOM.h1({className: "standalone-header-title"},
React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
)
);
}
});
var ConversationHeader = React.createClass({displayName: 'ConversationHeader',
render: function() {
var cx = React.addons.classSet;
@ -138,10 +148,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
return (
/* jshint ignore:start */
React.DOM.header({className: "standalone-header container-box"},
React.DOM.h1({className: "standalone-header-title"},
React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
),
React.DOM.header({className: "standalone-header header-box container-box"},
ConversationBranding(null),
React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}),
React.DOM.h3({className: "call-url"},
conversationUrl
@ -165,6 +173,68 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
}
},
propTypes: {
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
.isRequired
},
componentDidMount: function() {
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
this.props.websocket.cancel();
},
render: function() {
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
return (
/* jshint ignore:start */
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
React.DOM.header({className: "pending-header header-box"},
ConversationBranding(null)
),
React.DOM.div({id: "cameraPreview"}),
React.DOM.div({id: "messages"}),
React.DOM.p({className: "standalone-btn-label"},
callState
),
React.DOM.div({className: "btn-pending-cancel-group btn-group"},
React.DOM.div({className: "flex-padding-1"}),
React.DOM.button({className: "btn btn-large btn-cancel",
onClick: this._cancelOutgoingCall},
React.DOM.span({className: "standalone-call-btn-text"},
mozL10n.get("initiate_call_cancel_button")
)
),
React.DOM.div({className: "flex-padding-1"})
)
),
ConversationFooter(null)
)
/* jshint ignore:end */
);
}
});
/**
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
@ -286,7 +356,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
ConversationHeader({
urlCreationDateString: this.state.urlCreationDateString}),
React.DOM.p({className: "standalone-call-btn-label"},
React.DOM.p({className: "standalone-btn-label"},
mozL10n.get("initiate_call_button_label2")
),
@ -352,6 +422,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
"unsupportedDevice": "unsupportedDevice",
"unsupportedBrowser": "unsupportedBrowser",
"call/expired": "expired",
"call/pending/:token": "pendingConversation",
"call/ongoing/:token": "loadConversation",
"call/:token": "initiate"
},
@ -364,8 +435,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Load default view
this.loadReactComponent(HomeView(null));
this.listenTo(this._conversation, "timeout", this._onTimeout);
},
_onSessionExpired: function() {
@ -417,7 +486,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this._setupWebSocketAndCallView(loopToken);
this.navigate("call/pending/" + loopToken, {
trigger: true
});
}
},
@ -427,16 +498,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function(loopToken) {
_setupWebSocketAndCallView: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
@ -464,30 +532,48 @@ loop.webapp = (function($, _, OT, mozL10n) {
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
if (progressData.state === "terminated") {
// XXX Before adding more states here, the basic protocol messages to the
// server need implementing on both the standalone and desktop side.
// These are covered by bug 1045643, but also check the dependencies on
// bug 1034041.
//
// Failure to do this will break desktop - standalone call setup. We're
// ok to handle reject, as that is a specific message from the destkop via
// the server.
switch (progressData.reason) {
case "reject":
this._handleCallRejected();
switch(progressData.state) {
case "connecting": {
this._handleCallConnecting();
break;
}
case "terminated": {
// At the moment, we show the same text regardless
// of the terminated reason.
this._handleCallTerminated(progressData.reason);
break;
}
}
},
/**
* Handles call rejection.
* XXX This should really display the call failed view - bug 1046959
* will implement this.
* Handles a call moving to the connecting stage.
*/
_handleCallRejected: function() {
_handleCallConnecting: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
return;
}
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.endCall();
this._notifications.errorL10n("call_timeout_notification_text");
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this._notifications.errorL10n("call_timeout_notification_text");
}
},
/**
@ -501,10 +587,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.navigate(route, {trigger: true});
},
_onTimeout: function() {
this._notifications.errorL10n("call_timeout_notification_text");
},
/**
* Default entry point.
*/
@ -549,6 +631,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.loadReactComponent(startView);
},
pendingConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this._setupWebSocketAndCallView();
this.loadReactComponent(PendingConversationView({
websocket: this._websocket
}));
},
/**
* Loads conversation establishment view.
*
@ -596,8 +689,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
notifications: new sharedModels.NotificationCollection(),
client: client,
conversation: new sharedModels.ConversationModel({}, {
sdk: OT,
pendingCallTimeout: loop.config.pendingCallTimeout
sdk: OT
})
});
@ -616,6 +708,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
return {
baseServerUrl: baseServerUrl,
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,

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

@ -120,6 +120,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
var ConversationBranding = React.createClass({
render: function() {
return (
<h1 className="standalone-header-title">
<strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")}
</h1>
);
}
});
var ConversationHeader = React.createClass({
render: function() {
var cx = React.addons.classSet;
@ -138,10 +148,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
return (
/* jshint ignore:start */
<header className="standalone-header container-box">
<h1 className="standalone-header-title">
<strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")}
</h1>
<header className="standalone-header header-box container-box">
<ConversationBranding />
<div className="loop-logo" title="Firefox WebRTC! logo"></div>
<h3 className="call-url">
{conversationUrl}
@ -165,6 +173,68 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
var PendingConversationView = React.createClass({
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
}
},
propTypes: {
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
.isRequired
},
componentDidMount: function() {
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.setState({callState: "ringing"});
},
_cancelOutgoingCall: function() {
this.props.websocket.cancel();
},
render: function() {
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
return (
/* jshint ignore:start */
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
<ConversationBranding />
</header>
<div id="cameraPreview"></div>
<div id="messages"></div>
<p className="standalone-btn-label">
{callState}
</p>
<div className="btn-pending-cancel-group btn-group">
<div className="flex-padding-1"></div>
<button className="btn btn-large btn-cancel"
onClick={this._cancelOutgoingCall} >
<span className="standalone-call-btn-text">
{mozL10n.get("initiate_call_cancel_button")}
</span>
</button>
<div className="flex-padding-1"></div>
</div>
</div>
<ConversationFooter />
</div>
/* jshint ignore:end */
);
}
});
/**
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
@ -286,7 +356,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
<ConversationHeader
urlCreationDateString={this.state.urlCreationDateString} />
<p className="standalone-call-btn-label">
<p className="standalone-btn-label">
{mozL10n.get("initiate_call_button_label2")}
</p>
@ -352,6 +422,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
"unsupportedDevice": "unsupportedDevice",
"unsupportedBrowser": "unsupportedBrowser",
"call/expired": "expired",
"call/pending/:token": "pendingConversation",
"call/ongoing/:token": "loadConversation",
"call/:token": "initiate"
},
@ -364,8 +435,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
// Load default view
this.loadReactComponent(<HomeView />);
this.listenTo(this._conversation, "timeout", this._onTimeout);
},
_onSessionExpired: function() {
@ -417,7 +486,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
this._notifications.errorL10n("missing_conversation_info");
this.navigate("home", {trigger: true});
} else {
this._setupWebSocketAndCallView(loopToken);
this.navigate("call/pending/" + loopToken, {
trigger: true
});
}
},
@ -427,16 +498,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
*
* @param {string} loopToken The session token to use.
*/
_setupWebSocketAndCallView: function(loopToken) {
_setupWebSocketAndCallView: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this._conversation.get("progressURL"),
websocketToken: this._conversation.get("websocketToken"),
callId: this._conversation.get("callId"),
});
this._websocket.promiseConnect().then(function() {
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
}.bind(this), function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
@ -464,30 +532,48 @@ loop.webapp = (function($, _, OT, mozL10n) {
* it if appropraite.
*/
_handleWebSocketProgress: function(progressData) {
if (progressData.state === "terminated") {
// XXX Before adding more states here, the basic protocol messages to the
// server need implementing on both the standalone and desktop side.
// These are covered by bug 1045643, but also check the dependencies on
// bug 1034041.
//
// Failure to do this will break desktop - standalone call setup. We're
// ok to handle reject, as that is a specific message from the destkop via
// the server.
switch (progressData.reason) {
case "reject":
this._handleCallRejected();
switch(progressData.state) {
case "connecting": {
this._handleCallConnecting();
break;
}
case "terminated": {
// At the moment, we show the same text regardless
// of the terminated reason.
this._handleCallTerminated(progressData.reason);
break;
}
}
},
/**
* Handles call rejection.
* XXX This should really display the call failed view - bug 1046959
* will implement this.
* Handles a call moving to the connecting stage.
*/
_handleCallRejected: function() {
_handleCallConnecting: function() {
var loopToken = this._conversation.get("loopToken");
if (!loopToken) {
this._notifications.errorL10n("missing_conversation_info");
return;
}
this.navigate("call/ongoing/" + loopToken, {
trigger: true
});
},
/**
* Handles call rejection.
*
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.endCall();
this._notifications.errorL10n("call_timeout_notification_text");
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this._notifications.errorL10n("call_timeout_notification_text");
}
},
/**
@ -501,10 +587,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.navigate(route, {trigger: true});
},
_onTimeout: function() {
this._notifications.errorL10n("call_timeout_notification_text");
},
/**
* Default entry point.
*/
@ -549,6 +631,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.loadReactComponent(startView);
},
pendingConversation: function(loopToken) {
if (!this._conversation.isSessionReady()) {
// User has loaded this url directly, actually setup the call.
return this.navigate("call/" + loopToken, {trigger: true});
}
this._setupWebSocketAndCallView();
this.loadReactComponent(PendingConversationView({
websocket: this._websocket
}));
},
/**
* Loads conversation establishment view.
*
@ -596,8 +689,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
notifications: new sharedModels.NotificationCollection(),
client: client,
conversation: new sharedModels.ConversationModel({}, {
sdk: OT,
pendingCallTimeout: loop.config.pendingCallTimeout
sdk: OT
})
});
@ -616,6 +708,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
return {
baseServerUrl: baseServerUrl,
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,

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

@ -36,7 +36,7 @@ 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
reject_incoming_call=Cancel
initiate_call_cancel_button=Cancel
legal_text_and_links=By using this product 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

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

@ -15,8 +15,7 @@ app.get('/content/config.js', function (req, res) {
res.send(
"var loop = loop || {};" +
"loop.config = loop.config || {};" +
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
"loop.config.pendingCallTimeout = 20000;"
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';"
);
});

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

@ -242,7 +242,7 @@ describe("loop.conversation", function() {
});
});
it("should create the view with video.enabled=false", function(done) {
it("should create the view with video=false", function(done) {
sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
router._setupWebSocketAndCallView();
@ -252,7 +252,7 @@ describe("loop.conversation", function() {
sinon.assert.calledOnce(loop.conversation.IncomingCallView);
sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
{model: conversation,
video: {enabled: false}});
video: false});
done();
});
});
@ -584,11 +584,104 @@ describe("loop.conversation", function() {
var Model = Backbone.Model.extend({});
model = new Model();
sandbox.spy(model, "trigger");
sandbox.stub(model, "set");
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model
model: model,
video: true
}));
});
describe("default answer mode", function() {
it("should display video as primary answer mode", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
expect(primaryBtn).not.to.eql(null);
});
it("should display audio as primary answer mode", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
expect(primaryBtn).not.to.eql(null);
});
it("should accept call with video", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with video when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-video-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-audio-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
});
describe("click event on .btn-accept", function() {
it("should trigger an 'accept' conversation model event", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
@ -601,7 +694,6 @@ describe("loop.conversation", function() {
it("should set selectedCallType to audio-video", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
sandbox.stub(model, "set");
TestUtils.Simulate.click(buttonAccept);
@ -611,29 +703,6 @@ describe("loop.conversation", function() {
});
});
describe("click event on .call-audio-only", function() {
it("should trigger an 'accept' conversation model event", function () {
var buttonAccept = view.getDOMNode().querySelector(".call-audio-only");
model.trigger.withArgs("accept");
TestUtils.Simulate.click(buttonAccept);
/* Setting a model property triggers 2 events */
sinon.assert.calledOnce(model.trigger.withArgs("accept"));
});
it("should set selectedCallType to audio", function() {
var buttonAccept = view.getDOMNode().querySelector(".call-audio-only");
sandbox.stub(model, "set");
TestUtils.Simulate.click(buttonAccept);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
});
});
describe("click event on .btn-decline", function() {
it("should trigger an 'decline' conversation model event", function() {
var buttonDecline = view.getDOMNode().querySelector(".btn-decline");

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

@ -33,6 +33,7 @@
</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/views.js"></script>
@ -43,6 +44,7 @@
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="mixins_test.js"></script>
<script src="utils_test.js"></script>
<script src="views_test.js"></script>
<script src="router_test.js"></script>
<script src="websocket_test.js"></script>

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

@ -53,13 +53,6 @@ describe("loop.shared.models", function() {
new sharedModels.ConversationModel({}, {});
}).to.Throw(Error, /missing required sdk/);
});
it("should accept a pendingCallTimeout option", function() {
expect(new sharedModels.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000
}).pendingCallTimeout).eql(1000);
});
});
describe("constructed", function() {
@ -68,8 +61,7 @@ describe("loop.shared.models", function() {
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {
sdk: fakeSDK,
pendingCallTimeout: 1000
sdk: fakeSDK
});
conversation.set("loopToken", "fakeToken");
fakeBaseServerUrl = "http://fakeBaseServerUrl";
@ -121,25 +113,6 @@ describe("loop.shared.models", function() {
conversation.outgoing();
});
it("should end the session on outgoing call timeout", function() {
conversation.outgoing();
sandbox.clock.tick(1001);
sinon.assert.calledOnce(conversation.endSession);
});
it("should trigger a `timeout` event on outgoing call timeout",
function(done) {
conversation.once("timeout", function() {
done();
});
conversation.outgoing();
sandbox.clock.tick(1001);
});
});
describe("#setSessionData", function() {
@ -168,11 +141,8 @@ describe("loop.shared.models", function() {
var model;
beforeEach(function() {
sandbox.stub(sharedModels.ConversationModel.prototype,
"_clearPendingCallTimer");
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK,
pendingCallTimeout: 1000
sdk: fakeSDK
});
model.startSession();
});
@ -281,18 +251,6 @@ describe("loop.shared.models", function() {
expect(model.get("ongoing")).eql(false);
});
it("should clear a pending timer on session:ended", function() {
model.trigger("session:ended");
sinon.assert.calledOnce(model._clearPendingCallTimer);
});
it("should clear a pending timer on session:error", function() {
model.trigger("session:error");
sinon.assert.calledOnce(model._clearPendingCallTimer);
});
describe("connectionDestroyed event received", function() {
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
@ -341,8 +299,7 @@ describe("loop.shared.models", function() {
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK,
pendingCallTimeout: 1000
sdk: fakeSDK
});
model.startSession();
});
@ -381,8 +338,7 @@ describe("loop.shared.models", function() {
beforeEach(function() {
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK,
pendingCallTimeout: 1000
sdk: fakeSDK
});
model.startSession();
});

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

@ -0,0 +1,58 @@
/* 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/. */
/* global loop, sinon */
/* jshint newcap:false */
var expect = chai.expect;
describe("loop.shared.utils", function() {
"use strict";
var sandbox;
var sharedUtils = loop.shared.utils;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
sandbox.restore();
});
describe("#getBoolPreference", function() {
afterEach(function() {
navigator.mozLoop = undefined;
localStorage.removeItem("test.true");
});
describe("mozLoop set", function() {
beforeEach(function() {
navigator.mozLoop = {
getLoopBoolPref: function(prefName) {
return prefName === "test.true";
}
};
});
it("should return the mozLoop preference", function() {
expect(sharedUtils.getBoolPreference("test.true")).eql(true);
});
it("should not use the localStorage value", function() {
localStorage.setItem("test.false", true);
expect(sharedUtils.getBoolPreference("test.false")).eql(false);
});
});
describe("mozLoop not set", function() {
it("should return the localStorage value", function() {
localStorage.setItem("test.true", true);
expect(sharedUtils.getBoolPreference("test.true")).eql(true);
});
});
});
});

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

@ -176,6 +176,22 @@ describe("loop.CallConnectionWebSocket", function() {
});
});
describe("#cancel", function() {
it("should send a terminate message to the server with a reason of cancel",
function() {
callWebSocket.promiseConnect();
callWebSocket.cancel();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "terminate",
reason: "cancel"
}));
});
});
describe("Events", function() {
beforeEach(function() {
sandbox.stub(callWebSocket, "trigger");
@ -195,9 +211,24 @@ describe("loop.CallConnectionWebSocket", function() {
data: JSON.stringify(eventData)
});
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.called(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
});
it("should trigger a progress:<state> event on the callWebSocket", function() {
var eventData = {
messageType: "progress",
state: "terminate",
reason: "reject"
};
dummySocket.onmessage({
data: JSON.stringify(eventData)
});
sinon.assert.called(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
});
});
describe("Error", function() {

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

@ -88,6 +88,14 @@ describe("loop.webapp", function() {
sandbox.stub(router, "navigate");
});
describe("#initialize", function() {
it("should require a conversation option", function() {
expect(function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
});
});
describe("#startCall", function() {
beforeEach(function() {
sandbox.stub(router, "_setupWebSocketAndCallView");
@ -109,14 +117,15 @@ describe("loop.webapp", function() {
"missing_conversation_info");
});
it("should setup the websocket if session token is available", function() {
conversation.set("loopToken", "fake");
it("should navigate to the pending view if session token is available",
function() {
conversation.set("loopToken", "fake");
router.startCall();
router.startCall();
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView, "fake");
});
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/pending/fake");
});
});
describe("#_setupWebSocketAndCallView", function() {
@ -126,7 +135,7 @@ describe("loop.webapp", function() {
sessionToken: "sessionToken",
apiKey: "apiKey",
callId: "Hello",
progressURL: "http://progress.example.com",
progressURL: "http://invalid/url",
websocketToken: 123
});
});
@ -154,23 +163,13 @@ describe("loop.webapp", function() {
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
url: "http://invalid/url",
// The websocket token is converted to a hex string.
websocketToken: "7b"
});
done();
});
});
it("should navigate to call/ongoing/:token", function(done) {
router._setupWebSocketAndCallView("fake");
promise.then(function () {
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
done();
});
});
});
describe("Websocket connection failed", function() {
@ -226,6 +225,7 @@ describe("loop.webapp", function() {
describe("state: terminate, reason: reject", function() {
beforeEach(function() {
sandbox.stub(router, "endCall");
sandbox.stub(notifications, "errorL10n");
});
it("should end the call", function() {
@ -237,17 +237,39 @@ describe("loop.webapp", function() {
sinon.assert.calledOnce(router.endCall);
});
it("should display an error message", function() {
sandbox.stub(notifications, "errorL10n");
it("should display an error message if the reason is not 'cancel'",
function() {
router._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
router._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"call_timeout_notification_text");
});
sinon.assert.calledOnce(router._notifications.errorL10n);
sinon.assert.calledWithExactly(router._notifications.errorL10n,
"call_timeout_notification_text");
it("should not display an error message if the reason is 'cancel'",
function() {
router._websocket.trigger("progress", {
state: "terminated",
reason: "cancel"
});
sinon.assert.notCalled(notifications.errorL10n);
});
});
describe("state: connecting", function() {
it("should navigate to the ongoing view", function() {
conversation.set({"loopToken": "fakeToken"});
router._websocket.trigger("progress", {
state: "connecting"
});
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
});
});
});
@ -334,6 +356,38 @@ describe("loop.webapp", function() {
});
});
describe("#pendingConversation", function() {
beforeEach(function() {
sandbox.stub(router, "_setupWebSocketAndCallView");
conversation.setOutgoingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
});
it("should setup the websocket", function() {
router.pendingConversation();
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
});
it("should load the PendingConversationView", function() {
router.pendingConversation();
sinon.assert.calledOnce(router.loadReactComponent);
sinon.assert.calledWith(router.loadReactComponent,
sinon.match(function(value) {
return React.addons.TestUtils.isDescriptorOfType(
value, loop.webapp.PendingConversationView);
}));
});
});
describe("#loadConversation", function() {
it("should load the ConversationView if session is set", function() {
conversation.set("sessionId", "fakeSessionId");
@ -548,15 +602,46 @@ describe("loop.webapp", function() {
});
});
describe("StartConversationView", function() {
describe("#initialize", function() {
it("should require a conversation option", function() {
expect(function() {
new loop.webapp.WebappRouter();
}).to.Throw(Error, /missing required conversation/);
describe("PendingConversationView", function() {
var view, websocket;
beforeEach(function() {
websocket = new loop.CallConnectionWebSocket({
url: "wss://fake/",
callId: "callId",
websocketToken: "7b"
});
sinon.stub(websocket, "cancel");
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.PendingConversationView({
websocket: websocket
})
);
});
describe("#_cancelOutgoingCall", function() {
it("should inform the websocket to cancel the setup", function() {
var button = view.getDOMNode().querySelector(".btn-cancel");
React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(websocket.cancel);
});
});
describe("Events", function() {
describe("progress:alerting", function() {
it("should update the callstate to ringing", function () {
websocket.trigger("progress:alerting");
expect(view.state.callState).to.be.equal("ringing");
});
});
});
});
describe("StartConversationView", function() {
describe("#initiate", function() {
var conversation, setupOutgoingCall, view, fakeSubmitEvent,
requestCallUrlInfo;

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

@ -21,6 +21,7 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
// 3. Shared components
@ -134,9 +135,17 @@
),
Section({name: "IncomingCallView"},
Example({summary: "Default", dashed: "true", style: {width: "280px"}},
Example({summary: "Default / incoming video call", dashed: "true", style: {width: "280px"}},
React.DOM.div({className: "fx-embedded"},
IncomingCallView({model: mockConversationModel})
IncomingCallView({model: mockConversationModel,
video: {enabled: true}})
)
),
Example({summary: "Default / incoming audio only call", dashed: "true", style: {width: "280px"}},
React.DOM.div({className: "fx-embedded"},
IncomingCallView({model: mockConversationModel,
video: {enabled: false}})
)
)
),
@ -144,7 +153,9 @@
Section({name: "IncomingCallView-ActiveState"},
Example({summary: "Default", dashed: "true", style: {width: "280px"}},
React.DOM.div({className: "fx-embedded"},
IncomingCallView({model: mockConversationModel, showDeclineMenu: true})
IncomingCallView({model: mockConversationModel,
showDeclineMenu: true,
video: {enabled: true}})
)
)
),
@ -195,6 +206,19 @@
)
),
Section({name: "PendingConversationView"},
Example({summary: "Pending conversation view (connecting)", dashed: "true"},
React.DOM.div({className: "standalone"},
PendingConversationView(null)
)
),
Example({summary: "Pending conversation view (ringing)", dashed: "true"},
React.DOM.div({className: "standalone"},
PendingConversationView({callState: "ringing"})
)
)
),
Section({name: "StartConversationView"},
Example({summary: "Start conversation view", dashed: "true"},
React.DOM.div({className: "standalone"},

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

@ -21,6 +21,7 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
// 3. Shared components
@ -134,9 +135,17 @@
</Section>
<Section name="IncomingCallView">
<Example summary="Default" dashed="true" style={{width: "280px"}}>
<Example summary="Default / incoming video call" dashed="true" style={{width: "280px"}}>
<div className="fx-embedded">
<IncomingCallView model={mockConversationModel} />
<IncomingCallView model={mockConversationModel}
video={{enabled: true}} />
</div>
</Example>
<Example summary="Default / incoming audio only call" dashed="true" style={{width: "280px"}}>
<div className="fx-embedded">
<IncomingCallView model={mockConversationModel}
video={{enabled: false}} />
</div>
</Example>
</Section>
@ -144,7 +153,9 @@
<Section name="IncomingCallView-ActiveState">
<Example summary="Default" dashed="true" style={{width: "280px"}}>
<div className="fx-embedded" >
<IncomingCallView model={mockConversationModel} showDeclineMenu={true} />
<IncomingCallView model={mockConversationModel}
showDeclineMenu={true}
video={{enabled: true}} />
</div>
</Example>
</Section>
@ -195,6 +206,19 @@
</div>
</Section>
<Section name="PendingConversationView">
<Example summary="Pending conversation view (connecting)" dashed="true">
<div className="standalone">
<PendingConversationView />
</div>
</Example>
<Example summary="Pending conversation view (ringing)" dashed="true">
<div className="standalone">
<PendingConversationView callState="ringing"/>
</div>
</Example>
</Section>
<Section name="StartConversationView">
<Example summary="Start conversation view" dashed="true">
<div className="standalone">

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

@ -91,39 +91,6 @@ Tools.options = {
}
}
Tools.webConsole = {
id: "webconsole",
key: l10n("cmd.commandkey", webConsoleStrings),
accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
ordinal: 2,
icon: "chrome://browser/skin/devtools/tool-webconsole.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/webconsole.xul",
label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
panelLabel: l10n("ToolboxWebConsole.panelLabel", webConsoleStrings),
tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
inMenu: true,
commands: "devtools/webconsole/console-commands",
preventClosingOnKey: true,
onkey: function(panel, toolbox) {
if (toolbox.splitConsole)
return toolbox.focusConsoleInput();
panel.focusInput();
},
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
return new WebConsolePanel(iframeWindow, toolbox);
}
};
Tools.inspector = {
id: "inspector",
accesskey: l10n("inspector.accesskey", inspectorStrings),
@ -157,6 +124,39 @@ Tools.inspector = {
}
};
Tools.webConsole = {
id: "webconsole",
key: l10n("cmd.commandkey", webConsoleStrings),
accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
ordinal: 2,
icon: "chrome://browser/skin/devtools/tool-webconsole.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/webconsole.xul",
label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
panelLabel: l10n("ToolboxWebConsole.panelLabel", webConsoleStrings),
tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
inMenu: true,
commands: "devtools/webconsole/console-commands",
preventClosingOnKey: true,
onkey: function(panel, toolbox) {
if (toolbox.splitConsole)
return toolbox.focusConsoleInput();
panel.focusInput();
},
isTargetSupported: function(target) {
return true;
},
build: function(iframeWindow, toolbox) {
return new WebConsolePanel(iframeWindow, toolbox);
}
};
Tools.jsdebugger = {
id: "jsdebugger",
key: l10n("debuggerMenu.commandkey", debuggerStrings),
@ -248,26 +248,6 @@ Tools.canvasDebugger = {
}
};
Tools.webAudioEditor = {
id: "webaudioeditor",
ordinal: 10,
visibilityswitch: "devtools.webaudioeditor.enabled",
icon: "chrome://browser/skin/devtools/tool-webaudio.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/webaudioeditor.xul",
label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings),
panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings),
tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings),
isTargetSupported: function(target) {
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
return new WebAudioEditorPanel(iframeWindow, toolbox);
}
};
Tools.jsprofiler = {
id: "jsprofiler",
accesskey: l10n("profiler.accesskey", profilerStrings),
@ -366,9 +346,29 @@ Tools.storage = {
}
};
Tools.webAudioEditor = {
id: "webaudioeditor",
ordinal: 11,
visibilityswitch: "devtools.webaudioeditor.enabled",
icon: "chrome://browser/skin/devtools/tool-webaudio.svg",
invertIconForLightTheme: true,
url: "chrome://browser/content/devtools/webaudioeditor.xul",
label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings),
panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings),
tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings),
isTargetSupported: function(target) {
return !target.isAddon;
},
build: function(iframeWindow, toolbox) {
return new WebAudioEditorPanel(iframeWindow, toolbox);
}
};
Tools.scratchpad = {
id: "scratchpad",
ordinal: 11,
ordinal: 12,
visibilityswitch: "devtools.scratchpad.enabled",
icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
invertIconForLightTheme: true,

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

@ -0,0 +1,16 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;
// We also need a valid nsIXulAppInfo service as Webapps.jsm is querying it
Cu.import("resource://testing-common/AppInfo.jsm");
updateAppInfo();
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let require = devtools.require;

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

@ -7,9 +7,6 @@
// Tests the BezierCanvas API in the CubicBezierWidget module
const Cu = Components.utils;
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let require = devtools.require;
let {CubicBezier, BezierCanvas} = require("devtools/shared/widgets/CubicBezierWidget");
function run_test() {

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

@ -7,9 +7,6 @@
// Tests the CubicBezier API in the CubicBezierWidget module
const Cu = Components.utils;
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let require = devtools.require;
let {CubicBezier} = require("devtools/shared/widgets/CubicBezierWidget");
function run_test() {

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

@ -3,7 +3,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const Cu = Components.utils;
let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
let loader = new Loader.Loader({

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

@ -1,5 +1,5 @@
[DEFAULT]
head =
head = head.js
tail =
firefox-appdir = browser

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

@ -0,0 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;
// We also need a valid nsIXulAppInfo
Cu.import("resource://testing-common/AppInfo.jsm");
updateAppInfo();
Cu.import("resource://gre/modules/devtools/Loader.jsm");

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

@ -3,8 +3,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/Loader.jsm");
const {parseDeclarations} = devtools.require("devtools/styleinspector/css-parsing-utils");
const TEST_DATA = [

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

@ -3,8 +3,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/Loader.jsm");
const {parseSingleValue} = devtools.require("devtools/styleinspector/css-parsing-utils");
const TEST_DATA = [

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

@ -1,5 +1,5 @@
[DEFAULT]
head =
head = head.js
tail =
firefox-appdir = browser

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

@ -132,10 +132,11 @@ let UI = {
break;
case "project":
this.updateTitle();
this.closeToolbox();
this.destroyToolbox();
this.updateCommands();
this.updateProjectButton();
this.openProject();
this.autoStartProject();
break;
case "project-is-not-running":
case "project-is-running":
@ -481,6 +482,26 @@ let UI = {
}, console.error);
},
autoStartProject: function() {
let project = AppManager.selectedProject;
if (!project) {
return;
}
if (!(project.type == "runtimeApp" ||
project.type == "mainProcess" ||
project.type == "tab")) {
return; // For something that is not an editable app, we're done.
}
Task.spawn(function() {
if (project.type == "runtimeApp") {
yield UI.busyUntil(AppManager.runRuntimeApp(), "running app");
}
yield UI.createToolbox();
});
},
/********** DECK **********/
setupDeck: function() {
@ -629,7 +650,7 @@ let UI = {
} catch(e) { console.error(e); }
},
closeToolbox: function() {
destroyToolbox: function() {
if (this.toolboxPromise) {
this.toolboxPromise.then(toolbox => {
toolbox.destroy();
@ -638,6 +659,13 @@ let UI = {
}
},
createToolbox: function() {
this.toolboxPromise = AppManager.getTarget().then((target) => {
return this.showToolbox(target);
}, console.error);
return this.busyUntil(this.toolboxPromise, "opening toolbox");
},
showToolbox: function(target) {
if (this.toolboxIframe) {
return;
@ -999,14 +1027,10 @@ let Cmds = {
toggleToolbox: function() {
if (UI.toolboxIframe) {
UI.closeToolbox();
UI.destroyToolbox();
return promise.resolve();
} else {
UI.toolboxPromise = AppManager.getTarget().then((target) => {
return UI.showToolbox(target);
}, console.error);
UI.busyUntil(UI.toolboxPromise, "opening toolbox");
return UI.toolboxPromise;
return UI.createToolbox();
}
},

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

@ -333,9 +333,6 @@ exports.AppManager = AppManager = {
this.tabStore.selectedTab = null;
if (this.selectedProject) {
if (this.selectedProject.type == "runtimeApp") {
this.runRuntimeApp();
}
if (this.selectedProject.type == "packaged" ||
this.selectedProject.type == "hosted") {
this.validateProject(this.selectedProject);

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

@ -58,6 +58,8 @@ TabStore.prototype = {
_resetStore: function() {
this.response = null;
this.tabs = [];
this._selectedTab = null;
this._selectedTabTargetPromise = null;
},
_onStatusChanged: function() {
@ -115,6 +117,7 @@ TabStore.prototype = {
// which is the selected project. This should be done as part of the
// project-agnostic work.
_selectedTab: null,
_selectedTabTargetPromise: null,
get selectedTab() {
return this._selectedTab;
},
@ -134,13 +137,18 @@ TabStore.prototype = {
return tab.actor === this._selectedTab.actor;
});
if (!alive) {
this._selectedTab = null;
this._selectedTabTargetPromise = null;
this.emit("closed");
}
},
getTargetForTab: function() {
if (this._selectedTabTargetPromise) {
return this._selectedTabTargetPromise;
}
let store = this;
return Task.spawn(function*() {
this._selectedTabTargetPromise = Task.spawn(function*() {
// If you connect to a tab, then detach from it, the root actor may have
// de-listed the actors that belong to the tab. This breaks the toolbox
// if you try to connect to the same tab again. To work around this
@ -152,6 +160,7 @@ TabStore.prototype = {
chrome: false
});
});
return this._selectedTabTargetPromise;
},
};

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

@ -101,6 +101,18 @@ function nextTick() {
return deferred.promise;
}
function waitForUpdate(win, update) {
let deferred = promise.defer();
win.AppManager.on("app-manager-update", function onUpdate(e, what) {
if (what !== update) {
return;
}
win.AppManager.off("app-manager-update", onUpdate);
deferred.resolve();
});
return deferred.promise;
}
function documentIsLoaded(doc) {
let deferred = promise.defer();
if (doc.readyState == "complete") {

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

@ -95,17 +95,28 @@
ok(!isPlayActive(), "play button is disabled 4");
ok(!isStopActive(), "stop button is disabled 4");
deferred = promise.defer();
win.AppManager.connection.once(
win.Connection.Events.CONNECTED,
() => deferred.resolve());
win.document.querySelectorAll(".runtime-panel-item-custom")[1].click();
yield deferred.promise;
yield waitForUpdate(win, "list-tabs-response");
is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
// Select main process
yield win.Cmds.showProjectPanel();
SimpleTest.executeSoon(() => {
win.document.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
});
yield waitForUpdate(win, "project");
// Toolbox opens automatically for main process / runtime apps
ok(win.UI.toolboxPromise, "Toolbox promise exists");
yield win.UI.toolboxPromise;
ok(win.UI.toolboxIframe, "Toolbox iframe exists");
yield win.Cmds.disconnectRuntime();
yield closeWebIDE(win);

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

@ -174,6 +174,7 @@ initiate_call_button_label2=Ready to start your conversation?
incoming_call_title2=Conversation Request
incoming_call_accept_button=Accept
incoming_call_accept_audio_only_tooltip=Accept with voice
incoming_call_accept_audio_video_tooltip=Accept with video
incoming_call_cancel_button=Cancel
incoming_call_cancel_and_block_button=Cancel and Block
incoming_call_block_button=Block

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

@ -647,6 +647,28 @@ nsDOMCameraControl::Capabilities()
return caps.forget();
}
class ImmediateErrorCallback : public nsRunnable
{
public:
ImmediateErrorCallback(CameraErrorCallback* aCallback, const nsAString& aMessage)
: mCallback(aCallback)
, mMessage(aMessage)
{ }
NS_IMETHODIMP
Run()
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult ignored;
mCallback->Call(mMessage, ignored);
return NS_OK;
}
protected:
nsRefPtr<CameraErrorCallback> mCallback;
nsString mMessage;
};
// Methods.
void
nsDOMCameraControl::StartRecording(const CameraStartRecordingOptions& aOptions,
@ -658,6 +680,20 @@ nsDOMCameraControl::StartRecording(const CameraStartRecordingOptions& aOptions,
{
MOZ_ASSERT(mCameraControl);
nsRefPtr<CameraStartRecordingCallback> cb = mStartRecordingOnSuccessCb;
if (cb) {
if (aOnError.WasPassed()) {
DOM_CAMERA_LOGT("%s:onError WasPassed\n", __func__);
NS_DispatchToMainThread(new ImmediateErrorCallback(&aOnError.Value(),
NS_LITERAL_STRING("StartRecordingInProgress")));
} else {
DOM_CAMERA_LOGT("%s:onError NS_ERROR_FAILURE\n", __func__);
// Only throw if no error callback was passed in.
aRv = NS_ERROR_FAILURE;
}
return;
}
NotifyRecordingStatusChange(NS_LITERAL_STRING("starting"));
#ifdef MOZ_B2G
@ -745,28 +781,6 @@ nsDOMCameraControl::ResumePreview(ErrorResult& aRv)
aRv = mCameraControl->StartPreview();
}
class ImmediateErrorCallback : public nsRunnable
{
public:
ImmediateErrorCallback(CameraErrorCallback* aCallback, const nsAString& aMessage)
: mCallback(aCallback)
, mMessage(aMessage)
{ }
NS_IMETHODIMP
Run()
{
MOZ_ASSERT(NS_IsMainThread());
ErrorResult ignored;
mCallback->Call(mMessage, ignored);
return NS_OK;
}
protected:
nsRefPtr<CameraErrorCallback> mCallback;
nsString mMessage;
};
void
nsDOMCameraControl::SetConfiguration(const CameraConfiguration& aConfiguration,
const Optional<OwningNonNull<CameraSetConfigurationCallback> >& aOnSuccess,

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

@ -2473,6 +2473,11 @@ public class BrowserApp extends GeckoApp
@Override
public void openOptionsMenu() {
// Disable menu access (for hardware buttons) when the software menu button is inaccessible.
if (mBrowserToolbar.isEditing()) {
return;
}
if (areTabsShown()) {
mTabsPanel.showMenu();
return;

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

@ -13,12 +13,13 @@ import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
@ -78,6 +79,7 @@ public class BookmarksPanel extends HomeFragment {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.TITLE));
info.bookmarkId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
info.itemType = RemoveItemType.BOOKMARKS;
return info;
}
});

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

@ -18,8 +18,8 @@ import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.AlertDialog;
import android.content.ContentResolver;
@ -95,6 +95,7 @@ public class HistoryPanel extends HomeFragment {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(Combined.TITLE));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(Combined.HISTORY_ID));
info.itemType = RemoveItemType.HISTORY;
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a

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

@ -7,7 +7,6 @@ package org.mozilla.gecko.home;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomePager;
import android.content.Context;
import android.os.Bundle;

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

@ -24,6 +24,12 @@ public class HomeContextMenuInfo extends AdapterContextMenuInfo {
public int historyId = -1;
public int bookmarkId = -1;
public int readingListItemId = -1;
public RemoveItemType itemType = null;
// Item type to be handled with "Remove" selection.
public static enum RemoveItemType {
BOOKMARKS, HISTORY, READING_LIST
}
public HomeContextMenuInfo(View targetView, int position, long id) {
super(targetView, position, id);

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

@ -21,6 +21,7 @@ import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
import org.mozilla.gecko.util.Clipboard;
@ -248,15 +249,11 @@ public abstract class HomeFragment extends Fragment {
}
if (itemId == R.id.home_remove) {
if (info instanceof TopSitesGridContextMenuInfo) {
(new RemoveItemByUrlTask(context, info.url, info.position)).execute();
return true;
}
// For Top Sites grid items, position is required in case item is Pinned.
final int position = info instanceof TopSitesGridContextMenuInfo ? info.position : -1;
if (info.isInReadingList() || info.hasBookmarkId() || info.hasHistoryId()) {
(new RemoveItemByUrlTask(context, info.url)).execute();
return true;
}
(new RemoveItemByUrlTask(context, info.url, info.itemType, position)).execute();
return true;
}
return false;
@ -316,7 +313,6 @@ public abstract class HomeFragment extends Fragment {
return mCanLoadHint;
}
protected abstract void load();
protected boolean canLoad() {
@ -332,27 +328,22 @@ public abstract class HomeFragment extends Fragment {
mIsLoaded = true;
}
private static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams<Void> {
protected static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams<Void> {
private final Context mContext;
private final String mUrl;
private final RemoveItemType mType;
private final int mPosition;
/**
* Remove bookmark/history/reading list item by url.
*/
public RemoveItemByUrlTask(Context context, String url) {
this(context, url, -1);
}
/**
* Remove bookmark/history/reading list item by url, and also unpin the
* Remove bookmark/history/reading list type item by url, and also unpin the
* Top Sites grid item at index <code>position</code>.
*/
public RemoveItemByUrlTask(Context context, String url, int position) {
public RemoveItemByUrlTask(Context context, String url, RemoveItemType type, int position) {
super(ThreadUtils.getBackgroundHandler());
mContext = context;
mUrl = url;
mType = type;
mPosition = position;
}
@ -367,22 +358,34 @@ public abstract class HomeFragment extends Fragment {
}
}
BrowserDB.removeBookmarksWithURL(cr, mUrl);
BrowserDB.removeHistoryEntry(cr, mUrl);
switch(mType) {
case BOOKMARKS:
BrowserDB.removeBookmarksWithURL(cr, mUrl);
break;
BrowserDB.removeReadingListItemWithURL(cr, mUrl);
case HISTORY:
BrowserDB.removeHistoryEntry(cr, mUrl);
break;
final JSONObject json = new JSONObject();
try {
json.put("url", mUrl);
json.put("notify", false);
} catch (JSONException e) {
Log.e(LOGTAG, "error building JSON arguments");
case READING_LIST:
BrowserDB.removeReadingListItemWithURL(cr, mUrl);
final JSONObject json = new JSONObject();
try {
json.put("url", mUrl);
json.put("notify", false);
} catch (JSONException e) {
Log.e(LOGTAG, "error building JSON arguments");
}
GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", json.toString());
GeckoAppShell.sendEventToGecko(e);
break;
default:
Log.e(LOGTAG, "Can't remove item type " + mType.toString());
break;
}
GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", json.toString());
GeckoAppShell.sendEventToGecko(e);
return null;
}

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

@ -14,6 +14,7 @@ import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import android.content.Context;
@ -97,6 +98,7 @@ public class ReadingListPanel extends HomeFragment {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(ReadingListItems.TITLE));
info.readingListItemId = cursor.getInt(cursor.getColumnIndexOrThrow(ReadingListItems._ID));
info.itemType = RemoveItemType.READING_LIST;
return info;
}
});

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

@ -12,7 +12,6 @@ import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.ThumbnailHelper;
import org.mozilla.gecko.db.BrowserContract.TopSites;
import org.mozilla.gecko.db.TopSitesCursorWrapper;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.util.StringUtils;
@ -20,7 +19,6 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
@ -277,6 +275,7 @@ public class TopSitesGridView extends GridView {
public TopSitesGridContextMenuInfo(View targetView, int position, long id) {
super(targetView, position, id);
this.itemType = RemoveItemType.HISTORY;
}
}
}

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

@ -25,6 +25,7 @@ import org.mozilla.gecko.db.URLMetadata;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
@ -179,6 +180,7 @@ public class TopSitesPanel extends HomeFragment {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.TITLE));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.HISTORY_ID));
info.itemType = RemoveItemType.HISTORY;
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(TopSites.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a

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

@ -100,6 +100,8 @@
"to bookmark", not the noun "a bookmark". -->
<!ENTITY overlay_share_bookmark_btn_label "Bookmark">
<!ENTITY overlay_share_reading_list_btn_label "Add to Reading List">
<!ENTITY overlay_share_bookmark_btn_label_already "Already bookmarked">
<!ENTITY overlay_share_reading_list_btn_label_already "Already in Reading List">
<!ENTITY overlay_share_send_other "Send to other devices">
<!-- Localization note (overlay_share_send_tab_btn_label) : Used on the

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

@ -397,6 +397,7 @@ gbjar.sources += [
'tabs/TabsLayoutItemView.java',
'tabs/TabsListLayout.java',
'tabs/TabsPanel.java',
'tabs/TabStrip.java',
'tabs/TabStripAdapter.java',
'tabs/TabStripItemView.java',
'tabs/TabStripView.java',
@ -495,11 +496,13 @@ if CONFIG['MOZ_ANDROID_SHARE_OVERLAY']:
gbjar.sources += [
'overlays/OverlayConstants.java',
'overlays/service/OverlayActionService.java',
'overlays/service/ShareData.java',
'overlays/service/sharemethods/AddBookmark.java',
'overlays/service/sharemethods/AddToReadingList.java',
'overlays/service/sharemethods/ParcelableClientRecord.java',
'overlays/service/sharemethods/SendTab.java',
'overlays/service/sharemethods/ShareMethod.java',
'overlays/ui/OverlayDialogButton.java',
'overlays/ui/OverlayToastHelper.java',
'overlays/ui/SendTabDeviceListArrayAdapter.java',
'overlays/ui/SendTabList.java',

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

@ -48,7 +48,7 @@ public class OverlayActionService extends Service {
private static final String LOGTAG = "GeckoOverlayService";
// Map used for selecting the appropriate helper object when handling a share.
private final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
final Map<ShareMethod.Type, ShareMethod> shareTypes = new EnumMap<>(ShareMethod.Type.class);
// Map relating Strings representing share types to the corresponding ShareMethods.
// Share methods are initialised (and shown in the UI) in the order they are given here.
@ -88,34 +88,34 @@ public class OverlayActionService extends Service {
/**
* Reinitialise all ShareMethods, causing them to broadcast any UI update events necessary.
*/
private void initShareMethods(Context context) {
shareTypes.clear();
private void initShareMethods(final Context context) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
shareTypes.clear();
shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
shareTypes.put(ShareMethod.Type.ADD_TO_READING_LIST, new AddToReadingList(context));
shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
shareTypes.put(ShareMethod.Type.ADD_BOOKMARK, new AddBookmark(context));
shareTypes.put(ShareMethod.Type.ADD_TO_READING_LIST, new AddToReadingList(context));
shareTypes.put(ShareMethod.Type.SEND_TAB, new SendTab(context));
}
});
}
public void handleShare(final Intent intent) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
Bundle extras = intent.getExtras();
// Fish the parameters out of the Intent.
final String url = extras.getString(OverlayConstants.EXTRA_URL);
final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
if (url == null) {
Log.e(LOGTAG, "Null url passed to handleShare!");
ShareData shareData;
try {
shareData = ShareData.fromIntent(intent);
} catch (IllegalArgumentException e) {
Log.e(LOGTAG, "Error parsing share intent: ", e);
return;
}
ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
ShareMethod shareMethod = shareTypes.get(shareMethodType);
ShareMethod shareMethod = shareTypes.get(shareData.shareMethodType);
final ShareMethod.Result result = shareMethod.handle(title, url, extra);
final ShareMethod.Result result = shareMethod.handle(shareData);
// Dispatch the share to the targeted ShareMethod.
switch (result) {
case SUCCESS:

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

@ -0,0 +1,48 @@
/* 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/. */
package org.mozilla.gecko.overlays.service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
import static org.mozilla.gecko.overlays.OverlayConstants.EXTRA_SHARE_METHOD;
/**
* Class to hold information related to a particular request to perform a share.
*/
public class ShareData {
private static final String LOGTAG = "GeckoShareRequest";
public final String url;
public final String title;
public final Parcelable extra;
public final ShareMethod.Type shareMethodType;
public ShareData(String url, String title, Parcelable extra, ShareMethod.Type shareMethodType) {
if (url == null) {
throw new IllegalArgumentException("Null url passed to ShareData!");
}
this.url = url;
this.title = title;
this.extra = extra;
this.shareMethodType = shareMethodType;
}
public static ShareData fromIntent(Intent intent) {
Bundle extras = intent.getExtras();
// Fish the parameters out of the Intent.
final String url = extras.getString(OverlayConstants.EXTRA_URL);
final String title = extras.getString(OverlayConstants.EXTRA_TITLE);
final Parcelable extra = extras.getParcelable(OverlayConstants.EXTRA_PARAMETERS);
ShareMethod.Type shareMethodType = (ShareMethod.Type) extras.get(EXTRA_SHARE_METHOD);
return new ShareData(url, title, extra, shareMethodType);
}
}

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

@ -6,20 +6,20 @@ package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Parcelable;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.overlays.service.ShareData;
public class AddBookmark extends ShareMethod {
private static final String LOGTAG = "GeckoAddBookmark";
@Override
public Result handle(String title, String url, Parcelable unused) {
public Result handle(ShareData shareData) {
ContentResolver resolver = context.getContentResolver();
LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
browserDB.addBookmark(resolver, title, url);
browserDB.addBookmark(resolver, shareData.title, shareData.url);
return Result.SUCCESS;
}

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

@ -7,10 +7,10 @@ package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.os.Parcelable;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.overlays.service.ShareData;
import static org.mozilla.gecko.db.BrowserContract.Bookmarks;
@ -25,14 +25,14 @@ public class AddToReadingList extends ShareMethod {
private static final String LOGTAG = "GeckoAddToReadingList";
@Override
public Result handle(String title, String url, Parcelable unused) {
public Result handle(ShareData shareData) {
ContentResolver resolver = context.getContentResolver();
LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
ContentValues values = new ContentValues();
values.put(Bookmarks.TITLE, title);
values.put(Bookmarks.URL, url);
values.put(Bookmarks.TITLE, shareData.title);
values.put(Bookmarks.URL, shareData.url);
browserDB.addReadingListItem(resolver, values);

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

@ -22,6 +22,7 @@ import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.ShareData;
import org.mozilla.gecko.sync.CommandProcessor;
import org.mozilla.gecko.sync.CommandRunner;
import org.mozilla.gecko.sync.GlobalSession;
@ -66,15 +67,15 @@ public class SendTab extends ShareMethod {
private TabSender tabSender;
@Override
public Result handle(String title, String url, Parcelable extra) {
if (extra == null) {
public Result handle(ShareData shareData) {
if (shareData.extra == null) {
Log.e(LOGTAG, "No target devices specified!");
// Retrying with an identical lack of devices ain't gonna fix it...
return Result.PERMANENT_FAILURE;
}
String[] targetGUIDs = ((Bundle) extra).getStringArray(SEND_TAB_TARGET_DEVICES);
String[] targetGUIDs = ((Bundle) shareData.extra).getStringArray(SEND_TAB_TARGET_DEVICES);
// Ensure all target GUIDs are devices we actually know about.
if (!validGUIDs.containsAll(Arrays.asList(targetGUIDs))) {
@ -108,7 +109,7 @@ public class SendTab extends ShareMethod {
// Remember that ShareMethod.handle is always run on the background thread, so the database
// access here is of no concern.
for (int i = 0; i < targetGUIDs.length; i++) {
processor.sendURIToClientForDisplay(url, targetGUIDs[i], title, accountGUID, context);
processor.sendURIToClientForDisplay(shareData.url, targetGUIDs[i], shareData.title, accountGUID, context);
}
// Request an immediate sync to push these new commands to the network ASAP.

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

@ -7,6 +7,7 @@ package org.mozilla.gecko.overlays.service.sharemethods;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import org.mozilla.gecko.overlays.service.ShareData;
/**
* Represents a method of sharing a URL/title. Add a bookmark? Send to a device? Add to reading list?
@ -30,14 +31,7 @@ public abstract class ShareMethod {
* the ShareMethod (such as the device to share to in the case of SendTab).
* @return true if the attempt to share was a success. False in the event of an error.
*/
public abstract Result handle(String title, String url, Parcelable extra);
/**
* Convenience method for calling handlers on objects that don't require extra data.
*/
public Result handle(String title, String url) {
return handle(title, url, null);
}
public abstract Result handle(ShareData shareData);
public abstract String getSuccessMesssage();
public abstract String getFailureMessage();

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

@ -0,0 +1,152 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.overlays.ui;
import android.util.AttributeSet;
import org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A button in the share overlay, such as the "Add to Reading List" button.
* Has an associated icon and label, and two states: enabled and disabled.
*
* When disabled, tapping results in a "pop" animation causing the icon to pulse. When enabled,
* tapping calls the OnClickListener set by the consumer in the usual way.
*/
public class OverlayDialogButton extends LinearLayout {
private static final String LOGTAG = "GeckoOverlayDialogButton";
// The views making up this button.
private ImageView icon;
private TextView label;
// Label/icon used when enabled.
private String enabledLabel;
private Drawable enabledIcon;
// Label/icon used when disabled.
private String disabledLabel;
private Drawable disabledIcon;
// Click listeners used when enabled/disabled. Currently, disabledOnClickListener is set
// intenally to something that causes the icon to pulse.
private OnClickListener enabledOnClickListener;
private OnClickListener disabledOnClickListener;
private boolean isEnabled = true;
public OverlayDialogButton(Context context) {
super(context);
init(context);
}
public OverlayDialogButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public OverlayDialogButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
setOrientation(HORIZONTAL);
setPadding(0, 0, 0, 0);
setBackgroundResource(R.drawable.overlay_share_button_background);
LayoutInflater.from(context).inflate(R.layout.overlay_share_button, this);
icon = (ImageView) findViewById(R.id.overlaybtn_icon);
label = (TextView) findViewById(R.id.overlaybtn_label);
}
public void setEnabledLabelAndIcon(String s, Drawable d) {
enabledLabel = s;
enabledIcon = d;
if (isEnabled) {
updateViews();
}
}
public void setDisabledLabelAndIcon(String s, Drawable d) {
disabledLabel = s;
disabledIcon = d;
if (!isEnabled) {
updateViews();
}
}
/**
* Assign the appropriate label and icon to the views, and update the onClickListener for this
* view to the correct one (based on current enabledness state).
*/
private void updateViews() {
label.setEnabled(isEnabled);
if (isEnabled) {
label.setText(enabledLabel);
icon.setImageDrawable(enabledIcon);
super.setOnClickListener(enabledOnClickListener);
} else {
label.setText(disabledLabel);
icon.setImageDrawable(disabledIcon);
super.setOnClickListener(getPopListener());
}
}
/**
* Helper method to lazily-initialise disabledOnClickListener to a listener that performs the
* "pop" animation on the icon.
* updateViews handles making this the actual onClickListener for this view.
*/
private OnClickListener getPopListener() {
if (disabledOnClickListener == null) {
disabledOnClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.overlay_pop);
icon.startAnimation(anim);
}
};
}
return disabledOnClickListener;
}
@Override
public void setOnClickListener(OnClickListener l) {
enabledOnClickListener = l;
if (isEnabled) {
updateViews();
}
}
/**
* Set the enabledness state of this view. We don't call super.setEnabled, as we want to remain
* clickable even in the disabled state (but with a different click listener).
*/
@Override
public void setEnabled(boolean enabled) {
if (enabled == isEnabled) {
return;
}
isEnabled = enabled;
updateViews();
}
}

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

@ -6,9 +6,12 @@
package org.mozilla.gecko.overlays.ui;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
@ -22,7 +25,9 @@ import android.widget.TextView;
import android.widget.Toast;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.OverlayActionService;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
@ -30,6 +35,8 @@ import org.mozilla.gecko.overlays.service.sharemethods.SendTab;
import org.mozilla.gecko.overlays.service.sharemethods.ShareMethod;
import org.mozilla.gecko.LocaleAware;
import org.mozilla.gecko.sync.setup.activities.WebURLFinder;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
/**
* A transparent activity that displays the share overlay.
@ -91,17 +98,18 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
getWindow().setWindowAnimations(0);
Intent intent = getIntent();
final Resources resources = getResources();
// The URL is usually hiding somewhere in the extra text. Extract it.
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
String pageUrl = new WebURLFinder(extraText).bestWebURL();
final String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
final String pageUrl = new WebURLFinder(extraText).bestWebURL();
if (TextUtils.isEmpty(pageUrl)) {
Log.e(LOGTAG, "Unable to process shared intent. No URL found!");
// Display toast notifying the user of failure (most likely a developer who screwed up
// trying to send a share intent).
Toast toast = Toast.makeText(this, getResources().getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
Toast toast = Toast.makeText(this, resources.getText(R.string.overlay_share_no_url), Toast.LENGTH_SHORT);
toast.show();
finish();
@ -110,8 +118,9 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
setContentView(R.layout.overlay_share_dialog);
LocalBroadcastManager.getInstance(this).registerReceiver(uiEventListener,
new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
new IntentFilter(OverlayConstants.SHARE_METHOD_UI_EVENT));
// Have the service start any initialisation work that's necessary for us to show the correct
// UI. The results of such work will come in via the BroadcastListener.
@ -143,16 +152,35 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
Animation anim = AnimationUtils.loadAnimation(this, R.anim.overlay_slide_up);
findViewById(R.id.sharedialog).startAnimation(anim);
// Add button event listeners.
// Configure buttons.
final OverlayDialogButton bookmarkBtn = (OverlayDialogButton) findViewById(R.id.overlay_share_bookmark_btn);
findViewById(R.id.overlay_share_bookmark_btn).setOnClickListener(new View.OnClickListener() {
final String bookmarkEnabledLabel = resources.getString(R.string.overlay_share_bookmark_btn_label);
final Drawable bookmarkEnabledIcon = resources.getDrawable(R.drawable.overlay_bookmark_icon);
bookmarkBtn.setEnabledLabelAndIcon(bookmarkEnabledLabel, bookmarkEnabledIcon);
final String bookmarkDisabledLabel = resources.getString(R.string.overlay_share_bookmark_btn_label_already);
final Drawable bookmarkDisabledIcon = resources.getDrawable(R.drawable.overlay_bookmarked_already_icon);
bookmarkBtn.setDisabledLabelAndIcon(bookmarkDisabledLabel, bookmarkDisabledIcon);
bookmarkBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addBookmark();
}
});
findViewById(R.id.overlay_share_reading_list_btn).setOnClickListener(new View.OnClickListener() {
final OverlayDialogButton readinglistBtn = (OverlayDialogButton) findViewById(R.id.overlay_share_reading_list_btn);
final String readingListEnabledLabel = resources.getString(R.string.overlay_share_reading_list_btn_label);
final Drawable readingListEnabledIcon = resources.getDrawable(R.drawable.overlay_readinglist_icon);
readinglistBtn.setEnabledLabelAndIcon(readingListEnabledLabel, readingListEnabledIcon);
final String readingListDisabledLabel = resources.getString(R.string.overlay_share_reading_list_btn_label_already);
final Drawable readingListDisabledIcon = resources.getDrawable(R.drawable.overlay_readinglist_already_icon);
readinglistBtn.setDisabledLabelAndIcon(readingListDisabledLabel, readingListDisabledIcon);
readinglistBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
addToReadingList();
@ -168,6 +196,42 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
sendTabList.setSendTabTargetSelectedListener(this);
}
@Override
protected void onResume() {
super.onResume();
LocalBrowserDB browserDB = new LocalBrowserDB(getCurrentProfile());
disableButtonsIfAlreadyAdded(url, browserDB);
}
/**
* Disables the bookmark/reading list buttons if the given URL is already in the corresponding
* list.
*/
private void disableButtonsIfAlreadyAdded(final String pageURL, final LocalBrowserDB browserDB) {
new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
// Flags to hold the result
boolean isBookmark;
boolean isReadingListItem;
@Override
protected Void doInBackground() {
final ContentResolver contentResolver = getApplicationContext().getContentResolver();
isBookmark = browserDB.isBookmark(contentResolver, pageURL);
isReadingListItem = browserDB.isReadingListItem(contentResolver, pageURL);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark);
findViewById(R.id.overlay_share_reading_list_btn).setEnabled(!isReadingListItem);
}
}.execute();
}
/**
* Helper method to get an overlay service intent populated with the data held in this dialog.
*/
@ -236,6 +300,10 @@ public class ShareDialog extends LocaleAware.LocaleAwareActivity implements Send
slideOut();
}
private String getCurrentProfile() {
return GeckoProfile.DEFAULT_PROFILE;
}
/**
* Slide the overlay down off the screen and destroy it.
*/

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

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" >
<scale
android:duration="300"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="2"
android:toYScale="2" >
</scale>
<scale
android:duration="300"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.5"
android:toYScale="0.5" >
</scale>
</set>

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

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

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

До

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

После

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

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

После

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

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

После

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

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