/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const {Cc: Cc, Ci: Ci, Cr: Cr, Cu: Cu} = SpecialPowers; let Promise = Cu.import("resource://gre/modules/Promise.jsm").Promise; /** * Push a list of preference settings. Never reject. * * Fulfill params: (none) * * @param aPrefs * An JS object. For example: * * {'set': [['foo.bar', 2], ['magic.pref', 'baz']], * 'clear': [['clear.this'], ['also.this']] }; * * @return A deferred promise. */ function pushPrefEnv(aPrefs) { let deferred = Promise.defer(); SpecialPowers.pushPrefEnv(aPrefs, function() { ok(true, "preferences pushed: " + JSON.stringify(aPrefs)); deferred.resolve(); }); return deferred.promise; } /** * Push required permissions and test if |navigator.mozMobileMessage| exists. * Resolve if it does, reject otherwise. * * Fulfill params: * manager -- an reference to navigator.mozMobileMessage. * * Reject params: (none) * * @return A deferred promise. */ let manager; function ensureMobileMessage() { let deferred = Promise.defer(); let permissions = [{ "type": "sms", "allow": 1, "context": document, }]; SpecialPowers.pushPermissions(permissions, function() { ok(true, "permissions pushed: " + JSON.stringify(permissions)); manager = window.navigator.mozMobileMessage; if (manager) { log("navigator.mozMobileMessage is instance of " + manager.constructor); } else { log("navigator.mozMobileMessage is undefined."); } if (manager instanceof MozMobileMessageManager) { deferred.resolve(manager); } else { deferred.reject(); } }); return deferred.promise; } /** * Wait for one named MobileMessageManager 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(); manager.addEventListener(aEventName, function onevent(aEvent) { if (aMatchFunc && !aMatchFunc(aEvent)) { ok(true, "MobileMessageManager event '" + aEventName + "' got" + " but is not interested."); return; } ok(true, "MobileMessageManager event '" + aEventName + "' got."); manager.removeEventListener(aEventName, onevent); deferred.resolve(aEvent); }); return deferred.promise; } /** * Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject. * * Fulfill params: A DOMEvent. * Reject params: A DOMEvent. * * @param aRequest * A DOMRequest instance. * * @return A deferred promise. */ function wrapDomRequestAsPromise(aRequest) { let deferred = Promise.defer(); ok(aRequest instanceof DOMRequest, "aRequest is instanceof " + aRequest.constructor); aRequest.addEventListener("success", function(aEvent) { deferred.resolve(aEvent); }); aRequest.addEventListener("error", function(aEvent) { deferred.reject(aEvent); }); return deferred.promise; } /** * Send a SMS message to a single receiver. Resolve if it succeeds, reject * otherwise. * * Fulfill params: * message -- the sent SmsMessage. * * Reject params: * error -- a DOMError. * * @param aReceiver the address of the receiver. * @param aText the text body of the message. * * @return A deferred promise. */ function sendSmsWithSuccess(aReceiver, aText) { let request = manager.send(aReceiver, aText); return wrapDomRequestAsPromise(request) .then((aEvent) => { return aEvent.target.result; }, (aEvent) => { throw aEvent.target.error; }); } /** * Send a SMS message to a single receiver. * Resolve if it fails, reject otherwise. * * Fulfill params: * { * message, -- the failed MmsMessage * error, -- error of the send request * } * * Reject params: (none) * * @param aReceiver the address of the receiver. * @param aText the text body of the message. * * @return A deferred promise. */ function sendSmsWithFailure(aReceiver, aText) { let promises = []; promises.push(waitForManagerEvent("failed") .then((aEvent) => { return aEvent.message; })); let request = manager.send(aReceiver, aText); promises.push(wrapDomRequestAsPromise(request) .then((aEvent) => { throw aEvent; }, (aEvent) => { return aEvent.target.error; })); return Promise.all(promises) .then((aResults) => { return { message: aResults[0], error: aResults[1] }; }); } /** * Send a MMS message with specified parameters. Resolve if it fails, reject * otherwise. * * Fulfill params: * { * message, -- the failed MmsMessage * error, -- error of the send request * } * * Reject params: (none) * * @param aMmsParameters a MmsParameters instance. * * @param aSendParameters a MmsSendParameters instance. * * @return A deferred promise. */ function sendMmsWithFailure(aMmsParameters, aSendParameters) { let promises = []; promises.push(waitForManagerEvent("failed") .then((aEvent) => { return aEvent.message; })); let request = manager.sendMMS(aMmsParameters, aSendParameters); promises.push(wrapDomRequestAsPromise(request) .then((aEvent) => { throw aEvent; }, (aEvent) => { return aEvent.target.error; })); return Promise.all(promises) .then((aResults) => { return { message: aResults[0], error: aResults[1] }; }); } /** * Retrieve message by message id. * * Fulfill params: MozSmsMessage * Reject params: * event -- a DOMEvent * * @param aId * A numeric message id. * * @return A deferred promise. */ function getMessage(aId) { let request = manager.getMessage(aId); return wrapDomRequestAsPromise(request) .then((aEvent) => { return aEvent.target.result; }); } /** * Retrieve messages from database. * * Fulfill params: * messages -- an array of {Sms,Mms}Message instances. * * Reject params: * event -- a DOMEvent * * @param aFilter an optional MozSmsFilter instance. * @param aReverse a boolean value indicating whether the order of the messages * should be reversed. * * @return A deferred promise. */ function getMessages(aFilter, aReverse) { let deferred = Promise.defer(); if (!aFilter) { aFilter = new MozSmsFilter; } let messages = []; let cursor = manager.getMessages(aFilter, aReverse || false); cursor.onsuccess = function(aEvent) { if (cursor.result) { messages.push(cursor.result); cursor.continue(); return; } deferred.resolve(messages); }; cursor.onerror = deferred.reject.bind(deferred); return deferred.promise; } /** * Retrieve all messages from database. * * Fulfill params: * messages -- an array of {Sms,Mms}Message instances. * * Reject params: * event -- a DOMEvent * * @return A deferred promise. */ function getAllMessages() { return getMessages(null, false); } /** * Retrieve all threads from database. * * Fulfill params: * threads -- an array of MozMobileMessageThread instances. * * Reject params: * event -- a DOMEvent * * @return A deferred promise. */ function getAllThreads() { let deferred = Promise.defer(); let threads = []; let cursor = manager.getThreads(); cursor.onsuccess = function(aEvent) { if (cursor.result) { threads.push(cursor.result); cursor.continue(); return; } deferred.resolve(threads); }; cursor.onerror = deferred.reject.bind(deferred); return deferred.promise; } /** * Retrieve a single specified thread from database. * * Fulfill params: * thread -- a MozMobileMessageThread instance. * * Reject params: * event -- a DOMEvent if an error occurs in the retrieving process, or * undefined if there's no such thread. * * @aThreadId a numeric value identifying the target thread. * * @return A deferred promise. */ function getThreadById(aThreadId) { return getAllThreads() .then(function(aThreads) { for (let thread of aThreads) { if (thread.id === aThreadId) { return thread; } } throw undefined; }); } /** * Delete messages specified from database. * * Fulfill params: * result -- an array of boolean values indicating whether delesion was * actually performed on the message record with corresponding id. * * Reject params: * event -- a DOMEvent. * * @aMessageId an array of numeric values identifying the target messages. * * @return An empty array if nothing to be deleted; otherwise, a deferred promise. */ function deleteMessagesById(aMessageIds) { if (!aMessageIds.length) { ok(true, "no message to be deleted"); return []; } let promises = []; promises.push(waitForManagerEvent("deleted")); let request = manager.delete(aMessageIds); promises.push(wrapDomRequestAsPromise(request)); return Promise.all(promises) .then((aResults) => { return { deletedInfo: aResults[0], deletedFlags: aResults[1].target.result }; }); } /** * Delete messages specified from database. * * Fulfill params: * result -- an array of boolean values indicating whether delesion was * actually performed on the message record with corresponding id. * * Reject params: * event -- a DOMEvent. * * @aMessages an array of {Sms,Mms}Message instances. * * @return A deferred promise. */ function deleteMessages(aMessages) { let ids = messagesToIds(aMessages); return deleteMessagesById(ids); } /** * Delete all messages from database. * * Fulfill params: * ids -- an array of numeric values identifying those deleted * {Sms,Mms}Messages. * * Reject params: * event -- a DOMEvent. * * @return A deferred promise. */ function deleteAllMessages() { return getAllMessages().then(deleteMessages); } let 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; } /** * Send simple text SMS to emulator. * * Fulfill params: * result -- an array of emulator response lines. * * Reject params: * result -- an array of emulator response lines. * * @param aFrom * A string-typed from address. * @param aText * A string-typed message body. * * @return A deferred promise. */ function sendTextSmsToEmulator(aFrom, aText) { let command = "sms send " + aFrom + " " + aText; return runEmulatorCmdSafe(command); } /** * Send simple text SMS to emulator and wait for a received event. * * Fulfill params: MozSmsMessage * Reject params: (none) * * @param aFrom * A string-typed from address. * @param aText * A string-typed message body. * * @return A deferred promise. */ function sendTextSmsToEmulatorAndWait(aFrom, aText) { let promises = []; promises.push(waitForManagerEvent("received")); promises.push(sendTextSmsToEmulator(aFrom, aText)); return Promise.all(promises).then(aResults => aResults[0].message); } /** * Send raw SMS TPDU to emulator. * * @param: aPdu * A hex string representing the whole SMS T-PDU. * * Fulfill params: * result -- an array of emulator response lines. * * Reject params: * result -- an array of emulator response lines. * * @return A deferred promise. */ function sendRawSmsToEmulator(aPdu) { let command = "sms pdu " + aPdu; return runEmulatorCmdSafe(command); } /** * Send multiple raw SMS TPDU to emulator and wait * * @param: aPdus * A array of hex strings. Each represents a SMS T-PDU. * * Fulfill params: * result -- array of resolved Promise, where * result[0].message representing the received message. * result[1-n] represents the response of sent emulator command. * * Reject params: * result -- an array of emulator response lines. * * @return A deferred promise. */ function sendMultipleRawSmsToEmulatorAndWait(aPdus) { let promises = []; promises.push(waitForManagerEvent("received")); for (let pdu of aPdus) { promises.push(sendRawSmsToEmulator(pdu)); } return Promise.all(promises); } /** * Create a new array of id attribute of input messages. * * @param aMessages an array of {Sms,Mms}Message instances. * * @return an array of numeric values. */ function messagesToIds(aMessages) { let ids = []; for (let message of aMessages) { ids.push(message.id); } return ids; } /** * Convenient function to compare two SMS messages. */ function compareSmsMessage(aFrom, aTo) { const FIELDS = ["id", "threadId", "iccId", "body", "delivery", "deliveryStatus", "read", "receiver", "sender", "messageClass", "timestamp", "deliveryTimestamp", "sentTimestamp"]; for (let field of FIELDS) { is(aFrom[field], aTo[field], "message." + field); } } /** * 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 mobile message 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 mobile message tests. * * This function ensures global |manager| 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 ensureMobileMessage() .then(deleteAllMessages) .then(aTestCaseMain) .then(deleteAllMessages); }); } /** * Helper to run the test case only needed in Multi-SIM environment. * * @param aTest * A function which will be invoked w/o parameter. * @return a Promise object. */ function runIfMultiSIM(aTest) { let numRIL; try { numRIL = SpecialPowers.getIntPref("ril.numRadioInterfaces"); } catch (ex) { numRIL = 1; // Pref not set. } if (numRIL > 1) { return aTest(); } else { log("Not a Multi-SIM environment. Test is skipped."); return Promise.resolve(); } }