michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: let Promise = SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise; michael@0: let telephony; michael@0: let conference; michael@0: michael@0: /** michael@0: * Emulator helper. michael@0: */ michael@0: let emulator = (function() { michael@0: let pendingCmdCount = 0; michael@0: let originalRunEmulatorCmd = runEmulatorCmd; michael@0: michael@0: // Overwritten it so people could not call this function directly. michael@0: runEmulatorCmd = function() { michael@0: throw "Use emulator.run(cmd, callback) instead of runEmulatorCmd"; michael@0: }; michael@0: michael@0: function run(cmd, callback) { michael@0: pendingCmdCount++; michael@0: originalRunEmulatorCmd(cmd, function(result) { michael@0: pendingCmdCount--; michael@0: if (callback && typeof callback === "function") { michael@0: callback(result); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * @return Promise michael@0: */ michael@0: function waitFinish() { michael@0: let deferred = Promise.defer(); michael@0: michael@0: waitFor(function() { michael@0: deferred.resolve(); michael@0: }, function() { michael@0: return pendingCmdCount === 0; michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: return { michael@0: run: run, michael@0: waitFinish: waitFinish michael@0: }; michael@0: }()); michael@0: michael@0: // Delay 1s before each telephony.dial() michael@0: // The workaround here should be removed after bug 1005816. michael@0: michael@0: let originalDial; michael@0: michael@0: function delayTelephonyDial() { michael@0: originalDial = telephony.dial; michael@0: telephony.dial = function(number, serviceId) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let startTime = Date.now(); michael@0: waitFor(function() { michael@0: originalDial.call(telephony, number, serviceId).then(call => { michael@0: deferred.resolve(call); michael@0: }, cause => { michael@0: deferred.reject(cause); michael@0: }); michael@0: }, function() { michael@0: duration = Date.now() - startTime; michael@0: return (duration >= 1000); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }; michael@0: } michael@0: michael@0: function restoreTelephonyDial() { michael@0: telephony.dial = originalDial; michael@0: } michael@0: michael@0: /** michael@0: * Telephony related helper functions. michael@0: */ michael@0: (function() { michael@0: /** michael@0: * @return Promise michael@0: */ michael@0: function clearCalls() { michael@0: let deferred = Promise.defer(); michael@0: michael@0: log("Clear existing calls."); michael@0: emulator.run("gsm clear", function(result) { michael@0: if (result[0] == "OK") { michael@0: waitFor(function() { michael@0: deferred.resolve(); michael@0: }, function() { michael@0: return telephony.calls.length === 0; michael@0: }); michael@0: } else { michael@0: log("Failed to clear existing calls."); michael@0: deferred.reject(); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Provide a string with format of the emulator call list result. michael@0: * michael@0: * @param prefix michael@0: * Possible values are "outbound" and "inbound". michael@0: * @param number michael@0: * Call number. michael@0: * @return A string with format of the emulator call list result. michael@0: */ michael@0: function callStrPool(prefix, number) { michael@0: let padding = " : "; michael@0: let numberInfo = prefix + number + padding.substr(number.length); michael@0: michael@0: let info = {}; michael@0: let states = ['ringing', 'incoming', 'active', 'held']; michael@0: for (let state of states) { michael@0: info[state] = numberInfo + state; michael@0: } michael@0: michael@0: return info; michael@0: } michael@0: michael@0: /** michael@0: * Provide a corresponding string of an outgoing call. The string is with michael@0: * format of the emulator call list result. michael@0: * michael@0: * @param number michael@0: * Number of an outgoing call. michael@0: * @return A string with format of the emulator call list result. michael@0: */ michael@0: function outCallStrPool(number) { michael@0: return callStrPool("outbound to ", number); michael@0: } michael@0: michael@0: /** michael@0: * Provide a corresponding string of an incoming call. The string is with michael@0: * format of the emulator call list result. michael@0: * michael@0: * @param number michael@0: * Number of an incoming call. michael@0: * @return A string with format of the emulator call list result. michael@0: */ michael@0: function inCallStrPool(number) { michael@0: return callStrPool("inbound from ", number); michael@0: } michael@0: michael@0: /** michael@0: * Check utility functions. michael@0: */ michael@0: michael@0: function checkInitialState() { michael@0: log("Verify initial state."); michael@0: ok(telephony.calls, 'telephony.call'); michael@0: checkTelephonyActiveAndCalls(null, []); michael@0: ok(conference.calls, 'conference.calls'); michael@0: checkConferenceStateAndCalls('', []); michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to compare a TelephonyCall and a received call event. michael@0: */ michael@0: function checkEventCallState(event, call, state) { michael@0: is(call, event.call, "event.call"); michael@0: is(call.state, state, "call state"); michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to check mozTelephony.active and mozTelephony.calls. michael@0: */ michael@0: function checkTelephonyActiveAndCalls(active, calls) { michael@0: is(telephony.active, active, "telephony.active"); michael@0: is(telephony.calls.length, calls.length, "telephony.calls"); michael@0: for (let i = 0; i < calls.length; ++i) { michael@0: is(telephony.calls[i], calls[i]); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to check mozTelephony.conferenceGroup.state and michael@0: * .conferenceGroup.calls. michael@0: */ michael@0: function checkConferenceStateAndCalls(state, calls) { michael@0: is(conference.state, state, "conference.state"); michael@0: is(conference.calls.length, calls.length, "conference.calls"); michael@0: for (let i = 0; i < calls.length; i++) { michael@0: is(conference.calls[i], calls[i]); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to handle *.oncallschanged event. michael@0: * michael@0: * @param container michael@0: * Representation of "mozTelephony" or "mozTelephony.conferenceGroup." michael@0: * @param containerName michael@0: * Name of container. Could be an arbitrary string, used for debug michael@0: * messages only. michael@0: * @param expectedCalls michael@0: * An array of calls. michael@0: * @param callback michael@0: * A callback function. michael@0: */ michael@0: function check_oncallschanged(container, containerName, expectedCalls, michael@0: callback) { michael@0: container.oncallschanged = function(event) { michael@0: log("Received 'callschanged' event for the " + containerName); michael@0: if (event.call) { michael@0: let index = expectedCalls.indexOf(event.call); michael@0: ok(index != -1); michael@0: expectedCalls.splice(index, 1); michael@0: michael@0: if (expectedCalls.length === 0) { michael@0: container.oncallschanged = null; michael@0: callback(); michael@0: } michael@0: } michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to handle *.ongroupchange event. michael@0: * michael@0: * @param call michael@0: * A TelephonyCall object. michael@0: * @param callName michael@0: * Name of a call. Could be an arbitrary string, used for debug messages michael@0: * only. michael@0: * @param group michael@0: * Representation of mozTelephony.conferenceGroup. michael@0: * @param callback michael@0: * A callback function. michael@0: */ michael@0: function check_ongroupchange(call, callName, group, callback) { michael@0: call.ongroupchange = function(event) { michael@0: log("Received 'groupchange' event for the " + callName); michael@0: call.ongroupchange = null; michael@0: michael@0: is(call.group, group); michael@0: callback(); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to handle *.onstatechange event. michael@0: * michael@0: * @param container michael@0: * Representation of a TelephonyCall or mozTelephony.conferenceGroup. michael@0: * @param containerName michael@0: * Name of container. Could be an arbitrary string, used for debug messages michael@0: * only. michael@0: * @param state michael@0: * A string. michael@0: * @param callback michael@0: * A callback function. michael@0: */ michael@0: function check_onstatechange(container, containerName, state, callback) { michael@0: container.onstatechange = function(event) { michael@0: log("Received 'statechange' event for the " + containerName); michael@0: container.onstatechange = null; michael@0: michael@0: is(container.state, state); michael@0: callback(); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to check the sequence of call state and event handlers. michael@0: * michael@0: * @param state michael@0: * A string of the expected call state. michael@0: * @param previousEvent michael@0: * A string of the event that should come before the expected state. michael@0: */ michael@0: function StateEventChecker(state, previousEvent) { michael@0: let event = 'on' + state; michael@0: michael@0: return function(call, callName, callback) { michael@0: call[event] = function() { michael@0: log("Received '" + state + "' event for the " + callName); michael@0: call[event] = null; michael@0: michael@0: if (previousEvent) { michael@0: // We always clear the event handler when the event is received. michael@0: // Therefore, if the corresponding handler is not existed, the expected michael@0: // previous event has been already received. michael@0: ok(!call[previousEvent]); michael@0: } michael@0: is(call.state, state); michael@0: callback(); michael@0: }; michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Convenient helper to check the call list existing in the emulator. michael@0: * michael@0: * @param expectedCallList michael@0: * An array of call info with the format of "callStrPool()[state]". michael@0: * @return A deferred promise. michael@0: */ michael@0: function checkEmulatorCallList(expectedCallList) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: emulator.run("gsm list", function(result) { michael@0: log("Call list is now: " + result); michael@0: for (let i = 0; i < expectedCallList.length; ++i) { michael@0: is(result[i], expectedCallList[i], "emulator calllist"); michael@0: } michael@0: is(result[expectedCallList.length], "OK", "emulator calllist"); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Super convenient helper to check calls and state of mozTelephony and michael@0: * mozTelephony.conferenceGroup. michael@0: * michael@0: * @param active michael@0: * A TelephonyCall object. Should be the expected active call. michael@0: * @param calls michael@0: * An array of TelephonyCall objects. Should be the expected list of michael@0: * mozTelephony.calls. michael@0: * @param conferenceState michael@0: * A string. Should be the expected conference state. michael@0: * @param conferenceCalls michael@0: * An array of TelephonyCall objects. Should be the expected list of michael@0: * mozTelephony.conferenceGroup.calls. michael@0: */ michael@0: function checkState(active, calls, conferenceState, conferenceCalls) { michael@0: checkTelephonyActiveAndCalls(active, calls); michael@0: checkConferenceStateAndCalls(conferenceState, conferenceCalls); michael@0: } michael@0: michael@0: /** michael@0: * Super convenient helper to check calls and state of mozTelephony and michael@0: * mozTelephony.conferenceGroup as well as the calls existing in the emulator. michael@0: * michael@0: * @param active michael@0: * A TelephonyCall object. Should be the expected active call. michael@0: * @param calls michael@0: * An array of TelephonyCall objects. Should be the expected list of michael@0: * mozTelephony.calls. michael@0: * @param conferenceState michael@0: * A string. Should be the expected conference state. michael@0: * @param conferenceCalls michael@0: * An array of TelephonyCall objects. Should be the expected list of michael@0: * mozTelephony.conferenceGroup.calls. michael@0: * @param callList michael@0: * An array of call info with the format of "callStrPool()[state]". michael@0: * @return A deferred promise. michael@0: */ michael@0: function checkAll(active, calls, conferenceState, conferenceCalls, callList) { michael@0: checkState(active, calls, conferenceState, conferenceCalls); michael@0: return checkEmulatorCallList(callList); michael@0: } michael@0: michael@0: /** michael@0: * Request utility functions. michael@0: */ michael@0: michael@0: /** michael@0: * Make sure there's no pending event before we jump to the next action. michael@0: * michael@0: * @param received michael@0: * A string of the received event. michael@0: * @param pending michael@0: * An array of the pending events. michael@0: * @param nextAction michael@0: * A callback function that is called when there's no pending event. michael@0: */ michael@0: function receivedPending(received, pending, nextAction) { michael@0: let index = pending.indexOf(received); michael@0: if (index != -1) { michael@0: pending.splice(index, 1); michael@0: } michael@0: if (pending.length === 0) { michael@0: nextAction(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Make an outgoing call. michael@0: * michael@0: * @param number michael@0: * A string. michael@0: * @param serviceId [optional] michael@0: * Identification of a service. 0 is set as default. michael@0: * @return A deferred promise. michael@0: */ michael@0: function dial(number, serviceId) { michael@0: serviceId = typeof serviceId !== "undefined" ? serviceId : 0; michael@0: log("Make an outgoing call: " + number + ", serviceId: " + serviceId); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: telephony.dial(number, serviceId).then(call => { michael@0: ok(call); michael@0: is(call.number, number); michael@0: is(call.state, "dialing"); michael@0: is(call.serviceId, serviceId); michael@0: michael@0: call.onalerting = function onalerting(event) { michael@0: call.onalerting = null; michael@0: log("Received 'onalerting' call event."); michael@0: checkEventCallState(event, call, "alerting"); michael@0: deferred.resolve(call); michael@0: }; michael@0: }, cause => { michael@0: deferred.reject(cause); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Answer an incoming call. michael@0: * michael@0: * @param call michael@0: * An incoming TelephonyCall object. michael@0: * @param conferenceStateChangeCallback [optional] michael@0: * A callback function which is called if answering an incoming call michael@0: * triggers conference state change. michael@0: * @return A deferred promise. michael@0: */ michael@0: function answer(call, conferenceStateChangeCallback) { michael@0: log("Answering the incoming call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(call); michael@0: }; michael@0: michael@0: let pending = ["call.onconnected"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: // When there's already a connected conference call, answering a new incoming michael@0: // call triggers conference state change. We should wait for michael@0: // |conference.onstatechange| before checking the state of the conference call. michael@0: if (conference.state === "connected") { michael@0: pending.push("conference.onstatechange"); michael@0: check_onstatechange(conference, "conference", "held", function() { michael@0: if (typeof conferenceStateChangeCallback === "function") { michael@0: conferenceStateChangeCallback(); michael@0: } michael@0: receive("conference.onstatechange"); michael@0: }); michael@0: } michael@0: michael@0: call.onconnecting = function onconnectingIn(event) { michael@0: log("Received 'connecting' call event for incoming call."); michael@0: call.onconnecting = null; michael@0: checkEventCallState(event, call, "connecting"); michael@0: }; michael@0: michael@0: call.onconnected = function onconnectedIn(event) { michael@0: log("Received 'connected' call event for incoming call."); michael@0: call.onconnected = null; michael@0: checkEventCallState(event, call, "connected"); michael@0: ok(!call.onconnecting); michael@0: receive("call.onconnected"); michael@0: }; michael@0: call.answer(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Hold a call. michael@0: * michael@0: * @param call michael@0: * A TelephonyCall object. michael@0: * @return A deferred promise. michael@0: */ michael@0: function hold(call) { michael@0: log("Putting the call on hold."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: let gotHolding = false; michael@0: call.onholding = function onholding(event) { michael@0: log("Received 'holding' call event"); michael@0: call.onholding = null; michael@0: checkEventCallState(event, call, "holding"); michael@0: gotHolding = true; michael@0: }; michael@0: michael@0: call.onheld = function onheld(event) { michael@0: log("Received 'held' call event"); michael@0: call.onheld = null; michael@0: checkEventCallState(event, call, "held"); michael@0: ok(gotHolding); michael@0: deferred.resolve(call); michael@0: }; michael@0: call.hold(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Simulate an incoming call. michael@0: * michael@0: * @param number michael@0: * A string. michael@0: * @return A deferred promise. michael@0: */ michael@0: function remoteDial(number) { michael@0: log("Simulating an incoming call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: telephony.onincoming = function onincoming(event) { michael@0: log("Received 'incoming' call event."); michael@0: telephony.onincoming = null; michael@0: michael@0: let call = event.call; michael@0: michael@0: ok(call); michael@0: is(call.number, number); michael@0: is(call.state, "incoming"); michael@0: michael@0: deferred.resolve(call); michael@0: }; michael@0: emulator.run("gsm call " + number); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Remote party answers the call. michael@0: * michael@0: * @param call michael@0: * A TelephonyCall object. michael@0: * @return A deferred promise. michael@0: */ michael@0: function remoteAnswer(call) { michael@0: log("Remote answering the call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: call.onconnected = function onconnected(event) { michael@0: log("Received 'connected' call event."); michael@0: call.onconnected = null; michael@0: checkEventCallState(event, call, "connected"); michael@0: deferred.resolve(call); michael@0: }; michael@0: emulator.run("gsm accept " + call.number); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Remote party hangs up the call. michael@0: * michael@0: * @param call michael@0: * A TelephonyCall object. michael@0: * @return A deferred promise. michael@0: */ michael@0: function remoteHangUp(call) { michael@0: log("Remote hanging up the call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: call.ondisconnected = function ondisconnected(event) { michael@0: log("Received 'disconnected' call event."); michael@0: call.ondisconnected = null; michael@0: checkEventCallState(event, call, "disconnected"); michael@0: deferred.resolve(call); michael@0: }; michael@0: emulator.run("gsm cancel " + call.number); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Remote party hangs up all the calls. michael@0: * michael@0: * @param calls michael@0: * An array of TelephonyCall objects. michael@0: * @return A deferred promise. michael@0: */ michael@0: function remoteHangUpCalls(calls) { michael@0: let promise = Promise.resolve(); michael@0: michael@0: for (let call of calls) { michael@0: promise = promise.then(remoteHangUp.bind(null, call)); michael@0: } michael@0: michael@0: return promise; michael@0: } michael@0: michael@0: /** michael@0: * Add calls to conference. michael@0: * michael@0: * @param callsToAdd michael@0: * An array of TelephonyCall objects to be added into conference. The michael@0: * length of the array should be 1 or 2. michael@0: * @param connectedCallback [optional] michael@0: * A callback function which is called when conference state becomes michael@0: * connected. michael@0: * @return A deferred promise. michael@0: */ michael@0: function addCallsToConference(callsToAdd, connectedCallback) { michael@0: log("Add " + callsToAdd.length + " calls into conference."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: let pending = ["conference.oncallschanged", "conference.onconnected"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: let check_onconnected = StateEventChecker('connected', 'onresuming'); michael@0: michael@0: for (let call of callsToAdd) { michael@0: let callName = "callToAdd (" + call.number + ')'; michael@0: michael@0: let ongroupchange = callName + ".ongroupchange"; michael@0: pending.push(ongroupchange); michael@0: check_ongroupchange(call, callName, conference, michael@0: receive.bind(null, ongroupchange)); michael@0: michael@0: let onstatechange = callName + ".onstatechange"; michael@0: pending.push(onstatechange); michael@0: check_onstatechange(call, callName, 'connected', michael@0: receive.bind(null, onstatechange)); michael@0: } michael@0: michael@0: check_oncallschanged(conference, 'conference', callsToAdd, michael@0: receive.bind(null, "conference.oncallschanged")); michael@0: michael@0: check_onconnected(conference, "conference", function() { michael@0: ok(!conference.oncallschanged); michael@0: if (typeof connectedCallback === 'function') { michael@0: connectedCallback(); michael@0: } michael@0: receive("conference.onconnected"); michael@0: }); michael@0: michael@0: // Cannot use apply() through webidl, so just separate the cases to handle. michael@0: if (callsToAdd.length == 2) { michael@0: conference.add(callsToAdd[0], callsToAdd[1]); michael@0: } else { michael@0: conference.add(callsToAdd[0]); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Hold the conference. michael@0: * michael@0: * @param calls michael@0: * An array of TelephonyCall objects existing in conference. michael@0: * @param heldCallback [optional] michael@0: * A callback function which is called when conference state becomes michael@0: * held. michael@0: * @return A deferred promise. michael@0: */ michael@0: function holdConference(calls, heldCallback) { michael@0: log("Holding the conference call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: let pending = ["conference.onholding", "conference.onheld"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: let check_onholding = StateEventChecker('holding', null); michael@0: let check_onheld = StateEventChecker('held', 'onholding'); michael@0: michael@0: for (let call of calls) { michael@0: let callName = "call (" + call.number + ')'; michael@0: michael@0: let onholding = callName + ".onholding"; michael@0: pending.push(onholding); michael@0: check_onholding(call, callName, receive.bind(null, onholding)); michael@0: michael@0: let onheld = callName + ".onheld"; michael@0: pending.push(onheld); michael@0: check_onheld(call, callName, receive.bind(null, onheld)); michael@0: } michael@0: michael@0: check_onholding(conference, "conference", michael@0: receive.bind(null, "conference.onholding")); michael@0: michael@0: check_onheld(conference, "conference", function() { michael@0: if (typeof heldCallback === 'function') { michael@0: heldCallback(); michael@0: } michael@0: receive("conference.onheld"); michael@0: }); michael@0: michael@0: conference.hold(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Resume the conference. michael@0: * michael@0: * @param calls michael@0: * An array of TelephonyCall objects existing in conference. michael@0: * @param connectedCallback [optional] michael@0: * A callback function which is called when conference state becomes michael@0: * connected. michael@0: * @return A deferred promise. michael@0: */ michael@0: function resumeConference(calls, connectedCallback) { michael@0: log("Resuming the held conference call."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: let pending = ["conference.onresuming", "conference.onconnected"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: let check_onresuming = StateEventChecker('resuming', null); michael@0: let check_onconnected = StateEventChecker('connected', 'onresuming'); michael@0: michael@0: for (let call of calls) { michael@0: let callName = "call (" + call.number + ')'; michael@0: michael@0: let onresuming = callName + ".onresuming"; michael@0: pending.push(onresuming); michael@0: check_onresuming(call, callName, receive.bind(null, onresuming)); michael@0: michael@0: let onconnected = callName + ".onconnected"; michael@0: pending.push(onconnected); michael@0: check_onconnected(call, callName, receive.bind(null, onconnected)); michael@0: } michael@0: michael@0: check_onresuming(conference, "conference", michael@0: receive.bind(null, "conference.onresuming")); michael@0: michael@0: check_onconnected(conference, "conference", function() { michael@0: if (typeof connectedCallback === 'function') { michael@0: connectedCallback(); michael@0: } michael@0: receive("conference.onconnected"); michael@0: }); michael@0: michael@0: conference.resume(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Remove a call out of conference. michael@0: * michael@0: * @param callToRemove michael@0: * A TelephonyCall object existing in conference. michael@0: * @param autoRemovedCalls michael@0: * An array of TelephonyCall objects which is going to be automatically michael@0: * removed. The length of the array should be 0 or 1. michael@0: * @param remainedCalls michael@0: * An array of TelephonyCall objects which remain in conference. michael@0: * @param stateChangeCallback [optional] michael@0: * A callback function which is called when conference state changes. michael@0: * @return A deferred promise. michael@0: */ michael@0: function removeCallInConference(callToRemove, autoRemovedCalls, remainedCalls, michael@0: statechangeCallback) { michael@0: log("Removing a participant from the conference call."); michael@0: michael@0: is(conference.state, 'connected'); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: let pending = ["callToRemove.ongroupchange", "telephony.oncallschanged", michael@0: "conference.oncallschanged", "conference.onstatechange"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: // Remained call in conference will be held. michael@0: for (let call of remainedCalls) { michael@0: let callName = "remainedCall (" + call.number + ')'; michael@0: michael@0: let onstatechange = callName + ".onstatechange"; michael@0: pending.push(onstatechange); michael@0: check_onstatechange(call, callName, 'held', michael@0: receive.bind(null, onstatechange)); michael@0: } michael@0: michael@0: // When a call is removed from conference with 2 calls, another one will be michael@0: // automatically removed from group and be put on hold. michael@0: for (let call of autoRemovedCalls) { michael@0: let callName = "autoRemovedCall (" + call.number + ')'; michael@0: michael@0: let ongroupchange = callName + ".ongroupchange"; michael@0: pending.push(ongroupchange); michael@0: check_ongroupchange(call, callName, null, michael@0: receive.bind(null, ongroupchange)); michael@0: michael@0: let onstatechange = callName + ".onstatechange"; michael@0: pending.push(onstatechange); michael@0: check_onstatechange(call, callName, 'held', michael@0: receive.bind(null, onstatechange)); michael@0: } michael@0: michael@0: check_ongroupchange(callToRemove, "callToRemove", null, function() { michael@0: is(callToRemove.state, 'connected'); michael@0: receive("callToRemove.ongroupchange"); michael@0: }); michael@0: michael@0: check_oncallschanged(telephony, 'telephony', michael@0: autoRemovedCalls.concat(callToRemove), michael@0: receive.bind(null, "telephony.oncallschanged")); michael@0: michael@0: check_oncallschanged(conference, 'conference', michael@0: autoRemovedCalls.concat(callToRemove), function() { michael@0: is(conference.calls.length, remainedCalls.length); michael@0: receive("conference.oncallschanged"); michael@0: }); michael@0: michael@0: check_onstatechange(conference, 'conference', michael@0: (remainedCalls.length ? 'held' : ''), function() { michael@0: ok(!conference.oncallschanged); michael@0: if (typeof statechangeCallback === 'function') { michael@0: statechangeCallback(); michael@0: } michael@0: receive("conference.onstatechange"); michael@0: }); michael@0: michael@0: conference.remove(callToRemove); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Hangup a call in conference. michael@0: * michael@0: * @param callToHangUp michael@0: * A TelephonyCall object existing in conference. michael@0: * @param autoRemovedCalls michael@0: * An array of TelephonyCall objects which is going to be automatically michael@0: * removed. The length of the array should be 0 or 1. michael@0: * @param remainedCalls michael@0: * An array of TelephonyCall objects which remain in conference. michael@0: * @param stateChangeCallback [optional] michael@0: * A callback function which is called when conference state changes. michael@0: * @return A deferred promise. michael@0: */ michael@0: function hangUpCallInConference(callToHangUp, autoRemovedCalls, remainedCalls, michael@0: statechangeCallback) { michael@0: log("Release one call in conference."); michael@0: michael@0: let deferred = Promise.defer(); michael@0: let done = function() { michael@0: deferred.resolve(); michael@0: }; michael@0: michael@0: let pending = ["conference.oncallschanged", "remoteHangUp"]; michael@0: let receive = function(name) { michael@0: receivedPending(name, pending, done); michael@0: }; michael@0: michael@0: // When a call is hang up from conference with 2 calls, another one will be michael@0: // automatically removed from group. michael@0: for (let call of autoRemovedCalls) { michael@0: let callName = "autoRemovedCall (" + call.number + ')'; michael@0: michael@0: let ongroupchange = callName + ".ongroupchange"; michael@0: pending.push(ongroupchange); michael@0: check_ongroupchange(call, callName, null, michael@0: receive.bind(null, ongroupchange)); michael@0: } michael@0: michael@0: if (autoRemovedCalls.length) { michael@0: pending.push("telephony.oncallschanged"); michael@0: check_oncallschanged(telephony, 'telephony', michael@0: autoRemovedCalls, michael@0: receive.bind(null, "telephony.oncallschanged")); michael@0: } michael@0: michael@0: check_oncallschanged(conference, 'conference', michael@0: autoRemovedCalls.concat(callToHangUp), function() { michael@0: is(conference.calls.length, remainedCalls.length); michael@0: receive("conference.oncallschanged"); michael@0: }); michael@0: michael@0: if (remainedCalls.length === 0) { michael@0: pending.push("conference.onstatechange"); michael@0: check_onstatechange(conference, 'conference', '', function() { michael@0: ok(!conference.oncallschanged); michael@0: if (typeof statechangeCallback === 'function') { michael@0: statechangeCallback(); michael@0: } michael@0: receive("conference.onstatechange"); michael@0: }); michael@0: } michael@0: michael@0: remoteHangUp(callToHangUp) michael@0: .then(receive.bind(null, "remoteHangUp")); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Setup a conference with an outgoing call and an incoming call. michael@0: * michael@0: * @param outNumber michael@0: * Number of an outgoing call. michael@0: * @param inNumber michael@0: * Number of an incoming call. michael@0: * @return Promise<[outCall, inCall]> michael@0: */ michael@0: function setupConferenceTwoCalls(outNumber, inNumber) { michael@0: log('Create conference with two calls.'); michael@0: michael@0: let outCall; michael@0: let inCall; michael@0: let outInfo = outCallStrPool(outNumber); michael@0: let inInfo = inCallStrPool(inNumber); michael@0: michael@0: return Promise.resolve() michael@0: .then(checkInitialState) michael@0: .then(() => dial(outNumber)) michael@0: .then(call => { outCall = call; }) michael@0: .then(() => checkAll(outCall, [outCall], '', [], [outInfo.ringing])) michael@0: .then(() => remoteAnswer(outCall)) michael@0: .then(() => checkAll(outCall, [outCall], '', [], [outInfo.active])) michael@0: .then(() => remoteDial(inNumber)) michael@0: .then(call => { inCall = call; }) michael@0: .then(() => checkAll(outCall, [outCall, inCall], '', [], michael@0: [outInfo.active, inInfo.incoming])) michael@0: .then(() => answer(inCall)) michael@0: .then(() => checkAll(inCall, [outCall, inCall], '', [], michael@0: [outInfo.held, inInfo.active])) michael@0: .then(() => addCallsToConference([outCall, inCall], function() { michael@0: checkState(conference, [], 'connected', [outCall, inCall]); michael@0: })) michael@0: .then(() => checkAll(conference, [], 'connected', [outCall, inCall], michael@0: [outInfo.active, inInfo.active])) michael@0: .then(() => { michael@0: return [outCall, inCall]; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Setup a conference with an outgoing call and two incoming calls. michael@0: * michael@0: * @param outNumber michael@0: * Number of an outgoing call. michael@0: * @param inNumber michael@0: * Number of an incoming call. michael@0: * @param inNumber2 michael@0: * Number of an incoming call. michael@0: * @return Promise<[outCall, inCall, inCall2]> michael@0: */ michael@0: function setupConferenceThreeCalls(outNumber, inNumber, inNumber2) { michael@0: log('Create conference with three calls.'); michael@0: michael@0: let outCall; michael@0: let inCall; michael@0: let inCall2; michael@0: let outInfo = outCallStrPool(outNumber); michael@0: let inInfo = inCallStrPool(inNumber); michael@0: let inInfo2 = inCallStrPool(inNumber2); michael@0: michael@0: return Promise.resolve() michael@0: .then(() => setupConferenceTwoCalls(outNumber, inNumber)) michael@0: .then(calls => { michael@0: outCall = calls[0]; michael@0: inCall = calls[1]; michael@0: }) michael@0: .then(() => remoteDial(inNumber2)) michael@0: .then(call => { inCall2 = call; }) michael@0: .then(() => checkAll(conference, [inCall2], 'connected', [outCall, inCall], michael@0: [outInfo.active, inInfo.active, inInfo2.incoming])) michael@0: .then(() => answer(inCall2, function() { michael@0: checkState(inCall2, [inCall2], 'held', [outCall, inCall]); michael@0: })) michael@0: .then(() => checkAll(inCall2, [inCall2], 'held', [outCall, inCall], michael@0: [outInfo.held, inInfo.held, inInfo2.active])) michael@0: .then(() => addCallsToConference([inCall2], function() { michael@0: checkState(conference, [], 'connected', [outCall, inCall, inCall2]); michael@0: })) michael@0: .then(() => checkAll(conference, [], michael@0: 'connected', [outCall, inCall, inCall2], michael@0: [outInfo.active, inInfo.active, inInfo2.active])) michael@0: .then(() => { michael@0: return [outCall, inCall, inCall2]; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Setup a conference with an outgoing call and four incoming calls. michael@0: * michael@0: * @param outNumber michael@0: * Number of an outgoing call. michael@0: * @param inNumber michael@0: * Number of an incoming call. michael@0: * @param inNumber2 michael@0: * Number of an incoming call. michael@0: * @param inNumber3 michael@0: * Number of an incoming call. michael@0: * @param inNumber4 michael@0: * Number of an incoming call. michael@0: * @return Promise<[outCall, inCall, inCall2, inCall3, inCall4]> michael@0: */ michael@0: function setupConferenceFiveCalls(outNumber, inNumber, inNumber2, inNumber3, michael@0: inNumber4) { michael@0: log('Create conference with five calls.'); michael@0: michael@0: let outCall; michael@0: let inCall; michael@0: let inCall2; michael@0: let inCall3; michael@0: let inCall4; michael@0: let outInfo = outCallStrPool(outNumber); michael@0: let inInfo = inCallStrPool(inNumber); michael@0: let inInfo2 = inCallStrPool(inNumber2); michael@0: let inInfo3 = inCallStrPool(inNumber3); michael@0: let inInfo4 = inCallStrPool(inNumber4); michael@0: michael@0: return Promise.resolve() michael@0: .then(() => setupConferenceThreeCalls(outNumber, inNumber, inNumber2)) michael@0: .then(calls => { michael@0: [outCall, inCall, inCall2] = calls; michael@0: }) michael@0: .then(() => remoteDial(inNumber3)) michael@0: .then(call => {inCall3 = call;}) michael@0: .then(() => checkAll(conference, [inCall3], 'connected', michael@0: [outCall, inCall, inCall2], michael@0: [outInfo.active, inInfo.active, inInfo2.active, michael@0: inInfo3.incoming])) michael@0: .then(() => answer(inCall3, function() { michael@0: checkState(inCall3, [inCall3], 'held', [outCall, inCall, inCall2]); michael@0: })) michael@0: .then(() => checkAll(inCall3, [inCall3], 'held', michael@0: [outCall, inCall, inCall2], michael@0: [outInfo.held, inInfo.held, inInfo2.held, michael@0: inInfo3.active])) michael@0: .then(() => addCallsToConference([inCall3], function() { michael@0: checkState(conference, [], 'connected', [outCall, inCall, inCall2, inCall3]); michael@0: })) michael@0: .then(() => checkAll(conference, [], 'connected', michael@0: [outCall, inCall, inCall2, inCall3], michael@0: [outInfo.active, inInfo.active, inInfo2.active, michael@0: inInfo3.active])) michael@0: .then(() => remoteDial(inNumber4)) michael@0: .then(call => {inCall4 = call;}) michael@0: .then(() => checkAll(conference, [inCall4], 'connected', michael@0: [outCall, inCall, inCall2, inCall3], michael@0: [outInfo.active, inInfo.active, inInfo2.active, michael@0: inInfo3.active, inInfo4.incoming])) michael@0: .then(() => answer(inCall4, function() { michael@0: checkState(inCall4, [inCall4], 'held', [outCall, inCall, inCall2, inCall3]); michael@0: })) michael@0: .then(() => checkAll(inCall4, [inCall4], 'held', michael@0: [outCall, inCall, inCall2, inCall3], michael@0: [outInfo.held, inInfo.held, inInfo2.held, michael@0: inInfo3.held, inInfo4.active])) michael@0: .then(() => addCallsToConference([inCall4], function() { michael@0: checkState(conference, [], 'connected', [outCall, inCall, inCall2, michael@0: inCall3, inCall4]); michael@0: })) michael@0: .then(() => checkAll(conference, [], 'connected', michael@0: [outCall, inCall, inCall2, inCall3, inCall4], michael@0: [outInfo.active, inInfo.active, inInfo2.active, michael@0: inInfo3.active, inInfo4.active])) michael@0: .then(() => { michael@0: return [outCall, inCall, inCall2, inCall3, inCall4]; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Public members. michael@0: */ michael@0: michael@0: this.gCheckInitialState = checkInitialState; michael@0: this.gClearCalls = clearCalls; michael@0: this.gOutCallStrPool = outCallStrPool; michael@0: this.gInCallStrPool = inCallStrPool; michael@0: this.gCheckState = checkState; michael@0: this.gCheckAll = checkAll; michael@0: this.gDial = dial; michael@0: this.gAnswer = answer; michael@0: this.gHold = hold; michael@0: this.gRemoteDial = remoteDial; michael@0: this.gRemoteAnswer = remoteAnswer; michael@0: this.gRemoteHangUp = remoteHangUp; michael@0: this.gRemoteHangUpCalls = remoteHangUpCalls; michael@0: this.gAddCallsToConference = addCallsToConference; michael@0: this.gHoldConference = holdConference; michael@0: this.gResumeConference = resumeConference; michael@0: this.gRemoveCallInConference = removeCallInConference; michael@0: this.gHangUpCallInConference = hangUpCallInConference; michael@0: this.gSetupConferenceTwoCalls = setupConferenceTwoCalls; michael@0: this.gSetupConferenceThreeCalls = setupConferenceThreeCalls; michael@0: this.gSetupConferenceFiveCalls = setupConferenceFiveCalls; michael@0: this.gReceivedPending = receivedPending; michael@0: }()); michael@0: michael@0: function _startTest(permissions, test) { michael@0: function permissionSetUp() { michael@0: SpecialPowers.setBoolPref("dom.mozSettings.enabled", true); michael@0: for (let per of permissions) { michael@0: SpecialPowers.addPermission(per, true, document); michael@0: } michael@0: } michael@0: michael@0: function permissionTearDown() { michael@0: SpecialPowers.clearUserPref("dom.mozSettings.enabled"); michael@0: for (let per of permissions) { michael@0: SpecialPowers.removePermission(per, document); michael@0: } michael@0: } michael@0: michael@0: function setUp() { michael@0: log("== Test SetUp =="); michael@0: permissionSetUp(); michael@0: // Make sure that we get the telephony after adding permission. michael@0: telephony = window.navigator.mozTelephony; michael@0: ok(telephony); michael@0: delayTelephonyDial(); michael@0: conference = telephony.conferenceGroup; michael@0: ok(conference); michael@0: return gClearCalls().then(gCheckInitialState); michael@0: } michael@0: michael@0: // Extend finish() with tear down. michael@0: finish = (function() { michael@0: let originalFinish = finish; michael@0: michael@0: function tearDown() { michael@0: log("== Test TearDown =="); michael@0: restoreTelephonyDial(); michael@0: emulator.waitFinish() michael@0: .then(permissionTearDown) michael@0: .then(function() { michael@0: originalFinish.apply(this, arguments); michael@0: }); michael@0: } michael@0: michael@0: return tearDown.bind(this); michael@0: }()); michael@0: michael@0: function mainTest() { michael@0: setUp() michael@0: .then(function onSuccess() { michael@0: log("== Test Start =="); michael@0: test(); michael@0: }, function onError(error) { michael@0: SpecialPowers.Cu.reportError(error); michael@0: ok(false, "SetUp error"); michael@0: }); michael@0: } michael@0: michael@0: mainTest(); michael@0: } michael@0: michael@0: function startTest(test) { michael@0: _startTest(["telephony"], test); michael@0: } michael@0: michael@0: function startTestWithPermissions(permissions, test) { michael@0: _startTest(permissions.concat("telephony"), test); michael@0: } michael@0: michael@0: function startDSDSTest(test) { michael@0: let numRIL; michael@0: try { michael@0: numRIL = SpecialPowers.getIntPref("ril.numRadioInterfaces"); michael@0: } catch (ex) { michael@0: numRIL = 1; // Pref not set. michael@0: } michael@0: michael@0: if (numRIL > 1) { michael@0: startTest(test); michael@0: } else { michael@0: log("Not a DSDS environment. Test is skipped."); michael@0: ok(true); // We should run at least one test. michael@0: finish(); michael@0: } michael@0: }