/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const {Cc: Cc, Ci: Ci, Cr: Cr, Cu: Cu} = SpecialPowers; var RIL = SpecialPowers.wrap(SpecialPowers.createBlankObject()); SpecialPowers.Cu.import("resource://gre/modules/ril_consts.js", RIL); // Emulate Promise.jsm semantics. Promise.defer = function() { return new Deferred(); } function Deferred() { this.promise = new Promise(function(resolve, reject) { this.resolve = resolve; this.reject = reject; }.bind(this)); Object.freeze(this); } const MWI_PDU_PREFIX = "0000"; const MWI_PDU_UDH_PREFIX = "0040"; const MWI_PID_DEFAULT = "00"; const MWI_DCS_DISCARD_INACTIVE = "C0"; const MWI_DCS_DISCARD_ACTIVE = "C8"; const MWI_TIMESTAMP = "00000000000000"; // Only bring in what we need from ril_worker/RadioInterfaceLayer here. Reusing // that code turns out to be a nightmare, so there is some code duplication. var PDUBuilder = { toHexString: function(n, length) { let str = n.toString(16); if (str.length < length) { for (let i = 0; i < length - str.length; i++) { str = "0" + str; } } return str.toUpperCase(); }, writeUint16: function(value) { this.buf += (value & 0xff).toString(16).toUpperCase(); this.buf += ((value >> 8) & 0xff).toString(16).toUpperCase(); }, writeHexOctet: function(octet) { this.buf += this.toHexString(octet, 2); }, writeSwappedNibbleBCD: function(data) { data = data.toString(); let zeroCharCode = '0'.charCodeAt(0); for (let i = 0; i < data.length; i += 2) { let low = data.charCodeAt(i) - zeroCharCode; let high; if (i + 1 < data.length) { high = data.charCodeAt(i + 1) - zeroCharCode; } else { high = 0xF; } this.writeHexOctet((high << 4) | low); } }, writeStringAsSeptets: function(message, paddingBits, langIndex, langShiftIndex) { const langTable = RIL.PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; const langShiftTable = RIL.PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; let dataBits = paddingBits; let data = 0; for (let i = 0; i < message.length; i++) { let septet = langTable.indexOf(message[i]); if (septet == RIL.PDU_NL_EXTENDED_ESCAPE) { continue; } if (septet >= 0) { data |= septet << dataBits; dataBits += 7; } else { septet = langShiftTable.indexOf(message[i]); if (septet == -1) { throw new Error(message[i] + " not in 7 bit alphabet " + langIndex + ":" + langShiftIndex + "!"); } if (septet == RIL.PDU_NL_RESERVED_CONTROL) { continue; } data |= RIL.PDU_NL_EXTENDED_ESCAPE << dataBits; dataBits += 7; data |= septet << dataBits; dataBits += 7; } for (; dataBits >= 8; dataBits -= 8) { this.writeHexOctet(data & 0xFF); data >>>= 8; } } if (dataBits != 0) { this.writeHexOctet(data & 0xFF); } }, buildAddress: function(address) { let addressFormat = RIL.PDU_TOA_ISDN; // 81 if (address[0] == '+') { addressFormat = RIL.PDU_TOA_INTERNATIONAL | RIL.PDU_TOA_ISDN; // 91 address = address.substring(1); } this.buf = ""; this.writeHexOctet(address.length); this.writeHexOctet(addressFormat); this.writeSwappedNibbleBCD(address); return this.buf; }, // assumes 7 bit encoding buildUserData: function(options) { let headerLength = 0; this.buf = ""; if (options.headers) { for (let header of options.headers) { headerLength += 2; // id + length octets if (header.octets) { headerLength += header.octets.length; } }; } let encodedBodyLength = (options.body) ? options.body.length : 0; let headerOctets = (headerLength ? headerLength + 1 : 0); let paddingBits; let userDataLengthInSeptets; let headerSeptets = Math.ceil(headerOctets * 8 / 7); userDataLengthInSeptets = headerSeptets + encodedBodyLength; paddingBits = headerSeptets * 7 - headerOctets * 8; this.writeHexOctet(userDataLengthInSeptets); if (options.headers) { this.writeHexOctet(headerLength); for (let header of options.headers) { this.writeHexOctet(header.id); this.writeHexOctet(header.length); if (header.octets) { for (let octet of header.octets) { this.writeHexOctet(octet); } } } } if (encodedBodyLength > 0) { this.writeStringAsSeptets(options.body, paddingBits, RIL.PDU_NL_IDENTIFIER_DEFAULT, RIL.PDU_NL_IDENTIFIER_DEFAULT); } return this.buf; }, buildLevel2DiscardMwi: function(aActive, aSender, aBody) { return MWI_PDU_PREFIX + this.buildAddress(aSender) + MWI_PID_DEFAULT + (aActive ? MWI_DCS_DISCARD_ACTIVE : MWI_DCS_DISCARD_INACTIVE) + MWI_TIMESTAMP + this.buildUserData({ body: aBody }); }, buildLevel3DiscardMwi: function(aMessageCount, aSender, aBody) { let options = { headers: [{ id: RIL.PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION, length: 2, octets: [ RIL.PDU_MWI_STORE_TYPE_DISCARD, aMessageCount || 0 ] }], body: aBody }; return MWI_PDU_UDH_PREFIX + this.buildAddress(aSender) + MWI_PID_DEFAULT + MWI_DCS_DISCARD_ACTIVE + MWI_TIMESTAMP + this.buildUserData(options); } }; var pendingEmulatorCmdCount = 0; /** * Send emulator command with safe guard. * * We should only call |finish()| after all emulator command transactions * end, so here comes with the pending counter. Resolve when the emulator * gives positive response, and reject otherwise. * * Fulfill params: * result -- an array of emulator response lines. * * Reject params: * result -- an array of emulator response lines. * * @return A deferred promise. */ function runEmulatorCmdSafe(aCommand) { let deferred = Promise.defer(); ++pendingEmulatorCmdCount; runEmulatorCmd(aCommand, function(aResult) { --pendingEmulatorCmdCount; ok(true, "Emulator response: " + JSON.stringify(aResult)); if (Array.isArray(aResult) && aResult[0] === "OK") { deferred.resolve(aResult); } else { deferred.reject(aResult); } }); return deferred.promise; } /** * Promise wrapper for |SpecialPowers.pushPermissions|. * * Fulfill params: a MozVoicemail object. * Reject params: (none) * * @param aPermissions * A permission operation description array. See * |SpecialPowers.pushPermissions| for more details. * * @return A deferred promise. */ function pushPermissions(aPermissions) { let deferred = Promise.defer(); SpecialPowers.pushPermissions(aPermissions, function() { ok(true, "permissions pushed: " + JSON.stringify(aPermissions)); deferred.resolve(); }); return deferred.promise; } var voicemail; /** * Add required permissions and test if |navigator.mozVoicemail| exists. * * Fulfill params: a MozVoicemail object. * Reject params: (none) * * @return A deferred promise. */ function ensureVoicemail() { let permissions = [{ "type": "voicemail", "allow": 1, "context": document, }]; return pushPermissions(permissions) .then(function() { voicemail = window.navigator.mozVoicemail; if (voicemail == null) { throw "navigator.mozVoicemail is undefined."; } if (!(voicemail instanceof MozVoicemail)) { throw "navigator.mozVoicemail is instance of " + voicemail.constructor; } return voicemail; }); } /** * Wait for one named voicemail event. * * Resolve if that named event occurs. Never reject. * * Fulfill params: the DOMEvent passed. * * @param aEventName * A string event name. * @param aMatchFunc [optional] * An additional callback function to match the interested event * before removing the listener and going to resolve the promise. * * @return A deferred promise. */ function waitForManagerEvent(aEventName, aMatchFunc) { let deferred = Promise.defer(); voicemail.addEventListener(aEventName, function onevent(aEvent) { if (aMatchFunc && !aMatchFunc(aEvent)) { ok(true, "MozVoicemail event '" + aEventName + "' got" + " but is not interested."); return; } ok(true, "MozVoicemail event '" + aEventName + "' got."); voicemail.removeEventListener(aEventName, onevent); deferred.resolve(aEvent); }); return deferred.promise; } /** * Send raw voicemail indicator PDU. Resolve if the indicator PDU was sent with * success. * * Fulfill params: (none) * Reject params: (none) * * @return A deferred promise. */ function sendIndicatorPDU(aPDU) { return runEmulatorCmdSafe("sms pdu " + aPDU); } /** * Send raw voicemail indicator PDU and wait for "statuschanged" event. Resolve * if the indicator was sent and a "statuschanged" event was dispatched to * |navigator.mozVoicemail|. * * Fulfill params: (none) * Reject params: (none) * * @return A deferred promise. */ function sendIndicatorPDUAndWait(aPDU) { let promises = []; promises.push(waitForManagerEvent("statuschanged")); promises.push(sendIndicatorPDU(aPDU)); return Promise.all(promises); } /** * Check equalities of all attributes of two VoicemailStatus instances. */ function compareVoicemailStatus(aStatus1, aStatus2) { is(aStatus1.serviceId, aStatus2.serviceId, "VoicemailStatus::serviceId"); is(aStatus1.hasMessages, aStatus2.hasMessages, "VoicemailStatus::hasMessages"); is(aStatus1.messageCount, aStatus2.messageCount, "VoicemailStatus::messageCount"); is(aStatus1.returnNumber, aStatus2.returnNumber, "VoicemailStatus::returnNumber"); is(aStatus1.returnMessage, aStatus2.returnMessage, "VoicemailStatus::returnMessage"); } /** * Check if attributs of a VoicemailStatus match our expectations. */ function checkVoicemailStatus(aStatus, aServiceId, aHasMessages, aMessageCount, aReturnNumber, aReturnMessage) { compareVoicemailStatus(aStatus, { serviceId: aServiceId, hasMessages: aHasMessages, messageCount: aMessageCount, returnNumber: aReturnNumber, returnMessage: aReturnMessage }); } /** * Wait for pending emulator transactions and call |finish()|. */ function cleanUp() { ok(true, ":: CLEANING UP ::"); waitFor(finish, function() { return pendingEmulatorCmdCount === 0; }); } /** * Basic test routine helper for voicemail tests. * * This helper does nothing but clean-ups. * * @param aTestCaseMain * A function that takes no parameter. */ function startTestBase(aTestCaseMain) { Promise.resolve() .then(aTestCaseMain) .then(cleanUp, function() { ok(false, 'promise rejects during test.'); cleanUp(); }); } /** * Common test routine helper for voicemail tests. * * This function ensures global |voicemail| variable is available during the * process and performs clean-ups as well. * * @param aTestCaseMain * A function that takes no parameter. */ function startTestCommon(aTestCaseMain) { startTestBase(function() { return ensureVoicemail() .then(aTestCaseMain); }); }