michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: MARIONETTE_TIMEOUT = 30000; michael@0: michael@0: const PDU_DCS_CODING_GROUP_BITS = 0xF0; michael@0: const PDU_DCS_MSG_CODING_7BITS_ALPHABET = 0x00; michael@0: const PDU_DCS_MSG_CODING_8BITS_ALPHABET = 0x04; michael@0: const PDU_DCS_MSG_CODING_16BITS_ALPHABET = 0x08; michael@0: michael@0: const PDU_DCS_MSG_CLASS_BITS = 0x03; michael@0: const PDU_DCS_MSG_CLASS_NORMAL = 0xFF; michael@0: const PDU_DCS_MSG_CLASS_0 = 0x00; michael@0: const PDU_DCS_MSG_CLASS_ME_SPECIFIC = 0x01; michael@0: const PDU_DCS_MSG_CLASS_SIM_SPECIFIC = 0x02; michael@0: const PDU_DCS_MSG_CLASS_TE_SPECIFIC = 0x03; michael@0: const PDU_DCS_MSG_CLASS_USER_1 = 0x04; michael@0: const PDU_DCS_MSG_CLASS_USER_2 = 0x05; michael@0: michael@0: const GECKO_SMS_MESSAGE_CLASSES = {}; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL] = "normal"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0] = "class-0"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_ME_SPECIFIC] = "class-1"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_SIM_SPECIFIC] = "class-2"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_TE_SPECIFIC] = "class-3"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_1] = "user-1"; michael@0: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_2] = "user-2"; michael@0: michael@0: const CB_MESSAGE_SIZE_GSM = 88; michael@0: michael@0: const CB_GSM_MESSAGEID_ETWS_BEGIN = 0x1100; michael@0: const CB_GSM_MESSAGEID_ETWS_END = 0x1107; michael@0: michael@0: const CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [ michael@0: "cell-immediate", michael@0: "plmn", michael@0: "location-area", michael@0: "cell" michael@0: ]; michael@0: michael@0: const CB_ETWS_WARNING_TYPE_NAMES = [ michael@0: "earthquake", michael@0: "tsunami", michael@0: "earthquake-tsunami", michael@0: "test", michael@0: "other" michael@0: ]; michael@0: michael@0: const CB_DCS_LANG_GROUP_1 = [ michael@0: "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", michael@0: "no", "el", "tr", "hu", "pl", null michael@0: ]; michael@0: const CB_DCS_LANG_GROUP_2 = [ michael@0: "cs", "he", "ar", "ru", "is", null, null, null, null, null, michael@0: null, null, null, null, null, null michael@0: ]; michael@0: michael@0: const CB_MAX_CONTENT_7BIT = Math.floor((CB_MESSAGE_SIZE_GSM - 6) * 8 / 7); michael@0: const CB_MAX_CONTENT_UCS2 = Math.floor((CB_MESSAGE_SIZE_GSM - 6) / 2); michael@0: michael@0: const BODY_7BITS = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" michael@0: + "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" michael@0: + "@@@@@@@@@@@@@"; // 93 ascii chars. michael@0: const BODY_7BITS_IND = BODY_7BITS.substr(3); michael@0: const BODY_UCS2 = "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" michael@0: + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" michael@0: + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" michael@0: + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" michael@0: + "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" michael@0: + "\u0000"; // 41 unicode chars. michael@0: const BODY_UCS2_IND = BODY_UCS2.substr(1); michael@0: michael@0: SpecialPowers.addPermission("cellbroadcast", true, document); michael@0: SpecialPowers.addPermission("mobileconnection", true, document); michael@0: michael@0: is(BODY_7BITS.length, CB_MAX_CONTENT_7BIT, "BODY_7BITS.length"); michael@0: is(BODY_7BITS_IND.length, CB_MAX_CONTENT_7BIT - 3, "BODY_7BITS_IND.length"); michael@0: is(BODY_UCS2.length, CB_MAX_CONTENT_UCS2, "BODY_UCS2.length"); michael@0: is(BODY_UCS2_IND.length, CB_MAX_CONTENT_UCS2 - 1, "BODY_UCS2_IND.length") michael@0: michael@0: let cbs = window.navigator.mozCellBroadcast; michael@0: ok(cbs instanceof window.MozCellBroadcast, michael@0: "mozCellBroadcast is instanceof " + cbs.constructor); michael@0: michael@0: let pendingEmulatorCmdCount = 0; michael@0: function sendCellBroadcastMessage(pdu, callback) { michael@0: pendingEmulatorCmdCount++; michael@0: michael@0: let cmd = "cbs pdu " + pdu; michael@0: runEmulatorCmd(cmd, function(result) { michael@0: pendingEmulatorCmdCount--; michael@0: michael@0: is(result[0], "OK", "Emulator response"); michael@0: michael@0: if (callback) { michael@0: window.setTimeout(callback, 0); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: function buildHexStr(n, numSemiOctets) { michael@0: let str = n.toString(16); michael@0: ok(str.length <= numSemiOctets); michael@0: while (str.length < numSemiOctets) { michael@0: str = "0" + str; michael@0: } michael@0: return str; michael@0: } michael@0: michael@0: function seq(end, begin) { michael@0: let result = []; michael@0: for (let i = begin || 0; i < end; i++) { michael@0: result.push(i); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: function repeat(func, array, oncomplete) { michael@0: (function do_call(index) { michael@0: let next = index < (array.length - 1) ? do_call.bind(null, index + 1) : oncomplete; michael@0: func.apply(null, [array[index], next]); michael@0: })(0); michael@0: } michael@0: michael@0: function doTestHelper(pdu, nextTest, checkFunc) { michael@0: cbs.addEventListener("received", function onreceived(event) { michael@0: cbs.removeEventListener("received", onreceived); michael@0: michael@0: checkFunc(event.message); michael@0: michael@0: window.setTimeout(nextTest, 0); michael@0: }); michael@0: michael@0: if (Array.isArray(pdu)) { michael@0: repeat(sendCellBroadcastMessage, pdu); michael@0: } else { michael@0: sendCellBroadcastMessage(pdu); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Tests receiving Cell Broadcast messages, event instance type, all attributes michael@0: * of CellBroadcastMessage exist. michael@0: */ michael@0: function testGsmMessageAttributes() { michael@0: log("Test GSM Cell Broadcast message attributes"); michael@0: michael@0: cbs.addEventListener("received", function onreceived(event) { michael@0: cbs.removeEventListener("received", onreceived); michael@0: michael@0: // Bug 838542: following check throws an exception and fails this case. michael@0: // ok(event instanceof MozCellBroadcastEvent, michael@0: // "event is instanceof " + event.constructor) michael@0: ok(event, "event is valid"); michael@0: michael@0: let message = event.message; michael@0: ok(message, "event.message is valid"); michael@0: michael@0: // Attributes other than `language` and `body` should always be assigned. michael@0: ok(message.gsmGeographicalScope != null, "message.gsmGeographicalScope"); michael@0: ok(message.messageCode != null, "message.messageCode"); michael@0: ok(message.messageId != null, "message.messageId"); michael@0: ok(message.language != null, "message.language"); michael@0: ok(message.body != null, "message.body"); michael@0: ok(message.messageClass != null, "message.messageClass"); michael@0: ok(message.timestamp != null, "message.timestamp"); michael@0: ok('etws' in message, "message.etws"); michael@0: if (message.etws) { michael@0: ok('warningType' in message.etws, "message.etws.warningType"); michael@0: ok(message.etws.emergencyUserAlert != null, "message.etws.emergencyUserAlert"); michael@0: ok(message.etws.popup != null, "message.etws.popup"); michael@0: } michael@0: ok(message.cdmaServiceCategory != null, "message.cdmaServiceCategory"); michael@0: michael@0: window.setTimeout(testReceiving_GSM_GeographicalScope, 0); michael@0: }); michael@0: michael@0: // Here we use a simple GSM message for test. michael@0: let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2); michael@0: sendCellBroadcastMessage(pdu); michael@0: } michael@0: michael@0: function testReceiving_GSM_GeographicalScope() { michael@0: log("Test receiving GSM Cell Broadcast - Geographical Scope"); michael@0: michael@0: function do_test(gs, nextTest) { michael@0: let pdu = buildHexStr(((gs & 0x03) << 14), 4) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2); michael@0: michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: is(message.gsmGeographicalScope, CB_GSM_GEOGRAPHICAL_SCOPE_NAMES[gs], michael@0: "message.gsmGeographicalScope"); michael@0: }); michael@0: } michael@0: michael@0: repeat(do_test, seq(CB_GSM_GEOGRAPHICAL_SCOPE_NAMES.length), michael@0: testReceiving_GSM_MessageCode); michael@0: } michael@0: michael@0: function testReceiving_GSM_MessageCode() { michael@0: log("Test receiving GSM Cell Broadcast - Message Code"); michael@0: michael@0: // Message Code has 10 bits, and is ORed into a 16 bits 'serial' number. Here michael@0: // we test every single bit to verify the operation doesn't go wrong. michael@0: let messageCodesToTest = [ michael@0: 0x000, 0x001, 0x002, 0x004, 0x008, 0x010, 0x020, 0x040, michael@0: 0x080, 0x100, 0x200, 0x251 michael@0: ]; michael@0: michael@0: function do_test(messageCode, nextTest) { michael@0: let pdu = buildHexStr(((messageCode & 0x3FF) << 4), 4) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 2) * 2); michael@0: michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: is(message.messageCode, messageCode, "message.messageCode"); michael@0: }); michael@0: } michael@0: michael@0: repeat(do_test, messageCodesToTest, testReceiving_GSM_MessageId); michael@0: } michael@0: michael@0: function testReceiving_GSM_MessageId() { michael@0: log("Test receiving GSM Cell Broadcast - Message Identifier"); michael@0: michael@0: // Message Identifier has 16 bits, but no bitwise operation is needed. michael@0: // Test some selected values only. michael@0: let messageIdsToTest = [ michael@0: 0x0000, 0x0001, 0x0010, 0x0100, 0x1000, 0x1111, 0x8888, 0x8811, michael@0: ]; michael@0: michael@0: function do_test(messageId, nextTest) { michael@0: let pdu = buildHexStr(0, 4) michael@0: + buildHexStr((messageId & 0xFFFF), 4) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2); michael@0: michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: is(message.messageId, messageId, "message.messageId"); michael@0: ok(message.etws == null, "message.etws"); michael@0: }); michael@0: } michael@0: michael@0: repeat(do_test, messageIdsToTest, testReceiving_GSM_Language_and_Body); michael@0: } michael@0: michael@0: // Copied from GsmPDUHelper.readCbDataCodingScheme michael@0: function decodeDataCodingScheme(dcs) { michael@0: let language = null; michael@0: let hasLanguageIndicator = false; michael@0: let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; michael@0: let messageClass = PDU_DCS_MSG_CLASS_NORMAL; michael@0: michael@0: switch (dcs & PDU_DCS_CODING_GROUP_BITS) { michael@0: case 0x00: // 0000 michael@0: language = CB_DCS_LANG_GROUP_1[dcs & 0x0F]; michael@0: break; michael@0: michael@0: case 0x10: // 0001 michael@0: switch (dcs & 0x0F) { michael@0: case 0x00: michael@0: hasLanguageIndicator = true; michael@0: break; michael@0: case 0x01: michael@0: encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; michael@0: hasLanguageIndicator = true; michael@0: break; michael@0: } michael@0: break; michael@0: michael@0: case 0x20: // 0010 michael@0: language = CB_DCS_LANG_GROUP_2[dcs & 0x0F]; michael@0: break; michael@0: michael@0: case 0x40: // 01xx michael@0: case 0x50: michael@0: //case 0x60: michael@0: //case 0x70: michael@0: case 0x90: // 1001 michael@0: encoding = (dcs & 0x0C); michael@0: if (encoding == 0x0C) { michael@0: encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; michael@0: } michael@0: messageClass = (dcs & PDU_DCS_MSG_CLASS_BITS); michael@0: break; michael@0: michael@0: case 0xF0: michael@0: encoding = (dcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET michael@0: : PDU_DCS_MSG_CODING_7BITS_ALPHABET; michael@0: switch(dcs & PDU_DCS_MSG_CLASS_BITS) { michael@0: case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break; michael@0: case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break; michael@0: case 0x03: messageClass = PDU_DCS_MSG_CLASS_TE_SPECIFIC; break; michael@0: } michael@0: break; michael@0: michael@0: case 0x30: // 0011 (Reserved) michael@0: case 0x80: // 1000 (Reserved) michael@0: case 0xA0: // 1010..1100 (Reserved) michael@0: case 0xB0: michael@0: case 0xC0: michael@0: break; michael@0: default: michael@0: throw new Error("Unsupported CBS data coding scheme: " + dcs); michael@0: } michael@0: michael@0: return [encoding, language, hasLanguageIndicator, michael@0: GECKO_SMS_MESSAGE_CLASSES[messageClass]]; michael@0: } michael@0: michael@0: function testReceiving_GSM_Language_and_Body() { michael@0: log("Test receiving GSM Cell Broadcast - Language & Body"); michael@0: michael@0: function do_test(dcs) { michael@0: let encoding, language, indicator, messageClass; michael@0: try { michael@0: [encoding, language, indicator, messageClass] = decodeDataCodingScheme(dcs); michael@0: } catch (e) { michael@0: // Unsupported coding group, skip. michael@0: let nextGroup = (dcs & PDU_DCS_CODING_GROUP_BITS) + 0x10; michael@0: window.setTimeout(do_test.bind(null, nextGroup), 0); michael@0: return; michael@0: } michael@0: michael@0: let pdu = buildHexStr(0, 8) michael@0: + buildHexStr(dcs, 2) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 5) * 2); michael@0: michael@0: let nextTest = (dcs < 0xFF) ? do_test.bind(null, dcs + 1) michael@0: : testReceiving_GSM_Timestamp; michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: if (language) { michael@0: is(message.language, language, "message.language"); michael@0: } else if (indicator) { michael@0: is(message.language, "@@", "message.language"); michael@0: } else { michael@0: ok(message.language == null, "message.language"); michael@0: } michael@0: michael@0: switch (encoding) { michael@0: case PDU_DCS_MSG_CODING_7BITS_ALPHABET: michael@0: is(message.body, indicator ? BODY_7BITS_IND : BODY_7BITS, "message.body"); michael@0: break; michael@0: case PDU_DCS_MSG_CODING_8BITS_ALPHABET: michael@0: ok(message.body == null, "message.body"); michael@0: break; michael@0: case PDU_DCS_MSG_CODING_16BITS_ALPHABET: michael@0: is(message.body, indicator ? BODY_UCS2_IND : BODY_UCS2, "message.body"); michael@0: break; michael@0: } michael@0: michael@0: is(message.messageClass, messageClass, "message.messageClass"); michael@0: }); michael@0: } michael@0: michael@0: do_test(0); michael@0: } michael@0: michael@0: function testReceiving_GSM_Timestamp() { michael@0: log("Test receiving GSM Cell Broadcast - Timestamp"); michael@0: michael@0: let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2); michael@0: doTestHelper(pdu, testReceiving_GSM_WarningType, function(message) { michael@0: // Cell Broadcast messages do not contain a timestamp field (however, ETWS michael@0: // does). We only check the timestamp doesn't go too far (60 seconds) here. michael@0: let msMessage = message.timestamp.getTime(); michael@0: let msNow = Date.now(); michael@0: ok(Math.abs(msMessage - msNow) < (1000 * 60), "message.timestamp"); michael@0: }); michael@0: } michael@0: michael@0: function testReceiving_GSM_WarningType() { michael@0: log("Test receiving GSM Cell Broadcast - Warning Type"); michael@0: michael@0: let messageIdsToTest = []; michael@0: for (let i = CB_GSM_MESSAGEID_ETWS_BEGIN; i <= CB_GSM_MESSAGEID_ETWS_END; i++) { michael@0: messageIdsToTest.push(i); michael@0: } michael@0: michael@0: function do_test(messageId, nextTest) { michael@0: let pdu = buildHexStr(0, 4) michael@0: + buildHexStr((messageId & 0xFFFF), 4) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2); michael@0: michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: is(message.messageId, messageId, "message.messageId"); michael@0: ok(message.etws != null, "message.etws"); michael@0: michael@0: let offset = messageId - CB_GSM_MESSAGEID_ETWS_BEGIN; michael@0: if (offset < CB_ETWS_WARNING_TYPE_NAMES.length) { michael@0: is(message.etws.warningType, CB_ETWS_WARNING_TYPE_NAMES[offset], michael@0: "message.etws.warningType"); michael@0: } else { michael@0: ok(message.etws.warningType == null, "message.etws.warningType"); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: repeat(do_test, messageIdsToTest, testReceiving_GSM_EmergencyUserAlert); michael@0: } michael@0: michael@0: function doTestEmergencyUserAlert_or_Popup(name, mask, nextTest) { michael@0: let pdu = buildHexStr(mask, 4) michael@0: + buildHexStr(CB_GSM_MESSAGEID_ETWS_BEGIN, 4) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 4) * 2); michael@0: michael@0: doTestHelper(pdu, nextTest, function(message) { michael@0: is(message.messageId, CB_GSM_MESSAGEID_ETWS_BEGIN, "message.messageId"); michael@0: ok(message.etws != null, "message.etws"); michael@0: is(message.etws[name], mask != 0, "message.etws." + name); michael@0: }); michael@0: } michael@0: michael@0: function testReceiving_GSM_EmergencyUserAlert() { michael@0: log("Test receiving GSM Cell Broadcast - Emergency User Alert"); michael@0: michael@0: repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "emergencyUserAlert"), michael@0: [0x2000, 0x0000], testReceiving_GSM_Popup); michael@0: } michael@0: michael@0: function testReceiving_GSM_Popup() { michael@0: log("Test receiving GSM Cell Broadcast - Popup"); michael@0: michael@0: repeat(doTestEmergencyUserAlert_or_Popup.bind(null, "popup"), michael@0: [0x1000, 0x0000], testReceiving_GSM_Multipart); michael@0: } michael@0: michael@0: function testReceiving_GSM_Multipart() { michael@0: log("Test receiving GSM Cell Broadcast - Multipart Messages"); michael@0: michael@0: function do_test(numParts, nextTest) { michael@0: let pdus = []; michael@0: for (let i = 1; i <= numParts; i++) { michael@0: let pdu = buildHexStr(0, 10) michael@0: + buildHexStr((i << 4) + numParts, 2) michael@0: + buildHexStr(0, (CB_MESSAGE_SIZE_GSM - 6) * 2); michael@0: pdus.push(pdu); michael@0: } michael@0: michael@0: doTestHelper(pdus, nextTest, function(message) { michael@0: is(message.body.length, (numParts * CB_MAX_CONTENT_7BIT), michael@0: "message.body"); michael@0: }); michael@0: } michael@0: michael@0: repeat(do_test, seq(16, 1), testReceiving_GSM_ServiceCategory); michael@0: } michael@0: michael@0: function testReceiving_GSM_ServiceCategory() { michael@0: log("Test receiving GSM Cell Broadcast - Service Category"); michael@0: michael@0: cbs.addEventListener("received", function onreceived(event) { michael@0: cbs.removeEventListener("received", onreceived); michael@0: michael@0: let message = event.message; michael@0: michael@0: // Bug 910091 michael@0: // "Service Category" is not defined in GSM. We should always get '0' here. michael@0: is(message.cdmaServiceCategory, 0, "message.cdmaServiceCategory"); michael@0: michael@0: window.setTimeout(cleanUp, 0); michael@0: }); michael@0: michael@0: let pdu = buildHexStr(0, CB_MESSAGE_SIZE_GSM * 2); michael@0: sendCellBroadcastMessage(pdu); michael@0: } michael@0: michael@0: function cleanUp() { michael@0: if (pendingEmulatorCmdCount > 0) { michael@0: window.setTimeout(cleanUp, 100); michael@0: return; michael@0: } michael@0: michael@0: SpecialPowers.removePermission("mobileconnection", document); michael@0: SpecialPowers.removePermission("cellbroadcast", true, document); michael@0: michael@0: finish(); michael@0: } michael@0: michael@0: waitFor(testGsmMessageAttributes, function() { michael@0: return navigator.mozMobileConnections[0].voice.connected; michael@0: }); michael@0: