diff options
Diffstat (limited to 'dom/system/gonk/ril_worker.js')
-rw-r--r-- | dom/system/gonk/ril_worker.js | 15206 |
1 files changed, 15206 insertions, 0 deletions
diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js new file mode 100644 index 000000000..0608a5be3 --- /dev/null +++ b/dom/system/gonk/ril_worker.js @@ -0,0 +1,15206 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file implements the RIL worker thread. It communicates with + * the main thread to provide a high-level API to the phone's RIL + * stack, and with the RIL IPC thread to communicate with the RIL + * device itself. These communication channels use message events as + * known from Web Workers: + * + * - postMessage()/"message" events for main thread communication + * + * - postRILMessage()/"RILMessageEvent" events for RIL IPC thread + * communication. + * + * The two main objects in this file represent individual parts of this + * communication chain: + * + * - RILMessageEvent -> Buf -> RIL -> postMessage() -> nsIRadioInterfaceLayer + * - nsIRadioInterfaceLayer -> postMessage() -> RIL -> Buf -> postRILMessage() + * + * Note: The code below is purposely lean on abstractions to be as lean in + * terms of object allocations. As a result, it may look more like C than + * JavaScript, and that's intended. + */ + +/* global BufObject */ +/* global TelephonyRequestQueue */ + +"use strict"; + +importScripts("ril_consts.js"); +importScripts("resource://gre/modules/workers/require.js"); +importScripts("ril_worker_buf_object.js"); +importScripts("ril_worker_telephony_request_queue.js"); + +// set to true in ril_consts.js to see debug messages +var DEBUG = DEBUG_WORKER; +var GLOBAL = this; + +if (!this.debug) { + // Debugging stub that goes nowhere. + this.debug = function debug(message) { + dump("RIL Worker: " + message + "\n"); + }; +} + +// Timeout value for emergency callback mode. +const EMERGENCY_CB_MODE_TIMEOUT_MS = 300000; // 5 mins = 300000 ms. + +const ICC_MAX_LINEAR_FIXED_RECORDS = 0xfe; + +const GET_CURRENT_CALLS_RETRY_MAX = 3; + +var RILQUIRKS_CALLSTATE_EXTRA_UINT32; +var RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL; +var RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS; +var RILQUIRKS_SIGNAL_EXTRA_INT32; +var RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING; +// Needed for call-waiting on Peak device +var RILQUIRKS_EXTRA_UINT32_2ND_CALL; +// On the emulator we support querying the number of lock retries +var RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT; +// Ril quirk to Send STK Profile Download +var RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD; +// Ril quirk to attach data registration on demand. +var RILQUIRKS_DATA_REGISTRATION_ON_DEMAND; +// Ril quirk to control the uicc/data subscription. +var RILQUIRKS_SUBSCRIPTION_CONTROL; +// Ril quirk to describe the SMSC address format. +var RILQUIRKS_SMSC_ADDRESS_FORMAT; + +/** + * The RIL state machine. + * + * This object communicates with rild via parcels and with the main thread + * via post messages. It maintains state about the radio, ICC, calls, etc. + * and acts upon state changes accordingly. + */ +function RilObject(aContext) { + this.context = aContext; + + this.telephonyRequestQueue = new TelephonyRequestQueue(this); + this.currentConferenceState = CALL_STATE_UNKNOWN; + this._pendingSentSmsMap = {}; + this.pendingNetworkType = {}; + this._receivedSmsCbPagesMap = {}; + this._getCurrentCallsRetryCount = 0; +} +RilObject.prototype = { + context: null, + + /** + * RIL version. + */ + version: null, + + /** + * Call state of current conference group. + */ + currentConferenceState: null, + + /** + * Outgoing messages waiting for SMS-STATUS-REPORT. + */ + _pendingSentSmsMap: null, + + /** + * Marker object. + */ + pendingNetworkType: null, + + /** + * Global Cell Broadcast switch. + */ + cellBroadcastDisabled: false, + + /** + * Parsed Cell Broadcast search lists. + * cellBroadcastConfigs.MMI should be preserved over rild reset. + */ + cellBroadcastConfigs: null, + mergedCellBroadcastConfig: null, + + _receivedSmsCbPagesMap: null, + + initRILState: function() { + /** + * One of the RADIO_STATE_* constants. + */ + this.radioState = GECKO_RADIOSTATE_UNKNOWN; + + /** + * True if we are on a CDMA phone. + */ + this._isCdma = false; + + /** + * True if we are in emergency callback mode. + */ + this._isInEmergencyCbMode = false; + + /** + * Set when radio is ready but radio tech is unknown. That is, we are + * waiting for REQUEST_VOICE_RADIO_TECH + */ + this._waitingRadioTech = false; + + /** + * Card state + */ + this.cardState = GECKO_CARDSTATE_UNINITIALIZED; + + /** + * Device Identities including IMEI, IMEISV, ESN and MEID. + */ + this.deviceIdentities = null; + + /** + * ICC information that is not exposed to Gaia. + */ + this.iccInfoPrivate = {}; + + /** + * ICC information, such as MSISDN, MCC, MNC, SPN...etc. + */ + this.iccInfo = {}; + + /** + * CDMA specific information. ex. CDMA Network ID, CDMA System ID... etc. + */ + this.cdmaHome = null; + + /** + * Application identification for apps in ICC. + */ + this.aid = null; + + /** + * Application type for apps in ICC. + */ + this.appType = null; + + this.networkSelectionMode = GECKO_NETWORK_SELECTION_UNKNOWN; + + this.voiceRegistrationState = {}; + this.dataRegistrationState = {}; + + /** + * List of strings identifying the network operator. + */ + this.operator = null; + + /** + * String containing the baseband version. + */ + this.basebandVersion = null; + + // Clean up currentCalls: rild might have restarted. + this.sendChromeMessage({ + rilMessageType: "currentCalls", + calls: {} + }); + + // Don't clean up this._pendingSentSmsMap + // because on rild restart: we may continue with the pending segments. + + /** + * Whether or not the multiple requests in requestNetworkInfo() are currently + * being processed + */ + this._processingNetworkInfo = false; + + /** + * Multiple requestNetworkInfo() in a row before finishing the first + * request, hence we need to fire requestNetworkInfo() again after + * gathering all necessary stuffs. This is to make sure that ril_worker + * gets precise network information. + */ + this._needRepollNetworkInfo = false; + + /** + * Pending messages to be send in batch from requestNetworkInfo() + */ + this._pendingNetworkInfo = {rilMessageType: "networkinfochanged"}; + + /** + * Cell Broadcast Search Lists. + */ + let cbmmi = this.cellBroadcastConfigs && this.cellBroadcastConfigs.MMI; + this.cellBroadcastConfigs = { + MMI: cbmmi || null + }; + this.mergedCellBroadcastConfig = null; + + /** + * True when the request to report SMS Memory Status is pending. + */ + this.pendingToReportSmsMemoryStatus = false; + this.smsStorageAvailable = true; + }, + + /** + * Parse an integer from a string, falling back to a default value + * if the the provided value is not a string or does not contain a valid + * number. + * + * @param string + * String to be parsed. + * @param defaultValue [optional] + * Default value to be used. + * @param radix [optional] + * A number that represents the numeral system to be used. Default 10. + */ + parseInt: function(string, defaultValue, radix) { + let number = parseInt(string, radix || 10); + if (!isNaN(number)) { + return number; + } + if (defaultValue === undefined) { + defaultValue = null; + } + return defaultValue; + }, + + + /** + * Outgoing requests to the RIL. These can be triggered from the + * main thread via messages that look like this: + * + * {rilMessageType: "methodName", + * extra: "parameters", + * go: "here"} + * + * So if one of the following methods takes arguments, it takes only one, + * an object, which then contains all of the parameters as attributes. + * The "@param" documentation is to be interpreted accordingly. + */ + + /** + * Retrieve the ICC's status. + */ + getICCStatus: function() { + this.context.Buf.simpleRequest(REQUEST_GET_SIM_STATUS); + }, + + /** + * Helper function for unlocking ICC locks. + */ + iccUnlockCardLock: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + this.enterICCPIN(options); + break; + case GECKO_CARDLOCK_PIN2: + this.enterICCPIN2(options); + break; + case GECKO_CARDLOCK_PUK: + this.enterICCPUK(options); + break; + case GECKO_CARDLOCK_PUK2: + this.enterICCPUK2(options); + break; + case GECKO_CARDLOCK_NCK: + case GECKO_CARDLOCK_NSCK: + case GECKO_CARDLOCK_NCK1: + case GECKO_CARDLOCK_NCK2: + case GECKO_CARDLOCK_HNCK: + case GECKO_CARDLOCK_CCK: + case GECKO_CARDLOCK_SPCK: + case GECKO_CARDLOCK_PCK: + case GECKO_CARDLOCK_RCCK: + case GECKO_CARDLOCK_RSPCK: + case GECKO_CARDLOCK_NCK_PUK: + case GECKO_CARDLOCK_NSCK_PUK: + case GECKO_CARDLOCK_NCK1_PUK: + case GECKO_CARDLOCK_NCK2_PUK: + case GECKO_CARDLOCK_HNCK_PUK: + case GECKO_CARDLOCK_CCK_PUK: + case GECKO_CARDLOCK_SPCK_PUK: + case GECKO_CARDLOCK_PCK_PUK: + case GECKO_CARDLOCK_RCCK_PUK: // Fall through. + case GECKO_CARDLOCK_RSPCK_PUK: + this.enterDepersonalization(options); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + } + }, + + /** + * Enter a PIN to unlock the ICC. + * + * @param password + * String containing the PIN. + * @param [optional] aid + * AID value. + */ + enterICCPIN: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PIN, options); + Buf.writeInt32(2); + Buf.writeString(options.password); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Enter a PIN2 to unlock the ICC. + * + * @param password + * String containing the PIN2. + * @param [optional] aid + * AID value. + */ + enterICCPIN2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PIN2, options); + Buf.writeInt32(2); + Buf.writeString(options.password); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Requests a network personalization be deactivated. + * + * @param personlization + * One of CARD_PERSOSUBSTATE_* + * @param password + * String containing the password. + */ + enterDepersonalization: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE, options); + Buf.writeInt32(1); + Buf.writeString(options.password); + Buf.sendParcel(); + }, + + /** + * Change the current ICC PIN number. + * + * @param password + * String containing the old PIN value + * @param newPassword + * String containing the new PIN value + * @param [optional] aid + * AID value. + */ + changeICCPIN: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_SIM_PIN, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPassword); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Change the current ICC PIN2 number. + * + * @param password + * String containing the old PIN2 value + * @param newPassword + * String containing the new PIN2 value + * @param [optional] aid + * AID value. + */ + changeICCPIN2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_SIM_PIN2, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPassword); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Supplies ICC PUK and a new PIN to unlock the ICC. + * + * @param password + * String containing the PUK value. + * @param newPassword + * String containing the new PIN value. + * @param [optional] aid + * AID value. + */ + enterICCPUK: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PUK, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPin); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Supplies ICC PUK2 and a new PIN2 to unlock the ICC. + * + * @param password + * String containing the PUK2 value. + * @param newPassword + * String containing the new PIN2 value. + * @param [optional] aid + * AID value. + */ + enterICCPUK2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PUK2, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPin); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Helper function for changing ICC locks. + */ + iccChangeCardLockPassword: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + this.changeICCPIN(options); + break; + case GECKO_CARDLOCK_PIN2: + this.changeICCPIN2(options); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + } + }, + + /** + * Helper function for setting the state of ICC locks. + */ + iccSetCardLockEnabled: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: // Fall through. + case GECKO_CARDLOCK_FDN: + options.facility = GECKO_CARDLOCK_TO_FACILITY[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + options.serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + this.setICCFacilityLock(options); + }, + + /** + * Helper function for fetching the state of ICC locks. + */ + iccGetCardLockEnabled: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: // Fall through. + case GECKO_CARDLOCK_FDN: + options.facility = GECKO_CARDLOCK_TO_FACILITY[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + options.password = ""; // For query no need to provide pin. + options.serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + this.queryICCFacilityLock(options); + }, + + /** + * Helper function for fetching the number of unlock retries of ICC locks. + * + * We only query the retry count when we're on the emulator. The phones do + * not support the request id and their rild doesn't return an error. + */ + iccGetCardLockRetryCount: function(options) { + if (!RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT) { + // Only the emulator supports this request. + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + case GECKO_CARDLOCK_PIN2: + case GECKO_CARDLOCK_PUK: + case GECKO_CARDLOCK_PUK2: + case GECKO_CARDLOCK_NCK: + case GECKO_CARDLOCK_NSCK: + case GECKO_CARDLOCK_CCK: // Fall through. + case GECKO_CARDLOCK_SPCK: + // TODO: Bug 1116072: identify the mapping between RIL_PERSOSUBSTATE_SIM_SIM + // @ ril.h and TS 27.007, clause 8.65 for GECKO_CARDLOCK_SPCK. + options.selCode = GECKO_CARDLOCK_TO_SEL_CODE[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + this.queryICCLockRetryCount(options); + }, + + /** + * Query ICC lock retry count. + * + * @param selCode + * One of ICC_SEL_CODE_*. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + */ + queryICCLockRetryCount: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GET_UNLOCK_RETRY_COUNT, options); + Buf.writeInt32(1); + Buf.writeString(options.selCode); + Buf.sendParcel(); + }, + + /** + * Query ICC facility lock. + * + * @param facility + * One of ICC_CB_FACILITY_*. + * @param password + * Password for the facility, or "" if not required. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + * @param [optional] aid + * AID value. + */ + queryICCFacilityLock: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_FACILITY_LOCK, options); + Buf.writeInt32(4); + Buf.writeString(options.facility); + Buf.writeString(options.password); + Buf.writeString(options.serviceClass.toString()); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Set ICC facility lock. + * + * @param facility + * One of ICC_CB_FACILITY_*. + * @param enabled + * true to enable, false to disable. + * @param password + * Password for the facility, or "" if not required. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + * @param [optional] aid + * AID value. + */ + setICCFacilityLock: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_FACILITY_LOCK, options); + Buf.writeInt32(5); + Buf.writeString(options.facility); + Buf.writeString(options.enabled ? "1" : "0"); + Buf.writeString(options.password); + Buf.writeString(options.serviceClass.toString()); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Request an ICC I/O operation. + * + * See TS 27.007 "restricted SIM" operation, "AT Command +CRSM". + * The sequence is in the same order as how libril reads this parcel, + * see the struct RIL_SIM_IO_v5 or RIL_SIM_IO_v6 defined in ril.h + * + * @param command + * The I/O command, one of the ICC_COMMAND_* constants. + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param pathId + * String type, check the 'pathid' parameter from TS 27.007 +CRSM. + * @param p1, p2, p3 + * Arbitrary integer parameters for the command. + * @param [optional] dataWriter + * The function for writing string parameter for the ICC_COMMAND_UPDATE_RECORD. + * @param [optional] pin2 + * String containing the PIN2. + * @param [optional] aid + * AID value. + */ + iccIO: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_IO, options); + Buf.writeInt32(options.command); + Buf.writeInt32(options.fileId); + Buf.writeString(options.pathId); + Buf.writeInt32(options.p1); + Buf.writeInt32(options.p2); + Buf.writeInt32(options.p3); + + // Write data. + if (options.command == ICC_COMMAND_UPDATE_RECORD && + options.dataWriter) { + options.dataWriter(options.p3); + } else { + Buf.writeString(null); + } + + // Write pin2. + if (options.command == ICC_COMMAND_UPDATE_RECORD && + options.pin2) { + Buf.writeString(options.pin2); + } else { + Buf.writeString(null); + } + + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Get IMSI. + * + * @param [optional] aid + * AID value. + */ + getIMSI: function(aid) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GET_IMSI); + Buf.writeInt32(1); + Buf.writeString(aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Retrieve ICC's GID1 field. + */ + getGID1: function(options) { + options.gid1 = this.iccInfoPrivate.gid1; + this.sendChromeMessage(options); + }, + + /** + * Read UICC Phonebook contacts. + * + * @param contactType + * One of GECKO_CARDCONTACT_TYPE_*. + * @param requestId + * Request id from RadioInterfaceLayer. + */ + readICCContacts: function(options) { + if (!this.appType) { + options.errorMsg = CONTACT_ERR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + this.context.ICCContactHelper.readICCContacts( + this.appType, + options.contactType, + function onsuccess(contacts) { + for (let i = 0; i < contacts.length; i++) { + let contact = contacts[i]; + let pbrIndex = contact.pbrIndex || 0; + let recordIndex = pbrIndex * ICC_MAX_LINEAR_FIXED_RECORDS + contact.recordId; + contact.contactId = this.iccInfo.iccid + recordIndex; + } + // Reuse 'options' to get 'requestId' and 'contactType'. + options.contacts = contacts; + this.sendChromeMessage(options); + }.bind(this), + function onerror(errorMsg) { + options.errorMsg = errorMsg; + this.sendChromeMessage(options); + }.bind(this)); + }, + + /** + * Update UICC Phonebook. + * + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be updated. + * @param pin2 PIN2 is required for updating FDN. + * @param requestId Request id from RadioInterfaceLayer. + */ + updateICCContact: function(options) { + let onsuccess = function onsuccess(updatedContact) { + let recordIndex = + updatedContact.pbrIndex * ICC_MAX_LINEAR_FIXED_RECORDS + updatedContact.recordId; + updatedContact.contactId = this.iccInfo.iccid + recordIndex; + options.contact = updatedContact; + // Reuse 'options' to get 'requestId' and 'contactType'. + this.sendChromeMessage(options); + }.bind(this); + + let onerror = function onerror(errorMsg) { + options.errorMsg = errorMsg; + this.sendChromeMessage(options); + }.bind(this); + + if (!this.appType || !options.contact) { + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED ); + return; + } + + let contact = options.contact; + let iccid = this.iccInfo.iccid; + let isValidRecordId = false; + if (typeof contact.contactId === "string" && + contact.contactId.startsWith(iccid)) { + let recordIndex = contact.contactId.substring(iccid.length); + contact.pbrIndex = Math.floor(recordIndex / ICC_MAX_LINEAR_FIXED_RECORDS); + contact.recordId = recordIndex % ICC_MAX_LINEAR_FIXED_RECORDS; + isValidRecordId = contact.recordId > 0 && contact.recordId < 0xff; + } + + if (DEBUG) { + this.context.debug("Update ICC Contact " + JSON.stringify(contact)); + } + + let ICCContactHelper = this.context.ICCContactHelper; + // If contact has 'recordId' property, updates corresponding record. + // If not, inserts the contact into a free record. + if (isValidRecordId) { + ICCContactHelper.updateICCContact( + this.appType, options.contactType, contact, options.pin2, onsuccess, onerror); + } else { + ICCContactHelper.addICCContact( + this.appType, options.contactType, contact, options.pin2, onsuccess, onerror); + } + }, + + /** + * Check if operator name needs to be overriden by current voiceRegistrationState + * , EFOPL and EFPNN. See 3GPP TS 31.102 clause 4.2.58 EFPNN and 4.2.59 EFOPL + * for detail. + * + * @return true if operator name is overridden, false otherwise. + */ + overrideICCNetworkName: function() { + if (!this.operator) { + return false; + } + + // We won't get network name using PNN and OPL if voice registration isn't + // ready. + if (!this.voiceRegistrationState.cell || + this.voiceRegistrationState.cell.gsmLocationAreaCode == -1) { + return false; + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let networkName = ICCUtilsHelper.getNetworkNameFromICC( + this.operator.mcc, + this.operator.mnc, + this.voiceRegistrationState.cell.gsmLocationAreaCode); + + if (!networkName) { + return false; + } + + if (DEBUG) { + this.context.debug("Operator names will be overriden: " + + "longName = " + networkName.fullName + ", " + + "shortName = " + networkName.shortName); + } + + this.operator.longName = networkName.fullName; + this.operator.shortName = networkName.shortName; + + this._sendNetworkInfoMessage(NETWORK_INFO_OPERATOR, this.operator); + return true; + }, + + /** + * Request the phone's radio to be enabled or disabled. + * + * @param enabled + * Boolean indicating the desired state. + */ + setRadioEnabled: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_RADIO_POWER, options); + Buf.writeInt32(1); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Query call waiting status. + * + */ + queryCallWaiting: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_CALL_WAITING, options); + Buf.writeInt32(1); + // As per 3GPP TS 24.083, section 1.6 UE doesn't need to send service + // class parameter in call waiting interrogation to network. + Buf.writeInt32(ICC_SERVICE_CLASS_NONE); + Buf.sendParcel(); + }, + + /** + * Set call waiting status. + * + * @param enabled + * Boolean indicating the desired waiting status. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + setCallWaiting: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CALL_WAITING, options); + Buf.writeInt32(2); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.writeInt32(options.serviceClass); + Buf.sendParcel(); + }, + + /** + * Queries current CLIP status. + */ + queryCLIP: function(options) { + this.context.Buf.simpleRequest(REQUEST_QUERY_CLIP, options); + }, + + /** + * Queries current CLIR status. + * + */ + getCLIR: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_CLIR, options); + }, + + /** + * Enables or disables the presentation of the calling line identity (CLI) to + * the called party when originating a call. + * + * @param options.clirMode + * One of the CLIR_* constants. + */ + setCLIR: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CLIR, options); + Buf.writeInt32(1); + Buf.writeInt32(options.clirMode); + Buf.sendParcel(); + }, + + /** + * Set screen state. + * + * @param on + * Boolean indicating whether the screen should be on or off. + */ + setScreenState: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SCREEN_STATE); + Buf.writeInt32(1); + Buf.writeInt32(options.on ? 1 : 0); + Buf.sendParcel(); + }, + + getVoiceRegistrationState: function() { + this.context.Buf.simpleRequest(REQUEST_VOICE_REGISTRATION_STATE); + }, + + getVoiceRadioTechnology: function() { + this.context.Buf.simpleRequest(REQUEST_VOICE_RADIO_TECH); + }, + + getDataRegistrationState: function() { + this.context.Buf.simpleRequest(REQUEST_DATA_REGISTRATION_STATE); + }, + + getOperator: function() { + this.context.Buf.simpleRequest(REQUEST_OPERATOR); + }, + + /** + * Set the preferred network type. + * + * @param options An object contains a valid value of + * RIL_PREFERRED_NETWORK_TYPE_TO_GECKO as its `type` attribute. + */ + setPreferredNetworkType: function(options) { + let networkType = options.type; + if (networkType < 0 || networkType >= RIL_PREFERRED_NETWORK_TYPE_TO_GECKO.length) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_PREFERRED_NETWORK_TYPE, options); + Buf.writeInt32(1); + Buf.writeInt32(networkType); + Buf.sendParcel(); + }, + + /** + * Get the preferred network type. + */ + getPreferredNetworkType: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_PREFERRED_NETWORK_TYPE, options); + }, + + /** + * Request neighboring cell ids in GSM network. + */ + getNeighboringCellIds: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_NEIGHBORING_CELL_IDS, options); + }, + + /** + * Request all of the current cell information known to the radio. + */ + getCellInfoList: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_CELL_INFO_LIST, options); + }, + + /** + * Request various states about the network. + */ + requestNetworkInfo: function() { + if (this._processingNetworkInfo) { + if (DEBUG) { + this.context.debug("Network info requested, but we're already " + + "requesting network info."); + } + this._needRepollNetworkInfo = true; + return; + } + + if (DEBUG) this.context.debug("Requesting network info"); + + this._processingNetworkInfo = true; + this.getVoiceRegistrationState(); + this.getDataRegistrationState(); //TODO only GSM + this.getOperator(); + this.getNetworkSelectionMode(); + this.getSignalStrength(); + }, + + /** + * Get the available networks + */ + getAvailableNetworks: function(options) { + if (DEBUG) this.context.debug("Getting available networks"); + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_AVAILABLE_NETWORKS, options); + Buf.sendParcel(); + }, + + /** + * Request the radio's network selection mode + */ + getNetworkSelectionMode: function() { + if (DEBUG) this.context.debug("Getting network selection mode"); + this.context.Buf.simpleRequest(REQUEST_QUERY_NETWORK_SELECTION_MODE); + }, + + /** + * Tell the radio to automatically choose a voice/data network + */ + selectNetworkAuto: function(options) { + if (DEBUG) this.context.debug("Setting automatic network selection"); + this.context.Buf.simpleRequest(REQUEST_SET_NETWORK_SELECTION_AUTOMATIC, options); + }, + + /** + * Set the roaming preference mode + */ + setRoamingPreference: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SET_ROAMING_PREFERENCE, options); + Buf.writeInt32(1); + Buf.writeInt32(options.mode); + Buf.sendParcel(); + }, + + /** + * Get the roaming preference mode + */ + queryRoamingPreference: function(options) { + this.context.Buf.simpleRequest(REQUEST_CDMA_QUERY_ROAMING_PREFERENCE, options); + }, + + /** + * Set the voice privacy mode + */ + setVoicePrivacyMode: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE, options); + Buf.writeInt32(1); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Get the voice privacy mode + */ + queryVoicePrivacyMode: function(options) { + this.context.Buf.simpleRequest(REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE, options); + }, + + /** + * Open Logical UICC channel (aid) for Secure Element access + */ + iccOpenChannel: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_OPEN_CHANNEL, options); + Buf.writeString(options.aid); + Buf.sendParcel(); + }, + + /** + * Exchange APDU data on an open Logical UICC channel + */ + iccExchangeAPDU: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_TRANSMIT_APDU_CHANNEL, options); + Buf.writeInt32(options.channel); + Buf.writeInt32(options.apdu.cla); + Buf.writeInt32(options.apdu.command); + Buf.writeInt32(options.apdu.p1); + Buf.writeInt32(options.apdu.p2); + Buf.writeInt32(options.apdu.p3); + Buf.writeString(options.apdu.data); + Buf.sendParcel(); + }, + + /** + * Close Logical UICC channel + */ + iccCloseChannel: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_CLOSE_CHANNEL, options); + Buf.writeInt32(1); + Buf.writeInt32(options.channel); + Buf.sendParcel(); + }, + + /** + * Get UICC service state + */ + getIccServiceState: function(options) { + switch (options.service) { + case GECKO_CARDSERVICE_FDN: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + options.result = ICCUtilsHelper.isICCServiceAvailable("FDN"); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + break; + } + this.sendChromeMessage(options); + }, + + /** + * Enable/Disable UICC subscription + */ + setUiccSubscription: function(options) { + if (DEBUG) { + this.context.debug("setUiccSubscription: " + JSON.stringify(options)); + } + + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_UICC_SUBSCRIPTION, options); + Buf.writeInt32(this.context.clientId); + Buf.writeInt32(options.appIndex); + Buf.writeInt32(this.context.clientId); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Tell the radio to choose a specific voice/data network + */ + selectNetwork: function(options) { + if (DEBUG) { + this.context.debug("Setting manual network selection: " + + options.mcc + ", " + options.mnc); + } + + let numeric = (options.mcc && options.mnc) ? options.mcc + options.mnc : null; + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_NETWORK_SELECTION_MANUAL, options); + Buf.writeString(numeric); + Buf.sendParcel(); + }, + + /** + * Get the signal strength. + */ + getSignalStrength: function() { + this.context.Buf.simpleRequest(REQUEST_SIGNAL_STRENGTH); + }, + + getDeviceIdentity: function() { + this.deviceIdentities || this.context.Buf.simpleRequest(REQUEST_DEVICE_IDENTITY); + }, + + getBasebandVersion: function() { + this.context.Buf.simpleRequest(REQUEST_BASEBAND_VERSION); + }, + + sendExitEmergencyCbModeRequest: function(options) { + this.context.Buf.simpleRequest(REQUEST_EXIT_EMERGENCY_CALLBACK_MODE, options); + }, + + getCdmaSubscription: function() { + this.context.Buf.simpleRequest(REQUEST_CDMA_SUBSCRIPTION); + }, + + exitEmergencyCbMode: function(options) { + // The function could be called by an API from RadioInterfaceLayer or by + // ril_worker itself. From ril_worker, we won't pass the parameter + // 'options'. In this case, it is marked as internal. + if (!options) { + options = {internal: true}; + } + this._cancelEmergencyCbModeTimeout(); + this.sendExitEmergencyCbModeRequest(options); + }, + + /** + * Dial a non-emergency number. + * + * @param isEmergency + * Whether the number is an emergency number. + * @param number + * String containing the number to dial. + * @param clirMode + * Integer for showing/hidding the caller Id to the called party. + * @param uusInfo + * Integer doing something XXX TODO + */ + dial: function(options) { + if (options.isEmergency) { + options.request = RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL ? + REQUEST_DIAL_EMERGENCY_CALL : REQUEST_DIAL; + + } else { + options.request = REQUEST_DIAL; + + // Exit emergency callback mode when user dial a non-emergency call. + if (this._isInEmergencyCbMode) { + this.exitEmergencyCbMode(); + } + } + + this.telephonyRequestQueue.push(options.request, () => { + let Buf = this.context.Buf; + Buf.newParcel(options.request, options); + Buf.writeString(options.number); + Buf.writeInt32(options.clirMode || 0); + Buf.writeInt32(options.uusInfo || 0); + // TODO Why do we need this extra 0? It was put it in to make this + // match the format of the binary message. + Buf.writeInt32(0); + Buf.sendParcel(); + }); + }, + + /** + * CDMA flash. + * + * @param featureStr (optional) + * Dialing number when the command is used for three-way-calling + */ + cdmaFlash: function(options) { + let Buf = this.context.Buf; + options.request = REQUEST_CDMA_FLASH; + Buf.newParcel(options.request, options); + Buf.writeString(options.featureStr || ""); + Buf.sendParcel(); + }, + + /** + * Hang up the phone. + * + * @param callIndex + * Call index (1-based) as reported by REQUEST_GET_CURRENT_CALLS. + */ + hangUpCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP, () => { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_HANGUP, options); + Buf.writeInt32(1); + Buf.writeInt32(options.callIndex); + Buf.sendParcel(); + }); + }, + + hangUpForeground: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, () => { + this.context.Buf.simpleRequest(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, + options); + }); + }, + + hangUpBackground: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP_WAITING_OR_BACKGROUND, () => { + this.context.Buf.simpleRequest(REQUEST_HANGUP_WAITING_OR_BACKGROUND, + options); + }); + }, + + switchActiveCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, () => { + this.context.Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, + options); + }); + }, + + udub: function(options) { + this.telephonyRequestQueue.push(REQUEST_UDUB, () => { + this.context.Buf.simpleRequest(REQUEST_UDUB, options); + }); + }, + + answerCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_ANSWER, () => { + this.context.Buf.simpleRequest(REQUEST_ANSWER, options); + }); + }, + + conferenceCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_CONFERENCE, () => { + this.context.Buf.simpleRequest(REQUEST_CONFERENCE, options); + }); + }, + + separateCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_SEPARATE_CONNECTION, () => { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SEPARATE_CONNECTION, options); + Buf.writeInt32(1); + Buf.writeInt32(options.callIndex); + Buf.sendParcel(); + }); + }, + + /** + * Get current calls. + */ + getCurrentCalls: function(options) { + this.telephonyRequestQueue.push(REQUEST_GET_CURRENT_CALLS, () => { + this.context.Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS, options); + }); + }, + + /** + * Mute or unmute the radio. + * + * @param mute + * Boolean to indicate whether to mute or unmute the radio. + */ + setMute: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_MUTE); + Buf.writeInt32(1); + Buf.writeInt32(options.muted ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Send an SMS. + * + * The `options` parameter object should contain the following attributes: + * + * @param number + * String containing the recipient number. + * @param body + * String containing the message text. + * @param envelopeId + * Numeric value identifying the sms request. + */ + sendSMS: function(options) { + options.langIndex = options.langIndex || PDU_NL_IDENTIFIER_DEFAULT; + options.langShiftIndex = options.langShiftIndex || PDU_NL_IDENTIFIER_DEFAULT; + + if (!options.segmentSeq) { + // Fist segment to send + options.segmentSeq = 1; + options.body = options.segments[0].body; + options.encodedBodyLength = options.segments[0].encodedBodyLength; + } + + let Buf = this.context.Buf; + if (this._isCdma) { + Buf.newParcel(REQUEST_CDMA_SEND_SMS, options); + this.context.CdmaPDUHelper.writeMessage(options); + } else { + Buf.newParcel(REQUEST_SEND_SMS, options); + Buf.writeInt32(2); + Buf.writeString(options.SMSC); + this.context.GsmPDUHelper.writeMessage(options); + } + Buf.sendParcel(); + }, + + /** + * Acknowledge the receipt and handling of an SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + * @param cause + * SMS_* constant indicating the reason for unsuccessful handling. + */ + acknowledgeGsmSms: function(success, cause) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SMS_ACKNOWLEDGE); + Buf.writeInt32(2); + Buf.writeInt32(success ? 1 : 0); + Buf.writeInt32(cause); + Buf.sendParcel(); + }, + + /** + * Acknowledge the receipt and handling of an SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + */ + ackSMS: function(options) { + if (options.result == PDU_FCS_RESERVED) { + return; + } + if (this._isCdma) { + this.acknowledgeCdmaSms(options.result == PDU_FCS_OK, options.result); + } else { + this.acknowledgeGsmSms(options.result == PDU_FCS_OK, options.result); + } + }, + + /** + * Acknowledge the receipt and handling of a CDMA SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + * @param cause + * SMS_* constant indicating the reason for unsuccessful handling. + */ + acknowledgeCdmaSms: function(success, cause) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SMS_ACKNOWLEDGE); + Buf.writeInt32(success ? 0 : 1); + Buf.writeInt32(cause); + Buf.sendParcel(); + }, + + /** + * Update received MWI into EF_MWIS. + */ + updateMwis: function(options) { + if (this.context.ICCUtilsHelper.isICCServiceAvailable("MWIS")) { + this.context.SimRecordHelper.updateMWIS(options.mwi); + } + }, + + /** + * Report SMS storage status to modem. + */ + _updateSmsMemoryStatus: function() { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_REPORT_SMS_MEMORY_STATUS); + Buf.writeInt32(1); + Buf.writeInt32(this.smsStorageAvailable ? 1 : 0); + Buf.sendParcel(); + }, + + reportSmsMemoryStatus: function(options) { + this.pendingToReportSmsMemoryStatus = true; + this.smsStorageAvailable = options.isAvailable; + this._updateSmsMemoryStatus(); + }, + + setCellBroadcastDisabled: function(options) { + this.cellBroadcastDisabled = options.disabled; + + // If |this.mergedCellBroadcastConfig| is null, either we haven't finished + // reading required SIM files, or no any channel is ever configured. In + // the former case, we'll call |this.updateCellBroadcastConfig()| later + // with correct configs; in the latter case, we don't bother resetting CB + // to disabled again. + if (this.mergedCellBroadcastConfig) { + this.updateCellBroadcastConfig(); + } + }, + + setCellBroadcastSearchList: function(options) { + let getSearchListStr = function(aSearchList) { + if (typeof aSearchList === "string" || aSearchList instanceof String) { + return aSearchList; + } + + // TODO: Set search list for CDMA/GSM individually. Bug 990926 + let prop = this._isCdma ? "cdma" : "gsm"; + + return aSearchList && aSearchList[prop]; + }.bind(this); + + try { + let str = getSearchListStr(options.searchList); + this.cellBroadcastConfigs.MMI = this._convertCellBroadcastSearchList(str); + } catch (e) { + if (DEBUG) { + this.context.debug("Invalid Cell Broadcast search list: " + e); + } + options.errorMsg = GECKO_ERROR_UNSPECIFIED_ERROR; + } + + this.sendChromeMessage(options); + if (options.errorMsg) { + return; + } + + this._mergeAllCellBroadcastConfigs(); + }, + + updateCellBroadcastConfig: function() { + let activate = !this.cellBroadcastDisabled && + (this.mergedCellBroadcastConfig != null) && + (this.mergedCellBroadcastConfig.length > 0); + if (activate) { + this.setSmsBroadcastConfig(this.mergedCellBroadcastConfig); + } else { + // It's unnecessary to set config first if we're deactivating. + this.setSmsBroadcastActivation(false); + } + }, + + setGsmSmsBroadcastConfig: function(config) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GSM_SET_BROADCAST_SMS_CONFIG); + + let numConfigs = config ? config.length / 2 : 0; + Buf.writeInt32(numConfigs); + for (let i = 0; i < config.length;) { + // convert [from, to) to [from, to - 1] + Buf.writeInt32(config[i++]); + Buf.writeInt32(config[i++] - 1); + Buf.writeInt32(0x00); + Buf.writeInt32(0xFF); + Buf.writeInt32(1); + } + + Buf.sendParcel(); + }, + + /** + * Send CDMA SMS broadcast config. + * + * @see 3GPP2 C.R1001 Sec. 9.2 and 9.3 + */ + setCdmaSmsBroadcastConfig: function(config) { + let Buf = this.context.Buf; + // |config| is an array of half-closed range: [[from, to), [from, to), ...]. + // It will be further decomposed, ex: [1, 4) => 1, 2, 3. + Buf.newParcel(REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG); + + let numConfigs = 0; + for (let i = 0; i < config.length; i += 2) { + numConfigs += (config[i+1] - config[i]); + } + + Buf.writeInt32(numConfigs); + for (let i = 0; i < config.length;) { + let begin = config[i++]; + let end = config[i++]; + + for (let j = begin; j < end; ++j) { + Buf.writeInt32(j); + Buf.writeInt32(0); // Language Indicator: Unknown or unspecified. + Buf.writeInt32(1); + } + } + + Buf.sendParcel(); + }, + + setSmsBroadcastConfig: function(config) { + if (this._isCdma) { + this.setCdmaSmsBroadcastConfig(config); + } else { + this.setGsmSmsBroadcastConfig(config); + } + }, + + setSmsBroadcastActivation: function(activate) { + let parcelType = this._isCdma ? REQUEST_CDMA_SMS_BROADCAST_ACTIVATION : + REQUEST_GSM_SMS_BROADCAST_ACTIVATION; + let Buf = this.context.Buf; + Buf.newParcel(parcelType); + Buf.writeInt32(1); + // See hardware/ril/include/telephony/ril.h, 0 - Activate, 1 - Turn off. + Buf.writeInt32(activate ? 0 : 1); + Buf.sendParcel(); + }, + + /** + * Start a DTMF Tone. + * + * @param dtmfChar + * DTMF signal to send, 0-9, *, + + */ + startTone: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DTMF_START, options); + Buf.writeString(options.dtmfChar); + Buf.sendParcel(); + }, + + stopTone: function() { + this.context.Buf.simpleRequest(REQUEST_DTMF_STOP); + }, + + /** + * Send a DTMF tone. + * + * @param dtmfChar + * DTMF signal to send, 0-9, *, + + */ + sendTone: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DTMF); + Buf.writeString(options.dtmfChar); + Buf.sendParcel(); + }, + + /** + * Get the Short Message Service Center address. + */ + getSmscAddress: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_SMSC_ADDRESS, options); + }, + + /** + * Set the Short Message Service Center address. + * + * @param smscAddress + * Number part of the SMSC address. + * @param typeOfNumber + * Type of number in integer, as defined in + * |Table 10.5.118: Called party BCD number| of 3GPP TS 24.008. + * @param numberPlanIdentification + * The index of number plan identification value in + * CALLED_PARTY_BCD_NPI array. + */ + setSmscAddress: function(options) { + let ton = options.typeOfNumber; + let npi = CALLED_PARTY_BCD_NPI[options.numberPlanIdentification]; + + // If any of the mandatory arguments is not available, return an error + // immediately. + if (ton === undefined || npi === undefined || !options.smscAddress) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + // Remove all illegal characters in the number string for user-input fault + // tolerance. + let numStart = options.smscAddress[0] === "+" ? 1 : 0; + let number = options.smscAddress.substring(0, numStart) + + options.smscAddress.substring(numStart) + .replace(/[^0-9*#abc]/ig, ""); + + // If the filtered number is an empty string, return an error immediately. + if (number.length === 0) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + // Init parcel. + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_SMSC_ADDRESS, options); + + // +---+-----------+---------------+ + // | 1 | TON | NPI | + // +---+-----------+---------------+ + let tosca = (0x1 << 7) + (ton << 4) + npi; + if (RILQUIRKS_SMSC_ADDRESS_FORMAT === "pdu") { + let pduHelper = this.context.GsmPDUHelper; + + // Remove the preceding '+', and covert the special BCD digits defined in + // |Called party BCD number| of 3GPP TS 24.008 to corresponding + // hexadecimal values (refer the following table). + // + // +=========+=======+=====+ + // | value | digit | hex | + // +======================== + // | 1 0 1 0 | * | 0xA | + // | 1 0 1 1 | # | 0xB | + // | 1 1 0 0 | a | 0xC | + // | 1 1 0 1 | b | 0xD | + // | 1 1 1 0 | c | 0xE | + // +=========+=======+=====+ + // + // The replace order is reversed intentionally, because if the digits are + // replaced in ascending order, "#" will be converted to "b" and then be + // converted again to "d", which generates incorrect result. + let pureNumber = number.substring(numStart) + .replace(/c/ig, "e") + .replace(/b/ig, "d") + .replace(/a/ig, "c") + .replace(/\#/g, "b") + .replace(/\*/g, "a"); + + // address length and string length + let length = Math.ceil(pureNumber.length / 2) + 1; // +1 octet for TOA + let strlen = length * 2 + 2; // +2 semi-octets for length octet + + Buf.writeInt32(strlen); + pduHelper.writeHexOctet(length); + pduHelper.writeHexOctet(tosca); + pduHelper.writeSwappedNibbleBCD(pureNumber); + Buf.writeStringDelimiter(strlen); + } else /* RILQUIRKS_SMSC_ADDRESS_FORMAT === "text" */ { + let sca; + sca = '"' + number + '"' + ',' + tosca; + Buf.writeString(sca); + } + + Buf.sendParcel(); + }, + + /** + * Setup a data call. + * + * @param radioTech + * Integer to indicate radio technology. + * DATACALL_RADIOTECHNOLOGY_CDMA => CDMA. + * DATACALL_RADIOTECHNOLOGY_GSM => GSM. + * @param apn + * String containing the name of the APN to connect to. + * @param user + * String containing the username for the APN. + * @param passwd + * String containing the password for the APN. + * @param chappap + * Integer containing CHAP/PAP auth type. + * DATACALL_AUTH_NONE => PAP and CHAP is never performed. + * DATACALL_AUTH_PAP => PAP may be performed. + * DATACALL_AUTH_CHAP => CHAP may be performed. + * DATACALL_AUTH_PAP_OR_CHAP => PAP / CHAP may be performed. + * @param pdptype + * String containing PDP type to request. ("IP", "IPV6", ...) + */ + setupDataCall: function(options) { + // From ./hardware/ril/include/telephony/ril.h: + // ((const char **)data)[0] Radio technology to use: 0-CDMA, 1-GSM/UMTS, 2... + // for values above 2 this is RIL_RadioTechnology + 2. + // + // From frameworks/base/telephony/java/com/android/internal/telephony/DataConnection.java: + // if the mRilVersion < 6, radio technology must be GSM/UMTS or CDMA. + // Otherwise, it must be + 2 + // + // See also bug 901232 and 867873 + let radioTech = options.radioTech + 2; + let Buf = this.context.Buf; + let token = Buf.newParcel(REQUEST_SETUP_DATA_CALL, options); + Buf.writeInt32(7); + Buf.writeString(radioTech.toString()); + Buf.writeString(DATACALL_PROFILE_DEFAULT.toString()); + Buf.writeString(options.apn); + Buf.writeString(options.user); + Buf.writeString(options.passwd); + Buf.writeString(options.chappap.toString()); + Buf.writeString(options.pdptype); + Buf.sendParcel(); + return token; + }, + + /** + * Deactivate a data call. + * + * @param cid + * String containing CID. + * @param reason + * One of DATACALL_DEACTIVATE_* constants. + */ + deactivateDataCall: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DEACTIVATE_DATA_CALL, options); + Buf.writeInt32(2); + Buf.writeString(options.cid.toString()); + Buf.writeString(options.reason !== undefined ? + options.reason.toString() : + DATACALL_DEACTIVATE_NO_REASON.toString()); + Buf.sendParcel(); + }, + + /** + * Get a list of data calls. + */ + getDataCallList: function(options) { + this.context.Buf.simpleRequest(REQUEST_DATA_CALL_LIST, options); + }, + + _attachDataRegistration: false, + /** + * Manually attach/detach data registration. + * + * @param attach + * Boolean value indicating attach or detach. + */ + setDataRegistration: function(options) { + this._attachDataRegistration = options.attach; + + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + let request = options.attach ? RIL_REQUEST_GPRS_ATTACH : + RIL_REQUEST_GPRS_DETACH; + this.context.Buf.simpleRequest(request, options); + return; + } else if (RILQUIRKS_SUBSCRIPTION_CONTROL && options.attach) { + this.context.Buf.simpleRequest(REQUEST_SET_DATA_SUBSCRIPTION, options); + return; + } + + // We don't really send a request to rild, so instantly reply success to + // RadioInterfaceLayer. + this.sendChromeMessage(options); + }, + + /** + * Get failure casue code for the most recently failed PDP context. + */ + getFailCause: function(options) { + this.context.Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE, options); + }, + + /** + * Send USSD. + * + * @param ussd + * String containing the USSD code. + */ + sendUSSD: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SEND_USSD, options); + Buf.writeString(options.ussd); + Buf.sendParcel(); + }, + + /** + * Cancel pending USSD. + */ + cancelUSSD: function(options) { + this.context.Buf.simpleRequest(REQUEST_CANCEL_USSD, options); + }, + + /** + * Queries current call forward rules. + * + * @param reason + * One of CALL_FORWARD_REASON_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + * @param number + * Phone number of forwarding address. + */ + queryCallForwardStatus: function(options) { + let Buf = this.context.Buf; + let number = options.number || ""; + Buf.newParcel(REQUEST_QUERY_CALL_FORWARD_STATUS, options); + Buf.writeInt32(CALL_FORWARD_ACTION_QUERY_STATUS); + Buf.writeInt32(options.reason); + Buf.writeInt32(options.serviceClass || ICC_SERVICE_CLASS_NONE); + Buf.writeInt32(this._toaFromString(number)); + Buf.writeString(number); + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Configures call forward rule. + * + * @param action + * One of CALL_FORWARD_ACTION_* constants. + * @param reason + * One of CALL_FORWARD_REASON_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + * @param number + * Phone number of forwarding address. + * @param timeSeconds + * Time in seconds to wait beforec all is forwarded. + */ + setCallForward: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CALL_FORWARD, options); + Buf.writeInt32(options.action); + Buf.writeInt32(options.reason); + Buf.writeInt32(options.serviceClass); + Buf.writeInt32(this._toaFromString(options.number)); + Buf.writeString(options.number); + Buf.writeInt32(options.timeSeconds); + Buf.sendParcel(); + }, + + /** + * Queries current call barring rules. + * + * @param program + * One of CALL_BARRING_PROGRAM_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + queryCallBarringStatus: function(options) { + options.facility = CALL_BARRING_PROGRAM_TO_FACILITY[options.program]; + options.password = ""; // For query no need to provide it. + + // For some operators, querying specific serviceClass doesn't work. We use + // serviceClass 0 instead, and then process the response to extract the + // answer for queryServiceClass. + options.queryServiceClass = options.serviceClass; + options.serviceClass = 0; + + this.queryICCFacilityLock(options); + }, + + /** + * Configures call barring rule. + * + * @param program + * One of CALL_BARRING_PROGRAM_* constants. + * @param enabled + * Enable or disable the call barring. + * @param password + * Barring password. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + setCallBarring: function(options) { + options.facility = CALL_BARRING_PROGRAM_TO_FACILITY[options.program]; + this.setICCFacilityLock(options); + }, + + /** + * Change call barring facility password. + * + * @param pin + * Old password. + * @param newPin + * New password. + */ + changeCallBarringPassword: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_BARRING_PASSWORD, options); + Buf.writeInt32(3); + // Set facility to ICC_CB_FACILITY_BA_ALL by following TS.22.030 clause + // 6.5.4 and Table B.1. + Buf.writeString(ICC_CB_FACILITY_BA_ALL); + Buf.writeString(options.pin); + Buf.writeString(options.newPin); + Buf.sendParcel(); + }, + + /** + * Handle STK CALL_SET_UP request. + * + * @param hasConfirmed + * Does use have confirmed the call requested from ICC? + */ + stkHandleCallSetup: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM); + Buf.writeInt32(1); + Buf.writeInt32(options.hasConfirmed ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Send STK Profile Download. + * + * @param profile Profile supported by ME. + */ + sendStkTerminalProfile: function(profile) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_STK_SET_PROFILE); + Buf.writeInt32(profile.length * 2); + for (let i = 0; i < profile.length; i++) { + GsmPDUHelper.writeHexOctet(profile[i]); + } + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Send STK terminal response. + * + * @param command + * @param deviceIdentities + * @param resultCode + * @param [optional] additionalInformation + * @param [optional] itemIdentifier + * @param [optional] input + * @param [optional] isYesNo + * @param [optional] hasConfirmed + * @param [optional] localInfo + * @param [optional] timer + */ + sendStkTerminalResponse: function(response) { + if (response.hasConfirmed !== undefined) { + this.stkHandleCallSetup(response); + return; + } + + let Buf = this.context.Buf; + let ComprehensionTlvHelper = this.context.ComprehensionTlvHelper; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let command = response.command; + Buf.newParcel(REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // 1st mark for Parcel size + Buf.startCalOutgoingSize(function(size) { + // Parcel size is in string length, which costs 2 uint8 per char. + Buf.writeInt32(size / 2); + }); + + // Command Details + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(3); + if (command) { + GsmPDUHelper.writeHexOctet(command.commandNumber); + GsmPDUHelper.writeHexOctet(command.typeOfCommand); + GsmPDUHelper.writeHexOctet(command.commandQualifier); + } else { + GsmPDUHelper.writeHexOctet(0x00); + GsmPDUHelper.writeHexOctet(0x00); + GsmPDUHelper.writeHexOctet(0x00); + } + + // Device Identifier + // According to TS102.223/TS31.111 section 6.8 Structure of + // TERMINAL RESPONSE, "For all SIMPLE-TLV objects with Min=N, + // the ME should set the CR(comprehension required) flag to + // comprehension not required.(CR=0)" + // Since DEVICE_IDENTITIES and DURATION TLVs have Min=N, + // the CR flag is not set. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_ME); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_SIM); + + // Result + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + if ("additionalInformation" in response) { + // In |12.12 Result| TS 11.14, the length of additional information is + // varied and all possible values are addressed in 12.12.1-11 of TS 11.14 + // and 8.12.1-13 in TS 31.111. + // However, + // 1. Only SEND SS requires info with more than 1 octet. + // 2. In rild design, SEND SS is expected to be handled by modem and + // UNSOLICITED_STK_EVENT_NOTIFY will be sent to application layer to + // indicate appropriate messages to users. TR is not required in this + // case. + // Hence, we simplify the structure of |additionalInformation| to a + // numeric value instead of a octet array. + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(response.resultCode); + GsmPDUHelper.writeHexOctet(response.additionalInformation); + } else { + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(response.resultCode); + } + + // Item Identifier + if (response.itemIdentifier != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ITEM_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(response.itemIdentifier); + } + + // No need to process Text data if user requests help information. + if (response.resultCode != STK_RESULT_HELP_INFO_REQUIRED) { + let text; + let coding = command.options.isUCS2 ? + STK_TEXT_CODING_UCS2 : + (command.options.isPacked ? + STK_TEXT_CODING_GSM_7BIT_PACKED : + STK_TEXT_CODING_GSM_8BIT); + if (response.isYesNo !== undefined) { + // Tag: GET_INKEY + // When the ME issues a successful TERMINAL RESPONSE for a GET INKEY + // ("Yes/No") command with command qualifier set to "Yes/No", it shall + // supply the value '01' when the answer is "positive" and the value + // '00' when the answer is "negative" in the Text string data object. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + // Length: 2 + GsmPDUHelper.writeHexOctet(2); + // Value: Coding, Yes/No. + GsmPDUHelper.writeHexOctet(coding); + GsmPDUHelper.writeHexOctet(response.isYesNo ? 0x01 : 0x00); + } else { + if (response.input !== undefined) { + ComprehensionTlvHelper.writeTextStringTlv(response.input, coding); + } + } + } + + // Duration + if (response.resultCode === STK_RESULT_NO_RESPONSE_FROM_USER) { + // In TS102 223, 6.4.2 GET INKEY, "if the UICC requests a variable timeout, + // the terminal shall wait until either the user enters a single character + // or the timeout expires. The timer starts when the text is displayed on + // the screen and stops when the TERMINAL RESPONSE is sent. The terminal + // shall pass the total display text duration (command execution duration) + // to the UICC using the TERMINAL RESPONSE. The time unit of the response + // is identical to the time unit of the requested variable timeout." + let duration = command && command.options && command.options.duration; + if (duration) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DURATION); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(duration.timeUnit); + GsmPDUHelper.writeHexOctet(duration.timeInterval); + } + } + + // Local Information + if (response.localInfo) { + let localInfo = response.localInfo; + + // Location Infomation + if (localInfo.locationInfo) { + ComprehensionTlvHelper.writeLocationInfoTlv(localInfo.locationInfo); + } + + // IMEI + if (localInfo.imei != null) { + let imei = localInfo.imei; + if (imei.length == 15) { + imei = imei + "0"; + } + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_IMEI); + GsmPDUHelper.writeHexOctet(8); + for (let i = 0; i < imei.length / 2; i++) { + GsmPDUHelper.writeHexOctet(parseInt(imei.substr(i * 2, 2), 16)); + } + } + + // Date and Time Zone + if (localInfo.date != null) { + ComprehensionTlvHelper.writeDateTimeZoneTlv(localInfo.date); + } + + // Language + if (localInfo.language) { + ComprehensionTlvHelper.writeLanguageTlv(localInfo.language); + } + } + + // Timer + if (response.timer) { + let timer = response.timer; + + if (timer.timerId) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(timer.timerId); + } + + if (timer.timerValue) { + ComprehensionTlvHelper.writeTimerValueTlv(timer.timerValue, false); + } + } + + // Calculate and write Parcel size to 1st mark + Buf.stopCalOutgoingSize(); + + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Send STK Envelope(Menu Selection) command. + * + * @param itemIdentifier + * @param helpRequested + */ + sendStkMenuSelection: function(command) { + command.tag = BER_MENU_SELECTION_TAG; + command.deviceId = { + sourceId :STK_DEVICE_ID_KEYPAD, + destinationId: STK_DEVICE_ID_SIM + }; + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send STK Envelope(Timer Expiration) command. + * + * @param timer + */ + sendStkTimerExpiration: function(command) { + command.tag = BER_TIMER_EXPIRATION_TAG; + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.timerId = command.timer.timerId; + command.timerValue = command.timer.timerValue; + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send STK Envelope(Event Download) command. + * @param event + */ + sendStkEventDownload: function(command) { + command.tag = BER_EVENT_DOWNLOAD_TAG; + command.eventList = command.event.eventType; + switch (command.eventList) { + case STK_EVENT_TYPE_LOCATION_STATUS: + command.deviceId = { + sourceId :STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.locationStatus = command.event.locationStatus; + // Location info should only be provided when locationStatus is normal. + if (command.locationStatus == STK_SERVICE_STATE_NORMAL) { + command.locationInfo = command.event.locationInfo; + } + break; + case STK_EVENT_TYPE_MT_CALL: + command.deviceId = { + sourceId: STK_DEVICE_ID_NETWORK, + destinationId: STK_DEVICE_ID_SIM + }; + command.transactionId = 0; + command.address = command.event.number; + break; + case STK_EVENT_TYPE_CALL_DISCONNECTED: + command.cause = command.event.error; + // Fall through. + case STK_EVENT_TYPE_CALL_CONNECTED: + command.deviceId = { + sourceId: (command.event.isIssuedByRemote ? + STK_DEVICE_ID_NETWORK : STK_DEVICE_ID_ME), + destinationId: STK_DEVICE_ID_SIM + }; + command.transactionId = 0; + break; + case STK_EVENT_TYPE_USER_ACTIVITY: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + break; + case STK_EVENT_TYPE_IDLE_SCREEN_AVAILABLE: + command.deviceId = { + sourceId: STK_DEVICE_ID_DISPLAY, + destinationId: STK_DEVICE_ID_SIM + }; + break; + case STK_EVENT_TYPE_LANGUAGE_SELECTION: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.language = command.event.language; + break; + case STK_EVENT_TYPE_BROWSER_TERMINATION: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.terminationCause = command.event.terminationCause; + break; + } + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send REQUEST_STK_SEND_ENVELOPE_COMMAND to ICC. + * + * @param tag + * @patam deviceId + * @param [optioanl] itemIdentifier + * @param [optional] helpRequested + * @param [optional] eventList + * @param [optional] locationStatus + * @param [optional] locationInfo + * @param [optional] address + * @param [optional] transactionId + * @param [optional] cause + * @param [optional] timerId + * @param [optional] timerValue + * @param [optional] terminationCause + */ + sendICCEnvelopeCommand: function(options) { + if (DEBUG) { + this.context.debug("Stk Envelope " + JSON.stringify(options)); + } + + let Buf = this.context.Buf; + let ComprehensionTlvHelper = this.context.ComprehensionTlvHelper; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // 1st mark for Parcel size + Buf.startCalOutgoingSize(function(size) { + // Parcel size is in string length, which costs 2 uint8 per char. + Buf.writeInt32(size / 2); + }); + + // Write a BER-TLV + GsmPDUHelper.writeHexOctet(options.tag); + // 2nd mark for BER length + Buf.startCalOutgoingSize(function(size) { + // BER length is in number of hexOctets, which costs 4 uint8 per hexOctet. + GsmPDUHelper.writeHexOctet(size / 4); + }); + + // Event List + if (options.eventList != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.eventList); + } + + // Device Identifies + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(options.deviceId.sourceId); + GsmPDUHelper.writeHexOctet(options.deviceId.destinationId); + + // Item Identifier + if (options.itemIdentifier != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ITEM_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.itemIdentifier); + } + + // Help Request + if (options.helpRequested) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_HELP_REQUEST | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(0); + // Help Request doesn't have value + } + + // Location Status + if (options.locationStatus != null) { + let len = options.locationStatus.length; + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LOCATION_STATUS | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.locationStatus); + } + + // Location Info + if (options.locationInfo) { + ComprehensionTlvHelper.writeLocationInfoTlv(options.locationInfo); + } + + // Transaction Id + if (options.transactionId != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TRANSACTION_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.transactionId); + } + + // Address + if (options.address) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ADDRESS | + COMPREHENSIONTLV_FLAG_CR); + let addressLength = options.address[0] == '+' ? options.address.length - 1 + : options.address.length; + ComprehensionTlvHelper.writeLength( + Math.ceil(addressLength / 2) + 1 // address BCD + TON + ); + this.context.ICCPDUHelper.writeDiallingNumber(options.address); + } + + // Cause of disconnection. + if (options.cause != null) { + ComprehensionTlvHelper.writeCauseTlv(options.cause); + } + + // Timer Identifier + if (options.timerId != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.timerId); + } + + // Timer Value + if (options.timerValue != null) { + ComprehensionTlvHelper.writeTimerValueTlv(options.timerValue, true); + } + + // Language + if (options.language) { + ComprehensionTlvHelper.writeLanguageTlv(options.language); + } + + // Browser Termination + if (options.terminationCause != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_BROWSER_TERMINATION_CAUSE | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.terminationCause); + } + + // Calculate and write BER length to 2nd mark + Buf.stopCalOutgoingSize(); + + // Calculate and write Parcel size to 1st mark + Buf.stopCalOutgoingSize(); + + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Report STK Service is running. + */ + reportStkServiceIsRunning: function() { + this.context.Buf.simpleRequest(REQUEST_REPORT_STK_SERVICE_IS_RUNNING); + }, + + /** + * Process ICC status. + */ + _processICCStatus: function(iccStatus) { + // If |_waitingRadioTech| is true, we should not get app information because + // the |_isCdma| flag is not ready yet. Otherwise we may use wrong index to + // get app information, especially for the case that icc card has both cdma + // and gsm subscription. + if (this._waitingRadioTech) { + return; + } + + // When |iccStatus.cardState| is not CARD_STATE_PRESENT, set cardState to + // undetected. + if (iccStatus.cardState !== CARD_STATE_PRESENT) { + if (this.cardState !== GECKO_CARDSTATE_UNDETECTED) { + this.operator = null; + // We should send |cardstatechange| before |iccinfochange|, otherwise we + // may lost cardstatechange event when icc card becomes undetected. + this.cardState = GECKO_CARDSTATE_UNDETECTED; + this.sendChromeMessage({rilMessageType: "cardstatechange", + cardState: this.cardState}); + + this.iccInfo = {iccType: null}; + this.context.ICCUtilsHelper.handleICCInfoChange(); + } + return; + } + + if (RILQUIRKS_SUBSCRIPTION_CONTROL) { + // All appIndex is -1 means the subscription is not activated yet. + // Note that we don't support "ims" for now, so we don't take it into + // account. + let neetToActivate = iccStatus.cdmaSubscriptionAppIndex === -1 && + iccStatus.gsmUmtsSubscriptionAppIndex === -1; + if (neetToActivate && + // Note: setUiccSubscription works abnormally when RADIO is OFF, + // which causes SMS function broken in Flame. + // See bug 1008557 for detailed info. + this.radioState === GECKO_RADIOSTATE_ENABLED) { + for (let i = 0; i < iccStatus.apps.length; i++) { + this.setUiccSubscription({appIndex: i, enabled: true}); + } + } + } + + let newCardState; + let index = this._isCdma ? iccStatus.cdmaSubscriptionAppIndex + : iccStatus.gsmUmtsSubscriptionAppIndex; + let app = iccStatus.apps[index]; + if (app) { + // fetchICCRecords will need to read aid, so read aid here. + this.aid = app.aid; + this.appType = app.app_type; + this.iccInfo.iccType = GECKO_CARD_TYPE[this.appType]; + + switch (app.app_state) { + case CARD_APPSTATE_ILLEGAL: + newCardState = GECKO_CARDSTATE_ILLEGAL; + break; + case CARD_APPSTATE_PIN: + newCardState = GECKO_CARDSTATE_PIN_REQUIRED; + break; + case CARD_APPSTATE_PUK: + newCardState = GECKO_CARDSTATE_PUK_REQUIRED; + break; + case CARD_APPSTATE_SUBSCRIPTION_PERSO: + newCardState = PERSONSUBSTATE[app.perso_substate]; + break; + case CARD_APPSTATE_READY: + newCardState = GECKO_CARDSTATE_READY; + break; + case CARD_APPSTATE_UNKNOWN: + case CARD_APPSTATE_DETECTED: + // Fall through. + default: + newCardState = GECKO_CARDSTATE_UNKNOWN; + } + + let pin1State = app.pin1_replaced ? iccStatus.universalPINState : + app.pin1; + if (pin1State === CARD_PINSTATE_ENABLED_PERM_BLOCKED) { + newCardState = GECKO_CARDSTATE_PERMANENT_BLOCKED; + } + } else { + // Having incorrect app information, set card state to unknown. + newCardState = GECKO_CARDSTATE_UNKNOWN; + } + + let ICCRecordHelper = this.context.ICCRecordHelper; + // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. + if (iccStatus.cardState === CARD_STATE_PRESENT && + (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || + this.cardState === GECKO_CARDSTATE_UNDETECTED)) { + ICCRecordHelper.readICCID(); + } + + if (this.cardState == newCardState) { + return; + } + + // This was moved down from CARD_APPSTATE_READY + this.requestNetworkInfo(); + if (newCardState == GECKO_CARDSTATE_READY) { + // For type SIM, we need to check EF_phase first. + // Other types of ICC we can send Terminal_Profile immediately. + if (this.appType == CARD_APPTYPE_SIM) { + this.context.SimRecordHelper.readSimPhase(); + } else if (RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD) { + this.sendStkTerminalProfile(STK_SUPPORTED_TERMINAL_PROFILE); + } + + ICCRecordHelper.fetchICCRecords(); + } + + this.cardState = newCardState; + this.sendChromeMessage({rilMessageType: "cardstatechange", + cardState: this.cardState}); + }, + + /** + * Helper for processing responses of functions such as enterICC* and changeICC*. + */ + _processEnterAndChangeICCResponses: function(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); + }, + + // We combine all of the NETWORK_INFO_MESSAGE_TYPES into one "networkinfochange" + // message to the RadioInterfaceLayer, so we can avoid sending multiple + // VoiceInfoChanged events for both operator / voice_data_registration + // + // State management here is a little tricky. We need to know both: + // 1. Whether or not a response was received for each of the + // NETWORK_INFO_MESSAGE_TYPES + // 2. The outbound message that corresponds with that response -- but this + // only happens when internal state changes (i.e. it isn't guaranteed) + // + // To collect this state, each message response function first calls + // _receivedNetworkInfo, to mark the response as received. When the + // final response is received, a call to _sendPendingNetworkInfo is placed + // on the next tick of the worker thread. + // + // Since the original call to _receivedNetworkInfo happens at the top + // of the response handler, this gives the final handler a chance to + // queue up it's "changed" message by calling _sendNetworkInfoMessage if/when + // the internal state has actually changed. + _sendNetworkInfoMessage: function(type, message) { + if (!this._processingNetworkInfo) { + // We only combine these messages in the case of the combined request + // in requestNetworkInfo() + this.sendChromeMessage(message); + return; + } + + if (DEBUG) { + this.context.debug("Queuing " + type + " network info message: " + + JSON.stringify(message)); + } + this._pendingNetworkInfo[type] = message; + }, + + _receivedNetworkInfo: function(type) { + if (DEBUG) this.context.debug("Received " + type + " network info."); + if (!this._processingNetworkInfo) { + return; + } + + let pending = this._pendingNetworkInfo; + + // We still need to track states for events that aren't fired. + if (!(type in pending)) { + pending[type] = this.pendingNetworkType; + } + + // Pending network info is ready to be sent when no more messages + // are waiting for responses, but the combined payload hasn't been sent. + for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) { + let msgType = NETWORK_INFO_MESSAGE_TYPES[i]; + if (!(msgType in pending)) { + if (DEBUG) { + this.context.debug("Still missing some more network info, not " + + "notifying main thread."); + } + return; + } + } + + // Do a pass to clean up the processed messages that didn't create + // a response message, so we don't have unused keys in the outbound + // networkinfochanged message. + for (let key in pending) { + if (pending[key] == this.pendingNetworkType) { + delete pending[key]; + } + } + + if (DEBUG) { + this.context.debug("All pending network info has been received: " + + JSON.stringify(pending)); + } + + // Send the message on the next tick of the worker's loop, so we give the + // last message a chance to call _sendNetworkInfoMessage first. + setTimeout(this._sendPendingNetworkInfo.bind(this), 0); + }, + + _sendPendingNetworkInfo: function() { + this.sendChromeMessage(this._pendingNetworkInfo); + + this._processingNetworkInfo = false; + for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) { + delete this._pendingNetworkInfo[NETWORK_INFO_MESSAGE_TYPES[i]]; + } + + if (this._needRepollNetworkInfo) { + this._needRepollNetworkInfo = false; + this.requestNetworkInfo(); + } + }, + + /** + * Normalize the signal strength in dBm to the signal level from 0 to 100. + * + * @param signal + * The signal strength in dBm to normalize. + * @param min + * The signal strength in dBm maps to level 0. + * @param max + * The signal strength in dBm maps to level 100. + * + * @return level + * The signal level from 0 to 100. + */ + _processSignalLevel: function(signal, min, max) { + if (signal <= min) { + return 0; + } + + if (signal >= max) { + return 100; + } + + return Math.floor((signal - min) * 100 / (max - min)); + }, + + /** + * Process LTE signal strength to the signal info object. + * + * @param signal + * The signal object reported from RIL/modem. + * + * @return The object of signal strength info. + * Or null if invalid signal input. + * + * TODO: Bug 982013: reconsider the format of signal strength APIs for + * GSM/CDMA/LTE to expose details, such as rsrp and rsnnr, + * individually. + */ + _processLteSignal: function(signal) { + let info = { + voice: { + signalStrength: null, + relSignalStrength: null + }, + data: { + signalStrength: null, + relSignalStrength: null + } + }; + + // Referring to AOSP, use lteRSRP for signalStrength in dBm. + let signalStrength = (signal.lteRSRP === undefined || signal.lteRSRP === 0x7FFFFFFF) ? + null : signal.lteRSRP; + info.voice.signalStrength = info.data.signalStrength = signalStrength; + + // Referring to AOSP, first determine signalLevel based on RSRP and RSSNR, + // then on lteSignalStrength if RSRP and RSSNR are invalid. + let rsrpLevel = -1; + let rssnrLevel = -1; + if (signal.lteRSRP !== undefined && + signal.lteRSRP !== 0x7FFFFFFF && + signal.lteRSRP >= 44 && + signal.lteRSRP <= 140) { + rsrpLevel = this._processSignalLevel(signal.lteRSRP * -1, -115, -85); + } + + if (signal.lteRSSNR !== undefined && + signal.lteRSSNR !== 0x7FFFFFFF && + signal.lteRSSNR >= -200 && + signal.lteRSSNR <= 300) { + rssnrLevel = this._processSignalLevel(signal.lteRSSNR, -30, 130); + } + + if (rsrpLevel !== -1 && rssnrLevel !== -1) { + info.voice.relSignalStrength = info.data.relSignalStrength = + Math.min(rsrpLevel, rssnrLevel); + return info; + } + + let level = Math.max(rsrpLevel, rssnrLevel); + if (level !== -1) { + info.voice.relSignalStrength = info.data.relSignalStrength = level; + return info; + } + + // Valid values are 0-63 as defined in TS 27.007 clause 8.69. + if (signal.lteSignalStrength !== undefined && + signal.lteSignalStrength >= 0 && + signal.lteSignalStrength <= 63) { + level = this._processSignalLevel(signal.lteSignalStrength, 0, 12); + info.voice.relSignalStrength = info.data.relSignalStrength = level; + return info; + } + + return null; + }, + + _processSignalStrength: function(signal) { + let info = { + voice: { + signalStrength: null, + relSignalStrength: null + }, + data: { + signalStrength: null, + relSignalStrength: null + } + }; + + // During startup, |radioTech| is not yet defined, so we need to + // check it separately. + if (("radioTech" in this.voiceRegistrationState) && + !this._isGsmTechGroup(this.voiceRegistrationState.radioTech)) { + // CDMA RSSI. + // Valid values are positive integers. This value is the actual RSSI value + // multiplied by -1. Example: If the actual RSSI is -75, then this + // response value will be 75. + if (signal.cdmaDBM && signal.cdmaDBM > 0) { + let signalStrength = -1 * signal.cdmaDBM; + info.voice.signalStrength = signalStrength; + + // -105 and -70 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -105, -70); + info.voice.relSignalStrength = signalLevel; + } + + // EVDO RSSI. + // Valid values are positive integers. This value is the actual RSSI value + // multiplied by -1. Example: If the actual RSSI is -75, then this + // response value will be 75. + if (signal.evdoDBM && signal.evdoDBM > 0) { + let signalStrength = -1 * signal.evdoDBM; + info.data.signalStrength = signalStrength; + + // -105 and -70 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -105, -70); + info.data.relSignalStrength = signalLevel; + } + } else { + // Check LTE level first, and check GSM/UMTS level next if LTE one is not + // valid. + let lteInfo = this._processLteSignal(signal); + if (lteInfo) { + info = lteInfo; + } else { + // GSM signal strength. + // Valid values are 0-31 as defined in TS 27.007 8.5. + // 0 : -113 dBm or less + // 1 : -111 dBm + // 2...30: -109...-53 dBm + // 31 : -51 dBm + if (signal.gsmSignalStrength && + signal.gsmSignalStrength >= 0 && + signal.gsmSignalStrength <= 31) { + let signalStrength = -113 + 2 * signal.gsmSignalStrength; + info.voice.signalStrength = info.data.signalStrength = signalStrength; + + // -115 and -85 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -110, -85); + info.voice.relSignalStrength = info.data.relSignalStrength = signalLevel; + } + } + } + + info.rilMessageType = "signalstrengthchange"; + this._sendNetworkInfoMessage(NETWORK_INFO_SIGNAL, info); + }, + + /** + * Process the network registration flags. + * + * @return true if the state changed, false otherwise. + */ + _processCREG: function(curState, newState) { + let changed = false; + + let regState = this.parseInt(newState[0], NETWORK_CREG_STATE_UNKNOWN); + if (curState.regState === undefined || curState.regState !== regState) { + changed = true; + curState.regState = regState; + + curState.state = NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[regState]; + curState.connected = regState == NETWORK_CREG_STATE_REGISTERED_HOME || + regState == NETWORK_CREG_STATE_REGISTERED_ROAMING; + curState.roaming = regState == NETWORK_CREG_STATE_REGISTERED_ROAMING; + curState.emergencyCallsOnly = !curState.connected; + } + + if (!curState.cell) { + curState.cell = {}; + } + + // From TS 23.003, 0000 and 0xfffe are indicated that no valid LAI exists + // in MS. So we still need to report the '0000' as well. + let lac = this.parseInt(newState[1], -1, 16); + if (curState.cell.gsmLocationAreaCode === undefined || + curState.cell.gsmLocationAreaCode !== lac) { + curState.cell.gsmLocationAreaCode = lac; + changed = true; + } + + let cid = this.parseInt(newState[2], -1, 16); + if (curState.cell.gsmCellId === undefined || + curState.cell.gsmCellId !== cid) { + curState.cell.gsmCellId = cid; + changed = true; + } + + let radioTech = (newState[3] === undefined ? + NETWORK_CREG_TECH_UNKNOWN : + this.parseInt(newState[3], NETWORK_CREG_TECH_UNKNOWN)); + if (curState.radioTech === undefined || curState.radioTech !== radioTech) { + changed = true; + curState.radioTech = radioTech; + curState.type = GECKO_RADIO_TECH[radioTech] || null; + } + return changed; + }, + + _processVoiceRegistrationState: function(state) { + let rs = this.voiceRegistrationState; + let stateChanged = this._processCREG(rs, state); + if (stateChanged && rs.connected) { + this.getSmscAddress(); + } + + let cell = rs.cell; + if (this._isCdma) { + // Some variables below are not used. Comment them instead of removing to + // keep the information about state[x]. + let cdmaBaseStationId = this.parseInt(state[4], -1); + let cdmaBaseStationLatitude = this.parseInt(state[5], -2147483648); + let cdmaBaseStationLongitude = this.parseInt(state[6], -2147483648); + // let cssIndicator = this.parseInt(state[7]); + let cdmaSystemId = this.parseInt(state[8], -1); + let cdmaNetworkId = this.parseInt(state[9], -1); + // let roamingIndicator = this.parseInt(state[10]); + // let systemIsInPRL = this.parseInt(state[11]); + // let defaultRoamingIndicator = this.parseInt(state[12]); + // let reasonForDenial = this.parseInt(state[13]); + + if (cell.cdmaBaseStationId !== cdmaBaseStationId || + cell.cdmaBaseStationLatitude !== cdmaBaseStationLatitude || + cell.cdmaBaseStationLongitude !== cdmaBaseStationLongitude || + cell.cdmaSystemId !== cdmaSystemId || + cell.cdmaNetworkId !== cdmaNetworkId) { + stateChanged = true; + cell.cdmaBaseStationId = cdmaBaseStationId; + cell.cdmaBaseStationLatitude = cdmaBaseStationLatitude; + cell.cdmaBaseStationLongitude = cdmaBaseStationLongitude; + cell.cdmaSystemId = cdmaSystemId; + cell.cdmaNetworkId = cdmaNetworkId; + } + } + + if (stateChanged) { + rs.rilMessageType = "voiceregistrationstatechange"; + this._sendNetworkInfoMessage(NETWORK_INFO_VOICE_REGISTRATION_STATE, rs); + } + }, + + _processDataRegistrationState: function(state) { + let rs = this.dataRegistrationState; + let stateChanged = this._processCREG(rs, state); + if (stateChanged) { + rs.rilMessageType = "dataregistrationstatechange"; + this._sendNetworkInfoMessage(NETWORK_INFO_DATA_REGISTRATION_STATE, rs); + } + }, + + _processOperator: function(operatorData) { + if (operatorData.length < 3) { + if (DEBUG) { + this.context.debug("Expected at least 3 strings for operator."); + } + } + + if (!this.operator) { + this.operator = { + rilMessageType: "operatorchange", + longName: null, + shortName: null + }; + } + + let [longName, shortName, networkTuple] = operatorData; + let thisTuple = (this.operator.mcc || "") + (this.operator.mnc || ""); + + if (this.operator.longName !== longName || + this.operator.shortName !== shortName || + thisTuple !== networkTuple) { + + this.operator.mcc = null; + this.operator.mnc = null; + + if (networkTuple) { + try { + this._processNetworkTuple(networkTuple, this.operator); + } catch (e) { + if (DEBUG) this.context.debug("Error processing operator tuple: " + e); + } + } else { + // According to ril.h, the operator fields will be NULL when the operator + // is not currently registered. We can avoid trying to parse the numeric + // tuple in that case. + if (DEBUG) { + this.context.debug("Operator is currently unregistered"); + } + } + + this.operator.longName = longName; + this.operator.shortName = shortName; + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.updateDisplayCondition()) { + ICCUtilsHelper.handleICCInfoChange(); + } + + // NETWORK_INFO_OPERATOR message will be sent out by overrideICCNetworkName + // itself if operator name is overridden after checking, or we have to + // do it by ourself. + if (!this.overrideICCNetworkName()) { + this._sendNetworkInfoMessage(NETWORK_INFO_OPERATOR, this.operator); + } + } + }, + + _processSuppSvcNotification: function(info) { + if (DEBUG) { + this.context.debug("handle supp svc notification: " + JSON.stringify(info)); + } + + if (info.notificationType !== 1) { + // We haven't supported MO intermediate result code, i.e. + // info.notificationType === 0, which refers to code1 defined in 3GPP + // 27.007 7.17. We only support partial MT unsolicited result code, + // referring to code2, for now. + return; + } + + let notification = null; + + switch (info.code) { + case SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD: + case SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED: + notification = GECKO_SUPP_SVC_NOTIFICATION_FROM_CODE2[info.code]; + break; + default: + // Notification type not supported. + return; + } + + let message = {rilMessageType: "suppSvcNotification", + number: info.number, // could be empty. + notification: notification}; + this.sendChromeMessage(message); + }, + + _cancelEmergencyCbModeTimeout: function() { + if (this._exitEmergencyCbModeTimeoutID) { + clearTimeout(this._exitEmergencyCbModeTimeoutID); + this._exitEmergencyCbModeTimeoutID = null; + } + }, + + _handleChangedEmergencyCbMode: function(active) { + this._isInEmergencyCbMode = active; + + // Clear the existed timeout event. + this._cancelEmergencyCbModeTimeout(); + + // Start a new timeout event when entering the mode. + if (active) { + this._exitEmergencyCbModeTimeoutID = setTimeout( + this.exitEmergencyCbMode.bind(this), EMERGENCY_CB_MODE_TIMEOUT_MS); + } + + let message = {rilMessageType: "emergencyCbModeChange", + active: active, + timeoutMs: EMERGENCY_CB_MODE_TIMEOUT_MS}; + this.sendChromeMessage(message); + }, + + _updateNetworkSelectionMode: function(mode) { + if (this.networkSelectionMode === mode) { + return; + } + + let options = { + rilMessageType: "networkselectionmodechange", + mode: mode + }; + this.networkSelectionMode = mode; + this._sendNetworkInfoMessage(NETWORK_INFO_NETWORK_SELECTION_MODE, options); + }, + + _processNetworks: function() { + let strings = this.context.Buf.readStringList(); + let networks = []; + + for (let i = 0; i < strings.length; + i += RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING ? 5 : 4) { + let network = { + longName: strings[i], + shortName: strings[i + 1], + mcc: null, + mnc: null, + state: null + }; + + let networkTuple = strings[i + 2]; + try { + this._processNetworkTuple(networkTuple, network); + } catch (e) { + if (DEBUG) this.context.debug("Error processing operator tuple: " + e); + } + + let state = strings[i + 3]; + network.state = RIL_QAN_STATE_TO_GECKO_STATE[state]; + + networks.push(network); + } + return networks; + }, + + /** + * The "numeric" portion of the operator info is a tuple + * containing MCC (country code) and MNC (network code). + * AFAICT, MCC should always be 3 digits, making the remaining + * portion the MNC. + */ + _processNetworkTuple: function(networkTuple, network) { + let tupleLen = networkTuple.length; + + if (tupleLen == 5 || tupleLen == 6) { + network.mcc = networkTuple.substr(0, 3); + network.mnc = networkTuple.substr(3); + } else { + network.mcc = null; + network.mnc = null; + + throw new Error("Invalid network tuple (should be 5 or 6 digits): " + networkTuple); + } + }, + + /** + * Check if GSM radio access technology group. + */ + _isGsmTechGroup: function(radioTech) { + if (!radioTech) { + return true; + } + + switch(radioTech) { + case NETWORK_CREG_TECH_GPRS: + case NETWORK_CREG_TECH_EDGE: + case NETWORK_CREG_TECH_UMTS: + case NETWORK_CREG_TECH_HSDPA: + case NETWORK_CREG_TECH_HSUPA: + case NETWORK_CREG_TECH_HSPA: + case NETWORK_CREG_TECH_LTE: + case NETWORK_CREG_TECH_HSPAP: + case NETWORK_CREG_TECH_GSM: + case NETWORK_CREG_TECH_DCHSPAP_1: + case NETWORK_CREG_TECH_DCHSPAP_2: + return true; + } + + return false; + }, + + /** + * Process radio technology change. + */ + _processRadioTech: function(radioTech) { + let isCdma = !this._isGsmTechGroup(radioTech); + this.radioTech = radioTech; + + if (DEBUG) { + this.context.debug("Radio tech is set to: " + GECKO_RADIO_TECH[radioTech] + + ", it is a " + (isCdma?"cdma":"gsm") + " technology"); + } + + // We should request SIM information when + // 1. Radio state has been changed, so we are waiting for radioTech or + // 2. isCdma is different from this._isCdma. + if (this._waitingRadioTech || isCdma != this._isCdma) { + this._isCdma = isCdma; + this._waitingRadioTech = false; + this.getICCStatus(); + } + }, + + /** + * Helper for returning the TOA for the given dial string. + */ + _toaFromString: function(number) { + let toa = TOA_UNKNOWN; + if (number && number.length > 0 && number[0] == '+') { + toa = TOA_INTERNATIONAL; + } + return toa; + }, + + /** + * @param message A decoded SMS-DELIVER message. + * + * @see 3GPP TS 31.111 section 7.1.1 + */ + dataDownloadViaSMSPP: function(message) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let options = { + pid: message.pid, + dcs: message.dcs, + encoding: message.encoding, + }; + Buf.newParcel(REQUEST_STK_SEND_ENVELOPE_WITH_STATUS, options); + + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE)); // Skip response_type & request_type. + let messageStringLength = Buf.readInt32(); // In semi-octets + let smscLength = GsmPDUHelper.readHexOctet(); // In octets, inclusive of TOA + let tpduLength = (messageStringLength / 2) - (smscLength + 1); // In octets + + // Device identities: 4 bytes + // Address: 0 or (2 + smscLength) + // SMS TPDU: (2 or 3) + tpduLength + let berLen = 4 + + (smscLength ? (2 + smscLength) : 0) + + (tpduLength <= 127 ? 2 : 3) + tpduLength; // In octets + + let parcelLength = (berLen <= 127 ? 2 : 3) + berLen; // In octets + Buf.writeInt32(parcelLength * 2); // In semi-octets + + // Write a BER-TLV + GsmPDUHelper.writeHexOctet(BER_SMS_PP_DOWNLOAD_TAG); + if (berLen > 127) { + GsmPDUHelper.writeHexOctet(0x81); + } + GsmPDUHelper.writeHexOctet(berLen); + + // Device Identifies-TLV + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(0x02); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_NETWORK); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_SIM); + + // Address-TLV + if (smscLength) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ADDRESS); + GsmPDUHelper.writeHexOctet(smscLength); + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * smscLength); + } + + // SMS TPDU-TLV + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_SMS_TPDU | + COMPREHENSIONTLV_FLAG_CR); + if (tpduLength > 127) { + GsmPDUHelper.writeHexOctet(0x81); + } + GsmPDUHelper.writeHexOctet(tpduLength); + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * tpduLength); + + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * @param success A boolean value indicating the result of previous + * SMS-DELIVER message handling. + * @param responsePduLen ICC IO response PDU length in octets. + * @param options An object that contains four attributes: `pid`, `dcs`, + * `encoding` and `responsePduLen`. + * + * @see 3GPP TS 23.040 section 9.2.2.1a + */ + acknowledgeIncomingGsmSmsWithPDU: function(success, responsePduLen, options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU); + + // Two strings. + Buf.writeInt32(2); + + // String 1: Success + Buf.writeString(success ? "1" : "0"); + + // String 2: RP-ACK/RP-ERROR PDU + Buf.writeInt32(2 * (responsePduLen + (success ? 5 : 6))); // In semi-octet + // 1. TP-MTI & TP-UDHI + GsmPDUHelper.writeHexOctet(PDU_MTI_SMS_DELIVER); + if (!success) { + // 2. TP-FCS + GsmPDUHelper.writeHexOctet(PDU_FCS_USIM_DATA_DOWNLOAD_ERROR); + } + // 3. TP-PI + GsmPDUHelper.writeHexOctet(PDU_PI_USER_DATA_LENGTH | + PDU_PI_DATA_CODING_SCHEME | + PDU_PI_PROTOCOL_IDENTIFIER); + // 4. TP-PID + GsmPDUHelper.writeHexOctet(options.pid); + // 5. TP-DCS + GsmPDUHelper.writeHexOctet(options.dcs); + // 6. TP-UDL + if (options.encoding == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + GsmPDUHelper.writeHexOctet(Math.floor(responsePduLen * 8 / 7)); + } else { + GsmPDUHelper.writeHexOctet(responsePduLen); + } + // TP-UD + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * responsePduLen); + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * @param message A decoded SMS-DELIVER message. + */ + writeSmsToSIM: function(message) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_WRITE_SMS_TO_SIM); + + // Write EFsms Status + Buf.writeInt32(EFSMS_STATUS_FREE); + + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE)); // Skip response_type & request_type. + let messageStringLength = Buf.readInt32(); // In semi-octets + let smscLength = GsmPDUHelper.readHexOctet(); // In octets, inclusive of TOA + let pduLength = (messageStringLength / 2) - (smscLength + 1); // In octets + + // 1. Write PDU first. + if (smscLength > 0) { + Buf.seekIncoming(smscLength * Buf.PDU_HEX_OCTET_SIZE); + } + // Write EFsms PDU string length + Buf.writeInt32(2 * pduLength); // In semi-octets + if (pduLength) { + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * pduLength); + } + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + // 2. Write SMSC + // Write EFsms SMSC string length + Buf.writeInt32(2 * (smscLength + 1)); // Plus smscLength itself, in semi-octets + // Write smscLength + GsmPDUHelper.writeHexOctet(smscLength); + // Write TOA & SMSC Address + if (smscLength) { + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE // Skip response_type, request_type. + - 2 * Buf.PDU_HEX_OCTET_SIZE)); // Skip messageStringLength & smscLength. + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * smscLength); + } + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * Helper to delegate the received sms segment to RadioInterface to process. + * + * @param message + * Received sms message. + * + * @return MOZ_FCS_WAIT_FOR_EXPLICIT_ACK + */ + _processSmsMultipart: function(message) { + message.rilMessageType = "sms-received"; + + this.sendChromeMessage(message); + + return MOZ_FCS_WAIT_FOR_EXPLICIT_ACK; + }, + + /** + * Helper for processing SMS-STATUS-REPORT PDUs. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processSmsStatusReport: function(length) { + let [message, result] = this.context.GsmPDUHelper.processReceivedSms(length); + if (!message) { + if (DEBUG) this.context.debug("invalid SMS-STATUS-REPORT"); + return PDU_FCS_UNSPECIFIED; + } + + let options = this._pendingSentSmsMap[message.messageRef]; + if (!options) { + if (DEBUG) this.context.debug("no pending SMS-SUBMIT message"); + return PDU_FCS_OK; + } + + let status = message.status; + + // 3GPP TS 23.040 9.2.3.15 `The MS shall interpret any reserved values as + // "Service Rejected"(01100011) but shall store them exactly as received.` + if ((status >= 0x80) + || ((status >= PDU_ST_0_RESERVED_BEGIN) + && (status < PDU_ST_0_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_1_RESERVED_BEGIN) + && (status < PDU_ST_1_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_2_RESERVED_BEGIN) + && (status < PDU_ST_2_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_3_RESERVED_BEGIN) + && (status < PDU_ST_3_SC_SPECIFIC_BEGIN)) + ) { + status = PDU_ST_3_SERVICE_REJECTED; + } + + // Pending. Waiting for next status report. + if ((status >>> 5) == 0x01) { + if (DEBUG) this.context.debug("SMS-STATUS-REPORT: delivery still pending"); + return PDU_FCS_OK; + } + + delete this._pendingSentSmsMap[message.messageRef]; + + let deliveryStatus = ((status >>> 5) === 0x00) + ? GECKO_SMS_DELIVERY_STATUS_SUCCESS + : GECKO_SMS_DELIVERY_STATUS_ERROR; + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + deliveryStatus: deliveryStatus + }); + + return PDU_FCS_OK; + }, + + /** + * Helper for processing CDMA SMS Delivery Acknowledgment Message + * + * @param message + * decoded SMS Delivery ACK message from CdmaPDUHelper. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processCdmaSmsStatusReport: function(message) { + let options = this._pendingSentSmsMap[message.msgId]; + if (!options) { + if (DEBUG) this.context.debug("no pending SMS-SUBMIT message"); + return PDU_FCS_OK; + } + + if (message.errorClass === 2) { + if (DEBUG) { + this.context.debug("SMS-STATUS-REPORT: delivery still pending, " + + "msgStatus: " + message.msgStatus); + } + return PDU_FCS_OK; + } + + delete this._pendingSentSmsMap[message.msgId]; + + if (message.errorClass === -1 && message.body) { + // Process as normal incoming SMS, if errorClass is invalid + // but message body is available. + return this._processSmsMultipart(message); + } + + let deliveryStatus = (message.errorClass === 0) + ? GECKO_SMS_DELIVERY_STATUS_SUCCESS + : GECKO_SMS_DELIVERY_STATUS_ERROR; + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + deliveryStatus: deliveryStatus + }); + + return PDU_FCS_OK; + }, + + /** + * Helper for processing CDMA SMS WAP Push Message + * + * @param message + * decoded WAP message from CdmaPDUHelper. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processCdmaSmsWapPush: function(message) { + if (!message.data) { + if (DEBUG) this.context.debug("no data inside WAP Push message."); + return PDU_FCS_OK; + } + + // See 6.5. MAPPING OF WDP TO CDMA SMS in WAP-295-WDP. + // + // Field | Length (bits) + // ----------------------------------------- + // MSG_TYPE | 8 + // TOTAL_SEGMENTS | 8 + // SEGMENT_NUMBER | 8 + // DATAGRAM | (NUM_FIELDS - 3) * 8 + let index = 0; + if (message.data[index++] !== 0) { + if (DEBUG) this.context.debug("Ignore a WAP Message which is not WDP."); + return PDU_FCS_OK; + } + + // 1. Originator Address in SMS-TL + Message_Id in SMS-TS are used to identify a unique WDP datagram. + // 2. TOTAL_SEGMENTS, SEGMENT_NUMBER are used to verify that a complete + // datagram has been received and is ready to be passed to a higher layer. + message.header = { + segmentRef: message.msgId, + segmentMaxSeq: message.data[index++], + segmentSeq: message.data[index++] + 1 // It's zero-based in CDMA WAP Push. + }; + + if (message.header.segmentSeq > message.header.segmentMaxSeq) { + if (DEBUG) this.context.debug("Wrong WDP segment info."); + return PDU_FCS_OK; + } + + // Ports are only specified in 1st segment. + if (message.header.segmentSeq == 1) { + message.header.originatorPort = message.data[index++] << 8; + message.header.originatorPort |= message.data[index++]; + message.header.destinationPort = message.data[index++] << 8; + message.header.destinationPort |= message.data[index++]; + } + + message.data = message.data.subarray(index); + + return this._processSmsMultipart(message); + }, + + /** + * Helper for processing sent multipart SMS. + */ + _processSentSmsSegment: function(options) { + // Setup attributes for sending next segment + let next = options.segmentSeq; + options.body = options.segments[next].body; + options.encodedBodyLength = options.segments[next].encodedBodyLength; + options.segmentSeq = next + 1; + + this.sendSMS(options); + }, + + /** + * Helper for processing result of send SMS. + * + * @param length + * Length of SMS string in the incoming parcel. + * @param options + * Sms information. + */ + _processSmsSendResult: function(length, options) { + if (options.errorMsg) { + if (DEBUG) { + this.context.debug("_processSmsSendResult: errorMsg = " + + options.errorMsg); + } + + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + errorMsg: options.errorMsg, + }); + return; + } + + let Buf = this.context.Buf; + options.messageRef = Buf.readInt32(); + options.ackPDU = Buf.readString(); + options.errorCode = Buf.readInt32(); + + if ((options.segmentMaxSeq > 1) + && (options.segmentSeq < options.segmentMaxSeq)) { + // Not last segment + this._processSentSmsSegment(options); + } else { + // Last segment sent with success. + if (options.requestStatusReport) { + if (DEBUG) { + this.context.debug("waiting SMS-STATUS-REPORT for messageRef " + + options.messageRef); + } + this._pendingSentSmsMap[options.messageRef] = options; + } + + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + }); + } + }, + + _processReceivedSmsCbPage: function(original) { + if (original.numPages <= 1) { + if (original.body) { + original.fullBody = original.body; + delete original.body; + } else if (original.data) { + original.fullData = original.data; + delete original.data; + } + return original; + } + + // Hash = <serial>:<mcc>:<mnc>:<lac>:<cid> + let hash = original.serial + ":" + this.iccInfo.mcc + ":" + + this.iccInfo.mnc + ":"; + switch (original.geographicalScope) { + case CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: + case CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE: + hash += this.voiceRegistrationState.cell.gsmLocationAreaCode + ":" + + this.voiceRegistrationState.cell.gsmCellId; + break; + case CB_GSM_GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE: + hash += this.voiceRegistrationState.cell.gsmLocationAreaCode + ":"; + break; + default: + hash += ":"; + break; + } + + let index = original.pageIndex; + + let options = this._receivedSmsCbPagesMap[hash]; + if (!options) { + options = original; + this._receivedSmsCbPagesMap[hash] = options; + + options.receivedPages = 0; + options.pages = []; + } else if (options.pages[index]) { + // Duplicated page? + if (DEBUG) { + this.context.debug("Got duplicated page no." + index + + " of a multipage SMSCB: " + JSON.stringify(original)); + } + return null; + } + + if (options.encoding == PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + options.pages[index] = original.data; + delete original.data; + } else { + options.pages[index] = original.body; + delete original.body; + } + options.receivedPages++; + if (options.receivedPages < options.numPages) { + if (DEBUG) { + this.context.debug("Got page no." + index + " of a multipage SMSCB: " + + JSON.stringify(options)); + } + return null; + } + + // Remove from map + delete this._receivedSmsCbPagesMap[hash]; + + // Rebuild full body + if (options.encoding == PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + // Uint8Array doesn't have `concat`, so we have to merge all pages by hand. + let fullDataLen = 0; + for (let i = 1; i <= options.numPages; i++) { + fullDataLen += options.pages[i].length; + } + + options.fullData = new Uint8Array(fullDataLen); + for (let d= 0, i = 1; i <= options.numPages; i++) { + let data = options.pages[i]; + for (let j = 0; j < data.length; j++) { + options.fullData[d++] = data[j]; + } + } + } else { + options.fullBody = options.pages.join(""); + } + + if (DEBUG) { + this.context.debug("Got full multipage SMSCB: " + JSON.stringify(options)); + } + + return options; + }, + + _mergeCellBroadcastConfigs: function(list, from, to) { + if (!list) { + return [from, to]; + } + + for (let i = 0, f1, t1; i < list.length;) { + f1 = list[i++]; + t1 = list[i++]; + if (to == f1) { + // ...[from]...[to|f1]...(t1) + list[i - 2] = from; + return list; + } + + if (to < f1) { + // ...[from]...(to)...[f1] or ...[from]...(to)[f1] + if (i > 2) { + // Not the first range pair, merge three arrays. + return list.slice(0, i - 2).concat([from, to]).concat(list.slice(i - 2)); + } else { + return [from, to].concat(list); + } + } + + if (from > t1) { + // ...[f1]...(t1)[from] or ...[f1]...(t1)...[from] + continue; + } + + // Have overlap or merge-able adjacency with [f1]...(t1). Replace it + // with [min(from, f1)]...(max(to, t1)). + + let changed = false; + if (from < f1) { + // [from]...[f1]...(t1) or [from][f1]...(t1) + // Save minimum from value. + list[i - 2] = from; + changed = true; + } + + if (to <= t1) { + // [from]...[to](t1) or [from]...(to|t1) + // Can't have further merge-able adjacency. Return. + return list; + } + + // Try merging possible next adjacent range. + let j = i; + for (let f2, t2; j < list.length;) { + f2 = list[j++]; + t2 = list[j++]; + if (to > t2) { + // [from]...[f2]...[t2]...(to) or [from]...[f2]...[t2](to) + // Merge next adjacent range again. + continue; + } + + if (to < t2) { + if (to < f2) { + // [from]...(to)[f2] or [from]...(to)...[f2] + // Roll back and give up. + j -= 2; + } else if (to < t2) { + // [from]...[to|f2]...(t2), or [from]...[f2]...[to](t2) + // Merge to [from]...(t2) and give up. + to = t2; + } + } + + break; + } + + // Save maximum to value. + list[i - 1] = to; + + if (j != i) { + // Remove merged adjacent ranges. + let ret = list.slice(0, i); + if (j < list.length) { + ret = ret.concat(list.slice(j)); + } + return ret; + } + + return list; + } + + // Append to the end. + list.push(from); + list.push(to); + + return list; + }, + + _isCellBroadcastConfigReady: function() { + if (!("MMI" in this.cellBroadcastConfigs)) { + return false; + } + + // CBMI should be ready in GSM. + if (!this._isCdma && + (!("CBMI" in this.cellBroadcastConfigs) || + !("CBMID" in this.cellBroadcastConfigs) || + !("CBMIR" in this.cellBroadcastConfigs))) { + return false; + } + + return true; + }, + + /** + * Merge all members of cellBroadcastConfigs into mergedCellBroadcastConfig. + */ + _mergeAllCellBroadcastConfigs: function() { + if (!this._isCellBroadcastConfigReady()) { + if (DEBUG) { + this.context.debug("cell broadcast configs not ready, waiting ..."); + } + return; + } + + // Prepare cell broadcast config. CBMI* are only used in GSM. + let usedCellBroadcastConfigs = {MMI: this.cellBroadcastConfigs.MMI}; + if (!this._isCdma) { + usedCellBroadcastConfigs.CBMI = this.cellBroadcastConfigs.CBMI; + usedCellBroadcastConfigs.CBMID = this.cellBroadcastConfigs.CBMID; + usedCellBroadcastConfigs.CBMIR = this.cellBroadcastConfigs.CBMIR; + } + + if (DEBUG) { + this.context.debug("Cell Broadcast search lists: " + + JSON.stringify(usedCellBroadcastConfigs)); + } + + let list = null; + for (let key in usedCellBroadcastConfigs) { + let ll = usedCellBroadcastConfigs[key]; + if (ll == null) { + continue; + } + + for (let i = 0; i < ll.length; i += 2) { + list = this._mergeCellBroadcastConfigs(list, ll[i], ll[i + 1]); + } + } + + if (DEBUG) { + this.context.debug("Cell Broadcast search lists(merged): " + + JSON.stringify(list)); + } + this.mergedCellBroadcastConfig = list; + this.updateCellBroadcastConfig(); + }, + + /** + * Check whether search list from settings is settable by MMI, that is, + * whether the range is bounded in any entries of CB_NON_MMI_SETTABLE_RANGES. + */ + _checkCellBroadcastMMISettable: function(from, to) { + if ((to <= from) || (from >= 65536) || (from < 0)) { + return false; + } + + if (!this._isCdma) { + // GSM not settable ranges. + for (let i = 0, f, t; i < CB_NON_MMI_SETTABLE_RANGES.length;) { + f = CB_NON_MMI_SETTABLE_RANGES[i++]; + t = CB_NON_MMI_SETTABLE_RANGES[i++]; + if ((from < t) && (to > f)) { + // Have overlap. + return false; + } + } + } + + return true; + }, + + /** + * Convert Cell Broadcast settings string into search list. + */ + _convertCellBroadcastSearchList: function(searchListStr) { + let parts = searchListStr && searchListStr.split(","); + if (!parts) { + return null; + } + + let list = null; + let result, from, to; + for (let range of parts) { + // Match "12" or "12-34". The result will be ["12", "12", null] or + // ["12-34", "12", "34"]. + result = range.match(/^(\d+)(?:-(\d+))?$/); + if (!result) { + throw "Invalid format"; + } + + from = parseInt(result[1], 10); + to = (result[2]) ? parseInt(result[2], 10) + 1 : from + 1; + if (!this._checkCellBroadcastMMISettable(from, to)) { + throw "Invalid range"; + } + + if (list == null) { + list = []; + } + list.push(from); + list.push(to); + } + + return list; + }, + + /** + * Handle incoming messages from the main UI thread. + * + * @param message + * Object containing the message. Messages are supposed + */ + handleChromeMessage: function(message) { + if (DEBUG) { + this.context.debug("Received chrome message " + JSON.stringify(message)); + } + let method = this[message.rilMessageType]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Don't know what to do with message " + + JSON.stringify(message)); + } + return; + } + method.call(this, message); + }, + + /** + * Process STK Proactive Command. + */ + processStkProactiveCommand: function() { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + let berTlv; + try { + berTlv = this.context.BerTlvHelper.decode(length / 2); + } catch (e) { + if (DEBUG) this.context.debug("processStkProactiveCommand : " + e); + this.sendStkTerminalResponse({ + resultCode: STK_RESULT_CMD_DATA_NOT_UNDERSTOOD}); + return; + } + + Buf.readStringDelimiter(length); + + let ctlvs = berTlv.value; + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_COMMAND_DETAILS, ctlvs); + if (!ctlv) { + this.sendStkTerminalResponse({ + resultCode: STK_RESULT_CMD_DATA_NOT_UNDERSTOOD}); + throw new Error("Can't find COMMAND_DETAILS ComprehensionTlv"); + } + + let cmdDetails = ctlv.value; + if (DEBUG) { + this.context.debug("commandNumber = " + cmdDetails.commandNumber + + " typeOfCommand = " + cmdDetails.typeOfCommand.toString(16) + + " commandQualifier = " + cmdDetails.commandQualifier); + } + + // STK_CMD_MORE_TIME need not to propagate event to chrome. + if (cmdDetails.typeOfCommand == STK_CMD_MORE_TIME) { + this.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_OK}); + return; + } + + this.context.StkCommandParamsFactory.createParam(cmdDetails, + ctlvs, + (aResult) => { + cmdDetails.options = aResult; + cmdDetails.rilMessageType = "stkcommand"; + this.sendChromeMessage(cmdDetails); + }); + }, + + sendDefaultResponse: function(options) { + if (!options.rilMessageType) { + return; + } + + this.sendChromeMessage(options); + }, + + /** + * Send messages to the main thread. + */ + sendChromeMessage: function(message) { + message.rilMessageClientId = this.context.clientId; + postMessage(message); + }, + + /** + * Handle incoming requests from the RIL. We find the method that + * corresponds to the request type. Incidentally, the request type + * _is_ the method name, so that's easy. + */ + + handleParcel: function(request_type, length, options) { + let method = this[request_type]; + if (typeof method == "function") { + if (DEBUG) this.context.debug("Handling parcel as " + method.name); + method.call(this, length, options); + } + + if (this.telephonyRequestQueue.isValidRequest(request_type)) { + this.telephonyRequestQueue.pop(request_type); + } + } +}; + +RilObject.prototype[REQUEST_GET_SIM_STATUS] = function REQUEST_GET_SIM_STATUS(length, options) { + if (options.errorMsg) { + return; + } + + let iccStatus = {}; + let Buf = this.context.Buf; + iccStatus.cardState = Buf.readInt32(); // CARD_STATE_* + iccStatus.universalPINState = Buf.readInt32(); // CARD_PINSTATE_* + iccStatus.gsmUmtsSubscriptionAppIndex = Buf.readInt32(); + iccStatus.cdmaSubscriptionAppIndex = Buf.readInt32(); + iccStatus.imsSubscriptionAppIndex = Buf.readInt32(); + + let apps_length = Buf.readInt32(); + if (apps_length > CARD_MAX_APPS) { + apps_length = CARD_MAX_APPS; + } + + iccStatus.apps = []; + for (let i = 0 ; i < apps_length ; i++) { + iccStatus.apps.push({ + app_type: Buf.readInt32(), // CARD_APPTYPE_* + app_state: Buf.readInt32(), // CARD_APPSTATE_* + perso_substate: Buf.readInt32(), // CARD_PERSOSUBSTATE_* + aid: Buf.readString(), + app_label: Buf.readString(), + pin1_replaced: Buf.readInt32(), + pin1: Buf.readInt32(), + pin2: Buf.readInt32() + }); + if (RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS) { + Buf.readInt32(); + Buf.readInt32(); + Buf.readInt32(); + Buf.readInt32(); + } + } + + if (DEBUG) this.context.debug("iccStatus: " + JSON.stringify(iccStatus)); + this._processICCStatus(iccStatus); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PIN] = function REQUEST_ENTER_SIM_PIN(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PUK] = function REQUEST_ENTER_SIM_PUK(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PIN2] = function REQUEST_ENTER_SIM_PIN2(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PUK2] = function REQUEST_ENTER_SIM_PUK(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_CHANGE_SIM_PIN] = function REQUEST_CHANGE_SIM_PIN(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_CHANGE_SIM_PIN2] = function REQUEST_CHANGE_SIM_PIN2(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE] = + function REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CALLS(length, options) { + // Retry getCurrentCalls several times when error occurs. + if (options.errorMsg) { + if (this._getCurrentCallsRetryCount < GET_CURRENT_CALLS_RETRY_MAX) { + this._getCurrentCallsRetryCount++; + this.getCurrentCalls(options); + } else { + this.sendDefaultResponse(options); + } + return; + } + + this._getCurrentCallsRetryCount = 0; + + let Buf = this.context.Buf; + let calls_length = 0; + // The RIL won't even send us the length integer if there are no active calls. + // So only read this integer if the parcel actually has it. + if (length) { + calls_length = Buf.readInt32(); + } + + let calls = {}; + for (let i = 0; i < calls_length; i++) { + let call = {}; + + // Extra uint32 field to get correct callIndex and rest of call data for + // call waiting feature. + if (RILQUIRKS_EXTRA_UINT32_2ND_CALL && i > 0) { + Buf.readInt32(); + } + + call.state = Buf.readInt32(); // CALL_STATE_* + call.callIndex = Buf.readInt32(); // GSM index (1-based) + call.toa = Buf.readInt32(); + call.isMpty = Boolean(Buf.readInt32()); + call.isMT = Boolean(Buf.readInt32()); + call.als = Buf.readInt32(); + call.isVoice = Boolean(Buf.readInt32()); + call.isVoicePrivacy = Boolean(Buf.readInt32()); + if (RILQUIRKS_CALLSTATE_EXTRA_UINT32) { + Buf.readInt32(); + } + call.number = Buf.readString(); + call.numberPresentation = Buf.readInt32(); // CALL_PRESENTATION_* + call.name = Buf.readString(); + call.namePresentation = Buf.readInt32(); + + call.uusInfo = null; + let uusInfoPresent = Buf.readInt32(); + if (uusInfoPresent == 1) { + call.uusInfo = { + type: Buf.readInt32(), + dcs: Buf.readInt32(), + userData: null //XXX TODO byte array?!? + }; + } + + if (call.isVoice) { + calls[call.callIndex] = call; + } + } + + options.calls = calls; + options.rilMessageType = options.rilMessageType || "currentCalls"; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DIAL] = function REQUEST_DIAL(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DIAL_EMERGENCY_CALL] = function REQUEST_DIAL_EMERGENCY_CALL(length, options) { + RilObject.prototype[REQUEST_DIAL].call(this, length, options); +}; +RilObject.prototype[REQUEST_GET_IMSI] = function REQUEST_GET_IMSI(length, options) { + if (options.errorMsg) { + return; + } + + this.iccInfoPrivate.imsi = this.context.Buf.readString(); + if (DEBUG) { + this.context.debug("IMSI: " + this.iccInfoPrivate.imsi); + } + + options.rilMessageType = "iccimsi"; + options.imsi = this.iccInfoPrivate.imsi; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_HANGUP] = function REQUEST_HANGUP(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_HANGUP_WAITING_OR_BACKGROUND] = function REQUEST_HANGUP_WAITING_OR_BACKGROUND(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND] = function REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE] = function REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_CONFERENCE] = function REQUEST_CONFERENCE(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_UDUB] = function REQUEST_UDUB(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) { + // Treat it as CALL_FAIL_ERROR_UNSPECIFIED if the request failed. + let failCause = CALL_FAIL_ERROR_UNSPECIFIED; + + if (!options.errorMsg) { + let Buf = this.context.Buf; + let num = length ? Buf.readInt32() : 0; + + if (num) { + let causeNum = Buf.readInt32(); + failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[causeNum] || failCause; + } + if (DEBUG) this.context.debug("Last call fail cause: " + failCause); + } + + options.failCause = failCause; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIGNAL_STRENGTH] = function REQUEST_SIGNAL_STRENGTH(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_SIGNAL); + + if (options.errorMsg) { + return; + } + + let Buf = this.context.Buf; + let signal = {}; + + signal.gsmSignalStrength = Buf.readInt32(); + signal.gsmBitErrorRate = Buf.readInt32(); + if (RILQUIRKS_SIGNAL_EXTRA_INT32) { + Buf.readInt32(); + } + signal.cdmaDBM = Buf.readInt32(); + signal.cdmaECIO = Buf.readInt32(); + signal.evdoDBM = Buf.readInt32(); + signal.evdoECIO = Buf.readInt32(); + signal.evdoSNR = Buf.readInt32(); + + signal.lteSignalStrength = Buf.readInt32(); + signal.lteRSRP = Buf.readInt32(); + signal.lteRSRQ = Buf.readInt32(); + signal.lteRSSNR = Buf.readInt32(); + signal.lteCQI = Buf.readInt32(); + + if (DEBUG) this.context.debug("signal strength: " + JSON.stringify(signal)); + + this._processSignalStrength(signal); +}; +RilObject.prototype[REQUEST_VOICE_REGISTRATION_STATE] = function REQUEST_VOICE_REGISTRATION_STATE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_VOICE_REGISTRATION_STATE); + + if (options.errorMsg) { + return; + } + + let state = this.context.Buf.readStringList(); + if (DEBUG) this.context.debug("voice registration state: " + state); + + this._processVoiceRegistrationState(state); +}; +RilObject.prototype[REQUEST_DATA_REGISTRATION_STATE] = function REQUEST_DATA_REGISTRATION_STATE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_DATA_REGISTRATION_STATE); + + if (options.errorMsg) { + return; + } + + let state = this.context.Buf.readStringList(); + this._processDataRegistrationState(state); +}; +RilObject.prototype[REQUEST_OPERATOR] = function REQUEST_OPERATOR(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_OPERATOR); + + if (options.errorMsg) { + return; + } + + let operatorData = this.context.Buf.readStringList(); + if (DEBUG) this.context.debug("Operator: " + operatorData); + this._processOperator(operatorData); +}; +RilObject.prototype[REQUEST_RADIO_POWER] = function REQUEST_RADIO_POWER(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DTMF] = null; +RilObject.prototype[REQUEST_SEND_SMS] = function REQUEST_SEND_SMS(length, options) { + this._processSmsSendResult(length, options); +}; +RilObject.prototype[REQUEST_SEND_SMS_EXPECT_MORE] = null; + +RilObject.prototype[REQUEST_SETUP_DATA_CALL] = function REQUEST_SETUP_DATA_CALL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let version = Buf.readInt32(); + // Skip number of data calls. + Buf.readInt32(); + + this.readDataCall(options, version); + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIM_IO] = function REQUEST_SIM_IO(length, options) { + if (options.errorMsg) { + if (options.onerror) { + options.onerror(options.errorMsg); + } + return; + } + + let Buf = this.context.Buf; + options.sw1 = Buf.readInt32(); + options.sw2 = Buf.readInt32(); + + // See 3GPP TS 11.11, clause 9.4.1 for operation success results. + if (options.sw1 !== ICC_STATUS_NORMAL_ENDING && + options.sw1 !== ICC_STATUS_NORMAL_ENDING_WITH_EXTRA && + options.sw1 !== ICC_STATUS_WITH_SIM_DATA && + options.sw1 !== ICC_STATUS_WITH_RESPONSE_DATA) { + if (DEBUG) { + this.context.debug("ICC I/O Error EF id = 0x" + options.fileId.toString(16) + + ", command = 0x" + options.command.toString(16) + + ", sw1 = 0x" + options.sw1.toString(16) + + ", sw2 = 0x" + options.sw2.toString(16)); + } + if (options.onerror) { + // We can get fail cause from sw1/sw2 (See TS 11.11 clause 9.4.1 and + // ISO 7816-4 clause 6). But currently no one needs this information, + // so simply reports "GenericFailure" for now. + options.onerror(GECKO_ERROR_GENERIC_FAILURE); + } + return; + } + this.context.ICCIOHelper.processICCIO(options); +}; +RilObject.prototype[REQUEST_SEND_USSD] = function REQUEST_SEND_USSD(length, options) { + if (DEBUG) { + this.context.debug("REQUEST_SEND_USSD " + JSON.stringify(options)); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CANCEL_USSD] = function REQUEST_CANCEL_USSD(length, options) { + if (DEBUG) { + this.context.debug("REQUEST_CANCEL_USSD" + JSON.stringify(options)); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_CLIR] = function REQUEST_GET_CLIR(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let bufLength = Buf.readInt32(); + if (!bufLength || bufLength < 2) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + options.n = Buf.readInt32(); // Will be TS 27.007 +CLIR parameter 'n'. + options.m = Buf.readInt32(); // Will be TS 27.007 +CLIR parameter 'm'. + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_CLIR] = function REQUEST_SET_CLIR(length, options) { + if (options.rilMessageType == null) { + // The request was made by ril_worker itself automatically. Don't report. + return; + } + + this.sendChromeMessage(options); +}; + +RilObject.prototype[REQUEST_QUERY_CALL_FORWARD_STATUS] = + function REQUEST_QUERY_CALL_FORWARD_STATUS(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let rulesLength = 0; + if (length) { + rulesLength = Buf.readInt32(); + } + if (!rulesLength) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + let rules = new Array(rulesLength); + for (let i = 0; i < rulesLength; i++) { + let rule = {}; + rule.active = Buf.readInt32() == 1; // CALL_FORWARD_STATUS_* + rule.reason = Buf.readInt32(); // CALL_FORWARD_REASON_* + rule.serviceClass = Buf.readInt32(); + rule.toa = Buf.readInt32(); + rule.number = Buf.readString(); + rule.timeSeconds = Buf.readInt32(); + rules[i] = rule; + } + options.rules = rules; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_CALL_FORWARD] = + function REQUEST_SET_CALL_FORWARD(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_CALL_WAITING] = + function REQUEST_QUERY_CALL_WAITING(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let results = Buf.readInt32List(); + let enabled = (results[0] === 1); + options.serviceClass = enabled ? results[1] : ICC_SERVICE_CLASS_NONE; + this.sendChromeMessage(options); +}; + +RilObject.prototype[REQUEST_SET_CALL_WAITING] = function REQUEST_SET_CALL_WAITING(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SMS_ACKNOWLEDGE] = null; +RilObject.prototype[REQUEST_GET_IMEI] = null; +RilObject.prototype[REQUEST_GET_IMEISV] = null; +RilObject.prototype[REQUEST_ANSWER] = function REQUEST_ANSWER(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DEACTIVATE_DATA_CALL] = function REQUEST_DEACTIVATE_DATA_CALL(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_FACILITY_LOCK] = function REQUEST_QUERY_FACILITY_LOCK(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + if (!length) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + // Buf.readInt32List()[0] for Call Barring is a bit vector of services. + options.serviceClass = this.context.Buf.readInt32List()[0]; + if (options.queryServiceClass) { + options.enabled = (options.serviceClass & options.queryServiceClass) ? true : false; + options.serviceClass = options.queryServiceClass; + } else { + options.enabled = options.serviceClass ? true : false; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_FACILITY_LOCK] = function REQUEST_SET_FACILITY_LOCK(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CHANGE_BARRING_PASSWORD] = + function REQUEST_CHANGE_BARRING_PASSWORD(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_NETWORK_SELECTION_MODE] = function REQUEST_QUERY_NETWORK_SELECTION_MODE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_NETWORK_SELECTION_MODE); + + if (options.errorMsg) { + return; + } + + let mode = this.context.Buf.readInt32List(); + let selectionMode; + + switch (mode[0]) { + case NETWORK_SELECTION_MODE_AUTOMATIC: + selectionMode = GECKO_NETWORK_SELECTION_AUTOMATIC; + break; + case NETWORK_SELECTION_MODE_MANUAL: + selectionMode = GECKO_NETWORK_SELECTION_MANUAL; + break; + default: + selectionMode = GECKO_NETWORK_SELECTION_UNKNOWN; + break; + } + + this._updateNetworkSelectionMode(selectionMode); +}; +RilObject.prototype[REQUEST_SET_NETWORK_SELECTION_AUTOMATIC] = function REQUEST_SET_NETWORK_SELECTION_AUTOMATIC(length, options) { + if (!options.errorMsg) { + this._updateNetworkSelectionMode(GECKO_NETWORK_SELECTION_AUTOMATIC); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_NETWORK_SELECTION_MANUAL] = function REQUEST_SET_NETWORK_SELECTION_MANUAL(length, options) { + if (!options.errorMsg) { + this._updateNetworkSelectionMode(GECKO_NETWORK_SELECTION_MANUAL); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_AVAILABLE_NETWORKS] = function REQUEST_QUERY_AVAILABLE_NETWORKS(length, options) { + if (!options.errorMsg) { + options.networks = this._processNetworks(); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DTMF_START] = function REQUEST_DTMF_START(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DTMF_STOP] = null; +RilObject.prototype[REQUEST_BASEBAND_VERSION] = function REQUEST_BASEBAND_VERSION(length, options) { + if (options.errorMsg) { + return; + } + + this.basebandVersion = this.context.Buf.readString(); + if (DEBUG) this.context.debug("Baseband version: " + this.basebandVersion); +}; +RilObject.prototype[REQUEST_SEPARATE_CONNECTION] = function REQUEST_SEPARATE_CONNECTION(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SET_MUTE] = null; +RilObject.prototype[REQUEST_GET_MUTE] = null; +RilObject.prototype[REQUEST_QUERY_CLIP] = function REQUEST_QUERY_CLIP(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let bufLength = Buf.readInt32(); + if (!bufLength) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + options.provisioned = Buf.readInt32(); + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_LAST_DATA_CALL_FAIL_CAUSE] = null; + +/** + * V6: + * # addresses - A space-delimited list of addresses with optional "/" prefix + * length. + * # dnses - A space-delimited list of DNS server addresses. + * # gateways - A space-delimited list of default gateway addresses. + * + * V10: + * # pcscf - A space-delimited list of Proxy Call State Control Function + * addresses. + */ + +RilObject.prototype.readDataCall = function(options, version) { + if (!options) { + options = {}; + } + let Buf = this.context.Buf; + options.failCause = Buf.readInt32(); // DATACALL_FAIL_* + options.suggestedRetryTime = Buf.readInt32(); + options.cid = Buf.readInt32().toString(); + options.active = Buf.readInt32(); // DATACALL_ACTIVE_* + options.type = Buf.readString(); + options.ifname = Buf.readString(); + options.addresses = Buf.readString(); + options.dnses = Buf.readString(); + options.gateways = Buf.readString(); + + if (version >= 10) { + options.pcscf = Buf.readString(); + } + + if (version >= 11) { + let mtu = Buf.readInt32(); + options.mtu = (mtu > 0) ? mtu : -1 ; + } + + return options; +}; + +RilObject.prototype[REQUEST_DATA_CALL_LIST] = function REQUEST_DATA_CALL_LIST(length, options) { + if (options.errorMsg) { + if (options.rilMessageType) { + this.sendChromeMessage(options); + } + return; + } + + if (!options.rilMessageType) { + // This is an unsolicited data call list changed. + options.rilMessageType = "datacalllistchanged"; + } + + if (!length) { + options.datacalls = []; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let version = Buf.readInt32(); + let num = Buf.readInt32(); + let datacalls = []; + for (let i = 0; i < num; i++) { + let datacall; + datacall = this.readDataCall({}, version); + datacalls.push(datacall); + } + + options.datacalls = datacalls; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_RESET_RADIO] = null; +RilObject.prototype[REQUEST_OEM_HOOK_RAW] = null; +RilObject.prototype[REQUEST_OEM_HOOK_STRINGS] = null; +RilObject.prototype[REQUEST_SCREEN_STATE] = null; +RilObject.prototype[REQUEST_SET_SUPP_SVC_NOTIFICATION] = null; +RilObject.prototype[REQUEST_WRITE_SMS_TO_SIM] = function REQUEST_WRITE_SMS_TO_SIM(length, options) { + if (options.errorMsg) { + // `The MS shall return a "protocol error, unspecified" error message if + // the short message cannot be stored in the (U)SIM, and there is other + // message storage available at the MS` ~ 3GPP TS 23.038 section 4. Here + // we assume we always have indexed db as another storage. + this.acknowledgeGsmSms(false, PDU_FCS_PROTOCOL_ERROR); + } else { + this.acknowledgeGsmSms(true, PDU_FCS_OK); + } +}; +RilObject.prototype[REQUEST_DELETE_SMS_ON_SIM] = null; +RilObject.prototype[REQUEST_SET_BAND_MODE] = null; +RilObject.prototype[REQUEST_QUERY_AVAILABLE_BAND_MODE] = null; +RilObject.prototype[REQUEST_STK_GET_PROFILE] = null; +RilObject.prototype[REQUEST_STK_SET_PROFILE] = null; +RilObject.prototype[REQUEST_STK_SEND_ENVELOPE_COMMAND] = null; +RilObject.prototype[REQUEST_STK_SEND_TERMINAL_RESPONSE] = null; +RilObject.prototype[REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM] = null; +RilObject.prototype[REQUEST_EXPLICIT_CALL_TRANSFER] = null; +RilObject.prototype[REQUEST_SET_PREFERRED_NETWORK_TYPE] = function REQUEST_SET_PREFERRED_NETWORK_TYPE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_PREFERRED_NETWORK_TYPE] = function REQUEST_GET_PREFERRED_NETWORK_TYPE(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + options.type = this.context.Buf.readInt32List()[0]; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_NEIGHBORING_CELL_IDS] = function REQUEST_GET_NEIGHBORING_CELL_IDS(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let radioTech = this.voiceRegistrationState.radioTech; + if (radioTech == undefined || radioTech == NETWORK_CREG_TECH_UNKNOWN) { + options.errorMsg = "RadioTechUnavailable"; + this.sendChromeMessage(options); + return; + } + if (!this._isGsmTechGroup(radioTech) || radioTech == NETWORK_CREG_TECH_LTE) { + options.errorMsg = "UnsupportedRadioTech"; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let neighboringCellIds = []; + let num = Buf.readInt32(); + + for (let i = 0; i < num; i++) { + let cellId = {}; + cellId.networkType = GECKO_RADIO_TECH[radioTech]; + cellId.signalStrength = Buf.readInt32(); + + let cid = Buf.readString(); + // pad cid string with leading "0" + let length = cid.length; + if (length > 8) { + continue; + } + if (length < 8) { + for (let j = 0; j < (8-length); j++) { + cid = "0" + cid; + } + } + + switch (radioTech) { + case NETWORK_CREG_TECH_GPRS: + case NETWORK_CREG_TECH_EDGE: + case NETWORK_CREG_TECH_GSM: + cellId.gsmCellId = this.parseInt(cid.substring(4), -1, 16); + cellId.gsmLocationAreaCode = this.parseInt(cid.substring(0, 4), -1, 16); + break; + case NETWORK_CREG_TECH_UMTS: + case NETWORK_CREG_TECH_HSDPA: + case NETWORK_CREG_TECH_HSUPA: + case NETWORK_CREG_TECH_HSPA: + case NETWORK_CREG_TECH_HSPAP: + case NETWORK_CREG_TECH_DCHSPAP_1: + case NETWORK_CREG_TECH_DCHSPAP_2: + cellId.wcdmaPsc = this.parseInt(cid, -1, 16); + break; + } + + neighboringCellIds.push(cellId); + } + + options.result = neighboringCellIds; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_CELL_INFO_LIST] = function REQUEST_GET_CELL_INFO_LIST(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let cellInfoList = []; + let num = Buf.readInt32(); + for (let i = 0; i < num; i++) { + let cellInfo = {}; + cellInfo.type = Buf.readInt32(); + cellInfo.registered = Buf.readInt32() ? true : false; + cellInfo.timestampType = Buf.readInt32(); + cellInfo.timestamp = Buf.readInt64(); + + switch(cellInfo.type) { + case CELL_INFO_TYPE_GSM: + case CELL_INFO_TYPE_WCDMA: + cellInfo.mcc = Buf.readInt32(); + cellInfo.mnc = Buf.readInt32(); + cellInfo.lac = Buf.readInt32(); + cellInfo.cid = Buf.readInt32(); + if (cellInfo.type == CELL_INFO_TYPE_WCDMA) { + cellInfo.psc = Buf.readInt32(); + } + cellInfo.signalStrength = Buf.readInt32(); + cellInfo.bitErrorRate = Buf.readInt32(); + break; + case CELL_INFO_TYPE_CDMA: + cellInfo.networkId = Buf.readInt32(); + cellInfo.systemId = Buf.readInt32(); + cellInfo.basestationId = Buf.readInt32(); + cellInfo.longitude = Buf.readInt32(); + cellInfo.latitude = Buf.readInt32(); + cellInfo.cdmaDbm = Buf.readInt32(); + cellInfo.cdmaEcio = Buf.readInt32(); + cellInfo.evdoDbm = Buf.readInt32(); + cellInfo.evdoEcio = Buf.readInt32(); + cellInfo.evdoSnr = Buf.readInt32(); + break; + case CELL_INFO_TYPE_LTE: + cellInfo.mcc = Buf.readInt32(); + cellInfo.mnc = Buf.readInt32(); + cellInfo.cid = Buf.readInt32(); + cellInfo.pcid = Buf.readInt32(); + cellInfo.tac = Buf.readInt32(); + cellInfo.signalStrength = Buf.readInt32(); + cellInfo.rsrp = Buf.readInt32(); + cellInfo.rsrq = Buf.readInt32(); + cellInfo.rssnr = Buf.readInt32(); + cellInfo.cqi = Buf.readInt32(); + break; + } + cellInfoList.push(cellInfo); + } + options.result = cellInfoList; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_LOCATION_UPDATES] = null; +RilObject.prototype[REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE] = null; +RilObject.prototype[REQUEST_CDMA_SET_ROAMING_PREFERENCE] = function REQUEST_CDMA_SET_ROAMING_PREFERENCE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_QUERY_ROAMING_PREFERENCE] = function REQUEST_CDMA_QUERY_ROAMING_PREFERENCE(length, options) { + if (!options.errorMsg) { + options.mode = this.context.Buf.readInt32List()[0]; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_TTY_MODE] = null; +RilObject.prototype[REQUEST_QUERY_TTY_MODE] = null; +RilObject.prototype[REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE] = function REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE] = function REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let enabled = this.context.Buf.readInt32List(); + options.enabled = enabled[0] ? true : false; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_FLASH] = function REQUEST_CDMA_FLASH(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_CDMA_BURST_DTMF] = null; +RilObject.prototype[REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY] = null; +RilObject.prototype[REQUEST_CDMA_SEND_SMS] = function REQUEST_CDMA_SEND_SMS(length, options) { + this._processSmsSendResult(length, options); +}; +RilObject.prototype[REQUEST_CDMA_SMS_ACKNOWLEDGE] = null; +RilObject.prototype[REQUEST_GSM_GET_BROADCAST_SMS_CONFIG] = null; +RilObject.prototype[REQUEST_GSM_SET_BROADCAST_SMS_CONFIG] = function REQUEST_GSM_SET_BROADCAST_SMS_CONFIG(length, options) { + if (options.errorMsg) { + return; + } + this.setSmsBroadcastActivation(true); +}; +RilObject.prototype[REQUEST_GSM_SMS_BROADCAST_ACTIVATION] = null; +RilObject.prototype[REQUEST_CDMA_GET_BROADCAST_SMS_CONFIG] = null; +RilObject.prototype[REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG] = function REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG(length, options) { + if (options.errorMsg) { + return; + } + this.setSmsBroadcastActivation(true); +}; +RilObject.prototype[REQUEST_CDMA_SMS_BROADCAST_ACTIVATION] = null; +RilObject.prototype[REQUEST_CDMA_SUBSCRIPTION] = function REQUEST_CDMA_SUBSCRIPTION(length, options) { + if (options.errorMsg) { + return; + } + + let result = this.context.Buf.readStringList(); + + this.iccInfo.mdn = result[0]; + // The result[1] is Home SID. (Already be handled in readCDMAHome()) + // The result[2] is Home NID. (Already be handled in readCDMAHome()) + // The result[3] is MIN. + this.iccInfo.prlVersion = parseInt(result[4], 10); + + this.context.ICCUtilsHelper.handleICCInfoChange(); +}; +RilObject.prototype[REQUEST_CDMA_WRITE_SMS_TO_RUIM] = null; +RilObject.prototype[REQUEST_CDMA_DELETE_SMS_ON_RUIM] = null; +RilObject.prototype[REQUEST_DEVICE_IDENTITY] = function REQUEST_DEVICE_IDENTITY(length, options) { + if (options.errorMsg) { + this.context.debug("Failed to get device identities:" + options.errorMsg); + return; + } + + let result = this.context.Buf.readStringList(); + this.deviceIdentities = { + imei: result[0] || null, + imeisv: result[1] || null, + esn: result[2] || null, + meid: result[3] || null, + }; + + this.sendChromeMessage({ + rilMessageType: "deviceidentitieschange", + deviceIdentities: this.deviceIdentities + }); +}; +RilObject.prototype[REQUEST_EXIT_EMERGENCY_CALLBACK_MODE] = function REQUEST_EXIT_EMERGENCY_CALLBACK_MODE(length, options) { + if (options.internal) { + return; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_SMSC_ADDRESS] = function REQUEST_GET_SMSC_ADDRESS(length, options) { + if (!options.rilMessageType || options.rilMessageType !== "getSmscAddress") { + return; + } + + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let tosca = TOA_UNKNOWN; + let smsc = ""; + let Buf = this.context.Buf; + if (RILQUIRKS_SMSC_ADDRESS_FORMAT === "pdu") { + let pduHelper = this.context.GsmPDUHelper; + let strlen = Buf.readInt32(); + let length = pduHelper.readHexOctet(); + + // As defined in |8.2.5.2 Destination address element| of 3GPP TS 24.011, + // the value of length field can not exceed 11. Since the content might be + // filled with 12 'F' when SMSC is cleared, we don't parse the TOA and + // address fields if reported length exceeds 11 here. Instead, keep the + // default value (TOA_UNKNOWN with an empty address) in this case. + const MAX_LENGTH = 11 + if (length <= MAX_LENGTH) { + tosca = pduHelper.readHexOctet(); + + // Read and covert the decimal values back to special BCD digits defined in + // |Called party BCD number| of 3GPP TS 24.008 (refer the following table). + // + // +=========+=======+=====+ + // | value | digit | hex | + // +======================== + // | 1 0 1 0 | * | 0xA | + // | 1 0 1 1 | # | 0xB | + // | 1 1 0 0 | a | 0xC | + // | 1 1 0 1 | b | 0xD | + // | 1 1 1 0 | c | 0xE | + // +=========+=======+=====+ + smsc = pduHelper.readSwappedNibbleBcdString(length - 1, true) + .replace(/a/ig, "*") + .replace(/b/ig, "#") + .replace(/c/ig, "a") + .replace(/d/ig, "b") + .replace(/e/ig, "c"); + + Buf.readStringDelimiter(strlen); + } + } else /* RILQUIRKS_SMSC_ADDRESS_FORMAT === "text" */ { + let text = Buf.readString(); + let segments = text.split(",", 2); + // Parse TOA only if it presents since some devices might omit the TOA + // segment in the reported SMSC address. If TOA does not present, keep the + // default value TOA_UNKNOWN. + if (segments.length === 2) { + tosca = this.parseInt(segments[1], TOA_UNKNOWN, 10); + } + + smsc = segments[0].replace(/\"/g, ""); + } + + // Convert the NPI value to the corresponding index of CALLED_PARTY_BCD_NPI + // array. If the value does not present in the array, use + // CALLED_PARTY_BCD_NPI_ISDN. + let npi = CALLED_PARTY_BCD_NPI.indexOf(tosca & 0xf); + if (npi === -1) { + npi = CALLED_PARTY_BCD_NPI.indexOf(CALLED_PARTY_BCD_NPI_ISDN); + } + + // Extract TON. + let ton = (tosca & 0x70) >> 4; + + // Ensure + sign if TON is international, and vice versa. + const TON_INTERNATIONAL = (TOA_INTERNATIONAL & 0x70) >> 4; + if (ton === TON_INTERNATIONAL && smsc.charAt(0) !== "+") { + smsc = "+" + smsc; + } else if (smsc.charAt(0) === "+" && ton !== TON_INTERNATIONAL) { + if (DEBUG) { + this.context.debug("SMSC address number begins with '+' while the TON is not international. Change TON to international."); + } + ton = TON_INTERNATIONAL; + } + + options.smscAddress = smsc; + options.typeOfNumber = ton; + options.numberPlanIdentification = npi; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_SMSC_ADDRESS] = function REQUEST_SET_SMSC_ADDRESS(length, options) { + if (!options.rilMessageType || options.rilMessageType !== "setSmscAddress") { + return; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_REPORT_SMS_MEMORY_STATUS] = function REQUEST_REPORT_SMS_MEMORY_STATUS(length, options) { + this.pendingToReportSmsMemoryStatus = !!options.errorMsg; +}; +RilObject.prototype[REQUEST_REPORT_STK_SERVICE_IS_RUNNING] = null; +RilObject.prototype[REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE] = null; +RilObject.prototype[REQUEST_ISIM_AUTHENTICATION] = null; +RilObject.prototype[REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU] = null; +RilObject.prototype[REQUEST_STK_SEND_ENVELOPE_WITH_STATUS] = function REQUEST_STK_SEND_ENVELOPE_WITH_STATUS(length, options) { + if (options.errorMsg) { + this.acknowledgeGsmSms(false, PDU_FCS_UNSPECIFIED); + return; + } + + let Buf = this.context.Buf; + let sw1 = Buf.readInt32(); + let sw2 = Buf.readInt32(); + if ((sw1 == ICC_STATUS_SAT_BUSY) && (sw2 === 0x00)) { + this.acknowledgeGsmSms(false, PDU_FCS_USAT_BUSY); + return; + } + + let success = ((sw1 == ICC_STATUS_NORMAL_ENDING) && (sw2 === 0x00)) + || (sw1 == ICC_STATUS_NORMAL_ENDING_WITH_EXTRA); + + let messageStringLength = Buf.readInt32(); // In semi-octets + let responsePduLen = messageStringLength / 2; // In octets + if (!responsePduLen) { + this.acknowledgeGsmSms(success, success ? PDU_FCS_OK + : PDU_FCS_USIM_DATA_DOWNLOAD_ERROR); + return; + } + + this.acknowledgeIncomingGsmSmsWithPDU(success, responsePduLen, options); +}; +RilObject.prototype[REQUEST_VOICE_RADIO_TECH] = function REQUEST_VOICE_RADIO_TECH(length, options) { + if (options.errorMsg) { + if (DEBUG) { + this.context.debug("Error when getting voice radio tech: " + + options.errorMsg); + } + return; + } + let radioTech = this.context.Buf.readInt32List(); + this._processRadioTech(radioTech[0]); +}; +RilObject.prototype[REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE] = null; +RilObject.prototype[REQUEST_SET_INITIAL_ATTACH_APN] = null; +RilObject.prototype[REQUEST_IMS_REGISTRATION_STATE] = null; +RilObject.prototype[REQUEST_IMS_SEND_SMS] = null; +RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_BASIC] = null; +RilObject.prototype[REQUEST_SIM_OPEN_CHANNEL] = function REQUEST_SIM_OPEN_CHANNEL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + options.channel = this.context.Buf.readInt32List()[0]; + // onwards may optionally contain the select response for the open channel + // command with one byte per integer. + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIM_CLOSE_CHANNEL] = function REQUEST_SIM_CLOSE_CHANNEL(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_CHANNEL] = function REQUEST_SIM_TRANSMIT_APDU_CHANNEL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + options.sw1 = Buf.readInt32(); + options.sw2 = Buf.readInt32(); + options.simResponse = Buf.readString(); + if (DEBUG) { + this.context.debug("Setting return values for RIL[REQUEST_SIM_TRANSMIT_APDU_CHANNEL]: [" + + options.sw1 + "," + + options.sw2 + ", " + + options.simResponse + "]"); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_NV_READ_ITEM] = null; +RilObject.prototype[REQUEST_NV_WRITE_ITEM] = null; +RilObject.prototype[REQUEST_NV_WRITE_CDMA_PRL] = null; +RilObject.prototype[REQUEST_NV_RESET_CONFIG] = null; +RilObject.prototype[REQUEST_SET_UICC_SUBSCRIPTION] = function REQUEST_SET_UICC_SUBSCRIPTION(length, options) { + // Resend data subscription after uicc subscription. + if (this._attachDataRegistration) { + this.setDataRegistration({attach: true}); + } +}; +RilObject.prototype[REQUEST_ALLOW_DATA] = null; +RilObject.prototype[REQUEST_GET_HARDWARE_CONFIG] = null; +RilObject.prototype[REQUEST_SIM_AUTHENTICATION] = null; +RilObject.prototype[REQUEST_GET_DC_RT_INFO] = null; +RilObject.prototype[REQUEST_SET_DC_RT_INFO_RATE] = null; +RilObject.prototype[REQUEST_SET_DATA_PROFILE] = null; +RilObject.prototype[REQUEST_SHUTDOWN] = null; +RilObject.prototype[REQUEST_SET_DATA_SUBSCRIPTION] = function REQUEST_SET_DATA_SUBSCRIPTION(length, options) { + if (!options.rilMessageType) { + // The request was made by ril_worker itself. Don't report. + return; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_UNLOCK_RETRY_COUNT] = function REQUEST_GET_UNLOCK_RETRY_COUNT(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); +}; +RilObject.prototype[RIL_REQUEST_GPRS_ATTACH] = function RIL_REQUEST_GPRS_ATTACH(length, options) { + if (!options.rilMessageType) { + // The request was made by ril_worker itself. Don't report. + return; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[RIL_REQUEST_GPRS_DETACH] = function RIL_REQUEST_GPRS_DETACH(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_RADIO_STATE_CHANGED] = function UNSOLICITED_RESPONSE_RADIO_STATE_CHANGED() { + let radioState = this.context.Buf.readInt32(); + let newState; + switch (radioState) { + case RADIO_STATE_UNAVAILABLE: + newState = GECKO_RADIOSTATE_UNKNOWN; + break; + case RADIO_STATE_OFF: + newState = GECKO_RADIOSTATE_DISABLED; + break; + default: + newState = GECKO_RADIOSTATE_ENABLED; + } + + if (DEBUG) { + this.context.debug("Radio state changed from '" + this.radioState + + "' to '" + newState + "'"); + } + if (this.radioState == newState) { + return; + } + + if (radioState !== RADIO_STATE_UNAVAILABLE) { + // Retrieve device identities once radio is available. + this.getDeviceIdentity(); + } + + if (radioState == RADIO_STATE_ON) { + // This value is defined in RIL v7, we will retrieve radio tech by another + // request. We leave _isCdma untouched, and it will be set once we get the + // radio technology. + this._waitingRadioTech = true; + this.getVoiceRadioTechnology(); + } + + if ((this.radioState == GECKO_RADIOSTATE_UNKNOWN || + this.radioState == GECKO_RADIOSTATE_DISABLED) && + newState == GECKO_RADIOSTATE_ENABLED) { + // The radio became available, let's get its info. + this.getBasebandVersion(); + this.updateCellBroadcastConfig(); + if ((RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) && + this._attachDataRegistration) { + this.setDataRegistration({attach: true}); + } + + if (this.pendingToReportSmsMemoryStatus) { + this._updateSmsMemoryStatus(); + } + } + + this.radioState = newState; + this.sendChromeMessage({ + rilMessageType: "radiostatechange", + radioState: newState + }); + + // If the radio is up and on, so let's query the card state. + // On older RILs only if the card is actually ready, though. + // If _waitingRadioTech is set, we don't need to get icc status now. + if (radioState == RADIO_STATE_UNAVAILABLE || + radioState == RADIO_STATE_OFF || + this._waitingRadioTech) { + return; + } + this.getICCStatus(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_CALL_STATE_CHANGED] = function UNSOLICITED_RESPONSE_CALL_STATE_CHANGED() { + this.getCurrentCalls(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_VOICE_NETWORK_STATE_CHANGED] = function UNSOLICITED_RESPONSE_VOICE_NETWORK_STATE_CHANGED() { + if (DEBUG) { + this.context.debug("Network state changed, re-requesting phone state and " + + "ICC status"); + } + this.getICCStatus(); + this.requestNetworkInfo(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS] = function UNSOLICITED_RESPONSE_NEW_SMS(length) { + let [message, result] = this.context.GsmPDUHelper.processReceivedSms(length); + + if (message) { + result = this._processSmsMultipart(message); + } + + if (result == PDU_FCS_RESERVED || result == MOZ_FCS_WAIT_FOR_EXPLICIT_ACK) { + return; + } + + // Not reserved FCS values, send ACK now. + this.acknowledgeGsmSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS_STATUS_REPORT] = function UNSOLICITED_RESPONSE_NEW_SMS_STATUS_REPORT(length) { + let result = this._processSmsStatusReport(length); + this.acknowledgeGsmSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM] = function UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM(length) { + let recordNumber = this.context.Buf.readInt32List()[0]; + + this.context.SimRecordHelper.readSMS( + recordNumber, + function onsuccess(message) { + if (message && message.simStatus === 3) { //New Unread SMS + this._processSmsMultipart(message); + } + }.bind(this), + function onerror(errorMsg) { + if (DEBUG) { + this.context.debug("Failed to Read NEW SMS on SIM #" + recordNumber + + ", errorMsg: " + errorMsg); + } + }); +}; +RilObject.prototype[UNSOLICITED_ON_USSD] = function UNSOLICITED_ON_USSD() { + let [typeCode, message] = this.context.Buf.readStringList(); + if (DEBUG) { + this.context.debug("On USSD. Type Code: " + typeCode + " Message: " + message); + } + + this.sendChromeMessage({rilMessageType: "ussdreceived", + message: message, + // Per ril.h the USSD session is assumed to persist if + // the type code is "1", otherwise the current session + // (if any) is assumed to have terminated. + sessionEnded: typeCode !== "1"}); +}; +RilObject.prototype[UNSOLICITED_ON_USSD_REQUEST] = null; +RilObject.prototype[UNSOLICITED_NITZ_TIME_RECEIVED] = function UNSOLICITED_NITZ_TIME_RECEIVED() { + let dateString = this.context.Buf.readString(); + + // The data contained in the NITZ message is + // in the form "yy/mm/dd,hh:mm:ss(+/-)tz,dt" + // for example: 12/02/16,03:36:08-20,00,310410 + // See also bug 714352 - Listen for NITZ updates from rild. + + if (DEBUG) this.context.debug("DateTimeZone string " + dateString); + + let now = Date.now(); + + let year = parseInt(dateString.substr(0, 2), 10); + let month = parseInt(dateString.substr(3, 2), 10); + let day = parseInt(dateString.substr(6, 2), 10); + let hours = parseInt(dateString.substr(9, 2), 10); + let minutes = parseInt(dateString.substr(12, 2), 10); + let seconds = parseInt(dateString.substr(15, 2), 10); + // Note that |tz| is in 15-min units. + let tz = parseInt(dateString.substr(17, 3), 10); + // Note that |dst| is in 1-hour units and is already applied in |tz|. + let dst = parseInt(dateString.substr(21, 2), 10); + + let timeInMS = Date.UTC(year + PDU_TIMESTAMP_YEAR_OFFSET, month - 1, day, + hours, minutes, seconds); + + if (isNaN(timeInMS)) { + if (DEBUG) this.context.debug("NITZ failed to convert date"); + return; + } + + this.sendChromeMessage({rilMessageType: "nitzTime", + networkTimeInMS: timeInMS, + networkTimeZoneInMinutes: -(tz * 15), + networkDSTInMinutes: -(dst * 60), + receiveTimeInMS: now}); +}; + +RilObject.prototype[UNSOLICITED_SIGNAL_STRENGTH] = function UNSOLICITED_SIGNAL_STRENGTH(length) { + this[REQUEST_SIGNAL_STRENGTH](length, {}); +}; +RilObject.prototype[UNSOLICITED_DATA_CALL_LIST_CHANGED] = function UNSOLICITED_DATA_CALL_LIST_CHANGED(length) { + this[REQUEST_DATA_CALL_LIST](length, {}); +}; +RilObject.prototype[UNSOLICITED_SUPP_SVC_NOTIFICATION] = function UNSOLICITED_SUPP_SVC_NOTIFICATION(length) { + let Buf = this.context.Buf; + let info = {}; + info.notificationType = Buf.readInt32(); + info.code = Buf.readInt32(); + info.index = Buf.readInt32(); + info.type = Buf.readInt32(); + info.number = Buf.readString(); + + this._processSuppSvcNotification(info); +}; + +RilObject.prototype[UNSOLICITED_STK_SESSION_END] = function UNSOLICITED_STK_SESSION_END() { + this.sendChromeMessage({rilMessageType: "stksessionend"}); +}; +RilObject.prototype[UNSOLICITED_STK_PROACTIVE_COMMAND] = function UNSOLICITED_STK_PROACTIVE_COMMAND() { + this.processStkProactiveCommand(); +}; +RilObject.prototype[UNSOLICITED_STK_EVENT_NOTIFY] = function UNSOLICITED_STK_EVENT_NOTIFY() { + this.processStkProactiveCommand(); +}; +RilObject.prototype[UNSOLICITED_STK_CALL_SETUP] = null; +RilObject.prototype[UNSOLICITED_SIM_SMS_STORAGE_FULL] = null; +RilObject.prototype[UNSOLICITED_SIM_REFRESH] = null; +RilObject.prototype[UNSOLICITED_CALL_RING] = function UNSOLICITED_CALL_RING() { + let Buf = this.context.Buf; + let info = {rilMessageType: "callRing"}; + let isCDMA = false; //XXX TODO hard-code this for now + if (isCDMA) { + info.isPresent = Buf.readInt32(); + info.signalType = Buf.readInt32(); + info.alertPitch = Buf.readInt32(); + info.signal = Buf.readInt32(); + } + // At this point we don't know much other than the fact there's an incoming + // call, but that's enough to bring up the Phone app already. We'll know + // details once we get a call state changed notification and can then + // dispatch DOM events etc. + this.sendChromeMessage(info); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_SIM_STATUS_CHANGED] = function UNSOLICITED_RESPONSE_SIM_STATUS_CHANGED() { + this.getICCStatus(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_CDMA_NEW_SMS] = function UNSOLICITED_RESPONSE_CDMA_NEW_SMS(length) { + let [message, result] = this.context.CdmaPDUHelper.processReceivedSms(length); + + if (message) { + if (message.teleservice === PDU_CDMA_MSG_TELESERIVCIE_ID_WAP) { + result = this._processCdmaSmsWapPush(message); + } else if (message.subMsgType === PDU_CDMA_MSG_TYPE_DELIVER_ACK) { + result = this._processCdmaSmsStatusReport(message); + } else { + result = this._processSmsMultipart(message); + } + } + + if (result == PDU_FCS_RESERVED || result == MOZ_FCS_WAIT_FOR_EXPLICIT_ACK) { + return; + } + + // Not reserved FCS values, send ACK now. + this.acknowledgeCdmaSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS] = function UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS(length) { + let message; + try { + message = + this.context.GsmPDUHelper.readCbMessage(this.context.Buf.readInt32()); + + // "Data-Download" message is expected to be handled by the modem. + // Ignore it here to prevent any garbage messages to be displayed. + // See 9.4.1.2.2 Message Identifier of TS 32.041 for the range of + // Message-identifier of the Data-Download CB messages. + if (message.messageId >= 0x1000 && message.messageId <= 0x10FF) { + if (DEBUG) { + this.context.debug("Ignore a Data-Download message, messageId: " + + message.messageId); + } + return; + } + } catch (e) { + if (DEBUG) { + this.context.debug("Failed to parse Cell Broadcast message: " + e); + } + return; + } + + message = this._processReceivedSmsCbPage(message); + if (!message) { + return; + } + + // Bug 1235697, failed to deactivate CBS in some modem. + // Workaround it according to the settings. + // Note: ETWS/CMAS/PWS can be received even disabled. + // It will be displayed according to the setting in application layer. + if (this.cellBroadcastDisabled && ( + !(message.messageId >= 0x1100 && message.messageId <= 0x1107) && // ETWS + !(message.messageId >= 0x1112 && message.messageId <= 0x112F) && // CMAS + !(message.messageId >= 0x1130 && message.messageId <= 0x18FF) // PWS + )) { + if (DEBUG) { + this.context.debug("Ignore a CB message when disabled, messageId: " + + message.messageId); + } + return; + } + + message.rilMessageType = "cellbroadcast-received"; + this.sendChromeMessage(message); +}; +RilObject.prototype[UNSOLICITED_CDMA_RUIM_SMS_STORAGE_FULL] = null; +RilObject.prototype[UNSOLICITED_RESTRICTED_STATE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE] = function UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE() { + this._handleChangedEmergencyCbMode(true); +}; +RilObject.prototype[UNSOLICITED_CDMA_CALL_WAITING] = function UNSOLICITED_CDMA_CALL_WAITING(length) { + let Buf = this.context.Buf; + let call = {}; + call.number = Buf.readString(); + call.numberPresentation = Buf.readInt32(); + call.name = Buf.readString(); + call.namePresentation = Buf.readInt32(); + call.isPresent = Buf.readInt32(); + call.signalType = Buf.readInt32(); + call.alertPitch = Buf.readInt32(); + call.signal = Buf.readInt32(); + this.sendChromeMessage({rilMessageType: "cdmaCallWaiting", + waitingCall: call}); +}; +RilObject.prototype[UNSOLICITED_CDMA_OTA_PROVISION_STATUS] = function UNSOLICITED_CDMA_OTA_PROVISION_STATUS() { + let status = + CDMA_OTA_PROVISION_STATUS_TO_GECKO[this.context.Buf.readInt32List()[0]]; + if (!status) { + return; + } + this.sendChromeMessage({rilMessageType: "otastatuschange", + status: status}); +}; +RilObject.prototype[UNSOLICITED_CDMA_INFO_REC] = function UNSOLICITED_CDMA_INFO_REC(length) { + this.sendChromeMessage({ + rilMessageType: "cdma-info-rec-received", + records: this.context.CdmaPDUHelper.decodeInformationRecord() + }); +}; +RilObject.prototype[UNSOLICITED_OEM_HOOK_RAW] = null; +RilObject.prototype[UNSOLICITED_RINGBACK_TONE] = null; +RilObject.prototype[UNSOLICITED_RESEND_INCALL_MUTE] = null; +RilObject.prototype[UNSOLICITED_CDMA_SUBSCRIPTION_SOURCE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_CDMA_PRL_CHANGED] = function UNSOLICITED_CDMA_PRL_CHANGED(length) { + let version = this.context.Buf.readInt32List()[0]; + if (version !== this.iccInfo.prlVersion) { + this.iccInfo.prlVersion = version; + this.context.ICCUtilsHelper.handleICCInfoChange(); + } +}; +RilObject.prototype[UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE] = function UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE() { + this._handleChangedEmergencyCbMode(false); +}; +RilObject.prototype[UNSOLICITED_RIL_CONNECTED] = function UNSOLICITED_RIL_CONNECTED(length) { + // Prevent response id collision between UNSOLICITED_RIL_CONNECTED and + // UNSOLICITED_VOICE_RADIO_TECH_CHANGED for Akami on gingerbread branch. + if (!length) { + return; + } + + this.version = this.context.Buf.readInt32List()[0]; + if (DEBUG) { + this.context.debug("Detected RIL version " + this.version); + } + + this.initRILState(); + // rild might have restarted, ensure data call list. + this.getDataCallList(); + // Always ensure that we are not in emergency callback mode when init. + this.exitEmergencyCbMode(); + // Reset radio in the case that b2g restart (or crash). + this.setRadioEnabled({enabled: false}); +}; +RilObject.prototype[UNSOLICITED_VOICE_RADIO_TECH_CHANGED] = function UNSOLICITED_VOICE_RADIO_TECH_CHANGED(length) { + // This unsolicited response will be sent when the technology of a multi-tech + // modem is changed, ex. switch between gsm and cdma. + // TODO: We may need to do more on updating data when switching between gsm + // and cdma mode, e.g. IMEI, ESN, iccInfo, iccType ... etc. + // See Bug 866038. + this._processRadioTech(this.context.Buf.readInt32List()[0]); +}; +RilObject.prototype[UNSOLICITED_CELL_INFO_LIST] = null; +RilObject.prototype[UNSOLICITED_RESPONSE_IMS_NETWORK_STATE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_UICC_SUBSCRIPTION_STATUS_CHANGED] = null; +RilObject.prototype[UNSOLICITED_SRVCC_STATE_NOTIFY] = null; +RilObject.prototype[UNSOLICITED_HARDWARE_CONFIG_CHANGED] = null; +RilObject.prototype[UNSOLICITED_DC_RT_INFO_CHANGED] = null; + +/** + * This object exposes the functionality to parse and serialize PDU strings + * + * A PDU is a string containing a series of hexadecimally encoded octets + * or nibble-swapped binary-coded decimals (BCDs). It contains not only the + * message text but information about the sender, the SMS service center, + * timestamp, etc. + */ +function GsmPDUHelperObject(aContext) { + this.context = aContext; +} +GsmPDUHelperObject.prototype = { + context: null, + + /** + * Read one character (2 bytes) from a RIL string and decode as hex. + * + * @return the nibble as a number. + */ + readHexNibble: function() { + let nibble = this.context.Buf.readUint16(); + if (nibble >= 48 && nibble <= 57) { + nibble -= 48; // ASCII '0'..'9' + } else if (nibble >= 65 && nibble <= 70) { + nibble -= 55; // ASCII 'A'..'F' + } else if (nibble >= 97 && nibble <= 102) { + nibble -= 87; // ASCII 'a'..'f' + } else { + throw "Found invalid nibble during PDU parsing: " + + String.fromCharCode(nibble); + } + return nibble; + }, + + /** + * Encode a nibble as one hex character in a RIL string (2 bytes). + * + * @param nibble + * The nibble to encode (represented as a number) + */ + writeHexNibble: function(nibble) { + nibble &= 0x0f; + if (nibble < 10) { + nibble += 48; // ASCII '0' + } else { + nibble += 55; // ASCII 'A' + } + this.context.Buf.writeUint16(nibble); + }, + + /** + * Read a hex-encoded octet (two nibbles). + * + * @return the octet as a number. + */ + readHexOctet: function() { + return (this.readHexNibble() << 4) | this.readHexNibble(); + }, + + /** + * Write an octet as two hex-encoded nibbles. + * + * @param octet + * The octet (represented as a number) to encode. + */ + writeHexOctet: function(octet) { + this.writeHexNibble(octet >> 4); + this.writeHexNibble(octet); + }, + + /** + * Read an array of hex-encoded octets. + */ + readHexOctetArray: function(length) { + let array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = this.readHexOctet(); + } + return array; + }, + + /** + * Helper to write data into a temporary buffer for easier length encoding when + * the number of octets for the length encoding is varied. + * + * @param writeFunction + * Function of how the data to be written into temporary buffer. + * + * @return array of written octets. + **/ + writeWithBuffer: function(writeFunction) { + let buf = []; + let writeHexOctet = this.writeHexOctet; + this.writeHexOctet = function(octet) { + buf.push(octet); + } + + try { + writeFunction(); + } catch (e) { + if (DEBUG) { + debug("Error when writeWithBuffer: " + e); + } + buf = []; + } finally { + this.writeHexOctet = writeHexOctet; + } + + return buf; + }, + + /** + * Convert an octet (number) to a BCD number. + * + * Any nibbles that are not in the BCD range count as 0. + * + * @param octet + * The octet (a number, as returned by getOctet()) + * + * @return the corresponding BCD number. + */ + octetToBCD: function(octet) { + return ((octet & 0xf0) <= 0x90) * ((octet >> 4) & 0x0f) + + ((octet & 0x0f) <= 0x09) * (octet & 0x0f) * 10; + }, + + /** + * Convert a BCD number to an octet (number) + * + * Only take two digits with absolute value. + * + * @param bcd + * + * @return the corresponding octet. + */ + BCDToOctet: function(bcd) { + bcd = Math.abs(bcd); + return ((bcd % 10) << 4) + (Math.floor(bcd / 10) % 10); + }, + + /** + * Convert a semi-octet (number) to a GSM BCD char, or return empty + * string if invalid semiOctet and suppressException is set to true. + * + * @param semiOctet + * Nibble to be converted to. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return GSM BCD char, or empty string. + */ + bcdChars: "0123456789", + semiOctetToBcdChar: function(semiOctet, suppressException) { + if (semiOctet >= this.bcdChars.length) { + if (suppressException) { + return ""; + } else { + throw new RangeError(); + } + } + + return this.bcdChars.charAt(semiOctet); + }, + + /** + * Convert a semi-octet (number) to a GSM extended BCD char, or return empty + * string if invalid semiOctet and suppressException is set to true. + * + * @param semiOctet + * Nibble to be converted to. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return GSM extended BCD char, or empty string. + */ + extendedBcdChars: "0123456789*#,;", + semiOctetToExtendedBcdChar: function(semiOctet, suppressException) { + if (semiOctet >= this.extendedBcdChars.length) { + if (suppressException) { + return ""; + } else { + throw new RangeError(); + } + } + + return this.extendedBcdChars.charAt(semiOctet); + }, + + /** + * Convert string to a GSM extended BCD string + */ + stringToExtendedBcd: function(string) { + return string.replace(/[^0-9*#,]/g, "") + .replace(/\*/g, "a") + .replace(/\#/g, "b") + .replace(/\,/g, "c"); + }, + + /** + * Read a *swapped nibble* binary coded decimal (BCD) + * + * @param pairs + * Number of nibble *pairs* to read. + * + * @return the decimal as a number. + */ + readSwappedNibbleBcdNum: function(pairs) { + let number = 0; + for (let i = 0; i < pairs; i++) { + let octet = this.readHexOctet(); + // Ignore 'ff' octets as they're often used as filler. + if (octet == 0xff) { + continue; + } + // If the first nibble is an "F" , only the second nibble is to be taken + // into account. + if ((octet & 0xf0) == 0xf0) { + number *= 10; + number += octet & 0x0f; + continue; + } + number *= 100; + number += this.octetToBCD(octet); + } + return number; + }, + + /** + * Read a *swapped nibble* binary coded decimal (BCD) string + * + * @param pairs + * Number of nibble *pairs* to read. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return The BCD string. + */ + readSwappedNibbleBcdString: function(pairs, suppressException) { + let str = ""; + for (let i = 0; i < pairs; i++) { + let nibbleH = this.readHexNibble(); + let nibbleL = this.readHexNibble(); + if (nibbleL == 0x0F) { + break; + } + + str += this.semiOctetToBcdChar(nibbleL, suppressException); + if (nibbleH != 0x0F) { + str += this.semiOctetToBcdChar(nibbleH, suppressException); + } + } + + return str; + }, + + /** + * Read a *swapped nibble* extended binary coded decimal (BCD) string + * + * @param pairs + * Number of nibble *pairs* to read. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return The BCD string. + */ + readSwappedNibbleExtendedBcdString: function(pairs, suppressException) { + let str = ""; + for (let i = 0; i < pairs; i++) { + let nibbleH = this.readHexNibble(); + let nibbleL = this.readHexNibble(); + if (nibbleL == 0x0F) { + break; + } + + str += this.semiOctetToExtendedBcdChar(nibbleL, suppressException); + if (nibbleH != 0x0F) { + str += this.semiOctetToExtendedBcdChar(nibbleH, suppressException); + } + } + + return str; + }, + + /** + * Write numerical data as swapped nibble BCD. + * + * @param data + * Data to write (as a string or a number) + */ + writeSwappedNibbleBCD: function(data) { + data = data.toString(); + if (data.length % 2) { + data += "F"; + } + let Buf = this.context.Buf; + for (let i = 0; i < data.length; i += 2) { + Buf.writeUint16(data.charCodeAt(i + 1)); + Buf.writeUint16(data.charCodeAt(i)); + } + }, + + /** + * Write numerical data as swapped nibble BCD. + * If the number of digit of data is even, add '0' at the beginning. + * + * @param data + * Data to write (as a string or a number) + */ + writeSwappedNibbleBCDNum: function(data) { + data = data.toString(); + if (data.length % 2) { + data = "0" + data; + } + let Buf = this.context.Buf; + for (let i = 0; i < data.length; i += 2) { + Buf.writeUint16(data.charCodeAt(i + 1)); + Buf.writeUint16(data.charCodeAt(i)); + } + }, + + /** + * Read user data, convert to septets, look up relevant characters in a + * 7-bit alphabet, and construct string. + * + * @param length + * Number of septets to read (*not* octets) + * @param paddingBits + * Number of padding bits in the first byte of user data. + * @param langIndex + * Table index used for normal 7-bit encoded character lookup. + * @param langShiftIndex + * Table index used for escaped 7-bit encoded character lookup. + * + * @return a string. + */ + readSeptetsToString: function(length, paddingBits, langIndex, langShiftIndex) { + let ret = ""; + let byteLength = Math.ceil((length * 7 + paddingBits) / 8); + + /** + * |<- last byte in header ->| + * |<- incompleteBits ->|<- last header septet->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- 1st byte in user data ->| + * |<- data septet 1 ->|<-paddingBits->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- 2nd byte in user data ->| + * |<- data spetet 2 ->|<-ds1->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + */ + let data = 0; + let dataBits = 0; + if (paddingBits) { + data = this.readHexOctet() >> paddingBits; + dataBits = 8 - paddingBits; + --byteLength; + } + + let escapeFound = false; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; + do { + // Read as much as fits in 32bit word + let bytesToRead = Math.min(byteLength, dataBits ? 3 : 4); + for (let i = 0; i < bytesToRead; i++) { + data |= this.readHexOctet() << dataBits; + dataBits += 8; + --byteLength; + } + + // Consume available full septets + for (; dataBits >= 7; dataBits -= 7) { + let septet = data & 0x7F; + data >>>= 7; + + if (escapeFound) { + escapeFound = false; + if (septet == PDU_NL_EXTENDED_ESCAPE) { + // According to 3GPP TS 23.038, section 6.2.1.1, NOTE 1, "On + // receipt of this code, a receiving entity shall display a space + // until another extensiion table is defined." + ret += " "; + } else if (septet == PDU_NL_RESERVED_CONTROL) { + // According to 3GPP TS 23.038 B.2, "This code represents a control + // character and therefore must not be used for language specific + // characters." + ret += " "; + } else { + ret += langShiftTable[septet]; + } + } else if (septet == PDU_NL_EXTENDED_ESCAPE) { + escapeFound = true; + + // <escape> is not an effective character + --length; + } else { + ret += langTable[septet]; + } + } + } while (byteLength); + + if (ret.length != length) { + /** + * If num of effective characters does not equal to the length of read + * string, cut the tail off. This happens when the last octet of user + * data has following layout: + * + * |<- penultimate octet in user data ->| + * |<- data septet N ->|<- dsN-1 ->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- last octet in user data ->| + * |<- fill bits ->|<-dsN->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * The fill bits in the last octet may happen to form a full septet and + * be appended at the end of result string. + */ + ret = ret.slice(0, length); + } + return ret; + }, + + writeStringAsSeptets: function(message, paddingBits, langIndex, langShiftIndex) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; + + let dataBits = paddingBits; + let data = 0; + for (let i = 0; i < message.length; i++) { + let c = message.charAt(i); + let septet = langTable.indexOf(c); + if (septet == PDU_NL_EXTENDED_ESCAPE) { + continue; + } + + if (septet >= 0) { + data |= septet << dataBits; + dataBits += 7; + } else { + septet = langShiftTable.indexOf(c); + if (septet == -1) { + throw new Error("'" + c + "' is not in 7 bit alphabet " + + langIndex + ":" + langShiftIndex + "!"); + } + + if (septet == PDU_NL_RESERVED_CONTROL) { + continue; + } + + data |= 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); + } + }, + + writeStringAs8BitUnpacked: function(text) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + let len = text ? text.length : 0; + for (let i = 0; i < len; i++) { + let c = text.charAt(i); + let octet = langTable.indexOf(c); + + if (octet == -1) { + octet = langShiftTable.indexOf(c); + if (octet == -1) { + // Fallback to ASCII space. + octet = langTable.indexOf(' '); + } else { + this.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + } + } + this.writeHexOctet(octet); + } + }, + + /** + * Read user data and decode as a UCS2 string. + * + * @param numOctets + * Number of octets to be read as UCS2 string. + * + * @return a string. + */ + readUCS2String: function(numOctets) { + let str = ""; + let length = numOctets / 2; + for (let i = 0; i < length; ++i) { + let code = (this.readHexOctet() << 8) | this.readHexOctet(); + str += String.fromCharCode(code); + } + + if (DEBUG) this.context.debug("Read UCS2 string: " + str); + + return str; + }, + + /** + * Write user data as a UCS2 string. + * + * @param message + * Message string to encode as UCS2 in hex-encoded octets. + */ + writeUCS2String: function(message) { + for (let i = 0; i < message.length; ++i) { + let code = message.charCodeAt(i); + this.writeHexOctet((code >> 8) & 0xFF); + this.writeHexOctet(code & 0xFF); + } + }, + + /** + * Read 1 + UDHL octets and construct user data header. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.24 + */ + readUserDataHeader: function(msg) { + /** + * A header object with properties contained in received message. + * The properties set include: + * + * length: totoal length of the header, default 0. + * langIndex: used locking shift table index, default + * PDU_NL_IDENTIFIER_DEFAULT. + * langShiftIndex: used locking shift table index, default + * PDU_NL_IDENTIFIER_DEFAULT. + * + */ + let header = { + length: 0, + langIndex: PDU_NL_IDENTIFIER_DEFAULT, + langShiftIndex: PDU_NL_IDENTIFIER_DEFAULT + }; + + header.length = this.readHexOctet(); + if (DEBUG) this.context.debug("Read UDH length: " + header.length); + + let dataAvailable = header.length; + while (dataAvailable >= 2) { + let id = this.readHexOctet(); + let length = this.readHexOctet(); + if (DEBUG) this.context.debug("Read UDH id: " + id + ", length: " + length); + + dataAvailable -= 2; + + switch (id) { + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT: { + let ref = this.readHexOctet(); + let max = this.readHexOctet(); + let seq = this.readHexOctet(); + dataAvailable -= 3; + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_8BIT: { + let dstp = this.readHexOctet(); + let orip = this.readHexOctet(); + dataAvailable -= 2; + if ((dstp < PDU_APA_RESERVED_8BIT_PORTS) + || (orip < PDU_APA_RESERVED_8BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.3: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + break; + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_16BIT: { + let dstp = (this.readHexOctet() << 8) | this.readHexOctet(); + let orip = (this.readHexOctet() << 8) | this.readHexOctet(); + dataAvailable -= 4; + if ((dstp >= PDU_APA_VALID_16BIT_PORTS) || + (orip >= PDU_APA_VALID_16BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.4: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + // Bug 1130292, some carriers set originatorPort to reserved port + // numbers for wap push. We rise this as a warning in debug message + // instead of ingoring this IEI to allow user to receive Wap Push + // under these carriers. + this.context.debug("Warning: Invalid port numbers [dstp, orip]: " + + JSON.stringify([dstp, orip])); + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT: { + let ref = (this.readHexOctet() << 8) | this.readHexOctet(); + let max = this.readHexOctet(); + let seq = this.readHexOctet(); + dataAvailable -= 4; + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT: + let langShiftIndex = this.readHexOctet(); + --dataAvailable; + if (langShiftIndex < PDU_NL_SINGLE_SHIFT_TABLES.length) { + header.langShiftIndex = langShiftIndex; + } + break; + case PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT: + let langIndex = this.readHexOctet(); + --dataAvailable; + if (langIndex < PDU_NL_LOCKING_SHIFT_TABLES.length) { + header.langIndex = langIndex; + } + break; + case PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION: + let msgInd = this.readHexOctet() & 0xFF; + let msgCount = this.readHexOctet(); + dataAvailable -= 2; + + + /* + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + * bits 1 0 : basic message indication type + * bits 4 3 2 : extended message indication type + * bits 6 5 : Profile id + * bit 7 : storage type + */ + let storeType = msgInd & PDU_MWI_STORE_TYPE_BIT; + let mwi = msg.mwi; + if (!mwi) { + mwi = msg.mwi = {}; + } + + if (storeType == PDU_MWI_STORE_TYPE_STORE) { + // Store message because TP_UDH indicates so, note this may override + // the setting in DCS, but that is expected + mwi.discard = false; + } else if (mwi.discard === undefined) { + // storeType == PDU_MWI_STORE_TYPE_DISCARD + // only override mwi.discard here if it hasn't already been set + mwi.discard = true; + } + + mwi.msgCount = msgCount & 0xFF; + mwi.active = mwi.msgCount > 0; + + if (DEBUG) { + this.context.debug("MWI in TP_UDH received: " + JSON.stringify(mwi)); + } + + break; + default: + if (DEBUG) { + this.context.debug("readUserDataHeader: unsupported IEI(" + id + + "), " + length + " bytes."); + } + + // Read out unsupported data + if (length) { + let octets; + if (DEBUG) octets = new Uint8Array(length); + + for (let i = 0; i < length; i++) { + let octet = this.readHexOctet(); + if (DEBUG) octets[i] = octet; + } + dataAvailable -= length; + + if (DEBUG) { + this.context.debug("readUserDataHeader: " + Array.slice(octets)); + } + } + break; + } + } + + if (dataAvailable !== 0) { + throw new Error("Illegal user data header found!"); + } + + msg.header = header; + }, + + /** + * Write out user data header. + * + * @param options + * Options containing information for user data header write-out. The + * `userDataHeaderLength` property must be correctly pre-calculated. + */ + writeUserDataHeader: function(options) { + this.writeHexOctet(options.userDataHeaderLength); + + if (options.segmentMaxSeq > 1) { + if (options.segmentRef16Bit) { + this.writeHexOctet(PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT); + this.writeHexOctet(4); + this.writeHexOctet((options.segmentRef >> 8) & 0xFF); + } else { + this.writeHexOctet(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT); + this.writeHexOctet(3); + } + this.writeHexOctet(options.segmentRef & 0xFF); + this.writeHexOctet(options.segmentMaxSeq & 0xFF); + this.writeHexOctet(options.segmentSeq & 0xFF); + } + + if (options.dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + if (options.langIndex != PDU_NL_IDENTIFIER_DEFAULT) { + this.writeHexOctet(PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT); + this.writeHexOctet(1); + this.writeHexOctet(options.langIndex); + } + + if (options.langShiftIndex != PDU_NL_IDENTIFIER_DEFAULT) { + this.writeHexOctet(PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT); + this.writeHexOctet(1); + this.writeHexOctet(options.langShiftIndex); + } + } + }, + + /** + * Read SM-TL Address. + * + * @param len + * Length of useful semi-octets within the Address-Value field. For + * example, the lenth of "12345" should be 5, and 4 for "1234". + * + * @see 3GPP TS 23.040 9.1.2.5 + */ + readAddress: function(len) { + // Address Length + if (!len || (len < 0)) { + if (DEBUG) { + this.context.debug("PDU error: invalid sender address length: " + len); + } + return null; + } + if (len % 2 == 1) { + len += 1; + } + if (DEBUG) this.context.debug("PDU: Going to read address: " + len); + + // Type-of-Address + let toa = this.readHexOctet(); + let addr = ""; + + if ((toa & 0xF0) == PDU_TOA_ALPHANUMERIC) { + addr = this.readSeptetsToString(Math.floor(len * 4 / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT , PDU_NL_IDENTIFIER_DEFAULT ); + return addr; + } + addr = this.readSwappedNibbleExtendedBcdString(len / 2); + if (addr.length <= 0) { + if (DEBUG) this.context.debug("PDU error: no number provided"); + return null; + } + if ((toa & 0xF0) == (PDU_TOA_INTERNATIONAL)) { + addr = '+' + addr; + } + + return addr; + }, + + /** + * Read TP-Protocol-Indicator(TP-PID). + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.9 + */ + readProtocolIndicator: function(msg) { + // `The MS shall interpret reserved, obsolete, or unsupported values as the + // value 00000000 but shall store them exactly as received.` + msg.pid = this.readHexOctet(); + + msg.epid = msg.pid; + switch (msg.epid & 0xC0) { + case 0x40: + // Bit 7..0 = 01xxxxxx + switch (msg.epid) { + case PDU_PID_SHORT_MESSAGE_TYPE_0: + case PDU_PID_ANSI_136_R_DATA: + case PDU_PID_USIM_DATA_DOWNLOAD: + return; + } + break; + } + + msg.epid = PDU_PID_DEFAULT; + }, + + /** + * Read TP-Data-Coding-Scheme(TP-DCS) + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.10, 3GPP TS 23.038 4. + */ + readDataCodingScheme: function(msg) { + let dcs = this.readHexOctet(); + if (DEBUG) this.context.debug("PDU: read SMS dcs: " + dcs); + + // No message class by default. + let messageClass = PDU_DCS_MSG_CLASS_NORMAL; + // 7 bit is the default fallback encoding. + let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + switch (dcs & PDU_DCS_CODING_GROUP_BITS) { + case 0x40: // bits 7..4 = 01xx + case 0x50: + case 0x60: + case 0x70: + // Bit 5..0 are coded exactly the same as Group 00xx + case 0x00: // bits 7..4 = 00xx + case 0x10: + case 0x20: + case 0x30: + if (dcs & 0x10) { + messageClass = dcs & PDU_DCS_MSG_CLASS_BITS; + } + switch (dcs & 0x0C) { + case 0x4: + encoding = PDU_DCS_MSG_CODING_8BITS_ALPHABET; + break; + case 0x8: + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + break; + } + break; + + case 0xE0: // bits 7..4 = 1110 + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + // Bit 3..0 are coded exactly the same as Message Waiting Indication + // Group 1101. + // Fall through. + case 0xC0: // bits 7..4 = 1100 + case 0xD0: // bits 7..4 = 1101 + // Indiciates voicemail indicator set or clear + let active = (dcs & PDU_DCS_MWI_ACTIVE_BITS) == PDU_DCS_MWI_ACTIVE_VALUE; + + // If TP-UDH is present, these values will be overwritten + switch (dcs & PDU_DCS_MWI_TYPE_BITS) { + case PDU_DCS_MWI_TYPE_VOICEMAIL: + let mwi = msg.mwi; + if (!mwi) { + mwi = msg.mwi = {}; + } + + mwi.active = active; + mwi.discard = (dcs & PDU_DCS_CODING_GROUP_BITS) == 0xC0; + mwi.msgCount = active ? GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN : 0; + + if (DEBUG) { + this.context.debug("MWI in DCS received for voicemail: " + + JSON.stringify(mwi)); + } + break; + case PDU_DCS_MWI_TYPE_FAX: + if (DEBUG) this.context.debug("MWI in DCS received for fax"); + break; + case PDU_DCS_MWI_TYPE_EMAIL: + if (DEBUG) this.context.debug("MWI in DCS received for email"); + break; + default: + if (DEBUG) this.context.debug("MWI in DCS received for \"other\""); + break; + } + break; + + case 0xF0: // bits 7..4 = 1111 + if (dcs & 0x04) { + encoding = PDU_DCS_MSG_CODING_8BITS_ALPHABET; + } + messageClass = dcs & PDU_DCS_MSG_CLASS_BITS; + break; + + default: + // Falling back to default encoding. + break; + } + + msg.dcs = dcs; + msg.encoding = encoding; + msg.messageClass = GECKO_SMS_MESSAGE_CLASSES[messageClass]; + + if (DEBUG) this.context.debug("PDU: message encoding is " + encoding + " bit."); + }, + + /** + * Read GSM TP-Service-Centre-Time-Stamp(TP-SCTS). + * + * @see 3GPP TS 23.040 9.2.3.11 + */ + readTimestamp: function() { + let year = this.readSwappedNibbleBcdNum(1) + PDU_TIMESTAMP_YEAR_OFFSET; + let month = this.readSwappedNibbleBcdNum(1) - 1; + let day = this.readSwappedNibbleBcdNum(1); + let hour = this.readSwappedNibbleBcdNum(1); + let minute = this.readSwappedNibbleBcdNum(1); + let second = this.readSwappedNibbleBcdNum(1); + let timestamp = Date.UTC(year, month, day, hour, minute, second); + + // If the most significant bit of the least significant nibble is 1, + // the timezone offset is negative (fourth bit from the right => 0x08): + // localtime = UTC + tzOffset + // therefore + // UTC = localtime - tzOffset + let tzOctet = this.readHexOctet(); + let tzOffset = this.octetToBCD(tzOctet & ~0x08) * 15 * 60 * 1000; + tzOffset = (tzOctet & 0x08) ? -tzOffset : tzOffset; + timestamp -= tzOffset; + + return timestamp; + }, + + /** + * Write GSM TP-Service-Centre-Time-Stamp(TP-SCTS). + * + * @see 3GPP TS 23.040 9.2.3.11 + */ + writeTimestamp: function(date) { + this.writeSwappedNibbleBCDNum(date.getFullYear() - PDU_TIMESTAMP_YEAR_OFFSET); + + // The value returned by getMonth() is an integer between 0 and 11. + // 0 is corresponds to January, 1 to February, and so on. + this.writeSwappedNibbleBCDNum(date.getMonth() + 1); + this.writeSwappedNibbleBCDNum(date.getDate()); + this.writeSwappedNibbleBCDNum(date.getHours()); + this.writeSwappedNibbleBCDNum(date.getMinutes()); + this.writeSwappedNibbleBCDNum(date.getSeconds()); + + // the value returned by getTimezoneOffset() is the difference, + // in minutes, between UTC and local time. + // For example, if your time zone is UTC+10 (Australian Eastern Standard Time), + // -600 will be returned. + // In TS 23.040 9.2.3.11, the Time Zone field of TP-SCTS indicates + // the different between the local time and GMT. + // And expressed in quarters of an hours. (so need to divid by 15) + let zone = date.getTimezoneOffset() / 15; + let octet = this.BCDToOctet(zone); + + // the bit3 of the Time Zone field represents the algebraic sign. + // (0: positive, 1: negative). + // For example, if the time zone is -0800 GMT, + // 480 will be returned by getTimezoneOffset(). + // In this case, need to mark sign bit as 1. => 0x08 + if (zone > 0) { + octet = octet | 0x08; + } + this.writeHexOctet(octet); + }, + + /** + * User data can be 7 bit (default alphabet) data, 8 bit data, or 16 bit + * (UCS2) data. + * + * @param msg + * message object for output. + * @param length + * length of user data to read in octets. + */ + readUserData: function(msg, length) { + if (DEBUG) { + this.context.debug("Reading " + length + " bytes of user data."); + } + + let paddingBits = 0; + if (msg.udhi) { + this.readUserDataHeader(msg); + + if (msg.encoding == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + let headerBits = (msg.header.length + 1) * 8; + let headerSeptets = Math.ceil(headerBits / 7); + + length -= headerSeptets; + paddingBits = headerSeptets * 7 - headerBits; + } else { + length -= (msg.header.length + 1); + } + } + + if (DEBUG) { + this.context.debug("After header, " + length + " septets left of user data"); + } + + msg.body = null; + msg.data = null; + + if (length <= 0) { + // No data to read. + return; + } + + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + // 7 bit encoding allows 140 octets, which means 160 characters + // ((140x8) / 7 = 160 chars) + if (length > PDU_MAX_USER_DATA_7BIT) { + if (DEBUG) { + this.context.debug("PDU error: user data is too long: " + length); + } + break; + } + + let langIndex = msg.udhi ? msg.header.langIndex : PDU_NL_IDENTIFIER_DEFAULT; + let langShiftIndex = msg.udhi ? msg.header.langShiftIndex : PDU_NL_IDENTIFIER_DEFAULT; + msg.body = this.readSeptetsToString(length, paddingBits, langIndex, + langShiftIndex); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msg.data = this.readHexOctetArray(length); + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + msg.body = this.readUCS2String(length); + break; + } + }, + + /** + * Read extra parameters if TP-PI is set. + * + * @param msg + * message object for output. + */ + readExtraParams: function(msg) { + // Because each PDU octet is converted to two UCS2 char2, we should always + // get even messageStringLength in this#_processReceivedSms(). So, we'll + // always need two delimitors at the end. + if (this.context.Buf.getReadAvailable() <= 4) { + return; + } + + // TP-Parameter-Indicator + let pi; + do { + // `The most significant bit in octet 1 and any other TP-PI octets which + // may be added later is reserved as an extension bit which when set to a + // 1 shall indicate that another TP-PI octet follows immediately + // afterwards.` ~ 3GPP TS 23.040 9.2.3.27 + pi = this.readHexOctet(); + } while (pi & PDU_PI_EXTENSION); + + // `If the TP-UDL bit is set to "1" but the TP-DCS bit is set to "0" then + // the receiving entity shall for TP-DCS assume a value of 0x00, i.e. the + // 7bit default alphabet.` ~ 3GPP 23.040 9.2.3.27 + msg.dcs = 0; + msg.encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + + // TP-Protocol-Identifier + if (pi & PDU_PI_PROTOCOL_IDENTIFIER) { + this.readProtocolIndicator(msg); + } + // TP-Data-Coding-Scheme + if (pi & PDU_PI_DATA_CODING_SCHEME) { + this.readDataCodingScheme(msg); + } + // TP-User-Data-Length + if (pi & PDU_PI_USER_DATA_LENGTH) { + let userDataLength = this.readHexOctet(); + this.readUserData(msg, userDataLength); + } + }, + + /** + * Read and decode a PDU-encoded message from the stream. + * + * TODO: add some basic sanity checks like: + * - do we have the minimum number of chars available + */ + readMessage: function() { + // An empty message object. This gets filled below and then returned. + let msg = { + // D:DELIVER, DR:DELIVER-REPORT, S:SUBMIT, SR:SUBMIT-REPORT, + // ST:STATUS-REPORT, C:COMMAND + // M:Mandatory, O:Optional, X:Unavailable + // D DR S SR ST C + SMSC: null, // M M M M M M + mti: null, // M M M M M M + udhi: null, // M M O M M M + sender: null, // M X X X X X + recipient: null, // X X M X M M + pid: null, // M O M O O M + epid: null, // M O M O O M + dcs: null, // M O M O O X + mwi: null, // O O O O O O + replace: false, // O O O O O O + header: null, // M M O M M M + body: null, // M O M O O O + data: null, // M O M O O O + sentTimestamp: null, // M X X X X X + status: null, // X X X X M X + scts: null, // X X X M M X + dt: null, // X X X X M X + }; + + // SMSC info + let smscLength = this.readHexOctet(); + if (smscLength > 0) { + let smscTypeOfAddress = this.readHexOctet(); + // Subtract the type-of-address octet we just read from the length. + msg.SMSC = this.readSwappedNibbleExtendedBcdString(smscLength - 1); + if ((smscTypeOfAddress >> 4) == (PDU_TOA_INTERNATIONAL >> 4)) { + msg.SMSC = '+' + msg.SMSC; + } + } + + // First octet of this SMS-DELIVER or SMS-SUBMIT message + let firstOctet = this.readHexOctet(); + // Message Type Indicator + msg.mti = firstOctet & 0x03; + // User data header indicator + msg.udhi = firstOctet & PDU_UDHI; + + switch (msg.mti) { + case PDU_MTI_SMS_RESERVED: + // `If an MS receives a TPDU with a "Reserved" value in the TP-MTI it + // shall process the message as if it were an "SMS-DELIVER" but store + // the message exactly as received.` ~ 3GPP TS 23.040 9.2.3.1 + case PDU_MTI_SMS_DELIVER: + return this.readDeliverMessage(msg); + case PDU_MTI_SMS_STATUS_REPORT: + return this.readStatusReportMessage(msg); + default: + return null; + } + }, + + /** + * Helper for processing received SMS parcel data. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return Message parsed or null for invalid message. + */ + processReceivedSms: function(length) { + if (!length) { + if (DEBUG) this.context.debug("Received empty SMS!"); + return [null, PDU_FCS_UNSPECIFIED]; + } + + let Buf = this.context.Buf; + + // An SMS is a string, but we won't read it as such, so let's read the + // string length and then defer to PDU parsing helper. + let messageStringLength = Buf.readInt32(); + if (DEBUG) this.context.debug("Got new SMS, length " + messageStringLength); + let message = this.readMessage(); + if (DEBUG) this.context.debug("Got new SMS: " + JSON.stringify(message)); + + // Read string delimiters. See Buf.readString(). + Buf.readStringDelimiter(length); + + // Determine result + if (!message) { + return [null, PDU_FCS_UNSPECIFIED]; + } + + if (message.epid == PDU_PID_SHORT_MESSAGE_TYPE_0) { + // `A short message type 0 indicates that the ME must acknowledge receipt + // of the short message but shall discard its contents.` ~ 3GPP TS 23.040 + // 9.2.3.9 + return [null, PDU_FCS_OK]; + } + + if (message.messageClass == GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]) { + let RIL = this.context.RIL; + switch (message.epid) { + case PDU_PID_ANSI_136_R_DATA: + case PDU_PID_USIM_DATA_DOWNLOAD: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.isICCServiceAvailable("DATA_DOWNLOAD_SMS_PP")) { + // `If the service "data download via SMS Point-to-Point" is + // allocated and activated in the (U)SIM Service Table, ... then the + // ME shall pass the message transparently to the UICC using the + // ENVELOPE (SMS-PP DOWNLOAD).` ~ 3GPP TS 31.111 7.1.1.1 + RIL.dataDownloadViaSMSPP(message); + + // `the ME shall not display the message, or alert the user of a + // short message waiting.` ~ 3GPP TS 31.111 7.1.1.1 + return [null, PDU_FCS_RESERVED]; + } + + // If the service "data download via SMS-PP" is not available in the + // (U)SIM Service Table, ..., then the ME shall store the message in + // EFsms in accordance with TS 31.102` ~ 3GPP TS 31.111 7.1.1.1 + + // Fall through. + default: + RIL.writeSmsToSIM(message); + break; + } + } + + // TODO: Bug 739143: B2G SMS: Support SMS Storage Full event + if ((message.messageClass != GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]) && !true) { + // `When a mobile terminated message is class 0..., the MS shall display + // the message immediately and send a ACK to the SC ..., irrespective of + // whether there is memory available in the (U)SIM or ME.` ~ 3GPP 23.038 + // clause 4. + + if (message.messageClass == GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]) { + // `If all the short message storage at the MS is already in use, the + // MS shall return "memory capacity exceeded".` ~ 3GPP 23.038 clause 4. + return [null, PDU_FCS_MEMORY_CAPACITY_EXCEEDED]; + } + + return [null, PDU_FCS_UNSPECIFIED]; + } + + return [message, PDU_FCS_OK]; + }, + + /** + * Read and decode a SMS-DELIVER PDU. + * + * @param msg + * message object for output. + */ + readDeliverMessage: function(msg) { + // - Sender Address info - + let senderAddressLength = this.readHexOctet(); + msg.sender = this.readAddress(senderAddressLength); + // - TP-Protocolo-Identifier - + this.readProtocolIndicator(msg); + // - TP-Data-Coding-Scheme - + this.readDataCodingScheme(msg); + // - TP-Service-Center-Time-Stamp - + msg.sentTimestamp = this.readTimestamp(); + // - TP-User-Data-Length - + let userDataLength = this.readHexOctet(); + + // - TP-User-Data - + if (userDataLength > 0) { + this.readUserData(msg, userDataLength); + } + + return msg; + }, + + /** + * Read and decode a SMS-STATUS-REPORT PDU. + * + * @param msg + * message object for output. + */ + readStatusReportMessage: function(msg) { + // TP-Message-Reference + msg.messageRef = this.readHexOctet(); + // TP-Recipient-Address + let recipientAddressLength = this.readHexOctet(); + msg.recipient = this.readAddress(recipientAddressLength); + // TP-Service-Centre-Time-Stamp + msg.scts = this.readTimestamp(); + // TP-Discharge-Time + msg.dt = this.readTimestamp(); + // TP-Status + msg.status = this.readHexOctet(); + + this.readExtraParams(msg); + + return msg; + }, + + /** + * Serialize a SMS-SUBMIT PDU message and write it to the output stream. + * + * This method expects that a data coding scheme has been chosen already + * and that the length of the user data payload in that encoding is known, + * too. Both go hand in hand together anyway. + * + * @param address + * String containing the address (number) of the SMS receiver + * @param userData + * String containing the message to be sent as user data + * @param dcs + * Data coding scheme. One of the PDU_DCS_MSG_CODING_*BITS_ALPHABET + * constants. + * @param userDataHeaderLength + * Length of embedded user data header, in bytes. The whole header + * size will be userDataHeaderLength + 1; 0 for no header. + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param langIndex + * Table index used for normal 7-bit encoded character lookup. + * @param langShiftIndex + * Table index used for escaped 7-bit encoded character lookup. + * @param requestStatusReport + * Request status report. + */ + writeMessage: function(options) { + if (DEBUG) { + this.context.debug("writeMessage: " + JSON.stringify(options)); + } + let Buf = this.context.Buf; + let address = options.number; + let body = options.body; + let dcs = options.dcs; + let userDataHeaderLength = options.userDataHeaderLength; + let encodedBodyLength = options.encodedBodyLength; + let langIndex = options.langIndex; + let langShiftIndex = options.langShiftIndex; + + // SMS-SUBMIT Format: + // + // PDU Type - 1 octet + // Message Reference - 1 octet + // DA - Destination Address - 2 to 12 octets + // PID - Protocol Identifier - 1 octet + // DCS - Data Coding Scheme - 1 octet + // VP - Validity Period - 0, 1 or 7 octets + // UDL - User Data Length - 1 octet + // UD - User Data - 140 octets + + let addressFormat = PDU_TOA_ISDN; // 81 + if (address[0] == '+') { + addressFormat = PDU_TOA_INTERNATIONAL | PDU_TOA_ISDN; // 91 + address = address.substring(1); + } + //TODO validity is unsupported for now + let validity = 0; + + let headerOctets = (userDataHeaderLength ? userDataHeaderLength + 1 : 0); + let paddingBits; + let userDataLengthInSeptets; + let userDataLengthInOctets; + if (dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + let headerSeptets = Math.ceil(headerOctets * 8 / 7); + userDataLengthInSeptets = headerSeptets + encodedBodyLength; + userDataLengthInOctets = Math.ceil(userDataLengthInSeptets * 7 / 8); + paddingBits = headerSeptets * 7 - headerOctets * 8; + } else { + userDataLengthInOctets = headerOctets + encodedBodyLength; + paddingBits = 0; + } + + let pduOctetLength = 4 + // PDU Type, Message Ref, address length + format + Math.ceil(address.length / 2) + + 3 + // PID, DCS, UDL + userDataLengthInOctets; + if (validity) { + //TODO: add more to pduOctetLength + } + + // Start the string. Since octets are represented in hex, we will need + // twice as many characters as octets. + Buf.writeInt32(pduOctetLength * 2); + + // - PDU-TYPE- + + // +--------+----------+---------+---------+--------+---------+ + // | RP (1) | UDHI (1) | SRR (1) | VPF (2) | RD (1) | MTI (2) | + // +--------+----------+---------+---------+--------+---------+ + // RP: 0 Reply path parameter is not set + // 1 Reply path parameter is set + // UDHI: 0 The UD Field contains only the short message + // 1 The beginning of the UD field contains a header in addition + // of the short message + // SRR: 0 A status report is not requested + // 1 A status report is requested + // VPF: bit4 bit3 + // 0 0 VP field is not present + // 0 1 Reserved + // 1 0 VP field present an integer represented (relative) + // 1 1 VP field present a semi-octet represented (absolute) + // RD: Instruct the SMSC to accept(0) or reject(1) an SMS-SUBMIT + // for a short message still held in the SMSC which has the same + // MR and DA as a previously submitted short message from the + // same OA + // MTI: bit1 bit0 Message Type + // 0 0 SMS-DELIVER (SMSC ==> MS) + // 0 1 SMS-SUBMIT (MS ==> SMSC) + + // PDU type. MTI is set to SMS-SUBMIT + let firstOctet = PDU_MTI_SMS_SUBMIT; + + // Status-Report-Request + if (options.requestStatusReport) { + firstOctet |= PDU_SRI_SRR; + } + + // Validity period + if (validity) { + //TODO: not supported yet, OR with one of PDU_VPF_* + } + // User data header indicator + if (headerOctets) { + firstOctet |= PDU_UDHI; + } + this.writeHexOctet(firstOctet); + + // Message reference 00 + this.writeHexOctet(0x00); + + // - Destination Address - + this.writeHexOctet(address.length); + this.writeHexOctet(addressFormat); + this.writeSwappedNibbleBCD(address); + + // - Protocol Identifier - + this.writeHexOctet(0x00); + + // - Data coding scheme - + // For now it assumes bits 7..4 = 1111 except for the 16 bits use case + this.writeHexOctet(dcs); + + // - Validity Period - + if (validity) { + this.writeHexOctet(validity); + } + + // - User Data - + if (dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + this.writeHexOctet(userDataLengthInSeptets); + } else { + this.writeHexOctet(userDataLengthInOctets); + } + + if (headerOctets) { + this.writeUserDataHeader(options); + } + + switch (dcs) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + this.writeStringAsSeptets(body, paddingBits, langIndex, langShiftIndex); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + // Unsupported. + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + this.writeUCS2String(body); + break; + } + + // End of the string. The string length is always even by definition, so + // we write two \0 delimiters. + Buf.writeUint16(0); + Buf.writeUint16(0); + }, + + /** + * Read GSM CBS message serial number. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.1 + */ + readCbSerialNumber: function(msg) { + let Buf = this.context.Buf; + msg.serial = Buf.readUint8() << 8 | Buf.readUint8(); + msg.geographicalScope = (msg.serial >>> 14) & 0x03; + msg.messageCode = (msg.serial >>> 4) & 0x03FF; + msg.updateNumber = msg.serial & 0x0F; + }, + + /** + * Read GSM CBS message message identifier. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.2 + */ + readCbMessageIdentifier: function(msg) { + let Buf = this.context.Buf; + msg.messageId = Buf.readUint8() << 8 | Buf.readUint8(); + }, + + /** + * Read ETWS information from message identifier and serial Number + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.1 & 9.4.1.2.2 + */ + readCbEtwsInfo: function(msg) { + if ((msg.format != CB_FORMAT_ETWS) + && (msg.messageId >= CB_GSM_MESSAGEID_ETWS_BEGIN) + && (msg.messageId <= CB_GSM_MESSAGEID_ETWS_END)) { + // `In the case of transmitting CBS message for ETWS, a part of + // Message Code can be used to command mobile terminals to activate + // emergency user alert and message popup in order to alert the users.` + msg.etws = { + emergencyUserAlert: msg.messageCode & 0x0200 ? true : false, + popup: msg.messageCode & 0x0100 ? true : false + }; + + let warningType = msg.messageId - CB_GSM_MESSAGEID_ETWS_BEGIN; + if (warningType < CB_ETWS_WARNING_TYPE_NAMES.length) { + msg.etws.warningType = warningType; + } + } + }, + + /** + * Read CBS Data Coding Scheme. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.038 section 5. + */ + readCbDataCodingScheme: function(msg) { + let dcs = this.context.Buf.readUint8(); + if (DEBUG) this.context.debug("PDU: read CBS dcs: " + dcs); + + let language = null, hasLanguageIndicator = false; + // `Any reserved codings shall be assumed to be the GSM 7bit default + // alphabet.` + let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + let messageClass = PDU_DCS_MSG_CLASS_NORMAL; + + switch (dcs & PDU_DCS_CODING_GROUP_BITS) { + case 0x00: // 0000 + language = CB_DCS_LANG_GROUP_1[dcs & 0x0F]; + break; + + case 0x10: // 0001 + switch (dcs & 0x0F) { + case 0x00: + hasLanguageIndicator = true; + break; + case 0x01: + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + hasLanguageIndicator = true; + break; + } + break; + + case 0x20: // 0010 + language = CB_DCS_LANG_GROUP_2[dcs & 0x0F]; + break; + + case 0x40: // 01xx + case 0x50: + //case 0x60: Text Compression, not supported + //case 0x70: Text Compression, not supported + case 0x90: // 1001 + encoding = (dcs & 0x0C); + if (encoding == 0x0C) { + encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + } + messageClass = (dcs & PDU_DCS_MSG_CLASS_BITS); + break; + + case 0xF0: + encoding = (dcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET + : PDU_DCS_MSG_CODING_7BITS_ALPHABET; + switch(dcs & PDU_DCS_MSG_CLASS_BITS) { + case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break; + case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break; + case 0x03: messageClass = PDU_DCS_MSG_CLASS_3; break; + } + break; + + case 0x30: // 0011 (Reserved) + case 0x80: // 1000 (Reserved) + case 0xA0: // 1010..1100 (Reserved) + case 0xB0: + case 0xC0: + break; + + default: + throw new Error("Unsupported CBS data coding scheme: " + dcs); + } + + msg.dcs = dcs; + msg.encoding = encoding; + msg.language = language; + msg.messageClass = GECKO_SMS_MESSAGE_CLASSES[messageClass]; + msg.hasLanguageIndicator = hasLanguageIndicator; + }, + + /** + * Read GSM CBS message page parameter. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.4 + */ + readCbPageParameter: function(msg) { + let octet = this.context.Buf.readUint8(); + msg.pageIndex = (octet >>> 4) & 0x0F; + msg.numPages = octet & 0x0F; + if (!msg.pageIndex || !msg.numPages) { + // `If a mobile receives the code 0000 in either the first field or the + // second field then it shall treat the CBS message exactly the same as a + // CBS message with page parameter 0001 0001 (i.e. a single page message).` + msg.pageIndex = msg.numPages = 1; + } + }, + + /** + * Read ETWS Primary Notification message warning type. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.3.24 + */ + readCbWarningType: function(msg) { + let Buf = this.context.Buf; + let word = Buf.readUint8() << 8 | Buf.readUint8(); + msg.etws = { + warningType: (word >>> 9) & 0x7F, + popup: word & 0x80 ? true : false, + emergencyUserAlert: word & 0x100 ? true : false + }; + }, + + /** + * Read GSM CB Data + * + * This parameter is a copy of the 'CBS-Message-Information-Page' as sent + * from the CBC to the BSC. + * + * @see 3GPP TS 23.041 section 9.4.1.2.5 + */ + readGsmCbData: function(msg, length) { + let Buf = this.context.Buf; + let bufAdapter = { + context: this.context, + readHexOctet: function() { + return Buf.readUint8(); + } + }; + + msg.body = null; + msg.data = null; + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + msg.body = this.readSeptetsToString.call(bufAdapter, + Math.floor(length * 8 / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + if (msg.hasLanguageIndicator) { + msg.language = msg.body.substring(0, 2); + msg.body = msg.body.substring(3); + } + break; + + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msg.data = Buf.readUint8Array(length); + break; + + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + if (msg.hasLanguageIndicator) { + msg.language = this.readSeptetsToString.call(bufAdapter, 2, 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + length -= 2; + } + msg.body = this.readUCS2String.call(bufAdapter, length); + break; + } + + if (msg.data || !msg.body) { + return; + } + + // According to 9.3.19 CBS-Message-Information-Page in TS 23.041: + // " + // This parameter is of a fixed length of 82 octets and carries up to and + // including 82 octets of user information. Where the user information is + // less than 82 octets, the remaining octets must be filled with padding. + // " + // According to 6.2.1.1 GSM 7 bit Default Alphabet and 6.2.3 UCS2 in + // TS 23.038, the padding character is <CR>. + for (let i = msg.body.length - 1; i >= 0; i--) { + if (msg.body.charAt(i) !== '\r') { + msg.body = msg.body.substring(0, i + 1); + break; + } + } + }, + + /** + * Read UMTS CB Data + * + * Octet Number(s) Parameter + * 1 Number-of-Pages + * 2 - 83 CBS-Message-Information-Page 1 + * 84 CBS-Message-Information-Length 1 + * ... + * CBS-Message-Information-Page n + * CBS-Message-Information-Length n + * + * @see 3GPP TS 23.041 section 9.4.2.2.5 + */ + readUmtsCbData: function(msg) { + let Buf = this.context.Buf; + let numOfPages = Buf.readUint8(); + if (numOfPages < 0 || numOfPages > 15) { + throw new Error("Invalid numOfPages: " + numOfPages); + } + + let bufAdapter = { + context: this.context, + readHexOctet: function() { + return Buf.readUint8(); + } + }; + + let removePaddingCharactors = function (text) { + for (let i = text.length - 1; i >= 0; i--) { + if (text.charAt(i) !== '\r') { + return text.substring(0, i + 1); + } + } + return text; + }; + + let totalLength = 0, length, pageLengths = []; + for (let i = 0; i < numOfPages; i++) { + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE); + length = Buf.readUint8(); + totalLength += length; + pageLengths.push(length); + } + + // Seek back to beginning of CB Data. + Buf.seekIncoming(-numOfPages * (CB_MSG_PAGE_INFO_SIZE + 1)); + + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: { + let body; + msg.body = ""; + for (let i = 0; i < numOfPages; i++) { + body = this.readSeptetsToString.call(bufAdapter, + Math.floor(pageLengths[i] * 8 / 7), + 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + if (msg.hasLanguageIndicator) { + if (!msg.language) { + msg.language = body.substring(0, 2); + } + body = body.substring(3); + } + + msg.body += removePaddingCharactors(body); + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: { + msg.data = new Uint8Array(totalLength); + for (let i = 0, j = 0; i < numOfPages; i++) { + for (let pageLength = pageLengths[i]; pageLength > 0; pageLength--) { + msg.data[j++] = Buf.readUint8(); + } + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: { + msg.body = ""; + for (let i = 0; i < numOfPages; i++) { + let pageLength = pageLengths[i]; + if (msg.hasLanguageIndicator) { + if (!msg.language) { + msg.language = this.readSeptetsToString.call(bufAdapter, + 2, + 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + } else { + Buf.readUint16(); + } + + pageLength -= 2; + } + + msg.body += removePaddingCharactors( + this.readUCS2String.call(bufAdapter, pageLength)); + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + } + }, + + /** + * Read Cell GSM/ETWS/UMTS Broadcast Message. + * + * @param pduLength + * total length of the incoming PDU in octets. + */ + readCbMessage: function(pduLength) { + // Validity GSM ETWS UMTS + let msg = { + // Internally used in ril_worker: + serial: null, // O O O + updateNumber: null, // O O O + format: null, // O O O + dcs: 0x0F, // O X O + encoding: PDU_DCS_MSG_CODING_7BITS_ALPHABET, // O X O + hasLanguageIndicator: false, // O X O + data: null, // O X O + body: null, // O X O + pageIndex: 1, // O X X + numPages: 1, // O X X + + // DOM attributes: + geographicalScope: null, // O O O + messageCode: null, // O O O + messageId: null, // O O O + language: null, // O X O + fullBody: null, // O X O + fullData: null, // O X O + messageClass: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], // O x O + etws: null // ? O ? + /*{ + warningType: null, // X O X + popup: false, // X O X + emergencyUserAlert: false, // X O X + }*/ + }; + + if (pduLength <= CB_MESSAGE_SIZE_ETWS) { + msg.format = CB_FORMAT_ETWS; + return this.readEtwsCbMessage(msg); + } + + if (pduLength <= CB_MESSAGE_SIZE_GSM) { + msg.format = CB_FORMAT_GSM; + return this.readGsmCbMessage(msg, pduLength); + } + + if (pduLength >= CB_MESSAGE_SIZE_UMTS_MIN && + pduLength <= CB_MESSAGE_SIZE_UMTS_MAX) { + msg.format = CB_FORMAT_UMTS; + return this.readUmtsCbMessage(msg); + } + + throw new Error("Invalid PDU Length: " + pduLength); + }, + + /** + * Read UMTS CBS Message. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.2 + * @see 3GPP TS 25.324 section 10.2 + */ + readUmtsCbMessage: function(msg) { + let Buf = this.context.Buf; + let type = Buf.readUint8(); + if (type != CB_UMTS_MESSAGE_TYPE_CBS) { + throw new Error("Unsupported UMTS Cell Broadcast message type: " + type); + } + + this.readCbMessageIdentifier(msg); + this.readCbSerialNumber(msg); + this.readCbEtwsInfo(msg); + this.readCbDataCodingScheme(msg); + this.readUmtsCbData(msg); + + return msg; + }, + + /** + * Read GSM Cell Broadcast Message. + * + * @param msg + * message object for output. + * @param pduLength + * total length of the incomint PDU in octets. + * + * @see 3GPP TS 23.041 clause 9.4.1.2 + */ + readGsmCbMessage: function(msg, pduLength) { + this.readCbSerialNumber(msg); + this.readCbMessageIdentifier(msg); + this.readCbEtwsInfo(msg); + this.readCbDataCodingScheme(msg); + this.readCbPageParameter(msg); + + // GSM CB message header takes 6 octets. + this.readGsmCbData(msg, pduLength - 6); + + return msg; + }, + + /** + * Read ETWS Primary Notification Message. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 clause 9.4.1.3 + */ + readEtwsCbMessage: function(msg) { + this.readCbSerialNumber(msg); + this.readCbMessageIdentifier(msg); + this.readCbWarningType(msg); + + // Octet 7..56 is Warning Security Information. However, according to + // section 9.4.1.3.6, `The UE shall ignore this parameter.` So we just skip + // processing it here. + + return msg; + }, + + /** + * Read network name. + * + * @param len Length of the information element. + * @return + * { + * networkName: network name. + * shouldIncludeCi: Should Country's initials included in text string. + * } + * @see TS 24.008 clause 10.5.3.5a. + */ + readNetworkName: function(len) { + // According to TS 24.008 Sec. 10.5.3.5a, the first octet is: + // bit 8: must be 1. + // bit 5-7: Text encoding. + // 000 - GSM default alphabet. + // 001 - UCS2 (16 bit). + // else - reserved. + // bit 4: MS should add the letters for Country's Initials and a space + // to the text string if this bit is true. + // bit 1-3: number of spare bits in last octet. + + let codingInfo = this.readHexOctet(); + if (!(codingInfo & 0x80)) { + return null; + } + + let textEncoding = (codingInfo & 0x70) >> 4; + let shouldIncludeCountryInitials = !!(codingInfo & 0x08); + let spareBits = codingInfo & 0x07; + let resultString; + + switch (textEncoding) { + case 0: + // GSM Default alphabet. + resultString = this.readSeptetsToString( + Math.floor(((len - 1) * 8 - spareBits) / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + break; + case 1: + // UCS2 encoded. + resultString = this.context.ICCPDUHelper.readAlphaIdentifier(len - 1); + break; + default: + // Not an available text coding. + return null; + } + + // TODO - Bug 820286: According to shouldIncludeCountryInitials, add + // country initials to the resulting string. + return resultString; + } +}; + +/** + * Provide buffer with bitwise read/write function so make encoding/decoding easier. + */ +function BitBufferHelperObject(/* unused */aContext) { + this.readBuffer = []; + this.writeBuffer = []; +} +BitBufferHelperObject.prototype = { + readCache: 0, + readCacheSize: 0, + readBuffer: null, + readIndex: 0, + writeCache: 0, + writeCacheSize: 0, + writeBuffer: null, + + // Max length is 32 because we use integer as read/write cache. + // All read/write functions are implemented based on bitwise operation. + readBits: function(length) { + if (length <= 0 || length > 32) { + return null; + } + + if (length > this.readCacheSize) { + let bytesToRead = Math.ceil((length - this.readCacheSize) / 8); + for(let i = 0; i < bytesToRead; i++) { + this.readCache = (this.readCache << 8) | (this.readBuffer[this.readIndex++] & 0xFF); + this.readCacheSize += 8; + } + } + + let bitOffset = (this.readCacheSize - length), + resultMask = (1 << length) - 1, + result = 0; + + result = (this.readCache >> bitOffset) & resultMask; + this.readCacheSize -= length; + + return result; + }, + + backwardReadPilot: function(length) { + if (length <= 0) { + return; + } + + // Zero-based position. + let bitIndexToRead = this.readIndex * 8 - this.readCacheSize - length; + + if (bitIndexToRead < 0) { + return; + } + + // Update readIndex, readCache, readCacheSize accordingly. + let readBits = bitIndexToRead % 8; + this.readIndex = Math.floor(bitIndexToRead / 8) + ((readBits) ? 1 : 0); + this.readCache = (readBits) ? this.readBuffer[this.readIndex - 1] : 0; + this.readCacheSize = (readBits) ? (8 - readBits) : 0; + }, + + writeBits: function(value, length) { + if (length <= 0 || length > 32) { + return; + } + + let totalLength = length + this.writeCacheSize; + + // 8-byte cache not full + if (totalLength < 8) { + let valueMask = (1 << length) - 1; + this.writeCache = (this.writeCache << length) | (value & valueMask); + this.writeCacheSize += length; + return; + } + + // Deal with unaligned part + if (this.writeCacheSize) { + let mergeLength = 8 - this.writeCacheSize, + valueMask = (1 << mergeLength) - 1; + + this.writeCache = (this.writeCache << mergeLength) | ((value >> (length - mergeLength)) & valueMask); + this.writeBuffer.push(this.writeCache & 0xFF); + length -= mergeLength; + } + + // Aligned part, just copy + while (length >= 8) { + length -= 8; + this.writeBuffer.push((value >> length) & 0xFF); + } + + // Rest part is saved into cache + this.writeCacheSize = length; + this.writeCache = value & ((1 << length) - 1); + + return; + }, + + // Drop what still in read cache and goto next 8-byte alignment. + // There might be a better naming. + nextOctetAlign: function() { + this.readCache = 0; + this.readCacheSize = 0; + }, + + // Flush current write cache to Buf with padding 0s. + // There might be a better naming. + flushWithPadding: function() { + if (this.writeCacheSize) { + this.writeBuffer.push(this.writeCache << (8 - this.writeCacheSize)); + } + this.writeCache = 0; + this.writeCacheSize = 0; + }, + + startWrite: function(dataBuffer) { + this.writeBuffer = dataBuffer; + this.writeCache = 0; + this.writeCacheSize = 0; + }, + + startRead: function(dataBuffer) { + this.readBuffer = dataBuffer; + this.readCache = 0; + this.readCacheSize = 0; + this.readIndex = 0; + }, + + getWriteBufferSize: function() { + return this.writeBuffer.length; + }, + + overwriteWriteBuffer: function(position, data) { + let writeLength = data.length; + if (writeLength + position >= this.writeBuffer.length) { + writeLength = this.writeBuffer.length - position; + } + for (let i = 0; i < writeLength; i++) { + this.writeBuffer[i + position] = data[i]; + } + } +}; + +/** + * Helper for CDMA PDU + * + * Currently, some function are shared with GsmPDUHelper, they should be + * moved from GsmPDUHelper to a common object shared among GsmPDUHelper and + * CdmaPDUHelper. + */ +function CdmaPDUHelperObject(aContext) { + this.context = aContext; +} +CdmaPDUHelperObject.prototype = { + context: null, + + // 1..........C + // Only "1234567890*#" is defined in C.S0005-D v2.0 + dtmfChars: ".1234567890*#...", + + /** + * Entry point for SMS encoding, the options object is made compatible + * with existing writeMessage() of GsmPDUHelper, but less key is used. + * + * Current used key in options: + * @param number + * String containing the address (number) of the SMS receiver + * @param body + * String containing the message to be sent, segmented part + * @param dcs + * Data coding scheme. One of the PDU_DCS_MSG_CODING_*BITS_ALPHABET + * constants. + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param requestStatusReport + * Request status report. + * @param segmentRef + * Reference number of concatenated SMS message + * @param segmentMaxSeq + * Total number of concatenated SMS message + * @param segmentSeq + * Sequence number of concatenated SMS message + */ + writeMessage: function(options) { + if (DEBUG) { + this.context.debug("cdma_writeMessage: " + JSON.stringify(options)); + } + + // Get encoding + options.encoding = this.gsmDcsToCdmaEncoding(options.dcs); + + // Common Header + if (options.segmentMaxSeq > 1) { + this.writeInt(PDU_CDMA_MSG_TELESERIVCIE_ID_WEMT); + } else { + this.writeInt(PDU_CDMA_MSG_TELESERIVCIE_ID_SMS); + } + + this.writeInt(0); + this.writeInt(PDU_CDMA_MSG_CATEGORY_UNSPEC); + + // Just fill out address info in byte, rild will encap them for us + let addrInfo = this.encodeAddr(options.number); + this.writeByte(addrInfo.digitMode); + this.writeByte(addrInfo.numberMode); + this.writeByte(addrInfo.numberType); + this.writeByte(addrInfo.numberPlan); + this.writeByte(addrInfo.address.length); + for (let i = 0; i < addrInfo.address.length; i++) { + this.writeByte(addrInfo.address[i]); + } + + // Subaddress, not supported + this.writeByte(0); // Subaddress : Type + this.writeByte(0); // Subaddress : Odd + this.writeByte(0); // Subaddress : length + + // User Data + let encodeResult = this.encodeUserData(options); + this.writeByte(encodeResult.length); + for (let i = 0; i < encodeResult.length; i++) { + this.writeByte(encodeResult[i]); + } + + encodeResult = null; + }, + + /** + * Data writters + */ + writeInt: function(value) { + this.context.Buf.writeInt32(value); + }, + + writeByte: function(value) { + this.context.Buf.writeInt32(value & 0xFF); + }, + + /** + * Transform GSM DCS to CDMA encoding. + */ + gsmDcsToCdmaEncoding: function(encoding) { + switch (encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_7BITS_ASCII; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_OCTET; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_UNICODE; + } + throw new Error("gsmDcsToCdmaEncoding(): Invalid GSM SMS DCS value: " + encoding); + }, + + /** + * Encode address into CDMA address format, as a byte array. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.3 Address Parameters + * + * @param address + * String of address to be encoded + */ + encodeAddr: function(address) { + let result = {}; + + result.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + result.numberPlan = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + + if (address[0] === '+') { + address = address.substring(1); + } + + // Try encode with DTMF first + result.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF; + result.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ANSI; + + result.address = []; + for (let i = 0; i < address.length; i++) { + let addrDigit = this.dtmfChars.indexOf(address.charAt(i)); + if (addrDigit < 0) { + result.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_ASCII; + result.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ASCII; + result.address = []; + break; + } + result.address.push(addrDigit); + } + + // Address can't be encoded with DTMF, then use 7-bit ASCII + if (result.digitMode !== PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + if (address.indexOf("@") !== -1) { + result.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_NATIONAL; + } + + for (let i = 0; i < address.length; i++) { + result.address.push(address.charCodeAt(i) & 0x7F); + } + } + + return result; + }, + + /** + * Encode SMS contents in options into CDMA userData field. + * Corresponding and required subparameters will be added automatically. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.7 Bearer Data + * 4.5 Bearer Data Parameters + * + * Current used key in options: + * @param body + * String containing the message to be sent, segmented part + * @param encoding + * Encoding method of CDMA, can be transformed from GSM DCS by function + * cdmaPduHelp.gsmDcsToCdmaEncoding() + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param requestStatusReport + * Request status report. + * @param segmentRef + * Reference number of concatenated SMS message + * @param segmentMaxSeq + * Total number of concatenated SMS message + * @param segmentSeq + * Sequence number of concatenated SMS message + */ + encodeUserData: function(options) { + let userDataBuffer = []; + this.context.BitBufferHelper.startWrite(userDataBuffer); + + // Message Identifier + this.encodeUserDataMsgId(options); + + // User Data + this.encodeUserDataMsg(options); + + // Reply Option + this.encodeUserDataReplyOption(options); + + return userDataBuffer; + }, + + /** + * User data subparameter encoder : Message Identifier + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.1 Message Identifier + */ + encodeUserDataMsgId: function(options) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_MSG_ID, 8); + BitBufferHelper.writeBits(3, 8); + BitBufferHelper.writeBits(PDU_CDMA_MSG_TYPE_SUBMIT, 4); + BitBufferHelper.writeBits(1, 16); // TODO: How to get message ID? + if (options.segmentMaxSeq > 1) { + BitBufferHelper.writeBits(1, 1); + } else { + BitBufferHelper.writeBits(0, 1); + } + + BitBufferHelper.flushWithPadding(); + }, + + /** + * User data subparameter encoder : User Data + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.2 User Data + */ + encodeUserDataMsg: function(options) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_BODY, 8); + // Reserve space for length + BitBufferHelper.writeBits(0, 8); + let lengthPosition = BitBufferHelper.getWriteBufferSize(); + + BitBufferHelper.writeBits(options.encoding, 5); + + // Add user data header for message segement + let msgBody = options.body, + msgBodySize = (options.encoding === PDU_CDMA_MSG_CODING_7BITS_ASCII ? + options.encodedBodyLength : + msgBody.length); + if (options.segmentMaxSeq > 1) { + if (options.encoding === PDU_CDMA_MSG_CODING_7BITS_ASCII) { + BitBufferHelper.writeBits(msgBodySize + 7, 8); // Required length for user data header, in septet(7-bit) + + BitBufferHelper.writeBits(5, 8); // total header length 5 bytes + BitBufferHelper.writeBits(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT, 8); // header id 0 + BitBufferHelper.writeBits(3, 8); // length of element for id 0 is 3 + BitBufferHelper.writeBits(options.segmentRef & 0xFF, 8); // Segement reference + BitBufferHelper.writeBits(options.segmentMaxSeq & 0xFF, 8); // Max segment + BitBufferHelper.writeBits(options.segmentSeq & 0xFF, 8); // Current segment + BitBufferHelper.writeBits(0, 1); // Padding to make header data septet(7-bit) aligned + } else { + if (options.encoding === PDU_CDMA_MSG_CODING_UNICODE) { + BitBufferHelper.writeBits(msgBodySize + 3, 8); // Required length for user data header, in 16-bit + } else { + BitBufferHelper.writeBits(msgBodySize + 6, 8); // Required length for user data header, in octet(8-bit) + } + + BitBufferHelper.writeBits(5, 8); // total header length 5 bytes + BitBufferHelper.writeBits(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT, 8); // header id 0 + BitBufferHelper.writeBits(3, 8); // length of element for id 0 is 3 + BitBufferHelper.writeBits(options.segmentRef & 0xFF, 8); // Segement reference + BitBufferHelper.writeBits(options.segmentMaxSeq & 0xFF, 8); // Max segment + BitBufferHelper.writeBits(options.segmentSeq & 0xFF, 8); // Current segment + } + } else { + BitBufferHelper.writeBits(msgBodySize, 8); + } + + // Encode message based on encoding method + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + for (let i = 0; i < msgBody.length; i++) { + switch (options.encoding) { + case PDU_CDMA_MSG_CODING_OCTET: { + let msgDigit = msgBody.charCodeAt(i); + BitBufferHelper.writeBits(msgDigit, 8); + break; + } + case PDU_CDMA_MSG_CODING_7BITS_ASCII: { + let msgDigit = msgBody.charCodeAt(i), + msgDigitChar = msgBody.charAt(i); + + if (msgDigit >= 32) { + BitBufferHelper.writeBits(msgDigit, 7); + } else { + msgDigit = langTable.indexOf(msgDigitChar); + + if (msgDigit === PDU_NL_EXTENDED_ESCAPE) { + break; + } + if (msgDigit >= 0) { + BitBufferHelper.writeBits(msgDigit, 7); + } else { + msgDigit = langShiftTable.indexOf(msgDigitChar); + if (msgDigit == -1) { + throw new Error("'" + msgDigitChar + "' is not in 7 bit alphabet " + + langIndex + ":" + langShiftIndex + "!"); + } + + if (msgDigit === PDU_NL_RESERVED_CONTROL) { + break; + } + + BitBufferHelper.writeBits(PDU_NL_EXTENDED_ESCAPE, 7); + BitBufferHelper.writeBits(msgDigit, 7); + } + } + break; + } + case PDU_CDMA_MSG_CODING_UNICODE: { + let msgDigit = msgBody.charCodeAt(i); + BitBufferHelper.writeBits(msgDigit, 16); + break; + } + } + } + BitBufferHelper.flushWithPadding(); + + // Fill length + let currentPosition = BitBufferHelper.getWriteBufferSize(); + BitBufferHelper.overwriteWriteBuffer(lengthPosition - 1, [currentPosition - lengthPosition]); + }, + + /** + * User data subparameter encoder : Reply Option + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.11 Reply Option + */ + encodeUserDataReplyOption: function(options) { + if (options.requestStatusReport) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_REPLY_OPTION, 8); + BitBufferHelper.writeBits(1, 8); + BitBufferHelper.writeBits(0, 1); // USER_ACK_REQ + BitBufferHelper.writeBits(1, 1); // DAK_REQ + BitBufferHelper.flushWithPadding(); + } + }, + + /** + * Entry point for SMS decoding, the returned object is made compatible + * with existing readMessage() of GsmPDUHelper + */ + readMessage: function() { + let message = {}; + + // Teleservice Identifier + message.teleservice = this.readInt(); + + // Message Type + let isServicePresent = this.readByte(); + if (isServicePresent) { + message.messageType = PDU_CDMA_MSG_TYPE_BROADCAST; + } else { + if (message.teleservice) { + message.messageType = PDU_CDMA_MSG_TYPE_P2P; + } else { + message.messageType = PDU_CDMA_MSG_TYPE_ACK; + } + } + + // Service Category + message.service = this.readInt(); + + // Originated Address + let addrInfo = {}; + addrInfo.digitMode = (this.readInt() & 0x01); + addrInfo.numberMode = (this.readInt() & 0x01); + addrInfo.numberType = (this.readInt() & 0x01); + addrInfo.numberPlan = (this.readInt() & 0x01); + addrInfo.addrLength = this.readByte(); + addrInfo.address = []; + for (let i = 0; i < addrInfo.addrLength; i++) { + addrInfo.address.push(this.readByte()); + } + message.sender = this.decodeAddr(addrInfo); + + // Originated Subaddress + addrInfo.Type = (this.readInt() & 0x07); + addrInfo.Odd = (this.readByte() & 0x01); + addrInfo.addrLength = this.readByte(); + for (let i = 0; i < addrInfo.addrLength; i++) { + let addrDigit = this.readByte(); + message.sender += String.fromCharCode(addrDigit); + } + + // Bearer Data + this.decodeUserData(message); + + // Bearer Data Sub-Parameter: User Data + let userData = message[PDU_CDMA_MSG_USERDATA_BODY]; + [message.header, message.body, message.encoding, message.data] = + (userData) ? [userData.header, userData.body, userData.encoding, userData.data] + : [null, null, null, null]; + + // Bearer Data Sub-Parameter: Message Status + // Success Delivery (0) if both Message Status and User Data are absent. + // Message Status absent (-1) if only User Data is available. + let msgStatus = message[PDU_CDMA_MSG_USER_DATA_MSG_STATUS]; + [message.errorClass, message.msgStatus] = + (msgStatus) ? [msgStatus.errorClass, msgStatus.msgStatus] + : ((message.body) ? [-1, -1] : [0, 0]); + + // Transform message to GSM msg + let msg = { + SMSC: "", + mti: 0, + udhi: 0, + sender: message.sender, + recipient: null, + pid: PDU_PID_DEFAULT, + epid: PDU_PID_DEFAULT, + dcs: 0, + mwi: null, + replace: false, + header: message.header, + body: message.body, + data: message.data, + sentTimestamp: message[PDU_CDMA_MSG_USERDATA_TIMESTAMP], + language: message[PDU_CDMA_LANGUAGE_INDICATOR], + status: null, + scts: null, + dt: null, + encoding: message.encoding, + messageClass: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + messageType: message.messageType, + serviceCategory: message.service, + subMsgType: message[PDU_CDMA_MSG_USERDATA_MSG_ID].msgType, + msgId: message[PDU_CDMA_MSG_USERDATA_MSG_ID].msgId, + errorClass: message.errorClass, + msgStatus: message.msgStatus, + teleservice: message.teleservice + }; + + return msg; + }, + + /** + * Helper for processing received SMS parcel data. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return Message parsed or null for invalid message. + */ + processReceivedSms: function(length) { + if (!length) { + if (DEBUG) this.context.debug("Received empty SMS!"); + return [null, PDU_FCS_UNSPECIFIED]; + } + + let message = this.readMessage(); + if (DEBUG) this.context.debug("Got new SMS: " + JSON.stringify(message)); + + // Determine result + if (!message) { + return [null, PDU_FCS_UNSPECIFIED]; + } + + return [message, PDU_FCS_OK]; + }, + + /** + * Data readers + */ + readInt: function() { + return this.context.Buf.readInt32(); + }, + + readByte: function() { + return (this.context.Buf.readInt32() & 0xFF); + }, + + /** + * Decode CDMA address data into address string + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.3 Address Parameters + * + * Required key in addrInfo + * @param addrLength + * Length of address + * @param digitMode + * Address encoding method + * @param address + * Array of encoded address data + */ + decodeAddr: function(addrInfo) { + let result = ""; + for (let i = 0; i < addrInfo.addrLength; i++) { + if (addrInfo.digitMode === PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + result += this.dtmfChars.charAt(addrInfo.address[i]); + } else { + result += String.fromCharCode(addrInfo.address[i]); + } + } + return result; + }, + + /** + * Read userData in parcel buffer and decode into message object. + * Each subparameter will be stored in corresponding key. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.7 Bearer Data + * 4.5 Bearer Data Parameters + */ + decodeUserData: function(message) { + let userDataLength = this.readInt(); + + while (userDataLength > 0) { + let id = this.readByte(), + length = this.readByte(), + userDataBuffer = []; + + for (let i = 0; i < length; i++) { + userDataBuffer.push(this.readByte()); + } + + this.context.BitBufferHelper.startRead(userDataBuffer); + + switch (id) { + case PDU_CDMA_MSG_USERDATA_MSG_ID: + message[id] = this.decodeUserDataMsgId(); + break; + case PDU_CDMA_MSG_USERDATA_BODY: + message[id] = this.decodeUserDataMsg(message[PDU_CDMA_MSG_USERDATA_MSG_ID].userHeader); + break; + case PDU_CDMA_MSG_USERDATA_TIMESTAMP: + message[id] = this.decodeUserDataTimestamp(); + break; + case PDU_CDMA_MSG_USERDATA_REPLY_OPTION: + message[id] = this.decodeUserDataReplyOption(); + break; + case PDU_CDMA_LANGUAGE_INDICATOR: + message[id] = this.decodeLanguageIndicator(); + break; + case PDU_CDMA_MSG_USERDATA_CALLBACK_NUMBER: + message[id] = this.decodeUserDataCallbackNumber(); + break; + case PDU_CDMA_MSG_USER_DATA_MSG_STATUS: + message[id] = this.decodeUserDataMsgStatus(); + break; + } + + userDataLength -= (length + 2); + userDataBuffer = []; + } + }, + + /** + * User data subparameter decoder: Message Identifier + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.1 Message Identifier + */ + decodeUserDataMsgId: function() { + let result = {}; + let BitBufferHelper = this.context.BitBufferHelper; + result.msgType = BitBufferHelper.readBits(4); + result.msgId = BitBufferHelper.readBits(16); + result.userHeader = BitBufferHelper.readBits(1); + + return result; + }, + + /** + * Decode user data header, we only care about segment information + * on CDMA. + * + * This function is mostly copied from gsmPduHelper.readUserDataHeader() but + * change the read function, because CDMA user header decoding is't byte-wise + * aligned. + */ + decodeUserDataHeader: function(encoding) { + let BitBufferHelper = this.context.BitBufferHelper; + let header = {}, + headerSize = BitBufferHelper.readBits(8), + userDataHeaderSize = headerSize + 1, + headerPaddingBits = 0; + + // Calculate header size + if (encoding === PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + // Length is in 7-bit + header.length = Math.ceil(userDataHeaderSize * 8 / 7); + // Calulate padding length + headerPaddingBits = (header.length * 7) - (userDataHeaderSize * 8); + } else if (encoding === PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + header.length = userDataHeaderSize; + } else { + header.length = userDataHeaderSize / 2; + } + + while (headerSize) { + let identifier = BitBufferHelper.readBits(8), + length = BitBufferHelper.readBits(8); + + headerSize -= (2 + length); + + switch (identifier) { + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT: { + let ref = BitBufferHelper.readBits(8), + max = BitBufferHelper.readBits(8), + seq = BitBufferHelper.readBits(8); + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_8BIT: { + let dstp = BitBufferHelper.readBits(8), + orip = BitBufferHelper.readBits(8); + if ((dstp < PDU_APA_RESERVED_8BIT_PORTS) + || (orip < PDU_APA_RESERVED_8BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.3: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + break; + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_16BIT: { + let dstp = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8), + orip = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8); + // 3GPP TS 23.040 clause 9.2.3.24.4: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + if ((dstp < PDU_APA_VALID_16BIT_PORTS) + && (orip < PDU_APA_VALID_16BIT_PORTS)) { + header.destinationPort = dstp; + header.originatorPort = orip; + } + break; + } + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT: { + let ref = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8), + max = BitBufferHelper.readBits(8), + seq = BitBufferHelper.readBits(8); + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT: { + let langShiftIndex = BitBufferHelper.readBits(8); + if (langShiftIndex < PDU_NL_SINGLE_SHIFT_TABLES.length) { + header.langShiftIndex = langShiftIndex; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT: { + let langIndex = BitBufferHelper.readBits(8); + if (langIndex < PDU_NL_LOCKING_SHIFT_TABLES.length) { + header.langIndex = langIndex; + } + break; + } + case PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION: { + let msgInd = BitBufferHelper.readBits(8) & 0xFF, + msgCount = BitBufferHelper.readBits(8); + /* + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + * bits 1 0 : basic message indication type + * bits 4 3 2 : extended message indication type + * bits 6 5 : Profile id + * bit 7 : storage type + */ + let storeType = msgInd & PDU_MWI_STORE_TYPE_BIT; + header.mwi = {}; + mwi = header.mwi; + + if (storeType == PDU_MWI_STORE_TYPE_STORE) { + // Store message because TP_UDH indicates so, note this may override + // the setting in DCS, but that is expected + mwi.discard = false; + } else if (mwi.discard === undefined) { + // storeType == PDU_MWI_STORE_TYPE_DISCARD + // only override mwi.discard here if it hasn't already been set + mwi.discard = true; + } + + mwi.msgCount = msgCount & 0xFF; + mwi.active = mwi.msgCount > 0; + + if (DEBUG) { + this.context.debug("MWI in TP_UDH received: " + JSON.stringify(mwi)); + } + break; + } + default: + // Drop unsupported id + for (let i = 0; i < length; i++) { + BitBufferHelper.readBits(8); + } + } + } + + // Consume padding bits + if (headerPaddingBits) { + BitBufferHelper.readBits(headerPaddingBits); + } + + return header; + }, + + getCdmaMsgEncoding: function(encoding) { + // Determine encoding method + switch (encoding) { + case PDU_CDMA_MSG_CODING_7BITS_ASCII: + case PDU_CDMA_MSG_CODING_IA5: + case PDU_CDMA_MSG_CODING_7BITS_GSM: + return PDU_DCS_MSG_CODING_7BITS_ALPHABET; + case PDU_CDMA_MSG_CODING_OCTET: + case PDU_CDMA_MSG_CODING_IS_91: + case PDU_CDMA_MSG_CODING_LATIN_HEBREW: + case PDU_CDMA_MSG_CODING_LATIN: + return PDU_DCS_MSG_CODING_8BITS_ALPHABET; + case PDU_CDMA_MSG_CODING_UNICODE: + case PDU_CDMA_MSG_CODING_SHIFT_JIS: + case PDU_CDMA_MSG_CODING_KOREAN: + return PDU_DCS_MSG_CODING_16BITS_ALPHABET; + } + return null; + }, + + decodeCdmaPDUMsg: function(encoding, msgType, msgBodySize) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + let BitBufferHelper = this.context.BitBufferHelper; + let result = ""; + let msgDigit; + switch (encoding) { + case PDU_CDMA_MSG_CODING_OCTET: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(8)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_IS_91: // TODO : Require Test + // Referenced from android code + switch (msgType) { + case PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS: + case PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS_FULL: + case PDU_CDMA_MSG_CODING_IS_91_TYPE_VOICEMAIL_STATUS: + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(6) + 0x20); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_IS_91_TYPE_CLI: + let addrInfo = {}; + addrInfo.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF; + addrInfo.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ANSI; + addrInfo.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + addrInfo.numberPlan = PDU_CDMA_MSG_ADDR_NUMBER_PLAN_UNKNOWN; + addrInfo.addrLength = msgBodySize; + addrInfo.address = []; + for (let i = 0; i < addrInfo.addrLength; i++) { + addrInfo.address.push(BitBufferHelper.readBits(4)); + } + result = this.decodeAddr(addrInfo); + break; + } + // Fall through. + case PDU_CDMA_MSG_CODING_7BITS_ASCII: + case PDU_CDMA_MSG_CODING_IA5: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(7); + if (msgDigit >= 32) { + msgDigit = String.fromCharCode(msgDigit); + } else { + if (msgDigit !== PDU_NL_EXTENDED_ESCAPE) { + msgDigit = langTable[msgDigit]; + } else { + msgDigit = BitBufferHelper.readBits(7); + msgBodySize--; + msgDigit = langShiftTable[msgDigit]; + } + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_UNICODE: + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(16)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_7BITS_GSM: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(7); + if (msgDigit !== PDU_NL_EXTENDED_ESCAPE) { + msgDigit = langTable[msgDigit]; + } else { + msgDigit = BitBufferHelper.readBits(7); + msgBodySize--; + msgDigit = langShiftTable[msgDigit]; + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_LATIN: // TODO : Require Test + // Reference : http://en.wikipedia.org/wiki/ISO/IEC_8859-1 + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(8)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_LATIN_HEBREW: // TODO : Require Test + // Reference : http://en.wikipedia.org/wiki/ISO/IEC_8859-8 + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(8); + if (msgDigit === 0xDF) { + msgDigit = String.fromCharCode(0x2017); + } else if (msgDigit === 0xFD) { + msgDigit = String.fromCharCode(0x200E); + } else if (msgDigit === 0xFE) { + msgDigit = String.fromCharCode(0x200F); + } else if (msgDigit >= 0xE0 && msgDigit <= 0xFA) { + msgDigit = String.fromCharCode(0x4F0 + msgDigit); + } else { + msgDigit = String.fromCharCode(msgDigit); + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_SHIFT_JIS: + // Reference : http://msdn.microsoft.com/en-US/goglobal/cc305152.aspx + // http://demo.icu-project.org/icu-bin/convexp?conv=Shift_JIS + let shift_jis_message = []; + + while (msgBodySize > 0) { + shift_jis_message.push(BitBufferHelper.readBits(8)); + msgBodySize--; + } + + let decoder = new TextDecoder("shift_jis"); + result = decoder.decode(new Uint8Array(shift_jis_message)); + break; + case PDU_CDMA_MSG_CODING_KOREAN: + case PDU_CDMA_MSG_CODING_GSM_DCS: + // Fall through. + default: + break; + } + return result; + }, + + /** + * User data subparameter decoder : User Data + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.2 User Data + */ + decodeUserDataMsg: function(hasUserHeader) { + let BitBufferHelper = this.context.BitBufferHelper; + let result = {}, + encoding = BitBufferHelper.readBits(5), + msgType; + + if (encoding === PDU_CDMA_MSG_CODING_IS_91) { + msgType = BitBufferHelper.readBits(8); + } + result.encoding = this.getCdmaMsgEncoding(encoding); + + let msgBodySize = BitBufferHelper.readBits(8); + + // For segmented SMS, a user header is included before sms content + if (hasUserHeader) { + result.header = this.decodeUserDataHeader(result.encoding); + // header size is included in body size, they are decoded + msgBodySize -= result.header.length; + } + + // Store original payload if enconding is OCTET for further handling of WAP Push, etc. + if (encoding === PDU_CDMA_MSG_CODING_OCTET && msgBodySize > 0) { + result.data = new Uint8Array(msgBodySize); + for (let i = 0; i < msgBodySize; i++) { + result.data[i] = BitBufferHelper.readBits(8); + } + BitBufferHelper.backwardReadPilot(8 * msgBodySize); + } + + // Decode sms content + result.body = this.decodeCdmaPDUMsg(encoding, msgType, msgBodySize); + + return result; + }, + + decodeBcd: function(value) { + return ((value >> 4) & 0xF) * 10 + (value & 0x0F); + }, + + /** + * User data subparameter decoder : Time Stamp + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.4 Message Center Time Stamp + */ + decodeUserDataTimestamp: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let year = this.decodeBcd(BitBufferHelper.readBits(8)), + month = this.decodeBcd(BitBufferHelper.readBits(8)) - 1, + date = this.decodeBcd(BitBufferHelper.readBits(8)), + hour = this.decodeBcd(BitBufferHelper.readBits(8)), + min = this.decodeBcd(BitBufferHelper.readBits(8)), + sec = this.decodeBcd(BitBufferHelper.readBits(8)); + + if (year >= 96 && year <= 99) { + year += 1900; + } else { + year += 2000; + } + + let result = (new Date(year, month, date, hour, min, sec, 0)).valueOf(); + + return result; + }, + + /** + * User data subparameter decoder : Reply Option + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.11 Reply Option + */ + decodeUserDataReplyOption: function() { + let replyAction = this.context.BitBufferHelper.readBits(4), + result = { userAck: (replyAction & 0x8) ? true : false, + deliverAck: (replyAction & 0x4) ? true : false, + readAck: (replyAction & 0x2) ? true : false, + report: (replyAction & 0x1) ? true : false + }; + + return result; + }, + + /** + * User data subparameter decoder : Language Indicator + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.14 Language Indicator + */ + decodeLanguageIndicator: function() { + let language = this.context.BitBufferHelper.readBits(8); + let result = CB_CDMA_LANG_GROUP[language]; + return result; + }, + + /** + * User data subparameter decoder : Call-Back Number + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.15 Call-Back Number + */ + decodeUserDataCallbackNumber: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let digitMode = BitBufferHelper.readBits(1); + if (digitMode) { + let numberType = BitBufferHelper.readBits(3), + numberPlan = BitBufferHelper.readBits(4); + } + let numberFields = BitBufferHelper.readBits(8), + result = ""; + for (let i = 0; i < numberFields; i++) { + if (digitMode === PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + let addrDigit = BitBufferHelper.readBits(4); + result += this.dtmfChars.charAt(addrDigit); + } else { + let addrDigit = BitBufferHelper.readBits(8); + result += String.fromCharCode(addrDigit); + } + } + + return result; + }, + + /** + * User data subparameter decoder : Message Status + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.21 Message Status + */ + decodeUserDataMsgStatus: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let result = { + errorClass: BitBufferHelper.readBits(2), + msgStatus: BitBufferHelper.readBits(6) + }; + + return result; + }, + + /** + * Decode information record parcel. + */ + decodeInformationRecord: function() { + let Buf = this.context.Buf; + let records = []; + let numOfRecords = Buf.readInt32(); + + let type; + let record; + for (let i = 0; i < numOfRecords; i++) { + record = {}; + type = Buf.readInt32(); + + switch (type) { + /* + * Every type is encaped by ril, except extended display + */ + case PDU_CDMA_INFO_REC_TYPE_DISPLAY: + case PDU_CDMA_INFO_REC_TYPE_EXTENDED_DISPLAY: + record.display = Buf.readString(); + break; + case PDU_CDMA_INFO_REC_TYPE_CALLED_PARTY_NUMBER: + record.calledNumber = {}; + record.calledNumber.number = Buf.readString(); + record.calledNumber.type = Buf.readInt32(); + record.calledNumber.plan = Buf.readInt32(); + record.calledNumber.pi = Buf.readInt32(); + record.calledNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_CALLING_PARTY_NUMBER: + record.callingNumber = {}; + record.callingNumber.number = Buf.readString(); + record.callingNumber.type = Buf.readInt32(); + record.callingNumber.plan = Buf.readInt32(); + record.callingNumber.pi = Buf.readInt32(); + record.callingNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_CONNECTED_NUMBER: + record.connectedNumber = {}; + record.connectedNumber.number = Buf.readString(); + record.connectedNumber.type = Buf.readInt32(); + record.connectedNumber.plan = Buf.readInt32(); + record.connectedNumber.pi = Buf.readInt32(); + record.connectedNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_SIGNAL: + record.signal = {}; + if (!Buf.readInt32()) { // Non-zero if signal is present. + Buf.seekIncoming(3 * Buf.UINT32_SIZE); + continue; + } + record.signal.type = Buf.readInt32(); + record.signal.alertPitch = Buf.readInt32(); + record.signal.signal = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_REDIRECTING_NUMBER: + record.redirect = {}; + record.redirect.number = Buf.readString(); + record.redirect.type = Buf.readInt32(); + record.redirect.plan = Buf.readInt32(); + record.redirect.pi = Buf.readInt32(); + record.redirect.si = Buf.readInt32(); + record.redirect.reason = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_LINE_CONTROL: + record.lineControl = {}; + record.lineControl.polarityIncluded = Buf.readInt32(); + record.lineControl.toggle = Buf.readInt32(); + record.lineControl.reverse = Buf.readInt32(); + record.lineControl.powerDenial = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_CLIR: + record.clirCause = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_AUDIO_CONTROL: + record.audioControl = {}; + record.audioControl.upLink = Buf.readInt32(); + record.audioControl.downLink = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_RELEASE: + // Fall through + default: + throw new Error("UNSOLICITED_CDMA_INFO_REC(), Unsupported information record type " + type + "\n"); + } + + records.push(record); + } + + return records; + } +}; + +/** + * Helper for processing ICC PDUs. + */ +function ICCPDUHelperObject(aContext) { + this.context = aContext; +} +ICCPDUHelperObject.prototype = { + context: null, + + /** + * Read GSM 8-bit unpacked octets, + * which are default 7-bit alphabets with bit 8 set to 0. + * + * @param numOctets + * Number of octets to be read. + */ + read8BitUnpackedToString: function(numOctets) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let ret = ""; + let escapeFound = false; + let i; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + for(i = 0; i < numOctets; i++) { + let octet = GsmPDUHelper.readHexOctet(); + if (octet == 0xff) { + i++; + break; + } + + if (escapeFound) { + escapeFound = false; + if (octet == PDU_NL_EXTENDED_ESCAPE) { + // According to 3GPP TS 23.038, section 6.2.1.1, NOTE 1, "On + // receipt of this code, a receiving entity shall display a space + // until another extensiion table is defined." + ret += " "; + } else if (octet == PDU_NL_RESERVED_CONTROL) { + // According to 3GPP TS 23.038 B.2, "This code represents a control + // character and therefore must not be used for language specific + // characters." + ret += " "; + } else { + ret += langShiftTable[octet]; + } + } else if (octet == PDU_NL_EXTENDED_ESCAPE) { + escapeFound = true; + } else { + ret += langTable[octet]; + } + } + + let Buf = this.context.Buf; + Buf.seekIncoming((numOctets - i) * Buf.PDU_HEX_OCTET_SIZE); + return ret; + }, + + /** + * Write GSM 8-bit unpacked octets. + * + * @param numOctets Number of total octets to be writen, including trailing + * 0xff. + * @param str String to be written. Could be null. + * + * @return The string has been written into Buf. "" if str is null. + */ + writeStringTo8BitUnpacked: function(numOctets, str) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + let GsmPDUHelper = this.context.GsmPDUHelper; + + // If the character is GSM extended alphabet, two octets will be written. + // So we need to keep track of number of octets to be written. + let i, j; + let len = str ? str.length : 0; + for (i = 0, j = 0; i < len && j < numOctets; i++) { + let c = str.charAt(i); + let octet = langTable.indexOf(c); + + if (octet == -1) { + // Make sure we still have enough space to write two octets. + if (j + 2 > numOctets) { + break; + } + + octet = langShiftTable.indexOf(c); + if (octet == -1) { + // Fallback to ASCII space. + octet = langTable.indexOf(' '); + } else { + GsmPDUHelper.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + j++; + } + } + GsmPDUHelper.writeHexOctet(octet); + j++; + } + + // trailing 0xff + while (j++ < numOctets) { + GsmPDUHelper.writeHexOctet(0xff); + } + + return (str) ? str.substring(0, i) : ""; + }, + + /** + * Write UCS2 String on UICC. + * The default choose 0x81 or 0x82 encode, otherwise use 0x80 encode. + * + * @see TS 102.221, Annex A. + * @param numOctets + * Total number of octets to be written. This includes the length of + * alphaId and the length of trailing unused octets(0xff). + * @param str + * String to be written. + * + * @return The string has been written into Buf. + */ + writeICCUCS2String: function(numOctets, str) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let scheme = 0x80; + let basePointer; + + if (str.length > 2) { + let min = 0xFFFF; + let max = 0; + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + // filter out GSM Default Alphabet character + if (code & 0xFF80) { + if (min > code) { + min = code; + } + if (max < code) { + max = code; + } + } + } + + // 0x81 and 0x82 only support 'half-page', i.e., 128 characters. + if ((max - min) >= 0 && (max - min) < 128) { + // 0x81 base pointer is 0hhh hhhh h000 0000, and bit 16 is set to zero, + // therefore it can't compute 0x8000~0xFFFF. + // Since 0x81 only support 128 characters, + // either XX00~XX7f(bit 8 are 0) or XX80~XXff(bit 8 are 1) + if (((min & 0x7f80) == (max & 0x7f80)) && + ((max & 0x8000) == 0)) { + scheme = 0x81; + basePointer = min & 0x7f80; + } else { + scheme = 0x82; + basePointer = min; + } + } + } + + switch (scheme) { + /** + * +------+---------+---------+---------+---------+------+------+ + * | 0x80 | Ch1_msb | Ch1_lsb | Ch2_msb | Ch2_lsb | 0xff | 0xff | + * +------+---------+---------+---------+---------+------+------+ + */ + case 0x80: { + // 0x80 support UCS2 0000~ffff + GsmPDUHelper.writeHexOctet(0x80); + numOctets--; + // Now the str is UCS2 string, each character will take 2 octets. + if (str.length * 2 > numOctets) { + str = str.substring(0, Math.floor(numOctets / 2)); + } + GsmPDUHelper.writeUCS2String(str); + + // trailing 0xff + for (let i = str.length * 2; i < numOctets; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return str; + } + /** + * +------+-----+--------------+-----+-----+-----+--------+------+ + * | 0x81 | len | base_pointer | Ch1 | Ch2 | ... | Ch_len | 0xff | + * +------+-----+--------------+-----+-----+-----+--------+------+ + * + * len: The length of characters. + * base_pointer: 0hhh hhhh h000 0000 + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n - base_pointer) | 0x80 + * + */ + case 0x81: { + GsmPDUHelper.writeHexOctet(0x81); + + if (str.length > (numOctets - 3)) { + str = str.substring(0, numOctets - 3); + } + + GsmPDUHelper.writeHexOctet(str.length); + GsmPDUHelper.writeHexOctet((basePointer >> 7) & 0xff); + numOctets -= 3; + break; + } + /* +------+-----+------------------+------------------+-----+-----+-----+--------+ + * | 0x82 | len | base_pointer_msb | base_pointer_lsb | Ch1 | Ch2 | ... | Ch_len | + * +------+-----+------------------+------------------+-----+-----+-----+--------+ + * + * len: The length of characters. + * base_pointer_msb, base_pointer_lsn: base_pointer + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n - base_pointer) | 0x80 + */ + case 0x82: { + GsmPDUHelper.writeHexOctet(0x82); + + if (str.length > (numOctets - 4)) { + str = str.substring(0, numOctets - 4); + } + + GsmPDUHelper.writeHexOctet(str.length); + GsmPDUHelper.writeHexOctet((basePointer >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(basePointer & 0xff); + numOctets -= 4; + break; + } + } + + if (scheme == 0x81 || scheme == 0x82) { + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + + // bit 8 = 0, + // GSM default alphabets + if (code >> 8 == 0) { + GsmPDUHelper.writeHexOctet(code & 0x7F); + } else { + // bit 8 = 1, + // UCS2 character whose char code is (code - basePointer) | 0x80 + GsmPDUHelper.writeHexOctet((code - basePointer) | 0x80); + } + } + + // trailing 0xff + for (let i = 0; i < numOctets - str.length; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + } + return str; + }, + + /** + * Read UCS2 String on UICC. + * + * @see TS 101.221, Annex A. + * @param scheme + * Coding scheme for UCS2 on UICC. One of 0x80, 0x81 or 0x82. + * @param numOctets + * Number of octets to be read as UCS2 string. + */ + readICCUCS2String: function(scheme, numOctets) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let str = ""; + switch (scheme) { + /** + * +------+---------+---------+---------+---------+------+------+ + * | 0x80 | Ch1_msb | Ch1_lsb | Ch2_msb | Ch2_lsb | 0xff | 0xff | + * +------+---------+---------+---------+---------+------+------+ + */ + case 0x80: + let isOdd = numOctets % 2; + let i; + for (i = 0; i < numOctets - isOdd; i += 2) { + let code = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + if (code == 0xffff) { + i += 2; + break; + } + str += String.fromCharCode(code); + } + + // Skip trailing 0xff + Buf.seekIncoming((numOctets - i) * Buf.PDU_HEX_OCTET_SIZE); + break; + case 0x81: // Fall through + case 0x82: + /** + * +------+-----+--------+-----+-----+-----+--------+------+ + * | 0x81 | len | offset | Ch1 | Ch2 | ... | Ch_len | 0xff | + * +------+-----+--------+-----+-----+-----+--------+------+ + * + * len : The length of characters. + * offset : 0hhh hhhh h000 0000 + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n & 0x7f) + offset + * + * +------+-----+------------+------------+-----+-----+-----+--------+ + * | 0x82 | len | offset_msb | offset_lsb | Ch1 | Ch2 | ... | Ch_len | + * +------+-----+------------+------------+-----+-----+-----+--------+ + * + * len : The length of characters. + * offset_msb, offset_lsn: offset + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n & 0x7f) + offset + */ + let len = GsmPDUHelper.readHexOctet(); + let offset, headerLen; + if (scheme == 0x81) { + offset = GsmPDUHelper.readHexOctet() << 7; + headerLen = 2; + } else { + offset = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + headerLen = 3; + } + + for (let i = 0; i < len; i++) { + let ch = GsmPDUHelper.readHexOctet(); + if (ch & 0x80) { + // UCS2 + str += String.fromCharCode((ch & 0x7f) + offset); + } else { + // GSM 8bit + let count = 0, gotUCS2 = 0; + while ((i + count + 1 < len)) { + count++; + if (GsmPDUHelper.readHexOctet() & 0x80) { + gotUCS2 = 1; + break; + } + } + // Unread. + // +1 for the GSM alphabet indexed at i, + Buf.seekIncoming(-1 * (count + 1) * Buf.PDU_HEX_OCTET_SIZE); + str += this.read8BitUnpackedToString(count + 1 - gotUCS2); + i += count - gotUCS2; + } + } + + // Skipping trailing 0xff + Buf.seekIncoming((numOctets - len - headerLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + return str; + }, + + /** + * Read Alpha Id and Dialling number from TS TS 151.011 clause 10.5.1 + * + * @param recordSize The size of linear fixed record. + */ + readAlphaIdDiallingNumber: function(recordSize) { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + + let alphaLen = recordSize - ADN_FOOTER_SIZE_BYTES; + let alphaId = this.readAlphaIdentifier(alphaLen); + + let number = this.readNumberWithLength(); + + // Skip unused octet, CCP + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + + let extRecordNumber = this.context.GsmPDUHelper.readHexOctet(); + Buf.readStringDelimiter(length); + + let contact = null; + if (alphaId || number) { + contact = {alphaId: alphaId, + number: number, + extRecordNumber: extRecordNumber}; + } + + return contact; + }, + + /** + * Write Alpha Identifier and Dialling number from TS 151.011 clause 10.5.1 + * + * @param recordSize The size of linear fixed record. + * @param alphaId Alpha Identifier to be written. + * @param number Dialling Number to be written. + * @param extRecordNumber The record identifier of the EXT. + * + * @return An object contains the alphaId and number + * that have been written into Buf. + */ + writeAlphaIdDiallingNumber: function(recordSize, alphaId, number, extRecordNumber) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + let alphaLen = recordSize - ADN_FOOTER_SIZE_BYTES; + let writtenAlphaId = this.writeAlphaIdentifier(alphaLen, alphaId); + let writtenNumber = this.writeNumberWithLength(number); + + // Write unused CCP octet 0xff. + GsmPDUHelper.writeHexOctet(0xff); + GsmPDUHelper.writeHexOctet((extRecordNumber != null) ? extRecordNumber : 0xff); + + Buf.writeStringDelimiter(strLen); + + return {alphaId: writtenAlphaId, + number: writtenNumber}; + }, + + /** + * Read Alpha Identifier. + * + * @see TS 131.102 + * + * @param numOctets + * Number of octets to be read. + * + * It uses either + * 1. SMS default 7-bit alphabet with bit 8 set to 0. + * 2. UCS2 string. + * + * Unused bytes should be set to 0xff. + */ + readAlphaIdentifier: function(numOctets) { + if (numOctets === 0) { + return ""; + } + + let temp; + // Read the 1st octet to determine the encoding. + if ((temp = this.context.GsmPDUHelper.readHexOctet()) == 0x80 || + temp == 0x81 || + temp == 0x82) { + numOctets--; + return this.readICCUCS2String(temp, numOctets); + } else { + let Buf = this.context.Buf; + Buf.seekIncoming(-1 * Buf.PDU_HEX_OCTET_SIZE); + return this.read8BitUnpackedToString(numOctets); + } + }, + + /** + * Write Alpha Identifier. + * + * @param numOctets + * Total number of octets to be written. This includes the length of + * alphaId and the length of trailing unused octets(0xff). + * @param alphaId + * Alpha Identifier to be written. + * + * @return The Alpha Identifier has been written into Buf. + * + * Unused octets will be written as 0xff. + */ + writeAlphaIdentifier: function(numOctets, alphaId) { + if (numOctets === 0) { + return ""; + } + + // If alphaId is empty or it's of GSM 8 bit. + if (!alphaId || this.context.ICCUtilsHelper.isGsm8BitAlphabet(alphaId)) { + return this.writeStringTo8BitUnpacked(numOctets, alphaId); + } else { + return this.writeICCUCS2String(numOctets, alphaId); + } + }, + + /** + * Read Dialling number. + * + * @see TS 131.102 + * + * @param len + * The Length of BCD number. + * + * From TS 131.102, in EF_ADN, EF_FDN, the field 'Length of BCD number' + * means the total bytes should be allocated to store the TON/NPI and + * the dialing number. + * For example, if the dialing number is 1234567890, + * and the TON/NPI is 0x81, + * The field 'Length of BCD number' should be 06, which is + * 1 byte to store the TON/NPI, 0x81 + * 5 bytes to store the BCD number 2143658709. + * + * Here the definition of the length is different from SMS spec, + * TS 23.040 9.1.2.5, which the length means + * "number of useful semi-octets within the Address-Value field". + */ + readDiallingNumber: function(len) { + if (DEBUG) this.context.debug("PDU: Going to read Dialling number: " + len); + if (len === 0) { + return ""; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + + // TOA = TON + NPI + let toa = GsmPDUHelper.readHexOctet(); + + let number = GsmPDUHelper.readSwappedNibbleExtendedBcdString(len - 1); + if (number.length <= 0) { + if (DEBUG) this.context.debug("No number provided"); + return ""; + } + if ((toa >> 4) == (PDU_TOA_INTERNATIONAL >> 4)) { + number = '+' + number; + } + return number; + }, + + /** + * Write Dialling Number. + * + * @param number The Dialling number + */ + writeDiallingNumber: function(number) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let toa = PDU_TOA_ISDN; // 81 + if (number[0] == '+') { + toa = PDU_TOA_INTERNATIONAL | PDU_TOA_ISDN; // 91 + number = number.substring(1); + } + GsmPDUHelper.writeHexOctet(toa); + GsmPDUHelper.writeSwappedNibbleBCD(number); + }, + + readNumberWithLength: function() { + let Buf = this.context.Buf; + let number = ""; + let numLen = this.context.GsmPDUHelper.readHexOctet(); + if (numLen != 0xff) { + if (numLen > ADN_MAX_BCD_NUMBER_BYTES) { + if (DEBUG) { + this.context.debug( + "Error: invalid length of BCD number/SSC contents - " + numLen); + } + Buf.seekIncoming(ADN_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + return number; + } + + number = this.readDiallingNumber(numLen); + Buf.seekIncoming((ADN_MAX_BCD_NUMBER_BYTES - numLen) * Buf.PDU_HEX_OCTET_SIZE); + } else { + Buf.seekIncoming(ADN_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + } + + return number; + }, + + /** + * Write Number with Length + * + * @param number The value to be written. + * + * @return The number has been written into Buf. + */ + writeNumberWithLength: function(number) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + if (number) { + let numStart = number[0] == "+" ? 1 : 0; + let writtenNumber = number.substring(0, numStart) + + number.substring(numStart) + .replace(/[^0-9*#,]/g, ""); + let numDigits = writtenNumber.length - numStart; + + if (numDigits > ADN_MAX_NUMBER_DIGITS) { + writtenNumber = writtenNumber.substring(0, ADN_MAX_NUMBER_DIGITS + numStart); + numDigits = writtenNumber.length - numStart; + } + + // +1 for TON/NPI + let numLen = Math.ceil(numDigits / 2) + 1; + GsmPDUHelper.writeHexOctet(numLen); + this.writeDiallingNumber(writtenNumber.replace(/\*/g, "a") + .replace(/\#/g, "b") + .replace(/\,/g, "c")); + // Write trailing 0xff of Dialling Number. + for (let i = 0; i < ADN_MAX_BCD_NUMBER_BYTES - numLen; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return writtenNumber; + } else { + // +1 for numLen + for (let i = 0; i < ADN_MAX_BCD_NUMBER_BYTES + 1; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return ""; + } + } +}; + +function StkCommandParamsFactoryObject(aContext) { + this.context = aContext; +} +StkCommandParamsFactoryObject.prototype = { + context: null, + + createParam: function(cmdDetails, ctlvs, onComplete) { + let method = this[cmdDetails.typeOfCommand]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown proactive command " + + cmdDetails.typeOfCommand.toString(16)); + } + return; + } + method.call(this, cmdDetails, ctlvs, onComplete); + }, + + loadIcons: function(iconIdCtlvs, callback) { + if (!iconIdCtlvs || + !this.context.ICCUtilsHelper.isICCServiceAvailable("IMG")) { + callback(null); + return; + } + + let onerror = (function() { + callback(null); + }).bind(this); + + let onsuccess = (function(aIcons) { + callback(aIcons); + }).bind(this); + + this.context.IconLoader.loadIcons(iconIdCtlvs.map(aCtlv => aCtlv.value.identifier), + onsuccess, + onerror); + }, + + appendIconIfNecessary: function(iconIdCtlvs, result, onComplete) { + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + result.icons = aIcons[0]; + result.iconSelfExplanatory = + iconIdCtlvs[0].value.qualifier == 0 ? true : false; + } + + onComplete(result); + }); + }, + + /** + * Construct a param for Refresh. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processRefresh: function(cmdDetails, ctlvs, onComplete) { + let refreshType = cmdDetails.commandQualifier; + switch (refreshType) { + case STK_REFRESH_FILE_CHANGE: + case STK_REFRESH_NAA_INIT_AND_FILE_CHANGE: + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_FILE_LIST, ctlvs); + if (ctlv) { + let list = ctlv.value.fileList; + if (DEBUG) { + this.context.debug("Refresh, list = " + list); + } + this.context.ICCRecordHelper.fetchICCRecords(); + } + break; + } + + onComplete(null); + }, + + /** + * Construct a param for Poll Interval. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPollInterval: function(cmdDetails, ctlvs, onComplete) { + // Duration is mandatory. + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_DURATION, ctlvs); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Poll Interval: Required value missing : Duration"); + } + + onComplete(ctlv.value); + }, + + /** + * Construct a param for Poll Off. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPollOff: function(cmdDetails, ctlvs, onComplete) { + onComplete(null); + }, + + /** + * Construct a param for Set Up Event list. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetUpEventList: function(cmdDetails, ctlvs, onComplete) { + // Event list is mandatory. + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_EVENT_LIST, ctlvs); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Event List: Required value missing : Event List"); + } + + onComplete(ctlv.value || { eventList: null }); + }, + + /** + * Construct a param for Setup Menu. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetupMenu: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let menu = { + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ITEM, + COMPREHENSIONTLV_TAG_ITEM_ID, + COMPREHENSIONTLV_TAG_NEXT_ACTION_IND, + COMPREHENSIONTLV_TAG_ICON_ID, + COMPREHENSIONTLV_TAG_ICON_ID_LIST + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + menu.title = ctlv.value.identifier; + } + + // Item data object for item 1 is mandatory. + let menuCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ITEM]; + if (!menuCtlvs) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Menu: Required value missing : items"); + } + menu.items = menuCtlvs.map(aCtlv => aCtlv.value); + + // Item identifier is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ITEM_ID); + if (ctlv) { + menu.defaultItem = ctlv.value.identifier - 1; + } + + // Items next action indicator is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_NEXT_ACTION_IND); + if (ctlv) { + menu.nextActionList = ctlv.value; + } + + // Icon identifier is optional. + let iconIdCtlvs = null; + let menuIconCtlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ICON_ID); + if (menuIconCtlv) { + iconIdCtlvs = [menuIconCtlv]; + } + + // Item icon identifier list is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ICON_ID_LIST); + if (ctlv) { + if (!iconIdCtlvs) { + iconIdCtlvs = []; + }; + let iconIdList = ctlv.value; + iconIdCtlvs = iconIdCtlvs.concat(iconIdList.identifiers.map((aId) => { + return { + value: { qualifier: iconIdList.qualifier, identifier: aId } + }; + })); + } + + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + if (menuIconCtlv) { + menu.iconSelfExplanatory = + (iconIdCtlvs.shift().value.qualifier == 0) ? true: false; + menu.icons = aIcons.shift(); + } + + for (let i = 0; i < aIcons.length; i++) { + menu.items[i].icons = aIcons[i]; + menu.items[i].iconSelfExplanatory = + (iconIdCtlvs[i].value.qualifier == 0) ? true: false; + } + } + + onComplete(menu); + }); + }, + + /** + * Construct a param for Select Item. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSelectItem: function(cmdDetails, ctlvs, onComplete) { + this.processSetupMenu(cmdDetails, ctlvs, (menu) => { + // The 1st bit and 2nd bit determines the presentation type. + menu.presentationType = cmdDetails.commandQualifier & 0x03; + onComplete(menu); + }); + }, + + /** + * Construct a param for Display Text. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processDisplayText: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = { + isHighPriority: !!(cmdDetails.commandQualifier & 0x01), + userClear: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Display Text: Required value missing : Text String"); + } + textMsg.text = ctlv.value.textString; + + // Immediate response is optional. + textMsg.responseNeeded = + !!(selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE)); + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + textMsg.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + }, + + /** + * Construct a param for Setup Idle Mode Text. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetUpIdleModeText: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Set Up Idle Text: Required value missing : Text String"); + } + textMsg.text = ctlv.value.textString; + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + }, + + /** + * Construct a param for Get Inkey. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processGetInkey: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let input = { + minLength: 1, + maxLength: 1, + isAlphabet: !!(cmdDetails.commandQualifier & 0x01), + isUCS2: !!(cmdDetails.commandQualifier & 0x02), + // Character sets defined in bit 1 and bit 2 are disable and + // the YES/NO reponse is required. + isYesNoRequested: !!(cmdDetails.commandQualifier & 0x04), + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get InKey: Required value missing : Text String"); + } + input.text = ctlv.value.textString; + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + input.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + input, + onComplete); + }, + + /** + * Construct a param for Get Input. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processGetInput: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let input = { + isAlphabet: !!(cmdDetails.commandQualifier & 0x01), + isUCS2: !!(cmdDetails.commandQualifier & 0x02), + // User input shall not be revealed + hideInput: !!(cmdDetails.commandQualifier & 0x04), + // User input in SMS packed format + isPacked: !!(cmdDetails.commandQualifier & 0x08), + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_RESPONSE_LENGTH, + COMPREHENSIONTLV_TAG_DEFAULT_TEXT, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get Input: Required value missing : Text String"); + } + input.text = ctlv.value.textString; + + // Response length is mandatory. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_RESPONSE_LENGTH); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get Input: Required value missing : Response Length"); + } + input.minLength = ctlv.value.minLength; + input.maxLength = ctlv.value.maxLength; + + // Default text is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DEFAULT_TEXT); + if (ctlv) { + input.defaultText = ctlv.value.textString; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + input, + onComplete); + }, + + /** + * Construct a param for SendSS/SMS/USSD/DTMF. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processEventNotify: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + textMsg.text = ctlv.value.identifier; + } + + // According to section 6.4.10 of |ETSI TS 102 223|: + // + // - if the alpha identifier is provided by the UICC and is a null data + // object (i.e. length = '00' and no value part), this is an indication + // that the terminal should not give any information to the user on the + // fact that the terminal is sending a short message; + // + // - if the alpha identifier is not provided by the UICC, the terminal may + // give information to the user concerning what is happening. + // + // ICCPDUHelper reads alpha id as an empty string if the length is zero, + // hence we'll notify the caller when it's not an empty string. + if (textMsg.text !== "") { + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + } + }, + + /** + * Construct a param for Setup Call. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetupCall: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let call = {}; + let confirmMessage = {}; + let callMessage = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ADDRESS, + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID, + COMPREHENSIONTLV_TAG_DURATION + ]); + + // Address is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ADDRESS); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Set Up Call: Required value missing : Address"); + } + call.address = ctlv.value.number; + + // Alpha identifier (user confirmation phase) is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + confirmMessage.text = ctlv.value.identifier; + call.confirmMessage = confirmMessage; + } + + // Alpha identifier (call set up phase) is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + callMessage.text = ctlv.value.identifier; + call.callMessage = callMessage; + } + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + call.duration = ctlv.value; + } + + // Icon identifier is optional. + let iconIdCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null; + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + confirmMessage.icons = aIcons[0]; + confirmMessage.iconSelfExplanatory = + (iconIdCtlvs[0].value.qualifier == 0) ? true: false; + call.confirmMessage = confirmMessage; + + if (aIcons.length > 1) { + callMessage.icons = aIcons[1]; + callMessage.iconSelfExplanatory = + (iconIdCtlvs[1].value.qualifier == 0) ? true: false; + call.callMessage = callMessage; + } + } + + onComplete(call); + }); + }, + + /** + * Construct a param for Launch Browser. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processLaunchBrowser: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let browser = { + mode: cmdDetails.commandQualifier & 0x03 + }; + let confirmMessage = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_URL, + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // URL is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_URL); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Launch Browser: Required value missing : URL"); + } + browser.url = ctlv.value.url; + + // Alpha identifier is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + confirmMessage.text = ctlv.value.identifier; + browser.confirmMessage = confirmMessage; + } + + // Icon identifier is optional. + let iconIdCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null; + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + confirmMessage.icons = aIcons[0]; + confirmMessage.iconSelfExplanatory = + (iconIdCtlvs[0].value.qualifier == 0) ? true: false; + browser.confirmMessage = confirmMessage; + } + + onComplete(browser); + }); + }, + + /** + * Construct a param for Play Tone. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPlayTone: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let playTone = { + // The vibrate is only defined in TS 102.223. + isVibrate: !!(cmdDetails.commandQualifier & 0x01) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_TONE, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + playTone.text = ctlv.value.identifier; + } + + // Tone is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TONE); + if (ctlv) { + playTone.tone = ctlv.value.tone; + } + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + playTone.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + playTone, + onComplete); + }, + + /** + * Construct a param for Provide Local Information. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processProvideLocalInfo: function(cmdDetails, ctlvs, onComplete) { + let provideLocalInfo = { + localInfoType: cmdDetails.commandQualifier + }; + + onComplete(provideLocalInfo); + }, + + /** + * Construct a param for Timer Management. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processTimerManagement: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let timer = { + timerAction: cmdDetails.commandQualifier + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER, + COMPREHENSIONTLV_TAG_TIMER_VALUE + ]); + + // Timer identifier is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Timer Management: Required value missing : Timer Identifier"); + } + timer.timerId = ctlv.value.timerId; + + // Timer value is conditional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TIMER_VALUE); + if (ctlv) { + timer.timerValue = ctlv.value.timerValue; + } + + onComplete(timer); + }, + + /** + * Construct a param for BIP commands. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processBipMessage: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let bipMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + bipMsg.text = ctlv.value.identifier; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + bipMsg, + onComplete); + } +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_REFRESH] = function STK_CMD_REFRESH(cmdDetails, ctlvs, onComplete) { + return this.processRefresh(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_POLL_INTERVAL] = function STK_CMD_POLL_INTERVAL(cmdDetails, ctlvs, onComplete) { + return this.processPollInterval(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_POLL_OFF] = function STK_CMD_POLL_OFF(cmdDetails, ctlvs, onComplete) { + return this.processPollOff(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_PROVIDE_LOCAL_INFO] = function STK_CMD_PROVIDE_LOCAL_INFO(cmdDetails, ctlvs, onComplete) { + return this.processProvideLocalInfo(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_EVENT_LIST] = function STK_CMD_SET_UP_EVENT_LIST(cmdDetails, ctlvs, onComplete) { + return this.processSetUpEventList(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_MENU] = function STK_CMD_SET_UP_MENU(cmdDetails, ctlvs, onComplete) { + return this.processSetupMenu(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SELECT_ITEM] = function STK_CMD_SELECT_ITEM(cmdDetails, ctlvs, onComplete) { + return this.processSelectItem(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_DISPLAY_TEXT] = function STK_CMD_DISPLAY_TEXT(cmdDetails, ctlvs, onComplete) { + return this.processDisplayText(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_IDLE_MODE_TEXT] = function STK_CMD_SET_UP_IDLE_MODE_TEXT(cmdDetails, ctlvs, onComplete) { + return this.processSetUpIdleModeText(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_GET_INKEY] = function STK_CMD_GET_INKEY(cmdDetails, ctlvs, onComplete) { + return this.processGetInkey(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_GET_INPUT] = function STK_CMD_GET_INPUT(cmdDetails, ctlvs, onComplete) { + return this.processGetInput(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_SS] = function STK_CMD_SEND_SS(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_USSD] = function STK_CMD_SEND_USSD(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_SMS] = function STK_CMD_SEND_SMS(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_DTMF] = function STK_CMD_SEND_DTMF(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_CALL] = function STK_CMD_SET_UP_CALL(cmdDetails, ctlvs, onComplete) { + return this.processSetupCall(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_LAUNCH_BROWSER] = function STK_CMD_LAUNCH_BROWSER(cmdDetails, ctlvs, onComplete) { + return this.processLaunchBrowser(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_PLAY_TONE] = function STK_CMD_PLAY_TONE(cmdDetails, ctlvs, onComplete) { + return this.processPlayTone(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_TIMER_MANAGEMENT] = function STK_CMD_TIMER_MANAGEMENT(cmdDetails, ctlvs, onComplete) { + return this.processTimerManagement(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_OPEN_CHANNEL] = function STK_CMD_OPEN_CHANNEL(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_CLOSE_CHANNEL] = function STK_CMD_CLOSE_CHANNEL(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_RECEIVE_DATA] = function STK_CMD_RECEIVE_DATA(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_DATA] = function STK_CMD_SEND_DATA(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; + +function StkProactiveCmdHelperObject(aContext) { + this.context = aContext; +} +StkProactiveCmdHelperObject.prototype = { + context: null, + + retrieve: function(tag, length) { + let method = this[tag]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown comprehension tag " + tag.toString(16)); + } + let Buf = this.context.Buf; + Buf.seekIncoming(length * Buf.PDU_HEX_OCTET_SIZE); + return null; + } + return method.call(this, length); + }, + + /** + * Command Details. + * + * | Byte | Description | Length | + * | 1 | Command details Tag | 1 | + * | 2 | Length = 03 | 1 | + * | 3 | Command number | 1 | + * | 4 | Type of Command | 1 | + * | 5 | Command Qualifier | 1 | + */ + retrieveCommandDetails: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let cmdDetails = { + commandNumber: GsmPDUHelper.readHexOctet(), + typeOfCommand: GsmPDUHelper.readHexOctet(), + commandQualifier: GsmPDUHelper.readHexOctet() + }; + return cmdDetails; + }, + + /** + * Device Identities. + * + * | Byte | Description | Length | + * | 1 | Device Identity Tag | 1 | + * | 2 | Length = 02 | 1 | + * | 3 | Source device Identity | 1 | + * | 4 | Destination device Id | 1 | + */ + retrieveDeviceId: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let deviceId = { + sourceId: GsmPDUHelper.readHexOctet(), + destinationId: GsmPDUHelper.readHexOctet() + }; + return deviceId; + }, + + /** + * Alpha Identifier. + * + * | Byte | Description | Length | + * | 1 | Alpha Identifier Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 ~ | Alpha identfier | X | + * | (Y-1)+X+2 | | | + */ + retrieveAlphaId: function(length) { + let alphaId = { + identifier: this.context.ICCPDUHelper.readAlphaIdentifier(length) + }; + return alphaId; + }, + + /** + * Duration. + * + * | Byte | Description | Length | + * | 1 | Response Length Tag | 1 | + * | 2 | Lenth = 02 | 1 | + * | 3 | Time unit | 1 | + * | 4 | Time interval | 1 | + */ + retrieveDuration: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let duration = { + timeUnit: GsmPDUHelper.readHexOctet(), + timeInterval: GsmPDUHelper.readHexOctet(), + }; + return duration; + }, + + /** + * Address. + * + * | Byte | Description | Length | + * | 1 | Alpha Identifier Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | TON and NPI | 1 | + * | (Y-1)+4 ~ | Dialling number | X | + * | (Y-1)+X+2 | | | + */ + retrieveAddress: function(length) { + let address = { + number : this.context.ICCPDUHelper.readDiallingNumber(length) + }; + return address; + }, + + /** + * Text String. + * + * | Byte | Description | Length | + * | 1 | Text String Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Data coding scheme | 1 | + * | (Y-1)+4~ | Text String | X | + * | (Y-1)+X+2 | | | + */ + retrieveTextString: function(length) { + if (!length) { + // null string. + return {textString: null}; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + let text = { + codingScheme: GsmPDUHelper.readHexOctet() + }; + + length--; // -1 for the codingScheme. + switch (text.codingScheme & 0x0c) { + case STK_TEXT_CODING_GSM_7BIT_PACKED: + text.textString = + GsmPDUHelper.readSeptetsToString(Math.floor(length * 8 / 7), 0, 0, 0); + break; + case STK_TEXT_CODING_GSM_8BIT: + text.textString = + this.context.ICCPDUHelper.read8BitUnpackedToString(length); + break; + case STK_TEXT_CODING_UCS2: + text.textString = GsmPDUHelper.readUCS2String(length); + break; + } + return text; + }, + + /** + * Tone. + * + * | Byte | Description | Length | + * | 1 | Tone Tag | 1 | + * | 2 | Lenth = 01 | 1 | + * | 3 | Tone | 1 | + */ + retrieveTone: function(length) { + let tone = { + tone: this.context.GsmPDUHelper.readHexOctet(), + }; + return tone; + }, + + /** + * Item. + * + * | Byte | Description | Length | + * | 1 | Item Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Identifier of item | 1 | + * | (Y-1)+4 ~ | Text string of item | X | + * | (Y-1)+X+2 | | | + */ + retrieveItem: function(length) { + // TS 102.223 ,clause 6.6.7 SET-UP MENU + // If the "Item data object for item 1" is a null data object + // (i.e. length = '00' and no value part), this is an indication to the ME + // to remove the existing menu from the menu system in the ME. + if (!length) { + return null; + } + let item = { + identifier: this.context.GsmPDUHelper.readHexOctet(), + text: this.context.ICCPDUHelper.readAlphaIdentifier(length - 1) + }; + return item; + }, + + /** + * Item Identifier. + * + * | Byte | Description | Length | + * | 1 | Item Identifier Tag | 1 | + * | 2 | Lenth = 01 | 1 | + * | 3 | Identifier of Item chosen | 1 | + */ + retrieveItemId: function(length) { + let itemId = { + identifier: this.context.GsmPDUHelper.readHexOctet() + }; + return itemId; + }, + + /** + * Response Length. + * + * | Byte | Description | Length | + * | 1 | Response Length Tag | 1 | + * | 2 | Lenth = 02 | 1 | + * | 3 | Minimum length of response | 1 | + * | 4 | Maximum length of response | 1 | + */ + retrieveResponseLength: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let rspLength = { + minLength : GsmPDUHelper.readHexOctet(), + maxLength : GsmPDUHelper.readHexOctet() + }; + return rspLength; + }, + + /** + * File List. + * + * | Byte | Description | Length | + * | 1 | File List Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Number of files | 1 | + * | (Y-1)+4 ~ | Files | X | + * | (Y-1)+X+2 | | | + */ + retrieveFileList: function(length) { + let num = this.context.GsmPDUHelper.readHexOctet(); + let fileList = ""; + length--; // -1 for the num octet. + for (let i = 0; i < 2 * length; i++) { + // Didn't use readHexOctet here, + // otherwise 0x00 will be "0", not "00" + fileList += String.fromCharCode(this.context.Buf.readUint16()); + } + return { + fileList: fileList + }; + }, + + /** + * Default Text. + * + * Same as Text String. + */ + retrieveDefaultText: function(length) { + return this.retrieveTextString(length); + }, + + /** + * Event List. + */ + retrieveEventList: function(length) { + if (!length) { + // null means an indication to ME to remove the existing list of events + // in ME. + return null; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + let eventList = []; + for (let i = 0; i < length; i++) { + eventList.push(GsmPDUHelper.readHexOctet()); + } + return { + eventList: eventList + }; + }, + + /** + * Icon Id. + * + * | Byte | Description | Length | + * | 1 | Icon Identifier Tag | 1 | + * | 2 | Length = 02 | 1 | + * | 3 | Icon qualifier | 1 | + * | 4 | Icon identifier | 1 | + */ + retrieveIconId: function(length) { + if (!length) { + return null; + } + + let iconId = { + qualifier: this.context.GsmPDUHelper.readHexOctet(), + identifier: this.context.GsmPDUHelper.readHexOctet() + }; + return iconId; + }, + + /** + * Icon Id List. + * + * | Byte | Description | Length | + * | 1 | Icon Identifier Tag | 1 | + * | 2 | Length = X | 1 | + * | 3 | Icon qualifier | 1 | + * | 4~ | Icon identifier | X-1 | + * | 4+X-2 | | | + */ + retrieveIconIdList: function(length) { + if (!length) { + return null; + } + + let iconIdList = { + qualifier: this.context.GsmPDUHelper.readHexOctet(), + identifiers: [] + }; + for (let i = 0; i < length - 1; i++) { + iconIdList.identifiers.push(this.context.GsmPDUHelper.readHexOctet()); + } + return iconIdList; + }, + + /** + * Timer Identifier. + * + * | Byte | Description | Length | + * | 1 | Timer Identifier Tag | 1 | + * | 2 | Length = 01 | 1 | + * | 3 | Timer Identifier | 1 | + */ + retrieveTimerId: function(length) { + let id = { + timerId: this.context.GsmPDUHelper.readHexOctet() + }; + return id; + }, + + /** + * Timer Value. + * + * | Byte | Description | Length | + * | 1 | Timer Value Tag | 1 | + * | 2 | Length = 03 | 1 | + * | 3 | Hour | 1 | + * | 4 | Minute | 1 | + * | 5 | Second | 1 | + */ + retrieveTimerValue: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let value = { + timerValue: (GsmPDUHelper.readSwappedNibbleBcdNum(1) * 60 * 60) + + (GsmPDUHelper.readSwappedNibbleBcdNum(1) * 60) + + (GsmPDUHelper.readSwappedNibbleBcdNum(1)) + }; + return value; + }, + + /** + * Immediate Response. + * + * | Byte | Description | Length | + * | 1 | Immediate Response Tag | 1 | + * | 2 | Length = 00 | 1 | + */ + retrieveImmediaResponse: function(length) { + return {}; + }, + + /** + * URL + * + * | Byte | Description | Length | + * | 1 | URL Tag | 1 | + * | 2 ~ (Y+1) | Length(X) | Y | + * | (Y+2) ~ | URL | X | + * | (Y+1+X) | | | + */ + retrieveUrl: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let s = ""; + for (let i = 0; i < length; i++) { + s += String.fromCharCode(GsmPDUHelper.readHexOctet()); + } + return {url: s}; + }, + + /** + * Next Action Indicator List. + * + * | Byte | Description | Length | + * | 1 | Next Action tag | 1 | + * | 1 | Length(X) | 1 | + * | 3~ | Next Action List | X | + * | 3+X-1 | | | + */ + retrieveNextActionList: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let nextActionList = []; + for (let i = 0; i < length; i++) { + nextActionList.push(GsmPDUHelper.readHexOctet()); + } + return nextActionList; + }, + + searchForTag: function(tag, ctlvs) { + for (let ctlv of ctlvs) { + if ((ctlv.tag & ~COMPREHENSIONTLV_FLAG_CR) == tag) { + return ctlv; + } + } + return null; + }, + + searchForSelectedTags: function(ctlvs, tags) { + let ret = { + // Handy utility to de-queue the 1st ctlv of the specified tag. + retrieve: function(aTag) { + return (this[aTag]) ? this[aTag].shift() : null; + } + }; + + ctlvs.forEach((aCtlv) => { + tags.forEach((aTag) => { + if ((aCtlv.tag & ~COMPREHENSIONTLV_FLAG_CR) == aTag) { + if (!ret[aTag]) { + ret[aTag] = []; + } + ret[aTag].push(aCtlv); + } + }); + }); + + return ret; + }, +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_COMMAND_DETAILS] = function COMPREHENSIONTLV_TAG_COMMAND_DETAILS(length) { + return this.retrieveCommandDetails(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DEVICE_ID] = function COMPREHENSIONTLV_TAG_DEVICE_ID(length) { + return this.retrieveDeviceId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ALPHA_ID] = function COMPREHENSIONTLV_TAG_ALPHA_ID(length) { + return this.retrieveAlphaId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DURATION] = function COMPREHENSIONTLV_TAG_DURATION(length) { + return this.retrieveDuration(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ADDRESS] = function COMPREHENSIONTLV_TAG_ADDRESS(length) { + return this.retrieveAddress(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TEXT_STRING] = function COMPREHENSIONTLV_TAG_TEXT_STRING(length) { + return this.retrieveTextString(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TONE] = function COMPREHENSIONTLV_TAG_TONE(length) { + return this.retrieveTone(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ITEM] = function COMPREHENSIONTLV_TAG_ITEM(length) { + return this.retrieveItem(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ITEM_ID] = function COMPREHENSIONTLV_TAG_ITEM_ID(length) { + return this.retrieveItemId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_RESPONSE_LENGTH] = function COMPREHENSIONTLV_TAG_RESPONSE_LENGTH(length) { + return this.retrieveResponseLength(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_FILE_LIST] = function COMPREHENSIONTLV_TAG_FILE_LIST(length) { + return this.retrieveFileList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DEFAULT_TEXT] = function COMPREHENSIONTLV_TAG_DEFAULT_TEXT(length) { + return this.retrieveDefaultText(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_EVENT_LIST] = function COMPREHENSIONTLV_TAG_EVENT_LIST(length) { + return this.retrieveEventList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ICON_ID] = function COMPREHENSIONTLV_TAG_ICON_ID(length) { + return this.retrieveIconId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ICON_ID_LIST] = function COMPREHENSIONTLV_TAG_ICON_ID_LIST(length) { + return this.retrieveIconIdList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER] = function COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER(length) { + return this.retrieveTimerId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TIMER_VALUE] = function COMPREHENSIONTLV_TAG_TIMER_VALUE(length) { + return this.retrieveTimerValue(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE] = function COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE(length) { + return this.retrieveImmediaResponse(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_URL] = function COMPREHENSIONTLV_TAG_URL(length) { + return this.retrieveUrl(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_NEXT_ACTION_IND] = function COMPREHENSIONTLV_TAG_NEXT_ACTION_IND(length) { + return this.retrieveNextActionList(length); +}; + +function ComprehensionTlvHelperObject(aContext) { + this.context = aContext; +} +ComprehensionTlvHelperObject.prototype = { + context: null, + + /** + * Decode raw data to a Comprehension-TLV. + */ + decode: function() { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let hlen = 0; // For header(tag field + length field) length. + let temp = GsmPDUHelper.readHexOctet(); + hlen++; + + // TS 101.220, clause 7.1.1 + let tag, cr; + switch (temp) { + // TS 101.220, clause 7.1.1 + case 0x0: // Not used. + case 0xff: // Not used. + case 0x80: // Reserved for future use. + throw new Error("Invalid octet when parsing Comprehension TLV :" + temp); + case 0x7f: // Tag is three byte format. + // TS 101.220 clause 7.1.1.2. + // | Byte 1 | Byte 2 | Byte 3 | + // | | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | | + // | 0x7f |CR | Tag Value | + tag = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + hlen += 2; + cr = (tag & 0x8000) !== 0; + tag &= ~0x8000; + break; + default: // Tag is single byte format. + tag = temp; + // TS 101.220 clause 7.1.1.1. + // | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | + // |CR | Tag Value | + cr = (tag & 0x80) !== 0; + tag &= ~0x80; + } + + // TS 101.220 clause 7.1.2, Length Encoding. + // Length | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + // 0 - 127 | 00 - 7f | N/A | N/A | N/A | + // 128-255 | 81 | 80 - ff| N/A | N/A | + // 256-65535| 82 | 0100 - ffff | N/A | + // 65536- | 83 | 010000 - ffffff | + // 16777215 + // + // Length errors: TS 11.14, clause 6.10.6 + + let length; // Data length. + temp = GsmPDUHelper.readHexOctet(); + hlen++; + if (temp < 0x80) { + length = temp; + } else if (temp == 0x81) { + length = GsmPDUHelper.readHexOctet(); + hlen++; + if (length < 0x80) { + throw new Error("Invalid length in Comprehension TLV :" + length); + } + } else if (temp == 0x82) { + length = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + hlen += 2; + if (lenth < 0x0100) { + throw new Error("Invalid length in 3-byte Comprehension TLV :" + length); + } + } else if (temp == 0x83) { + length = (GsmPDUHelper.readHexOctet() << 16) | + (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + hlen += 3; + if (length < 0x010000) { + throw new Error("Invalid length in 4-byte Comprehension TLV :" + length); + } + } else { + throw new Error("Invalid octet in Comprehension TLV :" + temp); + } + + let ctlv = { + tag: tag, + length: length, + value: this.context.StkProactiveCmdHelper.retrieve(tag, length), + cr: cr, + hlen: hlen + }; + return ctlv; + }, + + decodeChunks: function(length) { + let chunks = []; + let index = 0; + while (index < length) { + let tlv = this.decode(); + chunks.push(tlv); + index += tlv.length; + index += tlv.hlen; + } + return chunks; + }, + + /** + * Write Location Info Comprehension TLV. + * + * @param loc location Information. + */ + writeLocationInfoTlv: function(loc) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(loc.gsmCellId > 0xffff ? 9 : 7); + // From TS 11.14, clause 12.19 + // "The mobile country code (MCC), the mobile network code (MNC), + // the location area code (LAC) and the cell ID are + // coded as in TS 04.08." + // And from TS 04.08 and TS 24.008, + // the format is as follows: + // + // MCC = MCC_digit_1 + MCC_digit_2 + MCC_digit_3 + // + // 8 7 6 5 4 3 2 1 + // +-------------+-------------+ + // | MCC digit 2 | MCC digit 1 | octet 2 + // | MNC digit 3 | MCC digit 3 | octet 3 + // | MNC digit 2 | MNC digit 1 | octet 4 + // +-------------+-------------+ + // + // Also in TS 24.008 + // "However a network operator may decide to + // use only two digits in the MNC in the LAI over the + // radio interface. In this case, bits 5 to 8 of octet 3 + // shall be coded as '1111'". + + // MCC & MNC, 3 octets + let mcc = loc.mcc, mnc; + if (loc.mnc.length == 2) { + mnc = "F" + loc.mnc; + } else { + mnc = loc.mnc[2] + loc.mnc[0] + loc.mnc[1]; + } + GsmPDUHelper.writeSwappedNibbleBCD(mcc + mnc); + + // LAC, 2 octets + GsmPDUHelper.writeHexOctet((loc.gsmLocationAreaCode >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmLocationAreaCode & 0xff); + + // Cell Id + if (loc.gsmCellId > 0xffff) { + // UMTS/WCDMA, gsmCellId is 28 bits. + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 24) & 0xff); + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 16) & 0xff); + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmCellId & 0xff); + } else { + // GSM, gsmCellId is 16 bits. + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmCellId & 0xff); + } + }, + + /** + * Given a geckoError string, this function translates it into cause value + * and write the value into buffer. + * + * @param geckoError Error string that is passed to gecko. + */ + writeCauseTlv: function(geckoError) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let cause = -1; + for (let errorNo in RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR) { + if (geckoError == RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[errorNo]) { + cause = errorNo; + break; + } + } + + // Causes specified in 10.5.4.11 of TS 04.08 are less than 128. + // we can ignore causes > 127 since Cause TLV is optional in + // STK_EVENT_TYPE_CALL_DISCONNECTED. + if (cause > 127) { + return; + } + + cause = (cause == -1) ? ERROR_SUCCESS : cause; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_CAUSE | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(2); // For single cause value. + + // TS 04.08, clause 10.5.4.11: + // Code Standard : Standard defined for GSM PLMNS + // Location: User + GsmPDUHelper.writeHexOctet(0x60); + + // TS 04.08, clause 10.5.4.11: ext bit = 1 + 7 bits for cause. + // +-----------------+----------------------------------+ + // | Ext = 1 (1 bit) | Cause (7 bits) | + // +-----------------+----------------------------------+ + GsmPDUHelper.writeHexOctet(0x80 | cause); + }, + + writeDateTimeZoneTlv: function(date) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DATE_TIME_ZONE); + GsmPDUHelper.writeHexOctet(7); + GsmPDUHelper.writeTimestamp(date); + }, + + writeLanguageTlv: function(language) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LANGUAGE); + GsmPDUHelper.writeHexOctet(2); + + // ISO 639-1, Alpha-2 code + // TS 123.038, clause 6.2.1, GSM 7 bit Default Alphabet + GsmPDUHelper.writeHexOctet( + PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT].indexOf(language[0])); + GsmPDUHelper.writeHexOctet( + PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT].indexOf(language[1])); + }, + + /** + * Write Timer Value Comprehension TLV. + * + * @param seconds length of time during of the timer. + * @param cr Comprehension Required or not + */ + writeTimerValueTlv: function(seconds, cr) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_VALUE | + (cr ? COMPREHENSIONTLV_FLAG_CR : 0)); + GsmPDUHelper.writeHexOctet(3); + + // TS 102.223, clause 8.38 + // +----------------+------------------+-------------------+ + // | hours (1 byte) | minutes (1 btye) | seconds (1 byte) | + // +----------------+------------------+-------------------+ + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds / 60 / 60)); + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds / 60) % 60); + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds) % 60); + }, + + writeTextStringTlv: function(text, coding) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let buf = GsmPDUHelper.writeWithBuffer(() => { + // Write Coding. + GsmPDUHelper.writeHexOctet(coding); + + // Write Text String. + switch (coding) { + case STK_TEXT_CODING_UCS2: + GsmPDUHelper.writeUCS2String(text); + break; + case STK_TEXT_CODING_GSM_7BIT_PACKED: + GsmPDUHelper.writeStringAsSeptets(text, 0, 0, 0); + break; + case STK_TEXT_CODING_GSM_8BIT: + GsmPDUHelper.writeStringAs8BitUnpacked(text); + break; + } + }); + + let length = buf.length; + if (length) { + // Write Tag. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + // Write Length. + this.writeLength(length); + // Write Value. + for (let i = 0; i < length; i++) { + GsmPDUHelper.writeHexOctet(buf[i]); + } + } + }, + + getSizeOfLengthOctets: function(length) { + if (length >= 0x10000) { + return 4; // 0x83, len_1, len_2, len_3 + } else if (length >= 0x100) { + return 3; // 0x82, len_1, len_2 + } else if (length >= 0x80) { + return 2; // 0x81, len + } else { + return 1; // len + } + }, + + writeLength: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + // TS 101.220 clause 7.1.2, Length Encoding. + // Length | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + // 0 - 127 | 00 - 7f | N/A | N/A | N/A | + // 128-255 | 81 | 80 - ff| N/A | N/A | + // 256-65535| 82 | 0100 - ffff | N/A | + // 65536- | 83 | 010000 - ffffff | + // 16777215 + if (length < 0x80) { + GsmPDUHelper.writeHexOctet(length); + } else if (0x80 <= length && length < 0x100) { + GsmPDUHelper.writeHexOctet(0x81); + GsmPDUHelper.writeHexOctet(length); + } else if (0x100 <= length && length < 0x10000) { + GsmPDUHelper.writeHexOctet(0x82); + GsmPDUHelper.writeHexOctet((length >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(length & 0xff); + } else if (0x10000 <= length && length < 0x1000000) { + GsmPDUHelper.writeHexOctet(0x83); + GsmPDUHelper.writeHexOctet((length >> 16) & 0xff); + GsmPDUHelper.writeHexOctet((length >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(length & 0xff); + } else { + throw new Error("Invalid length value :" + length); + } + }, +}; + +function BerTlvHelperObject(aContext) { + this.context = aContext; +} +BerTlvHelperObject.prototype = { + context: null, + + /** + * Decode Ber TLV. + * + * @param dataLen + * The length of data in bytes. + */ + decode: function(dataLen) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let hlen = 0; + let tag = GsmPDUHelper.readHexOctet(); + hlen++; + + // The length is coded onto 1 or 2 bytes. + // Length | Byte 1 | Byte 2 + // 0 - 127 | length ('00' to '7f') | N/A + // 128 - 255 | '81' | length ('80' to 'ff') + let length; + let temp = GsmPDUHelper.readHexOctet(); + hlen++; + if (temp < 0x80) { + length = temp; + } else if (temp === 0x81) { + length = GsmPDUHelper.readHexOctet(); + hlen++; + if (length < 0x80) { + throw new Error("Invalid length " + length); + } + } else { + throw new Error("Invalid length octet " + temp); + } + + // Header + body length check. + if (dataLen - hlen !== length) { + throw new Error("Unexpected BerTlvHelper value length!!"); + } + + let method = this[tag]; + if (typeof method != "function") { + throw new Error("Unknown Ber tag 0x" + tag.toString(16)); + } + + let value = method.call(this, length); + + return { + tag: tag, + length: length, + value: value + }; + }, + + /** + * Process the value part for FCP template TLV. + * + * @param length + * The length of data in bytes. + */ + processFcpTemplate: function(length) { + let tlvs = this.decodeChunks(length); + return tlvs; + }, + + /** + * Process the value part for proactive command TLV. + * + * @param length + * The length of data in bytes. + */ + processProactiveCommand: function(length) { + let ctlvs = this.context.ComprehensionTlvHelper.decodeChunks(length); + return ctlvs; + }, + + /** + * Decode raw data to a Ber-TLV. + */ + decodeInnerTlv: function() { + let GsmPDUHelper = this.context.GsmPDUHelper; + let tag = GsmPDUHelper.readHexOctet(); + let length = GsmPDUHelper.readHexOctet(); + return { + tag: tag, + length: length, + value: this.retrieve(tag, length) + }; + }, + + decodeChunks: function(length) { + let chunks = []; + let index = 0; + while (index < length) { + let tlv = this.decodeInnerTlv(); + if (tlv.value) { + chunks.push(tlv); + } + index += tlv.length; + // tag + length fields consume 2 bytes. + index += 2; + } + return chunks; + }, + + retrieve: function(tag, length) { + let method = this[tag]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown Ber tag : 0x" + tag.toString(16)); + } + let Buf = this.context.Buf; + Buf.seekIncoming(length * Buf.PDU_HEX_OCTET_SIZE); + return null; + } + return method.call(this, length); + }, + + /** + * File Size Data. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 to X+24 | Number of allocated data bytes in the file | X | + * | | , excluding structural information | | + */ + retrieveFileSizeData: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let fileSizeData = 0; + for (let i = 0; i < length; i++) { + fileSizeData = fileSizeData << 8; + fileSizeData += GsmPDUHelper.readHexOctet(); + } + + return {fileSizeData: fileSizeData}; + }, + + /** + * File Descriptor. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 | File descriptor byte | 1 | + * | 4 | Data coding byte | 1 | + * | 5 ~ 6 | Record length | 2 | + * | 7 | Number of records | 1 | + */ + retrieveFileDescriptor: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let fileDescriptorByte = GsmPDUHelper.readHexOctet(); + let dataCodingByte = GsmPDUHelper.readHexOctet(); + // See TS 102 221 Table 11.5, we only care the least 3 bits for the + // structure of file. + let fileStructure = fileDescriptorByte & 0x07; + + let fileDescriptor = { + fileStructure: fileStructure + }; + // byte 5 ~ 7 are mandatory for linear fixed and cyclic files, otherwise + // they are not applicable. + if (fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_LINEAR_FIXED] || + fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_CYCLIC]) { + fileDescriptor.recordLength = (GsmPDUHelper.readHexOctet() << 8) + + GsmPDUHelper.readHexOctet(); + fileDescriptor.numOfRecords = GsmPDUHelper.readHexOctet(); + } + + return fileDescriptor; + }, + + /** + * File identifier. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 ~ 4 | File identifier | 2 | + */ + retrieveFileIdentifier: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + return {fileId : (GsmPDUHelper.readHexOctet() << 8) + + GsmPDUHelper.readHexOctet()}; + }, + + searchForNextTag: function(tag, iter) { + for (let tlv of iter) { + if (tlv.tag === tag) { + return tlv; + } + } + return null; + } +}; +BerTlvHelperObject.prototype[BER_FCP_TEMPLATE_TAG] = function BER_FCP_TEMPLATE_TAG(length) { + return this.processFcpTemplate(length); +}; +BerTlvHelperObject.prototype[BER_PROACTIVE_COMMAND_TAG] = function BER_PROACTIVE_COMMAND_TAG(length) { + return this.processProactiveCommand(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_SIZE_DATA_TAG] = function BER_FCP_FILE_SIZE_DATA_TAG(length) { + return this.retrieveFileSizeData(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_DESCRIPTOR_TAG] = function BER_FCP_FILE_DESCRIPTOR_TAG(length) { + return this.retrieveFileDescriptor(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_IDENTIFIER_TAG] = function BER_FCP_FILE_IDENTIFIER_TAG(length) { + return this.retrieveFileIdentifier(length); +}; + +/** + * ICC Helper for getting EF path. + */ +function ICCFileHelperObject(aContext) { + this.context = aContext; +} +ICCFileHelperObject.prototype = { + context: null, + + /** + * This function handles only EFs that are common to RUIM, SIM, USIM + * and other types of ICC cards. + */ + getCommonEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_ICCID: + return EF_PATH_MF_SIM; + case ICC_EF_ADN: + case ICC_EF_SDN: // Fall through. + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + case ICC_EF_PBR: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK; + case ICC_EF_IMG: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_GRAPHICS; + } + return null; + }, + + /** + * This function handles EFs for SIM. + */ + getSimEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_FDN: + case ICC_EF_MSISDN: + case ICC_EF_SMS: + case ICC_EF_EXT1: + case ICC_EF_EXT2: + case ICC_EF_EXT3: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + case ICC_EF_AD: + case ICC_EF_MBDN: + case ICC_EF_MWIS: + case ICC_EF_PLMNsel: + case ICC_EF_SPN: + case ICC_EF_SPDI: + case ICC_EF_SST: + case ICC_EF_PHASE: + case ICC_EF_CBMI: + case ICC_EF_CBMID: + case ICC_EF_CBMIR: + case ICC_EF_OPL: + case ICC_EF_PNN: + case ICC_EF_GID1: + case ICC_EF_CPHS_INFO: + case ICC_EF_CPHS_MBN: + return EF_PATH_MF_SIM + EF_PATH_DF_GSM; + default: + return null; + } + }, + + /** + * This function handles EFs for USIM. + */ + getUSimEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_AD: + case ICC_EF_FDN: + case ICC_EF_MBDN: + case ICC_EF_MWIS: + case ICC_EF_UST: + case ICC_EF_MSISDN: + case ICC_EF_SPN: + case ICC_EF_SPDI: + case ICC_EF_CBMI: + case ICC_EF_CBMID: + case ICC_EF_CBMIR: + case ICC_EF_OPL: + case ICC_EF_PNN: + case ICC_EF_SMS: + case ICC_EF_GID1: + // CPHS spec was provided in 1997 based on SIM requirement, there is no + // detailed info about how these ICC_EF_CPHS_XXX are allocated in USIM. + // What we can do now is to follow what has been done in AOSP to have file + // path equal to MF_SIM/DF_GSM. + case ICC_EF_CPHS_INFO: + case ICC_EF_CPHS_MBN: + return EF_PATH_MF_SIM + EF_PATH_ADF_USIM; + default: + // The file ids in USIM phone book entries are decided by the + // card manufacturer. So if we don't match any of the cases + // above and if its a USIM return the phone book path. + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK; + } + }, + + /** + * This function handles EFs for RUIM + */ + getRuimEFPath: function(fileId) { + switch(fileId) { + case ICC_EF_CSIM_IMSI_M: + case ICC_EF_CSIM_CDMAHOME: + case ICC_EF_CSIM_CST: + case ICC_EF_CSIM_SPN: + return EF_PATH_MF_SIM + EF_PATH_DF_CDMA; + case ICC_EF_FDN: + case ICC_EF_EXT1: + case ICC_EF_EXT2: + case ICC_EF_EXT3: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + default: + return null; + } + }, + + /** + * Helper function for getting the pathId for the specific ICC record + * depeding on which type of ICC card we are using. + * + * @param fileId + * File id. + * @return The pathId or null in case of an error or invalid input. + */ + getEFPath: function(fileId) { + let path = this.getCommonEFPath(fileId); + if (path) { + return path; + } + + switch (this.context.RIL.appType) { + case CARD_APPTYPE_SIM: + return this.getSimEFPath(fileId); + case CARD_APPTYPE_USIM: + return this.getUSimEFPath(fileId); + case CARD_APPTYPE_RUIM: + return this.getRuimEFPath(fileId); + default: + return null; + } + } +}; + +/** + * Helper for ICC IO functionalities. + */ +function ICCIOHelperObject(aContext) { + this.context = aContext; +} +ICCIOHelperObject.prototype = { + context: null, + + /** + * Load EF with type 'Linear Fixed'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param recordNumber [optional] + * The number of the record shall be loaded. + * @param recordSize [optional] + * The size of the record. + * @param callback [optional] + * The callback function shall be called when the record(s) is read. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + loadLinearFixedEF: function(options) { + let cb; + let readRecord = (function(options) { + options.command = ICC_COMMAND_READ_RECORD; + options.p1 = options.recordNumber || 1; // Record number + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = options.recordSize; + options.callback = cb || options.callback; + this.context.RIL.iccIO(options); + }).bind(this); + + options.structure = EF_STRUCTURE_LINEAR_FIXED; + options.pathId = this.context.ICCFileHelper.getEFPath(options.fileId); + if (options.recordSize) { + readRecord(options); + return; + } + + cb = options.callback; + options.callback = readRecord; + this.getResponse(options); + }, + + /** + * Load next record from current record Id. + */ + loadNextRecord: function(options) { + options.p1++; + this.context.RIL.iccIO(options); + }, + + /** + * Update EF with type 'Linear Fixed'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param recordNumber + * The number of the record shall be updated. + * @param dataWriter [optional] + * The function for writing string parameter for the ICC_COMMAND_UPDATE_RECORD. + * @param pin2 [optional] + * PIN2 is required when updating ICC_EF_FDN. + * @param callback [optional] + * The callback function shall be called when the record is updated. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + updateLinearFixedEF: function(options) { + if (!options.fileId || !options.recordNumber) { + throw new Error("Unexpected fileId " + options.fileId + + " or recordNumber " + options.recordNumber); + } + + options.structure = EF_STRUCTURE_LINEAR_FIXED; + options.pathId = this.context.ICCFileHelper.getEFPath(options.fileId); + let cb = options.callback; + options.callback = function callback(options) { + options.callback = cb; + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = options.recordSize; + this.context.RIL.iccIO(options); + }.bind(this); + this.getResponse(options); + }, + + /** + * Load EF with type 'Transparent'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param callback [optional] + * The callback function shall be called when the record(s) is read. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + loadTransparentEF: function(options) { + options.structure = EF_STRUCTURE_TRANSPARENT; + let cb = options.callback; + options.callback = function callback(options) { + options.callback = cb; + options.command = ICC_COMMAND_READ_BINARY; + options.p2 = 0x00; + options.p3 = options.fileSize; + this.context.RIL.iccIO(options); + }.bind(this); + this.getResponse(options); + }, + + /** + * Use ICC_COMMAND_GET_RESPONSE to query the EF. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + */ + getResponse: function(options) { + options.command = ICC_COMMAND_GET_RESPONSE; + options.pathId = options.pathId || + this.context.ICCFileHelper.getEFPath(options.fileId); + if (!options.pathId) { + throw new Error("Unknown pathId for " + options.fileId.toString(16)); + } + options.p1 = 0; // For GET_RESPONSE, p1 = 0 + switch (this.context.RIL.appType) { + case CARD_APPTYPE_USIM: + options.p2 = GET_RESPONSE_FCP_TEMPLATE; + options.p3 = 0x00; + break; + // For RUIM, CSIM and ISIM, cf bug 955946: keep the old behavior + case CARD_APPTYPE_RUIM: + case CARD_APPTYPE_CSIM: + case CARD_APPTYPE_ISIM: + // For SIM, this is what we want + case CARD_APPTYPE_SIM: + default: + options.p2 = 0x00; + options.p3 = GET_RESPONSE_EF_SIZE_BYTES; + break; + } + this.context.RIL.iccIO(options); + }, + + /** + * Process ICC I/O response. + */ + processICCIO: function(options) { + let func = this[options.command]; + func.call(this, options); + }, + + /** + * Process a ICC_COMMAND_GET_RESPONSE type command for REQUEST_SIM_IO. + */ + processICCIOGetResponse: function(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + let peek = this.context.GsmPDUHelper.readHexOctet(); + Buf.seekIncoming(-1 * Buf.PDU_HEX_OCTET_SIZE); + if (peek === BER_FCP_TEMPLATE_TAG) { + this.processUSimGetResponse(options, strLen / 2); + } else { + this.processSimGetResponse(options); + } + Buf.readStringDelimiter(strLen); + + if (options.callback) { + options.callback(options); + } + }, + + /** + * Helper function for processing USIM get response. + */ + processUSimGetResponse: function(options, octetLen) { + let BerTlvHelper = this.context.BerTlvHelper; + + let berTlv = BerTlvHelper.decode(octetLen); + // See TS 102 221 Table 11.4 for the content order of getResponse. + let iter = berTlv.value.values(); + let tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_DESCRIPTOR_TAG, + iter); + if (!tlv || + (tlv.value.fileStructure !== UICC_EF_STRUCTURE[options.structure])) { + throw new Error("Expected EF structure " + + UICC_EF_STRUCTURE[options.structure] + + " but read " + tlv.value.fileStructure); + } + + if (tlv.value.fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_LINEAR_FIXED] || + tlv.value.fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_CYCLIC]) { + options.recordSize = tlv.value.recordLength; + options.totalRecords = tlv.value.numOfRecords; + } + + tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_IDENTIFIER_TAG, iter); + if (!tlv || (tlv.value.fileId !== options.fileId)) { + throw new Error("Expected file ID " + options.fileId.toString(16) + + " but read " + fileId.toString(16)); + } + + tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_SIZE_DATA_TAG, iter); + if (!tlv) { + throw new Error("Unexpected file size data"); + } + options.fileSize = tlv.value.fileSizeData; + }, + + /** + * Helper function for processing SIM get response. + */ + processSimGetResponse: function(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // The format is from TS 51.011, clause 9.2.1 + + // Skip RFU, data[0] data[1]. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + + // File size, data[2], data[3] + options.fileSize = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + + // 2 bytes File id. data[4], data[5] + let fileId = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + if (fileId != options.fileId) { + throw new Error("Expected file ID " + options.fileId.toString(16) + + " but read " + fileId.toString(16)); + } + + // Type of file, data[6] + let fileType = GsmPDUHelper.readHexOctet(); + if (fileType != TYPE_EF) { + throw new Error("Unexpected file type " + fileType); + } + + // Skip 1 byte RFU, data[7], + // 3 bytes Access conditions, data[8] data[9] data[10], + // 1 byte File status, data[11], + // 1 byte Length of the following data, data[12]. + Buf.seekIncoming(((RESPONSE_DATA_STRUCTURE - RESPONSE_DATA_FILE_TYPE - 1) * + Buf.PDU_HEX_OCTET_SIZE)); + + // Read Structure of EF, data[13] + let efStructure = GsmPDUHelper.readHexOctet(); + if (efStructure != options.structure) { + throw new Error("Expected EF structure " + options.structure + + " but read " + efStructure); + } + + // Length of a record, data[14]. + // Only available for LINEAR_FIXED and CYCLIC. + if (efStructure == EF_STRUCTURE_LINEAR_FIXED || + efStructure == EF_STRUCTURE_CYCLIC) { + options.recordSize = GsmPDUHelper.readHexOctet(); + options.totalRecords = options.fileSize / options.recordSize; + } else { + Buf.seekIncoming(1 * Buf.PDU_HEX_OCTET_SIZE); + } + }, + + /** + * Process a ICC_COMMAND_READ_RECORD type command for REQUEST_SIM_IO. + */ + processICCIOReadRecord: function(options) { + if (options.callback) { + options.callback(options); + } + }, + + /** + * Process a ICC_COMMAND_READ_BINARY type command for REQUEST_SIM_IO. + */ + processICCIOReadBinary: function(options) { + if (options.callback) { + options.callback(options); + } + }, + + /** + * Process a ICC_COMMAND_UPDATE_RECORD type command for REQUEST_SIM_IO. + */ + processICCIOUpdateRecord: function(options) { + if (options.callback) { + options.callback(options); + } + }, +}; +ICCIOHelperObject.prototype[ICC_COMMAND_SEEK] = null; +ICCIOHelperObject.prototype[ICC_COMMAND_READ_BINARY] = function ICC_COMMAND_READ_BINARY(options) { + this.processICCIOReadBinary(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_READ_RECORD] = function ICC_COMMAND_READ_RECORD(options) { + this.processICCIOReadRecord(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_GET_RESPONSE] = function ICC_COMMAND_GET_RESPONSE(options) { + this.processICCIOGetResponse(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_UPDATE_BINARY] = null; +ICCIOHelperObject.prototype[ICC_COMMAND_UPDATE_RECORD] = function ICC_COMMAND_UPDATE_RECORD(options) { + this.processICCIOUpdateRecord(options); +}; + +/** + * Helper for ICC records. + */ +function ICCRecordHelperObject(aContext) { + this.context = aContext; + // Cache the possible free record id for all files, use fileId as key. + this._freeRecordIds = {}; +} +ICCRecordHelperObject.prototype = { + context: null, + + /** + * Fetch ICC records. + */ + fetchICCRecords: function() { + switch (this.context.RIL.appType) { + case CARD_APPTYPE_SIM: + case CARD_APPTYPE_USIM: + this.context.SimRecordHelper.fetchSimRecords(); + break; + case CARD_APPTYPE_RUIM: + this.context.RuimRecordHelper.fetchRuimRecords(); + break; + } + }, + + /** + * Read the ICCID. + */ + readICCID: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + RIL.iccInfo.iccid = + GsmPDUHelper.readSwappedNibbleBcdString(octetLen, true); + // Consumes the remaining buffer if any. + let unReadBuffer = this.context.Buf.getReadAvailable() - + this.context.Buf.PDU_HEX_OCTET_SIZE; + if (unReadBuffer > 0) { + this.context.Buf.seekIncoming(unReadBuffer); + } + Buf.readStringDelimiter(strLen); + + if (DEBUG) this.context.debug("ICCID: " + RIL.iccInfo.iccid); + if (RIL.iccInfo.iccid) { + this.context.ICCUtilsHelper.handleICCInfoChange(); + RIL.reportStkServiceIsRunning(); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_ICCID, + callback: callback.bind(this) + }); + }, + + /** + * Read ICC ADN like EF, i.e. EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN, FDN or SDN. + * @param extFileId EF id of the EXT. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readADNLike: function(fileId, extFileId, onsuccess, onerror) { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let loadNextContactRecord = () => { + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + return; + } + if (DEBUG) { + for (let i = 0; i < contacts.length; i++) { + this.context.debug("contact [" + i + "] " + + JSON.stringify(contacts[i])); + } + } + if (onsuccess) { + onsuccess(contacts); + } + }; + + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (contact) { + let record = { + recordId: options.p1, + alphaId: contact.alphaId, + number: contact.number + }; + contacts.push(record); + + if (extFileId && contact.extRecordNumber != 0xff) { + this.readExtension(extFileId, contact.extRecordNumber, (number) => { + if (number) { + record.number += number; + } + loadNextContactRecord(); + }, () => loadNextContactRecord()); + return; + } + } + loadNextContactRecord(); + } + + let contacts = []; + ICCIOHelper.loadLinearFixedEF({fileId: fileId, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Update ICC ADN like EFs, like EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN or FDN. + * @param extRecordNumber The record identifier of the EXT. + * @param contact The contact will be updated. (Shall have recordId property) + * @param pin2 PIN2 is required when updating ICC_EF_FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateADNLike: function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + let updatedContact; + function dataWriter(recordSize) { + updatedContact = this.context.ICCPDUHelper.writeAlphaIdDiallingNumber(recordSize, + contact.alphaId, + contact.number, + extRecordNumber); + } + + function callback(options) { + if (onsuccess) { + onsuccess(updatedContact); + } + } + + if (!contact || !contact.recordId) { + if (onerror) onerror(GECKO_ERROR_INVALID_PARAMETER); + return; + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: contact.recordId, + dataWriter: dataWriter.bind(this), + pin2: pin2, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read USIM/RUIM Phonebook. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPBR: function(onsuccess, onerror) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let ICCIOHelper = this.context.ICCIOHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let RIL = this.context.RIL; + + function callback(options) { + let strLen = Buf.readInt32(); + let octetLen = strLen / 2, readLen = 0; + + let pbrTlvs = []; + while (readLen < octetLen) { + let tag = GsmPDUHelper.readHexOctet(); + if (tag == 0xff) { + readLen++; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + let tlvLen = GsmPDUHelper.readHexOctet(); + let tlvs = ICCUtilsHelper.decodeSimTlvs(tlvLen); + pbrTlvs.push({tag: tag, + length: tlvLen, + value: tlvs}); + + readLen += tlvLen + 2; // +2 for tag and tlvLen + } + Buf.readStringDelimiter(strLen); + + if (pbrTlvs.length > 0) { + let pbr = ICCUtilsHelper.parsePbrTlvs(pbrTlvs); + // EF_ADN is mandatory if and only if DF_PHONEBOOK is present. + if (!pbr.adn) { + if (onerror) onerror("Cannot access ADN."); + return; + } + pbrs.push(pbr); + } + + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (onsuccess) { + RIL.iccInfoPrivate.pbrs = pbrs; + onsuccess(pbrs); + } + } + } + + if (RIL.iccInfoPrivate.pbrs) { + onsuccess(RIL.iccInfoPrivate.pbrs); + return; + } + + let pbrs = []; + ICCIOHelper.loadLinearFixedEF({fileId : ICC_EF_PBR, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Cache EF_IAP record size. + */ + _iapRecordSize: null, + + /** + * Read ICC EF_IAP. (Index Administration Phonebook) + * + * @see TS 131.102, clause 4.4.2.2 + * + * @param fileId EF id of the IAP. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readIAP: function(fileId, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + this._iapRecordSize = options.recordSize; + + let iap = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(iap); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._iapRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_IAP. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param fileId EF id of the IAP. + * @param recordNumber The identifier of the record shall be updated. + * @param iap The IAP value to be written. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateIAP: function(fileId, recordNumber, iap, onsuccess, onerror) { + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + for (let i = 0; i < iap.length; i++) { + GsmPDUHelper.writeHexOctet(iap[i]); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Cache EF_Email record size. + */ + _emailRecordSize: null, + + /** + * Read USIM/RUIM Phonebook EF_EMAIL. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param fileId EF id of the EMAIL. + * @param fileType The type of the EMAIL, one of the ICC_USIM_TYPE* constants. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readEmail: function(fileId, fileType, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let ICCPDUHelper = this.context.ICCPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let email = null; + this._emailRecordSize = options.recordSize; + + // Read contact's email + // + // | Byte | Description | Length | M/O + // | 1 ~ X | E-mail Address | X | M + // | X+1 | ADN file SFI | 1 | C + // | X+2 | ADN file Record Identifier | 1 | C + // Note: The fields marked as C above are mandatort if the file + // is not type 1 (as specified in EF_PBR) + if (fileType == ICC_USIM_TYPE1_TAG) { + email = ICCPDUHelper.read8BitUnpackedToString(octetLen); + } else { + email = ICCPDUHelper.read8BitUnpackedToString(octetLen - 2); + + // Consumes the remaining buffer + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); // For ADN SFI and Record Identifier + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(email); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._emailRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_EMAIL. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param pbr Phonebook Reference File. + * @param recordNumber The identifier of the record shall be updated. + * @param email The value to be written. + * @param adnRecordId The record Id of ADN, only needed if the fileType of Email is TYPE2. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateEmail: function(pbr, recordNumber, email, adnRecordId, onsuccess, onerror) { + let fileId = pbr[USIM_PBR_EMAIL].fileId; + let fileType = pbr[USIM_PBR_EMAIL].fileType; + let writtenEmail; + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let ICCPDUHelper = this.context.ICCPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + if (fileType == ICC_USIM_TYPE1_TAG) { + writtenEmail = ICCPDUHelper.writeStringTo8BitUnpacked(recordSize, email); + } else { + writtenEmail = ICCPDUHelper.writeStringTo8BitUnpacked(recordSize - 2, email); + GsmPDUHelper.writeHexOctet(pbr.adn.sfi || 0xff); + GsmPDUHelper.writeHexOctet(adnRecordId); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + let callback = (options) => { + if (onsuccess) { + onsuccess(writtenEmail); + } + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: callback, + onerror: onerror + }); + }, + + /** + * Cache EF_ANR record size. + */ + _anrRecordSize: null, + + /** + * Read USIM/RUIM Phonebook EF_ANR. + * + * @see TS 131.102, clause 4.4.2.9 + * + * @param fileId EF id of the ANR. + * @param fileType One of the ICC_USIM_TYPE* constants. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readANR: function(fileId, fileType, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let number = null; + this._anrRecordSize = options.recordSize; + + // Skip EF_AAS Record ID. + Buf.seekIncoming(1 * Buf.PDU_HEX_OCTET_SIZE); + + number = this.context.ICCPDUHelper.readNumberWithLength(); + + // Skip 2 unused octets, CCP and EXT1. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + + // For Type 2 there are two extra octets. + if (fileType == ICC_USIM_TYPE2_TAG) { + // Skip 2 unused octets, ADN SFI and Record Identifier. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(number); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._anrRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_ANR. + * + * @see TS 131.102, clause 4.4.2.9 + * + * @param pbr Phonebook Reference File. + * @param recordNumber The identifier of the record shall be updated. + * @param number The value to be written. + * @param adnRecordId The record Id of ADN, only needed if the fileType of Email is TYPE2. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateANR: function(pbr, recordNumber, number, adnRecordId, onsuccess, onerror) { + let fileId = pbr[USIM_PBR_ANR0].fileId; + let fileType = pbr[USIM_PBR_ANR0].fileType; + let writtenNumber; + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + // EF_AAS record Id. Unused for now. + GsmPDUHelper.writeHexOctet(0xff); + + writtenNumber = this.context.ICCPDUHelper.writeNumberWithLength(number); + + // Write unused octets 0xff, CCP and EXT1. + GsmPDUHelper.writeHexOctet(0xff); + GsmPDUHelper.writeHexOctet(0xff); + + // For Type 2 there are two extra octets. + if (fileType == ICC_USIM_TYPE2_TAG) { + GsmPDUHelper.writeHexOctet(pbr.adn.sfi || 0xff); + GsmPDUHelper.writeHexOctet(adnRecordId); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + let callback = (options) => { + if (onsuccess) { + onsuccess(writtenNumber); + } + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: callback, + onerror: onerror + }); + }, + + /** + * Cache the possible free record id for all files. + */ + _freeRecordIds: null, + + /** + * Find free record id. + * + * @param fileId EF id. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findFreeRecordId: function(fileId, onsuccess, onerror) { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + + while (readLen < octetLen) { + let octet = GsmPDUHelper.readHexOctet(); + readLen++; + if (octet != 0xff) { + break; + } + } + + let nextRecord = (options.p1 % options.totalRecords) + 1; + + if (readLen == octetLen) { + // Find free record, assume next record is probably free. + this._freeRecordIds[fileId] = nextRecord; + if (onsuccess) { + onsuccess(options.p1); + } + return; + } else { + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + } + + Buf.readStringDelimiter(strLen); + + if (nextRecord !== recordNumber) { + options.p1 = nextRecord; + this.context.RIL.iccIO(options); + } else { + // No free record found. + delete this._freeRecordIds[fileId]; + if (DEBUG) { + this.context.debug(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + onerror(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + } + + // Start searching free records from the possible one. + let recordNumber = this._freeRecordIds[fileId] || 1; + ICCIOHelper.loadLinearFixedEF({fileId: fileId, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Read Extension Number from TS 151.011 clause 10.5.10, TS 31.102, clause 4.4.2.4 + * + * @param fileId EF Extension id + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readExtension: function(fileId, recordNumber, onsuccess, onerror) { + let callback = (options) => { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + let recordType = this.context.GsmPDUHelper.readHexOctet(); + let number = ""; + + // TS 31.102, clause 4.4.2.4 EFEXT1 + // Case 1, Extension1 record is additional data + if (recordType & 0x02) { + let numLen = this.context.GsmPDUHelper.readHexOctet(); + if (numLen != 0xff) { + if (numLen > EXT_MAX_BCD_NUMBER_BYTES) { + if (DEBUG) { + this.context.debug( + "Error: invalid length of BCD number/SSC contents - " + numLen); + } + // +1 to skip Identifier + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES + 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(length); + onerror(); + return; + } + + number = this.context.GsmPDUHelper.readSwappedNibbleExtendedBcdString(numLen); + if (DEBUG) this.context.debug("Contact Extension Number: "+ number); + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES - numLen) * Buf.PDU_HEX_OCTET_SIZE); + } else { + Buf.seekIncoming(EXT_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + } + } else { + // Don't support Case 2, Extension1 record is Called Party Subaddress. + // +1 skip numLen + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES + 1) * Buf.PDU_HEX_OCTET_SIZE); + } + + // Skip Identifier + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(length); + onsuccess(number); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + callback: callback, + onerror: onerror + }); + }, + + /** + * Update Extension. + * + * @param fileId EF id of the EXT. + * @param recordNumber The number of the record shall be updated. + * @param number Dialling Number to be written. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateExtension: function(fileId, recordNumber, number, onsuccess, onerror) { + let dataWriter = (recordSize) => { + let GsmPDUHelper = this.context.GsmPDUHelper; + // Write String length + let strLen = recordSize * 2; + let Buf = this.context.Buf; + Buf.writeInt32(strLen); + + // We don't support extension chain. + if (number.length > EXT_MAX_NUMBER_DIGITS) { + number = number.substring(0, EXT_MAX_NUMBER_DIGITS); + } + + let numLen = Math.ceil(number.length / 2); + // Write Extension record + GsmPDUHelper.writeHexOctet(0x02); + GsmPDUHelper.writeHexOctet(numLen); + GsmPDUHelper.writeSwappedNibbleBCD(number); + // Write trailing 0xff of Extension data. + for (let i = 0; i < EXT_MAX_BCD_NUMBER_BYTES - numLen; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + // Write trailing 0xff for Identifier. + GsmPDUHelper.writeHexOctet(0xff); + Buf.writeStringDelimiter(strLen); + }; + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Clean an EF record. + * + * @param fileId EF id. + * @param recordNumber The number of the record shall be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + cleanEFRecord: function(fileId, recordNumber, onsuccess, onerror) { + let dataWriter = (recordSize) => { + let GsmPDUHelper = this.context.GsmPDUHelper; + let Buf = this.context.Buf; + // Write String length + let strLen = recordSize * 2; + + Buf.writeInt32(strLen); + // Write record to 0xff + for (let i = 0; i < recordSize; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + Buf.writeStringDelimiter(strLen); + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Get ADNLike extension record number. + * + * @param fileId EF id of the ADN or FDN. + * @param recordNumber EF record id of the ADN or FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + getADNLikeExtensionRecordNumber: function(fileId, recordNumber, onsuccess, onerror) { + let callback = (options) => { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + + // Skip alphaLen, numLen, BCD Number, CCP octets. + Buf.seekIncoming((options.recordSize -1) * Buf.PDU_HEX_OCTET_SIZE); + + let extRecordNumber = this.context.GsmPDUHelper.readHexOctet(); + Buf.readStringDelimiter(length); + + onsuccess(extRecordNumber); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + callback: callback, + onerror: onerror + }); + }, +}; + +/** + * Helper for (U)SIM Records. + */ +function SimRecordHelperObject(aContext) { + this.context = aContext; +} +SimRecordHelperObject.prototype = { + context: null, + + /** + * Fetch (U)SIM records. + */ + fetchSimRecords: function() { + this.context.RIL.getIMSI(); + this.readAD(); + // CPHS was widely introduced in Europe during GSM(2G) era to provide easier + // access to carrier's core service like voicemail, call forwarding, manual + // PLMN selection, and etc. + // Addition EF like EF_CPHS_MBN, EF_CPHS_CPHS_CFF, EF_CPHS_VWI, etc are + // introduced to support these feature. + // In USIM, the replancement of these EFs are provided. (EF_MBDN, EF_MWIS, ...) + // However, some carriers in Europe still rely on these EFs. + this.readCphsInfo(() => this.readSST(), + (aErrorMsg) => { + this.context.debug("Failed to read CPHS_INFO: " + aErrorMsg); + this.readSST(); + }); + }, + + /** + * Read EF_phase. + * This EF is only available in SIM. + */ + readSimPhase: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + let GsmPDUHelper = this.context.GsmPDUHelper; + let phase = GsmPDUHelper.readHexOctet(); + // If EF_phase is coded '03' or greater, an ME supporting STK shall + // perform the PROFILE DOWNLOAD procedure. + if (RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD && + phase >= ICC_PHASE_2_PROFILE_DOWNLOAD_REQUIRED) { + this.context.RIL.sendStkTerminalProfile(STK_SUPPORTED_TERMINAL_PROFILE); + } + + Buf.readStringDelimiter(strLen); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_PHASE, + callback: callback.bind(this) + }); + }, + + /** + * Read the MSISDN from the (U)SIM. + */ + readMSISDN: function() { + function callback(options) { + let RIL = this.context.RIL; + + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (!contact || + (RIL.iccInfo.msisdn !== undefined && + RIL.iccInfo.msisdn === contact.number)) { + return; + } + RIL.iccInfo.msisdn = contact.number; + if (DEBUG) this.context.debug("MSISDN: " + RIL.iccInfo.msisdn); + this.context.ICCUtilsHelper.handleICCInfoChange(); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MSISDN, + callback: callback.bind(this) + }); + }, + + /** + * Read the AD (Administrative Data) from the (U)SIM. + */ + readAD: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let ad = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + let str = ""; + for (let i = 0; i < ad.length; i++) { + str += ad[i] + ", "; + } + this.context.debug("AD: " + str); + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let RIL = this.context.RIL; + // TS 31.102, clause 4.2.18 EFAD + let mncLength = 0; + if (ad && ad[3]) { + mncLength = ad[3] & 0x0f; + if (mncLength != 0x02 && mncLength != 0x03) { + mncLength = 0; + } + } + // The 4th byte of the response is the length of MNC. + let mccMnc = ICCUtilsHelper.parseMccMncFromImsi(RIL.iccInfoPrivate.imsi, + mncLength); + if (mccMnc) { + RIL.iccInfo.mcc = mccMnc.mcc; + RIL.iccInfo.mnc = mccMnc.mnc; + ICCUtilsHelper.handleICCInfoChange(); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_AD, + callback: callback.bind(this) + }); + }, + + /** + * Read the SPN (Service Provider Name) from the (U)SIM. + */ + readSPN: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let spnDisplayCondition = this.context.GsmPDUHelper.readHexOctet(); + // Minus 1 because the first octet is used to store display condition. + let spn = this.context.ICCPDUHelper.readAlphaIdentifier(octetLen - 1); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + this.context.debug("SPN: spn = " + spn + + ", spnDisplayCondition = " + spnDisplayCondition); + } + + let RIL = this.context.RIL; + RIL.iccInfoPrivate.spnDisplayCondition = spnDisplayCondition; + RIL.iccInfo.spn = spn; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + ICCUtilsHelper.updateDisplayCondition(); + ICCUtilsHelper.handleICCInfoChange(); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SPN, + callback: callback.bind(this) + }); + }, + + readIMG: function(recordNumber, onsuccess, onerror) { + function callback(options) { + let RIL = this.context.RIL; + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + + let numInstances = GsmPDUHelper.readHexOctet(); + + // Data length is defined as 9n+1 or 9n+2. See TS 31.102, sub-clause + // 4.6.1.1. However, it's likely to have padding appended so we have a + // rather loose check. + if (octetLen < (9 * numInstances + 1)) { + Buf.seekIncoming((octetLen - 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + if (onerror) { + onerror(); + } + return; + } + + let imgDescriptors = []; + for (let i = 0; i < numInstances; i++) { + imgDescriptors[i] = { + width: GsmPDUHelper.readHexOctet(), + height: GsmPDUHelper.readHexOctet(), + codingScheme: GsmPDUHelper.readHexOctet(), + fileId: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(), + offset: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(), + dataLen: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet() + }; + } + Buf.seekIncoming((octetLen - 9 * numInstances - 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + + let instances = []; + let currentInstance = 0; + let readNextInstance = (function(img) { + instances[currentInstance] = img; + currentInstance++; + + if (currentInstance < numInstances) { + let imgDescriptor = imgDescriptors[currentInstance]; + this.readIIDF(imgDescriptor.fileId, + imgDescriptor.offset, + imgDescriptor.dataLen, + imgDescriptor.codingScheme, + readNextInstance, + onerror); + } else { + if (onsuccess) { + onsuccess(instances); + } + } + }).bind(this); + + this.readIIDF(imgDescriptors[0].fileId, + imgDescriptors[0].offset, + imgDescriptors[0].dataLen, + imgDescriptors[0].codingScheme, + readNextInstance, + onerror); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_IMG, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror + }); + }, + + readIIDF: function(fileId, offset, dataLen, codingScheme, onsuccess, onerror) { + // Valid fileId is '4FXX', see TS 31.102, clause 4.6.1.2. + if ((fileId >> 8) != 0x4F) { + if (onerror) { + onerror(); + } + return; + } + + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + let GsmPDUHelper = this.context.GsmPDUHelper; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + + if (octetLen < offset + dataLen) { + // Data length is not enough. See TS 31.102, clause 4.6.1.1, the + // paragraph "Bytes 8 and 9: Length of Image Instance Data." + Buf.seekIncoming(octetLen * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + if (onerror) { + onerror(); + } + return; + } + + Buf.seekIncoming(offset * Buf.PDU_HEX_OCTET_SIZE); + + let rawData = { + width: GsmPDUHelper.readHexOctet(), + height: GsmPDUHelper.readHexOctet(), + codingScheme: codingScheme + }; + + switch (codingScheme) { + case ICC_IMG_CODING_SCHEME_BASIC: + rawData.body = GsmPDUHelper.readHexOctetArray( + dataLen - ICC_IMG_HEADER_SIZE_BASIC); + Buf.seekIncoming((octetLen - offset - dataLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + + case ICC_IMG_CODING_SCHEME_COLOR: + case ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY: + rawData.bitsPerImgPoint = GsmPDUHelper.readHexOctet(); + let num = GsmPDUHelper.readHexOctet(); + // The value 0 shall be interpreted as 256. See TS 31.102, Annex B.2. + rawData.numOfClutEntries = (num === 0) ? 0x100 : num; + rawData.clutOffset = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + rawData.body = GsmPDUHelper.readHexOctetArray( + dataLen - ICC_IMG_HEADER_SIZE_COLOR); + + Buf.seekIncoming((rawData.clutOffset - offset - dataLen) * + Buf.PDU_HEX_OCTET_SIZE); + let clut = GsmPDUHelper.readHexOctetArray(rawData.numOfClutEntries * + ICC_CLUT_ENTRY_SIZE); + + rawData.clut = clut; + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(rawData); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: fileId, + pathId: this.context.ICCFileHelper.getEFPath(ICC_EF_IMG), + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read the (U)SIM Service Table from the (U)SIM. + */ + readSST: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let sst = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + RIL.iccInfoPrivate.sst = sst; + if (DEBUG) { + let str = ""; + for (let i = 0; i < sst.length; i++) { + str += sst[i] + ", "; + } + this.context.debug("SST: " + str); + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.isICCServiceAvailable("MSISDN")) { + if (DEBUG) this.context.debug("MSISDN: MSISDN is available"); + this.readMSISDN(); + } else { + if (DEBUG) this.context.debug("MSISDN: MSISDN service is not available"); + } + + // Fetch SPN and PLMN list, if some of them are available. + if (ICCUtilsHelper.isICCServiceAvailable("SPN")) { + if (DEBUG) this.context.debug("SPN: SPN is available"); + this.readSPN(); + } else { + if (DEBUG) this.context.debug("SPN: SPN service is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("MDN")) { + if (DEBUG) this.context.debug("MDN: MDN available."); + this.readMBDN(); + } else { + if (DEBUG) this.context.debug("MDN: MDN service is not available"); + + if (ICCUtilsHelper.isCphsServiceAvailable("MBN")) { + // read CPHS_MBN in advance if MBDN is not available. + this.readCphsMBN(); + } else { + if (DEBUG) this.context.debug("CPHS_MBN: CPHS_MBN service is not available"); + } + } + + if (ICCUtilsHelper.isICCServiceAvailable("MWIS")) { + if (DEBUG) this.context.debug("MWIS: MWIS is available"); + this.readMWIS(); + } else { + if (DEBUG) this.context.debug("MWIS: MWIS is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("SPDI")) { + if (DEBUG) this.context.debug("SPDI: SPDI available."); + this.readSPDI(); + } else { + if (DEBUG) this.context.debug("SPDI: SPDI service is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("PNN")) { + if (DEBUG) this.context.debug("PNN: PNN is available"); + this.readPNN(); + } else { + if (DEBUG) this.context.debug("PNN: PNN is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("OPL")) { + if (DEBUG) this.context.debug("OPL: OPL is available"); + this.readOPL(); + } else { + if (DEBUG) this.context.debug("OPL: OPL is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("GID1")) { + if (DEBUG) this.context.debug("GID1: GID1 is available"); + this.readGID1(); + } else { + if (DEBUG) this.context.debug("GID1: GID1 is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("CBMI")) { + this.readCBMI(); + } else { + RIL.cellBroadcastConfigs.CBMI = null; + } + if (ICCUtilsHelper.isICCServiceAvailable("DATA_DOWNLOAD_SMS_CB")) { + this.readCBMID(); + } else { + RIL.cellBroadcastConfigs.CBMID = null; + } + if (ICCUtilsHelper.isICCServiceAvailable("CBMIR")) { + this.readCBMIR(); + } else { + RIL.cellBroadcastConfigs.CBMIR = null; + } + RIL._mergeAllCellBroadcastConfigs(); + } + + // ICC_EF_UST has the same value with ICC_EF_SST. + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SST, + callback: callback.bind(this) + }); + }, + + /** + * Read (U)SIM MBDN. (Mailbox Dialling Number) + * + * @see TS 131.102, clause 4.2.60 + */ + readMBDN: function() { + function callback(options) { + let RIL = this.context.RIL; + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if ((!contact || + ((!contact.alphaId || contact.alphaId == "") && + (!contact.number || contact.number == ""))) && + this.context.ICCUtilsHelper.isCphsServiceAvailable("MBN")) { + // read CPHS_MBN in advance if MBDN is invalid or empty. + this.readCphsMBN(); + return; + } + + if (!contact || + (RIL.iccInfoPrivate.mbdn !== undefined && + RIL.iccInfoPrivate.mbdn === contact.number)) { + return; + } + RIL.iccInfoPrivate.mbdn = contact.number; + if (DEBUG) { + this.context.debug("MBDN, alphaId=" + contact.alphaId + + " number=" + contact.number); + } + contact.rilMessageType = "iccmbdn"; + RIL.sendChromeMessage(contact); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MBDN, + callback: callback.bind(this) + }); + }, + + /** + * Read ICC MWIS. (Message Waiting Indication Status) + * + * @see TS 31.102, clause 4.2.63 for USIM and TS 51.011, clause 10.3.45 for SIM. + */ + readMWIS: function() { + function callback(options) { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let mwis = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + if (!mwis) { + return; + } + RIL.iccInfoPrivate.mwis = mwis; //Keep raw MWIS for updateMWIS() + + let mwi = {}; + // b8 b7 B6 b5 b4 b3 b2 b1 4.2.63, TS 31.102 version 11.6.0 + // | | | | | | | |__ Voicemail + // | | | | | | |_____ Fax + // | | | | | |________ Electronic Mail + // | | | | |___________ Other + // | | | |______________ Videomail + // |__|__|_________________ RFU + mwi.active = ((mwis[0] & 0x01) != 0); + + if (mwi.active) { + // In TS 23.040 msgCount is in the range from 0 to 255. + // The value 255 shall be taken to mean 255 or greater. + // + // However, There is no definition about 0 when MWI is active. + // + // Normally, when mwi is active, the msgCount must be larger than 0. + // Refer to other reference phone, + // 0 is usually treated as UNKNOWN for storing 2nd level MWI status (DCS). + mwi.msgCount = (mwis[1] === 0) ? GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN + : mwis[1]; + } else { + mwi.msgCount = 0; + } + + RIL.sendChromeMessage({ rilMessageType: "iccmwis", + mwi: mwi }); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MWIS, + recordNumber: 1, // Get 1st Subscriber Profile. + callback: callback.bind(this) + }); + }, + + /** + * Update ICC MWIS. (Message Waiting Indication Status) + * + * @see TS 31.102, clause 4.2.63 for USIM and TS 51.011, clause 10.3.45 for SIM. + */ + updateMWIS: function(mwi) { + let RIL = this.context.RIL; + if (!RIL.iccInfoPrivate.mwis) { + return; + } + + function dataWriter(recordSize) { + let mwis = RIL.iccInfoPrivate.mwis; + + let msgCount = + (mwi.msgCount === GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN) ? 0 : mwi.msgCount; + + [mwis[0], mwis[1]] = (mwi.active) ? [(mwis[0] | 0x01), msgCount] + : [(mwis[0] & 0xFE), 0]; + + let strLen = recordSize * 2; + let Buf = this.context.Buf; + Buf.writeInt32(strLen); + + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0; i < mwis.length; i++) { + GsmPDUHelper.writeHexOctet(mwis[i]); + } + + Buf.writeStringDelimiter(strLen); + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: ICC_EF_MWIS, + recordNumber: 1, // Update 1st Subscriber Profile. + dataWriter: dataWriter.bind(this) + }); + }, + + /** + * Read the SPDI (Service Provider Display Information) from the (U)SIM. + * + * See TS 131.102 section 4.2.66 for USIM and TS 51.011 section 10.3.50 + * for SIM. + */ + readSPDI: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + let endLoop = false; + + let RIL = this.context.RIL; + RIL.iccInfoPrivate.SPDI = null; + + let GsmPDUHelper = this.context.GsmPDUHelper; + while ((readLen < octetLen) && !endLoop) { + let tlvTag = GsmPDUHelper.readHexOctet(); + let tlvLen = GsmPDUHelper.readHexOctet(); + readLen += 2; // For tag and length fields. + switch (tlvTag) { + case SPDI_TAG_SPDI: + // The value part itself is a TLV. + continue; + case SPDI_TAG_PLMN_LIST: + // This PLMN list is what we want. + RIL.iccInfoPrivate.SPDI = this.readPLMNEntries(tlvLen / 3); + readLen += tlvLen; + endLoop = true; + break; + default: + // We don't care about its content if its tag is not SPDI nor + // PLMN_LIST. + endLoop = true; + break; + } + } + + // Consume unread octets. + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + this.context.debug("SPDI: " + JSON.stringify(RIL.iccInfoPrivate.SPDI)); + } + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.updateDisplayCondition()) { + ICCUtilsHelper.handleICCInfoChange(); + } + } + + // PLMN List is Servive 51 in USIM, EF_SPDI + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SPDI, + callback: callback.bind(this) + }); + }, + + _readCbmiHelper: function(which) { + let RIL = this.context.RIL; + + function callback() { + let Buf = this.context.Buf; + let strLength = Buf.readInt32(); + + // Each Message Identifier takes two octets and each octet is encoded + // into two chars. + let numIds = strLength / 4, list = null; + if (numIds) { + list = []; + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0, id; i < numIds; i++) { + id = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + // `Unused entries shall be set to 'FF FF'.` + if (id != 0xFFFF) { + list.push(id); + list.push(id + 1); + } + } + } + if (DEBUG) { + this.context.debug(which + ": " + JSON.stringify(list)); + } + + Buf.readStringDelimiter(strLength); + + RIL.cellBroadcastConfigs[which] = list; + RIL._mergeAllCellBroadcastConfigs(); + } + + function onerror() { + RIL.cellBroadcastConfigs[which] = null; + RIL._mergeAllCellBroadcastConfigs(); + } + + let fileId = GLOBAL["ICC_EF_" + which]; + this.context.ICCIOHelper.loadTransparentEF({ + fileId: fileId, + callback: callback.bind(this), + onerror: onerror.bind(this) + }); + }, + + /** + * Read EFcbmi (Cell Broadcast Message Identifier selection) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.14 EFcbmi + * @see 3GPP TS 51.011 v5.0.0 section 10.3.13 EFcbmi + */ + readCBMI: function() { + this._readCbmiHelper("CBMI"); + }, + + /** + * Read EFcbmid (Cell Broadcast Message Identifier for Data Download) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.20 EFcbmid + * @see 3GPP TS 51.011 v5.0.0 section 10.3.26 EFcbmid + */ + readCBMID: function() { + this._readCbmiHelper("CBMID"); + }, + + /** + * Read EFcbmir (Cell Broadcast Message Identifier Range selection) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.22 EFcbmir + * @see 3GPP TS 51.011 v5.0.0 section 10.3.28 EFcbmir + */ + readCBMIR: function() { + let RIL = this.context.RIL; + + function callback() { + let Buf = this.context.Buf; + let strLength = Buf.readInt32(); + + // Each Message Identifier range takes four octets and each octet is + // encoded into two chars. + let numIds = strLength / 8, list = null; + if (numIds) { + list = []; + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0, from, to; i < numIds; i++) { + // `Bytes one and two of each range identifier equal the lower value + // of a cell broadcast range, bytes three and four equal the upper + // value of a cell broadcast range.` + from = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + to = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + // `Unused entries shall be set to 'FF FF'.` + if ((from != 0xFFFF) && (to != 0xFFFF)) { + list.push(from); + list.push(to + 1); + } + } + } + if (DEBUG) { + this.context.debug("CBMIR: " + JSON.stringify(list)); + } + + Buf.readStringDelimiter(strLength); + + RIL.cellBroadcastConfigs.CBMIR = list; + RIL._mergeAllCellBroadcastConfigs(); + } + + function onerror() { + RIL.cellBroadcastConfigs.CBMIR = null; + RIL._mergeAllCellBroadcastConfigs(); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CBMIR, + callback: callback.bind(this), + onerror: onerror.bind(this) + }); + }, + + /** + * Read OPL (Operator PLMN List) from (U)SIM. + * + * See 3GPP TS 31.102 Sec. 4.2.59 for USIM + * 3GPP TS 51.011 Sec. 10.3.42 for SIM. + */ + readOPL: function() { + let ICCIOHelper = this.context.ICCIOHelper; + let opl = []; + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + // The first 7 bytes are LAI (for UMTS) and the format of LAI is defined + // in 3GPP TS 23.003, Sec 4.1 + // +-------------+---------+ + // | Octet 1 - 3 | MCC/MNC | + // +-------------+---------+ + // | Octet 4 - 7 | LAC | + // +-------------+---------+ + let mccMnc = [GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet()]; + if (mccMnc[0] != 0xFF || mccMnc[1] != 0xFF || mccMnc[2] != 0xFF) { + let oplElement = {}; + let semiOctets = []; + for (let i = 0; i < mccMnc.length; i++) { + semiOctets.push((mccMnc[i] & 0xf0) >> 4); + semiOctets.push(mccMnc[i] & 0x0f); + } + let reformat = [semiOctets[1], semiOctets[0], semiOctets[3], + semiOctets[5], semiOctets[4], semiOctets[2]]; + let buf = ""; + for (let i = 0; i < reformat.length; i++) { + if (reformat[i] != 0xF) { + buf += GsmPDUHelper.semiOctetToExtendedBcdChar(reformat[i]); + } + if (i === 2) { + // 0-2: MCC + oplElement.mcc = buf; + buf = ""; + } else if (i === 5) { + // 3-5: MNC + oplElement.mnc = buf; + } + } + // LAC/TAC + oplElement.lacTacStart = + (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + oplElement.lacTacEnd = + (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + // PLMN Network Name Record Identifier + oplElement.pnnRecordId = GsmPDUHelper.readHexOctet(); + if (DEBUG) { + this.context.debug("OPL: [" + (opl.length + 1) + "]: " + + JSON.stringify(oplElement)); + } + opl.push(oplElement); + } else { + Buf.seekIncoming(5 * Buf.PDU_HEX_OCTET_SIZE); + } + Buf.readStringDelimiter(strLen); + + let RIL = this.context.RIL; + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + RIL.iccInfoPrivate.OPL = opl; + RIL.overrideICCNetworkName(); + } + } + + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_OPL, + callback: callback.bind(this)}); + }, + + /** + * Read PNN (PLMN Network Name) from (U)SIM. + * + * See 3GPP TS 31.102 Sec. 4.2.58 for USIM + * 3GPP TS 51.011 Sec. 10.3.41 for SIM. + */ + readPNN: function() { + let ICCIOHelper = this.context.ICCIOHelper; + function callback(options) { + let pnnElement; + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + + let GsmPDUHelper = this.context.GsmPDUHelper; + while (readLen < octetLen) { + let tlvTag = GsmPDUHelper.readHexOctet(); + + if (tlvTag == 0xFF) { + // Unused byte + readLen++; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + // Needs this check to avoid initializing twice. + pnnElement = pnnElement || {}; + + let tlvLen = GsmPDUHelper.readHexOctet(); + + switch (tlvTag) { + case PNN_IEI_FULL_NETWORK_NAME: + pnnElement.fullName = GsmPDUHelper.readNetworkName(tlvLen); + break; + case PNN_IEI_SHORT_NETWORK_NAME: + pnnElement.shortName = GsmPDUHelper.readNetworkName(tlvLen); + break; + default: + Buf.seekIncoming(tlvLen * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + readLen += (tlvLen + 2); // +2 for tlvTag and tlvLen + } + Buf.readStringDelimiter(strLen); + + pnn.push(pnnElement); + + let RIL = this.context.RIL; + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (DEBUG) { + for (let i = 0; i < pnn.length; i++) { + this.context.debug("PNN: [" + i + "]: " + JSON.stringify(pnn[i])); + } + } + RIL.iccInfoPrivate.PNN = pnn; + RIL.overrideICCNetworkName(); + } + } + + let pnn = []; + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_PNN, + callback: callback.bind(this)}); + }, + + /** + * Read the list of PLMN (Public Land Mobile Network) entries + * We cannot directly rely on readSwappedNibbleBcdToString(), + * since it will no correctly handle some corner-cases that are + * not a problem in our case (0xFF 0xFF 0xFF). + * + * @param length The number of PLMN records. + * @return An array of string corresponding to the PLMNs. + */ + readPLMNEntries: function(length) { + let plmnList = []; + // Each PLMN entry has 3 bytes. + if (DEBUG) { + this.context.debug("PLMN entries length = " + length); + } + let GsmPDUHelper = this.context.GsmPDUHelper; + let index = 0; + while (index < length) { + // Unused entries will be 0xFFFFFF, according to EF_SPDI + // specs (TS 131 102, section 4.2.66) + try { + let plmn = [GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet()]; + if (DEBUG) { + this.context.debug("Reading PLMN entry: [" + index + "]: '" + plmn + "'"); + } + if (plmn[0] != 0xFF && + plmn[1] != 0xFF && + plmn[2] != 0xFF) { + let semiOctets = []; + for (let idx = 0; idx < plmn.length; idx++) { + semiOctets.push((plmn[idx] & 0xF0) >> 4); + semiOctets.push(plmn[idx] & 0x0F); + } + + // According to TS 24.301, 9.9.3.12, the semi octets is arranged + // in format: + // Byte 1: MCC[2] | MCC[1] + // Byte 2: MNC[3] | MCC[3] + // Byte 3: MNC[2] | MNC[1] + // Therefore, we need to rearrange them. + let reformat = [semiOctets[1], semiOctets[0], semiOctets[3], + semiOctets[5], semiOctets[4], semiOctets[2]]; + let buf = ""; + let plmnEntry = {}; + for (let i = 0; i < reformat.length; i++) { + if (reformat[i] != 0xF) { + buf += GsmPDUHelper.semiOctetToExtendedBcdChar(reformat[i]); + } + if (i === 2) { + // 0-2: MCC + plmnEntry.mcc = buf; + buf = ""; + } else if (i === 5) { + // 3-5: MNC + plmnEntry.mnc = buf; + } + } + if (DEBUG) { + this.context.debug("PLMN = " + plmnEntry.mcc + ", " + plmnEntry.mnc); + } + plmnList.push(plmnEntry); + } + } catch (e) { + if (DEBUG) { + this.context.debug("PLMN entry " + index + " is invalid."); + } + break; + } + index ++; + } + return plmnList; + }, + + /** + * Read the SMS from the ICC. + * + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readSMS: function(recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + // TS 51.011, 10.5.3 EF_SMS + // b3 b2 b1 + // 0 0 1 message received by MS from network; message read + // 0 1 1 message received by MS from network; message to be read + // 1 1 1 MS originating message; message to be sent + // 1 0 1 MS originating message; message sent to the network: + let GsmPDUHelper = this.context.GsmPDUHelper; + let status = GsmPDUHelper.readHexOctet(); + + let message = GsmPDUHelper.readMessage(); + message.simStatus = status; + + // Consumes the remaining buffer + Buf.seekIncoming(Buf.getReadAvailable() - Buf.PDU_HEX_OCTET_SIZE); + + Buf.readStringDelimiter(strLen); + + if (message) { + onsuccess(message); + } else { + onerror("Failed to decode SMS on SIM #" + recordNumber); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_SMS, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror + }); + }, + + readGID1: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + RIL.iccInfoPrivate.gid1 = Buf.readString(); + if (DEBUG) { + this.context.debug("GID1: " + RIL.iccInfoPrivate.gid1); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_GID1, + callback: callback.bind(this) + }); + }, + + /** + * Read CPHS Phase & Service Table from CPHS Info. + * + * @See B.3.1.1 CPHS Information in CPHS Phase 2. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readCphsInfo: function(onsuccess, onerror) { + function callback() { + try { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let cphsInfo = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + if (DEBUG) { + let str = ""; + for (let i = 0; i < cphsInfo.length; i++) { + str += cphsInfo[i] + ", "; + } + this.context.debug("CPHS INFO: " + str); + } + + /** + * CPHS INFORMATION + * + * Byte 1: CPHS Phase + * 01 phase 1 + * 02 phase 2 + * etc. + * + * Byte 2: CPHS Service Table + * +----+----+----+----+----+----+----+----+ + * | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | + * +----+----+----+----+----+----+----+----+ + * | ONSF | MBN | SST | CSP | + * | Phase 2 | ALL | Phase 1 | All | + * +----+----+----+----+----+----+----+----+ + * + * Byte 3: CPHS Service Table continued + * +----+----+----+----+----+----+----+----+ + * | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | + * +----+----+----+----+----+----+----+----+ + * | RFU | RFU | RFU | INFO_NUM| + * | | | | Phase 2 | + * +----+----+----+----+----+----+----+----+ + */ + let cphsPhase = cphsInfo[0]; + if (cphsPhase == 1) { + // Clear 'Phase 2 only' services. + cphsInfo[1] &= 0x3F; + // We don't know whether Byte 3 is available in CPHS phase 1 or not. + // Add boundary check before accessing it. + if (cphsInfo.length > 2) { + cphsInfo[2] = 0x00; + } + } else if (cphsPhase == 2) { + // Clear 'Phase 1 only' services. + cphsInfo[1] &= 0xF3; + } else { + throw new Error("Unknown CPHS phase: " + cphsPhase); + } + + RIL.iccInfoPrivate.cphsSt = cphsInfo.subarray(1); + onsuccess(); + } catch(e) { + onerror(e.toString()); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CPHS_INFO, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read CPHS MBN. (Mailbox Numbers) + * + * @See B.4.2.2 Voice Message Retrieval and Indicator Clearing + */ + readCphsMBN: function() { + function callback(options) { + let RIL = this.context.RIL; + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (!contact || + (RIL.iccInfoPrivate.mbdn !== undefined && + RIL.iccInfoPrivate.mbdn === contact.number)) { + return; + } + RIL.iccInfoPrivate.mbdn = contact.number; + if (DEBUG) { + this.context.debug("CPHS_MDN, alphaId=" + contact.alphaId + + " number=" + contact.number); + } + contact.rilMessageType = "iccmbdn"; + RIL.sendChromeMessage(contact); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_CPHS_MBN, + callback: callback.bind(this) + }); + } +}; + +function RuimRecordHelperObject(aContext) { + this.context = aContext; +} +RuimRecordHelperObject.prototype = { + context: null, + + fetchRuimRecords: function() { + this.getIMSI_M(); + this.readCST(); + this.readCDMAHome(); + this.context.RIL.getCdmaSubscription(); + }, + + /** + * Get IMSI_M from CSIM/RUIM. + * See 3GPP2 C.S0065 Sec. 5.2.2 + */ + getIMSI_M: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let encodedImsi = this.context.GsmPDUHelper.readHexOctetArray(strLen / 2); + Buf.readStringDelimiter(strLen); + + if ((encodedImsi[CSIM_IMSI_M_PROGRAMMED_BYTE] & 0x80)) { // IMSI_M programmed + let RIL = this.context.RIL; + RIL.iccInfoPrivate.imsi = this.decodeIMSI(encodedImsi); + RIL.sendChromeMessage({rilMessageType: "iccimsi", + imsi: RIL.iccInfoPrivate.imsi}); + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let mccMnc = ICCUtilsHelper.parseMccMncFromImsi(RIL.iccInfoPrivate.imsi); + if (mccMnc) { + RIL.iccInfo.mcc = mccMnc.mcc; + RIL.iccInfo.mnc = mccMnc.mnc; + ICCUtilsHelper.handleICCInfoChange(); + } + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_IMSI_M, + callback: callback.bind(this) + }); + }, + + /** + * Decode IMSI from IMSI_M + * See 3GPP2 C.S0005 Sec. 2.3.1 + * +---+---------+------------+---+--------+---------+---+---------+--------+ + * |RFU| MCC | programmed |RFU| MNC | MIN1 |RFU| MIN2 | CLASS | + * +---+---------+------------+---+--------+---------+---+---------+--------+ + * | 6 | 10 bits | 8 bits | 1 | 7 bits | 24 bits | 6 | 10 bits | 8 bits | + * +---+---------+------------+---+--------+---------+---+---------+--------+ + */ + decodeIMSI: function(encodedImsi) { + // MCC: 10 bits, 3 digits + let encodedMCC = ((encodedImsi[CSIM_IMSI_M_MCC_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MCC_BYTE] & 0xff); + let mcc = this.decodeIMSIValue(encodedMCC, 3); + + // MNC: 7 bits, 2 digits + let encodedMNC = encodedImsi[CSIM_IMSI_M_MNC_BYTE] & 0x7f; + let mnc = this.decodeIMSIValue(encodedMNC, 2); + + // MIN2: 10 bits, 3 digits + let encodedMIN2 = ((encodedImsi[CSIM_IMSI_M_MIN2_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MIN2_BYTE] & 0xff); + let min2 = this.decodeIMSIValue(encodedMIN2, 3); + + // MIN1: 10+4+10 bits, 3+1+3 digits + let encodedMIN1First3 = ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 2] & 0xff) << 2) + + ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0xc0) >> 6); + let min1First3 = this.decodeIMSIValue(encodedMIN1First3, 3); + + let encodedFourthDigit = (encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0x3c) >> 2; + if (encodedFourthDigit > 9) { + encodedFourthDigit = 0; + } + let fourthDigit = encodedFourthDigit.toString(); + + let encodedMIN1Last3 = ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MIN1_BYTE] & 0xff); + let min1Last3 = this.decodeIMSIValue(encodedMIN1Last3, 3); + + return mcc + mnc + min2 + min1First3 + fourthDigit + min1Last3; + }, + + /** + * Decode IMSI Helper function + * See 3GPP2 C.S0005 section 2.3.1.1 + */ + decodeIMSIValue: function(encoded, length) { + let offset = length === 3 ? 111 : 11; + let value = encoded + offset; + + for (let base = 10, temp = value, i = 0; i < length; i++) { + if (temp % 10 === 0) { + value -= base; + } + temp = Math.floor(value / base); + base = base * 10; + } + + let s = value.toString(); + while (s.length < length) { + s = "0" + s; + } + + return s; + }, + + /** + * Read CDMAHOME for CSIM. + * See 3GPP2 C.S0023 Sec. 3.4.8. + */ + readCDMAHome: function() { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let tempOctet = GsmPDUHelper.readHexOctet(); + cdmaHomeSystemId.push(((GsmPDUHelper.readHexOctet() & 0x7f) << 8) | tempOctet); + tempOctet = GsmPDUHelper.readHexOctet(); + cdmaHomeNetworkId.push(((GsmPDUHelper.readHexOctet() & 0xff) << 8) | tempOctet); + + // Consuming the last octet: band class. + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + + Buf.readStringDelimiter(strLen); + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (DEBUG) { + this.context.debug("CDMAHome system id: " + + JSON.stringify(cdmaHomeSystemId)); + this.context.debug("CDMAHome network id: " + + JSON.stringify(cdmaHomeNetworkId)); + } + this.context.RIL.cdmaHome = { + systemId: cdmaHomeSystemId, + networkId: cdmaHomeNetworkId + }; + } + } + + let cdmaHomeSystemId = [], cdmaHomeNetworkId = []; + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_CSIM_CDMAHOME, + callback: callback.bind(this)}); + }, + + /** + * Read CDMA Service Table. + * See 3GPP2 C.S0023 Sec. 3.4.18 + */ + readCST: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + RIL.iccInfoPrivate.cst = + this.context.GsmPDUHelper.readHexOctetArray(strLen / 2); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + let str = ""; + for (let i = 0; i < RIL.iccInfoPrivate.cst.length; i++) { + str += RIL.iccInfoPrivate.cst[i] + ", "; + } + this.context.debug("CST: " + str); + } + + if (this.context.ICCUtilsHelper.isICCServiceAvailable("SPN")) { + if (DEBUG) this.context.debug("SPN: SPN is available"); + this.readSPN(); + } + } + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_CST, + callback: callback.bind(this) + }); + }, + + readSPN: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + + let GsmPDUHelper = this.context.GsmPDUHelper; + let displayCondition = GsmPDUHelper.readHexOctet(); + let codingScheme = GsmPDUHelper.readHexOctet(); + // Skip one octet: language indicator. + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + let readLen = 3; + + // SPN String ends up with 0xff. + let userDataBuffer = []; + + while (readLen < octetLen) { + let octet = GsmPDUHelper.readHexOctet(); + readLen++; + if (octet == 0xff) { + break; + } + userDataBuffer.push(octet); + } + + this.context.BitBufferHelper.startRead(userDataBuffer); + + let CdmaPDUHelper = this.context.CdmaPDUHelper; + let msgLen; + switch (CdmaPDUHelper.getCdmaMsgEncoding(codingScheme)) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + msgLen = Math.floor(userDataBuffer.length * 8 / 7); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msgLen = userDataBuffer.length; + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + msgLen = Math.floor(userDataBuffer.length / 2); + break; + } + + let RIL = this.context.RIL; + RIL.iccInfo.spn = CdmaPDUHelper.decodeCdmaPDUMsg(codingScheme, null, msgLen); + if (DEBUG) { + this.context.debug("CDMA SPN: " + RIL.iccInfo.spn + + ", Display condition: " + displayCondition); + } + RIL.iccInfoPrivate.spnDisplayCondition = displayCondition; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_SPN, + callback: callback.bind(this) + }); + } +}; + +/** + * Helper functions for ICC utilities. + */ +function ICCUtilsHelperObject(aContext) { + this.context = aContext; +} +ICCUtilsHelperObject.prototype = { + context: null, + + /** + * Get network names by using EF_OPL and EF_PNN + * + * @See 3GPP TS 31.102 sec. 4.2.58 and sec. 4.2.59 for USIM, + * 3GPP TS 51.011 sec. 10.3.41 and sec. 10.3.42 for SIM. + * + * @param mcc The mobile country code of the network. + * @param mnc The mobile network code of the network. + * @param lac The location area code of the network. + */ + getNetworkNameFromICC: function(mcc, mnc, lac) { + let RIL = this.context.RIL; + let iccInfoPriv = RIL.iccInfoPrivate; + let iccInfo = RIL.iccInfo; + let pnnEntry; + + if (!mcc || !mnc || lac == null || lac < 0) { + return null; + } + + // We won't get network name if there is no PNN file. + if (!iccInfoPriv.PNN) { + return null; + } + + if (!this.isICCServiceAvailable("OPL")) { + // When OPL is not present: + // According to 3GPP TS 31.102 Sec. 4.2.58 and 3GPP TS 51.011 Sec. 10.3.41, + // If EF_OPL is not present, the first record in this EF is used for the + // default network name when registered to the HPLMN. + // If we haven't get pnnEntry assigned, we should try to assign default + // value to it. + if (mcc == iccInfo.mcc && mnc == iccInfo.mnc) { + pnnEntry = iccInfoPriv.PNN[0]; + } + } else { + let GsmPDUHelper = this.context.GsmPDUHelper; + let wildChar = GsmPDUHelper.extendedBcdChars.charAt(0x0d); + // According to 3GPP TS 31.102 Sec. 4.2.59 and 3GPP TS 51.011 Sec. 10.3.42, + // the ME shall use this EF_OPL in association with the EF_PNN in place + // of any network name stored within the ME's internal list and any network + // name received when registered to the PLMN. + let length = iccInfoPriv.OPL ? iccInfoPriv.OPL.length : 0; + for (let i = 0; i < length; i++) { + let unmatch = false; + let opl = iccInfoPriv.OPL[i]; + // Try to match the MCC/MNC. Besides, A BCD value of 'D' in any of the + // MCC and/or MNC digits shall be used to indicate a "wild" value for + // that corresponding MCC/MNC digit. + if (opl.mcc.indexOf(wildChar) !== -1) { + for (let j = 0; j < opl.mcc.length; j++) { + if (opl.mcc[j] !== wildChar && opl.mcc[j] !== mcc[j]) { + unmatch = true; + break; + } + } + if (unmatch) { + continue; + } + } else { + if (mcc !== opl.mcc) { + continue; + } + } + + if (mnc.length !== opl.mnc.length) { + continue; + } + + if (opl.mnc.indexOf(wildChar) !== -1) { + for (let j = 0; j < opl.mnc.length; j++) { + if (opl.mnc[j] !== wildChar && opl.mnc[j] !== mnc[j]) { + unmatch = true; + break; + } + } + if (unmatch) { + continue; + } + } else { + if (mnc !== opl.mnc) { + continue; + } + } + + // Try to match the location area code. If current local area code is + // covered by lac range that specified in the OPL entry, use the PNN + // that specified in the OPL entry. + if ((opl.lacTacStart === 0x0 && opl.lacTacEnd == 0xFFFE) || + (opl.lacTacStart <= lac && opl.lacTacEnd >= lac)) { + if (opl.pnnRecordId === 0) { + // See 3GPP TS 31.102 Sec. 4.2.59 and 3GPP TS 51.011 Sec. 10.3.42, + // A value of '00' indicates that the name is to be taken from other + // sources. + return null; + } + pnnEntry = iccInfoPriv.PNN[opl.pnnRecordId - 1]; + break; + } + } + } + + if (!pnnEntry) { + return null; + } + + // Return a new object to avoid global variable, PNN, be modified by accident. + return { fullName: pnnEntry.fullName || "", + shortName: pnnEntry.shortName || "" }; + }, + + /** + * This will compute the spnDisplay field of the network. + * See TS 22.101 Annex A and TS 51.011 10.3.11 for details. + * + * @return True if some of iccInfo is changed in by this function. + */ + updateDisplayCondition: function() { + let RIL = this.context.RIL; + + // If EFspn isn't existed in SIM or it haven't been read yet, we should + // just set isDisplayNetworkNameRequired = true and + // isDisplaySpnRequired = false + let iccInfo = RIL.iccInfo; + let iccInfoPriv = RIL.iccInfoPrivate; + let displayCondition = iccInfoPriv.spnDisplayCondition; + let origIsDisplayNetworkNameRequired = iccInfo.isDisplayNetworkNameRequired; + let origIsDisplaySPNRequired = iccInfo.isDisplaySpnRequired; + + if (displayCondition === undefined) { + iccInfo.isDisplayNetworkNameRequired = true; + iccInfo.isDisplaySpnRequired = false; + } else if (RIL._isCdma) { + // CDMA family display rule. + let cdmaHome = RIL.cdmaHome; + let cell = RIL.voiceRegistrationState.cell; + let sid = cell && cell.cdmaSystemId; + let nid = cell && cell.cdmaNetworkId; + + iccInfo.isDisplayNetworkNameRequired = false; + + // If display condition is 0x0, we don't even need to check network id + // or system id. + if (displayCondition === 0x0) { + iccInfo.isDisplaySpnRequired = false; + } else { + // CDMA SPN Display condition dosen't specify whenever network name is + // reqired. + if (!cdmaHome || + !cdmaHome.systemId || + cdmaHome.systemId.length === 0 || + cdmaHome.systemId.length != cdmaHome.networkId.length || + !sid || !nid) { + // CDMA Home haven't been ready, or we haven't got the system id and + // network id of the network we register to, assuming we are in home + // network. + iccInfo.isDisplaySpnRequired = true; + } else { + // Determine if we are registered in the home service area. + // System ID and Network ID are described in 3GPP2 C.S0005 Sec. 2.6.5.2. + let inHomeArea = false; + for (let i = 0; i < cdmaHome.systemId.length; i++) { + let homeSid = cdmaHome.systemId[i], + homeNid = cdmaHome.networkId[i]; + if (homeSid === 0 || homeNid === 0 // Reserved system id/network id + || homeSid != sid) { + continue; + } + // According to 3GPP2 C.S0005 Sec. 2.6.5.2, NID number 65535 means + // all networks in the system should be considered as home. + if (homeNid == 65535 || homeNid == nid) { + inHomeArea = true; + break; + } + } + iccInfo.isDisplaySpnRequired = inHomeArea; + } + } + } else { + // GSM family display rule. + let operatorMnc = RIL.operator ? RIL.operator.mnc : -1; + let operatorMcc = RIL.operator ? RIL.operator.mcc : -1; + + // First detect if we are on HPLMN or one of the PLMN + // specified by the SIM card. + let isOnMatchingPlmn = false; + + // If the current network is the one defined as mcc/mnc + // in SIM card, it's okay. + if (iccInfo.mcc == operatorMcc && iccInfo.mnc == operatorMnc) { + isOnMatchingPlmn = true; + } + + // Test to see if operator's mcc/mnc match mcc/mnc of PLMN. + if (!isOnMatchingPlmn && iccInfoPriv.SPDI) { + let iccSpdi = iccInfoPriv.SPDI; // PLMN list + for (let plmn in iccSpdi) { + let plmnMcc = iccSpdi[plmn].mcc; + let plmnMnc = iccSpdi[plmn].mnc; + isOnMatchingPlmn = (plmnMcc == operatorMcc) && (plmnMnc == operatorMnc); + if (isOnMatchingPlmn) { + break; + } + } + } + + // See 3GPP TS 22.101 A.4 Service Provider Name indication, and TS 31.102 + // clause 4.2.12 EF_SPN for detail. + if (isOnMatchingPlmn) { + // The first bit of display condition tells us if we should display + // registered PLMN. + if (DEBUG) { + this.context.debug("PLMN is HPLMN or PLMN " + "is in PLMN list"); + } + + // TS 31.102 Sec. 4.2.66 and TS 51.011 Sec. 10.3.50 + // EF_SPDI contains a list of PLMNs in which the Service Provider Name + // shall be displayed. + iccInfo.isDisplaySpnRequired = true; + iccInfo.isDisplayNetworkNameRequired = (displayCondition & 0x01) !== 0; + } else { + // The second bit of display condition tells us if we should display + // registered PLMN. + if (DEBUG) { + this.context.debug("PLMN isn't HPLMN and PLMN isn't in PLMN list"); + } + + iccInfo.isDisplayNetworkNameRequired = true; + iccInfo.isDisplaySpnRequired = (displayCondition & 0x02) === 0; + } + } + + if (DEBUG) { + this.context.debug("isDisplayNetworkNameRequired = " + + iccInfo.isDisplayNetworkNameRequired); + this.context.debug("isDisplaySpnRequired = " + iccInfo.isDisplaySpnRequired); + } + + return ((origIsDisplayNetworkNameRequired !== iccInfo.isDisplayNetworkNameRequired) || + (origIsDisplaySPNRequired !== iccInfo.isDisplaySpnRequired)); + }, + + decodeSimTlvs: function(tlvsLen) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let index = 0; + let tlvs = []; + while (index < tlvsLen) { + let simTlv = { + tag : GsmPDUHelper.readHexOctet(), + length : GsmPDUHelper.readHexOctet(), + }; + simTlv.value = GsmPDUHelper.readHexOctetArray(simTlv.length); + tlvs.push(simTlv); + index += simTlv.length + 2; // The length of 'tag' and 'length' field. + } + return tlvs; + }, + + /** + * Parse those TLVs and convert it to an object. + */ + parsePbrTlvs: function(pbrTlvs) { + let pbr = {}; + for (let i = 0; i < pbrTlvs.length; i++) { + let pbrTlv = pbrTlvs[i]; + let anrIndex = 0; + for (let j = 0; j < pbrTlv.value.length; j++) { + let tlv = pbrTlv.value[j]; + let tagName = USIM_TAG_NAME[tlv.tag]; + + // ANR could have multiple files. We save it as anr0, anr1,...etc. + if (tlv.tag == ICC_USIM_EFANR_TAG) { + tagName += anrIndex; + anrIndex++; + } + pbr[tagName] = tlv; + pbr[tagName].fileType = pbrTlv.tag; + pbr[tagName].fileId = (tlv.value[0] << 8) | tlv.value[1]; + pbr[tagName].sfi = tlv.value[2]; + + // For Type 2, the order of files is in the same order in IAP. + if (pbrTlv.tag == ICC_USIM_TYPE2_TAG) { + pbr[tagName].indexInIAP = j; + } + } + } + + return pbr; + }, + + /** + * Update the ICC information to RadioInterfaceLayer. + */ + handleICCInfoChange: function() { + let RIL = this.context.RIL; + RIL.iccInfo.rilMessageType = "iccinfochange"; + RIL.sendChromeMessage(RIL.iccInfo); + }, + + /** + * Get whether specificed (U)SIM service is available. + * + * @param geckoService + * Service name like "ADN", "BDN", etc. + * + * @return true if the service is enabled, false otherwise. + */ + isICCServiceAvailable: function(geckoService) { + let RIL = this.context.RIL; + let serviceTable = RIL._isCdma ? RIL.iccInfoPrivate.cst: + RIL.iccInfoPrivate.sst; + let index, bitmask; + if (RIL.appType == CARD_APPTYPE_SIM || RIL.appType == CARD_APPTYPE_RUIM) { + /** + * Service id is valid in 1..N, and 2 bits are used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not allocated. + * 1, service allocated. + * b2 = 0, service not activated. + * 1, service activated. + * + * @see 3GPP TS 51.011 10.3.7. + */ + let simService; + if (RIL.appType == CARD_APPTYPE_SIM) { + simService = GECKO_ICC_SERVICES.sim[geckoService]; + } else { + simService = GECKO_ICC_SERVICES.ruim[geckoService]; + } + if (!simService) { + return false; + } + simService -= 1; + index = Math.floor(simService / 4); + bitmask = 2 << ((simService % 4) << 1); + } else if (RIL.appType == CARD_APPTYPE_USIM) { + /** + * Service id is valid in 1..N, and 1 bit is used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not avaiable. + * 1, service available. + * + * @see 3GPP TS 31.102 4.2.8. + */ + let usimService = GECKO_ICC_SERVICES.usim[geckoService]; + if (!usimService) { + return false; + } + usimService -= 1; + index = Math.floor(usimService / 8); + bitmask = 1 << ((usimService % 8) << 0); + } + + return (serviceTable !== null) && + (index < serviceTable.length) && + ((serviceTable[index] & bitmask) !== 0); + }, + + /** + * Get whether specificed CPHS service is available. + * + * @param geckoService + * Service name like "MDN", etc. + * + * @return true if the service is enabled, false otherwise. + */ + isCphsServiceAvailable: function(geckoService) { + let RIL = this.context.RIL; + let serviceTable = RIL.iccInfoPrivate.cphsSt; + + if (!(serviceTable instanceof Uint8Array)) { + return false; + } + + /** + * Service id is valid in 1..N, and 2 bits are used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not allocated. + * 1, service allocated. + * b2 = 0, service not activated. + * 1, service activated. + * + * @See B.3.1.1 CPHS Information in CPHS Phase 2. + */ + let cphsService = GECKO_ICC_SERVICES.cphs[geckoService]; + + if (!cphsService) { + return false; + } + cphsService -= 1; + let index = Math.floor(cphsService / 4); + let bitmask = 2 << ((cphsService % 4) << 1); + + return (index < serviceTable.length) && + ((serviceTable[index] & bitmask) !== 0); + }, + + /** + * Check if the string is of GSM default 7-bit coded alphabets with bit 8 + * set to 0. + * + * @param str String to be checked. + */ + isGsm8BitAlphabet: function(str) { + if (!str) { + return false; + } + + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + for (let i = 0; i < str.length; i++) { + let c = str.charAt(i); + let octet = langTable.indexOf(c); + if (octet == -1) { + octet = langShiftTable.indexOf(c); + if (octet == -1) { + return false; + } + } + } + + return true; + }, + + /** + * Parse MCC/MNC from IMSI. If there is no available value for the length of + * mnc, it will use the data in MCC table to parse. + * + * @param imsi + * The imsi of icc. + * @param mncLength [optional] + * The length of mnc. + * Zero indicates we haven't got a valid mnc length. + * + * @return An object contains the parsing result of mcc and mnc. + * Or null if any error occurred. + */ + parseMccMncFromImsi: function(imsi, mncLength) { + if (!imsi) { + return null; + } + + // MCC is the first 3 digits of IMSI. + let mcc = imsi.substr(0,3); + if (!mncLength) { + // Check the MCC/MNC table for MNC length = 3 first for the case we don't + // have the 4th byte data from EF_AD. + if (PLMN_HAVING_3DIGITS_MNC[mcc] && + PLMN_HAVING_3DIGITS_MNC[mcc].indexOf(imsi.substr(3, 3)) !== -1) { + mncLength = 3; + } else { + // Check the MCC table to decide the length of MNC. + let index = MCC_TABLE_FOR_MNC_LENGTH_IS_3.indexOf(mcc); + mncLength = (index !== -1) ? 3 : 2; + } + } + let mnc = imsi.substr(3, mncLength); + if (DEBUG) { + this.context.debug("IMSI: " + imsi + " MCC: " + mcc + " MNC: " + mnc); + } + + return { mcc: mcc, mnc: mnc}; + }, +}; + +/** + * Helper for ICC Contacts. + */ +function ICCContactHelperObject(aContext) { + this.context = aContext; +} +ICCContactHelperObject.prototype = { + context: null, + + /** + * Helper function to check DF_PHONEBOOK. + */ + hasDfPhoneBook: function(appType) { + switch (appType) { + case CARD_APPTYPE_SIM: + return false; + case CARD_APPTYPE_USIM: + return true; + case CARD_APPTYPE_RUIM: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + return ICCUtilsHelper.isICCServiceAvailable("ENHANCED_PHONEBOOK"); + default: + return false; + } + }, + + /** + * Helper function to read ICC contacts. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readICCContacts: function(appType, contactType, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + ICCRecordHelper.readADNLike(ICC_EF_ADN, + (ICCUtilsHelper.isICCServiceAvailable("EXT1")) ? ICC_EF_EXT1 : null, + onsuccess, onerror); + } else { + this.readUSimContacts(onsuccess, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + if (!ICCUtilsHelper.isICCServiceAvailable("FDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + ICCRecordHelper.readADNLike(ICC_EF_FDN, + (ICCUtilsHelper.isICCServiceAvailable("EXT2")) ? ICC_EF_EXT2 : null, + onsuccess, onerror); + break; + case GECKO_CARDCONTACT_TYPE_SDN: + if (!ICCUtilsHelper.isICCServiceAvailable("SDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + + ICCRecordHelper.readADNLike(ICC_EF_SDN, + (ICCUtilsHelper.isICCServiceAvailable("EXT3")) ? ICC_EF_EXT3 : null, + onsuccess, onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Helper function to find free contact record. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findFreeICCContact: function(appType, contactType, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + ICCRecordHelper.findFreeRecordId(ICC_EF_ADN, onsuccess.bind(null, 0), onerror); + } else { + let gotPbrCb = function gotPbrCb(pbrs) { + this.findUSimFreeADNRecordId(pbrs, onsuccess, onerror); + }.bind(this); + + ICCRecordHelper.readPBR(gotPbrCb, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + ICCRecordHelper.findFreeRecordId(ICC_EF_FDN, onsuccess.bind(null, 0), onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Cache the pbr index of the possible free record. + */ + _freePbrIndex: 0, + + /** + * Find free ADN record id in USIM. + * + * @param pbrs All Phonebook Reference Files read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findUSimFreeADNRecordId: function(pbrs, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + function callback(pbrIndex, recordId) { + // Assume other free records are probably in the same phonebook set. + this._freePbrIndex = pbrIndex; + onsuccess(pbrIndex, recordId); + } + + let nextPbrIndex = -1; + (function findFreeRecordId(pbrIndex) { + if (nextPbrIndex === this._freePbrIndex) { + // No free record found, reset the pbr index of free record. + this._freePbrIndex = 0; + if (DEBUG) { + this.context.debug(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + onerror(CONTACT_ERR_NO_FREE_RECORD_FOUND); + return; + } + + let pbr = pbrs[pbrIndex]; + nextPbrIndex = (pbrIndex + 1) % pbrs.length; + ICCRecordHelper.findFreeRecordId( + pbr.adn.fileId, + callback.bind(this, pbrIndex), + findFreeRecordId.bind(this, nextPbrIndex)); + }).call(this, this._freePbrIndex); + }, + + /** + * Helper function to add a new ICC contact. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be added. + * @param pin2 PIN2 is required for FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + addICCContact: function(appType, contactType, contact, pin2, onsuccess, onerror) { + let foundFreeCb = (function foundFreeCb(pbrIndex, recordId) { + contact.pbrIndex = pbrIndex; + contact.recordId = recordId; + this.updateICCContact(appType, contactType, contact, pin2, onsuccess, onerror); + }).bind(this); + + // Find free record first. + this.findFreeICCContact(appType, contactType, foundFreeCb, onerror); + }, + + /** + * Helper function to update ICC contact. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be updated. + * @param pin2 PIN2 is required for FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateICCContact: function(appType, contactType, contact, pin2, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + + let updateContactCb = (updatedContact) => { + updatedContact.pbrIndex = contact.pbrIndex; + updatedContact.recordId = contact.recordId; + onsuccess(updatedContact); + } + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + if (ICCUtilsHelper.isICCServiceAvailable("EXT1")) { + this.updateADNLikeWithExtension(ICC_EF_ADN, ICC_EF_EXT1, + contact, null, + updateContactCb, onerror); + } else { + ICCRecordHelper.updateADNLike(ICC_EF_ADN, 0xff, + contact, null, + updateContactCb, onerror); + } + } else { + this.updateUSimContact(contact, updateContactCb, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + if (!pin2) { + onerror(GECKO_ERROR_SIM_PIN2); + return; + } + if (!ICCUtilsHelper.isICCServiceAvailable("FDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + if (ICCUtilsHelper.isICCServiceAvailable("EXT2")) { + this.updateADNLikeWithExtension(ICC_EF_FDN, ICC_EF_EXT2, + contact, pin2, + updateContactCb, onerror); + } else { + ICCRecordHelper.updateADNLike(ICC_EF_FDN, + 0xff, + contact, pin2, + updateContactCb, onerror); + } + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Read contacts from USIM. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readUSimContacts: function(onsuccess, onerror) { + let gotPbrCb = function gotPbrCb(pbrs) { + this.readAllPhonebookSets(pbrs, onsuccess, onerror); + }.bind(this); + + this.context.ICCRecordHelper.readPBR(gotPbrCb, onerror); + }, + + /** + * Read all Phonebook sets. + * + * @param pbrs All Phonebook Reference Files read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readAllPhonebookSets: function(pbrs, onsuccess, onerror) { + let allContacts = [], pbrIndex = 0; + let readPhonebook = function(contacts) { + if (contacts) { + allContacts = allContacts.concat(contacts); + } + + let cLen = contacts ? contacts.length : 0; + for (let i = 0; i < cLen; i++) { + contacts[i].pbrIndex = pbrIndex; + } + + pbrIndex++; + if (pbrIndex >= pbrs.length) { + if (onsuccess) { + onsuccess(allContacts); + } + return; + } + + this.readPhonebookSet(pbrs[pbrIndex], readPhonebook, onerror); + }.bind(this); + + this.readPhonebookSet(pbrs[pbrIndex], readPhonebook, onerror); + }, + + /** + * Read from Phonebook Reference File. + * + * @param pbr Phonebook Reference File to be read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPhonebookSet: function(pbr, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let gotAdnCb = function gotAdnCb(contacts) { + this.readSupportedPBRFields(pbr, contacts, onsuccess, onerror); + }.bind(this); + + ICCRecordHelper.readADNLike(pbr.adn.fileId, + (pbr.ext1) ? pbr.ext1.fileId : null, gotAdnCb, onerror); + }, + + /** + * Read supported Phonebook fields. + * + * @param pbr Phone Book Reference file. + * @param contacts Contacts stored on ICC. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readSupportedPBRFields: function(pbr, contacts, onsuccess, onerror) { + let fieldIndex = 0; + (function readField() { + let field = USIM_PBR_FIELDS[fieldIndex]; + fieldIndex += 1; + if (!field) { + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + this.readPhonebookField(pbr, contacts, field, readField.bind(this), onerror); + }).call(this); + }, + + /** + * Read Phonebook field. + * + * @param pbr The phonebook reference file. + * @param contacts Contacts stored on ICC. + * @param field Phonebook field to be retrieved. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPhonebookField: function(pbr, contacts, field, onsuccess, onerror) { + if (!pbr[field]) { + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + (function doReadContactField(n) { + if (n >= contacts.length) { + // All contact's fields are read. + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + // get n-th contact's field. + this.readContactField(pbr, contacts[n], field, + doReadContactField.bind(this, n + 1), onerror); + }).call(this, 0); + }, + + /** + * Read contact's field from USIM. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to get field. + * @param field Phonebook field to be retrieved. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readContactField: function(pbr, contact, field, onsuccess, onerror) { + let gotRecordIdCb = function gotRecordIdCb(recordId) { + if (recordId == 0xff) { + if (onsuccess) { + onsuccess(); + } + return; + } + + let fileId = pbr[field].fileId; + let fileType = pbr[field].fileType; + let gotFieldCb = function gotFieldCb(value) { + if (value) { + // Move anr0 anr1,.. into anr[]. + if (field.startsWith(USIM_PBR_ANR)) { + if (!contact[USIM_PBR_ANR]) { + contact[USIM_PBR_ANR] = []; + } + contact[USIM_PBR_ANR].push(value); + } else { + contact[field] = value; + } + } + + if (onsuccess) { + onsuccess(); + } + }.bind(this); + + let ICCRecordHelper = this.context.ICCRecordHelper; + // Detect EF to be read, for anr, it could have anr0, anr1,... + let ef = field.startsWith(USIM_PBR_ANR) ? USIM_PBR_ANR : field; + switch (ef) { + case USIM_PBR_EMAIL: + ICCRecordHelper.readEmail(fileId, fileType, recordId, gotFieldCb, onerror); + break; + case USIM_PBR_ANR: + ICCRecordHelper.readANR(fileId, fileType, recordId, gotFieldCb, onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + break; + } + }.bind(this); + + this.getContactFieldRecordId(pbr, contact, field, gotRecordIdCb, onerror); + }, + + /** + * Get the recordId. + * + * If the fileType of field is ICC_USIM_TYPE1_TAG, use corresponding ADN recordId. + * otherwise get the recordId from IAP. + * + * @see TS 131.102, clause 4.4.2.2 + * + * @param pbr The phonebook reference file. + * @param contact The contact will be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + getContactFieldRecordId: function(pbr, contact, field, onsuccess, onerror) { + if (pbr[field].fileType == ICC_USIM_TYPE1_TAG) { + // If the file type is ICC_USIM_TYPE1_TAG, use corresponding ADN recordId. + if (onsuccess) { + onsuccess(contact.recordId); + } + } else if (pbr[field].fileType == ICC_USIM_TYPE2_TAG) { + // If the file type is ICC_USIM_TYPE2_TAG, the recordId shall be got from IAP. + let gotIapCb = function gotIapCb(iap) { + let indexInIAP = pbr[field].indexInIAP; + let recordId = iap[indexInIAP]; + + if (onsuccess) { + onsuccess(recordId); + } + }.bind(this); + + this.context.ICCRecordHelper.readIAP(pbr.iap.fileId, contact.recordId, + gotIapCb, onerror); + } else { + if (DEBUG) { + this.context.debug("USIM PBR files in Type 3 format are not supported."); + } + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED); + } + }, + + /** + * Update USIM contact. + * + * @param contact The contact will be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateUSimContact: function(contact, onsuccess, onerror) { + let gotPbrCb = function gotPbrCb(pbrs) { + let pbr = pbrs[contact.pbrIndex]; + if (!pbr) { + if (DEBUG) { + this.context.debug(CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK); + } + onerror(CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK); + return; + } + this.updatePhonebookSet(pbr, contact, onsuccess, onerror); + }.bind(this); + + this.context.ICCRecordHelper.readPBR(gotPbrCb, onerror); + }, + + /** + * Update fields in Phonebook Reference File. + * + * @param pbr Phonebook Reference File to be read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updatePhonebookSet: function(pbr, contact, onsuccess, onerror) { + let updateAdnCb = function(updatedContact) { + this.updateSupportedPBRFields(pbr, contact, (updatedContactField) => { + onsuccess(Object.assign(updatedContact, updatedContactField)); + }, onerror); + }.bind(this); + + if (pbr.ext1) { + this.updateADNLikeWithExtension(pbr.adn.fileId, pbr.ext1.fileId, + contact, null, updateAdnCb, onerror); + } else { + this.context.ICCRecordHelper.updateADNLike(pbr.adn.fileId, 0xff, contact, + null, updateAdnCb, onerror); + } + }, + + /** + * Update supported Phonebook fields. + * + * @param pbr Phone Book Reference file. + * @param contact Contact to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateSupportedPBRFields: function(pbr, contact, onsuccess, onerror) { + let fieldIndex = 0; + let contactField = {}; + + (function updateField() { + let field = USIM_PBR_FIELDS[fieldIndex]; + fieldIndex += 1; + + if (!field) { + if (onsuccess) { + onsuccess(contactField); + } + return; + } + + // Check if PBR has this field. + if (!pbr[field]) { + updateField.call(this); + return; + } + + this.updateContactField(pbr, contact, field, (fieldEntry) => { + contactField = Object.assign(contactField, fieldEntry); + updateField.call(this); + }, (errorMsg) => { + // Bug 1194149, there are some sim cards without sufficient + // Type 2 USIM contact fields record. We allow user continue + // importing contacts. + if (errorMsg === CONTACT_ERR_NO_FREE_RECORD_FOUND) { + updateField.call(this); + return; + } + onerror(errorMsg); + }); + }).call(this); + }, + + /** + * Update contact's field from USIM. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactField: function(pbr, contact, field, onsuccess, onerror) { + if (pbr[field].fileType === ICC_USIM_TYPE1_TAG) { + this.updateContactFieldType1(pbr, contact, field, onsuccess, onerror); + } else if (pbr[field].fileType === ICC_USIM_TYPE2_TAG) { + this.updateContactFieldType2(pbr, contact, field, onsuccess, onerror); + } else { + if (DEBUG) { + this.context.debug("USIM PBR files in Type 3 format are not supported."); + } + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED); + } + }, + + /** + * Update Type 1 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactFieldType1: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, contact.recordId, contact.email, null, + (updatedEmail) => { + onsuccess({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + let anr = Array.isArray(contact.anr) ? contact.anr[0] : null; + ICCRecordHelper.updateANR(pbr, contact.recordId, anr, null, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + onsuccess((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } else { + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + } + }, + + /** + * Update Type 2 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactFieldType2: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + // Case 1 : EF_IAP[adnRecordId] doesn't have a value(0xff) + // Find a free recordId for EF_field + // Update field with that free recordId. + // Update IAP. + // + // Case 2: EF_IAP[adnRecordId] has a value + // update EF_field[iap[field.indexInIAP]] + + let gotIapCb = function gotIapCb(iap) { + let recordId = iap[pbr[field].indexInIAP]; + if (recordId === 0xff) { + // If the value in IAP[index] is 0xff, which means the contact stored on + // the SIM doesn't have the additional attribute (email or anr). + // So if the contact to be updated doesn't have the attribute either, + // we don't have to update it. + if ((field === USIM_PBR_EMAIL && contact.email) || + (field === USIM_PBR_ANR0 && + (Array.isArray(contact.anr) && contact.anr[0]))) { + // Case 1. + this.addContactFieldType2(pbr, contact, field, onsuccess, onerror); + } else { + if (onsuccess) { + onsuccess(); + } + } + return; + } + + // Case 2. + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, recordId, contact.email, contact.recordId, + (updatedEmail) => { + onsuccess({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + let anr = Array.isArray(contact.anr) ? contact.anr[0] : null; + ICCRecordHelper.updateANR(pbr, recordId, anr, contact.recordId, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + onsuccess((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } else { + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + } + + }.bind(this); + + ICCRecordHelper.readIAP(pbr.iap.fileId, contact.recordId, gotIapCb, onerror); + }, + + /** + * Add Type 2 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + addContactFieldType2: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let successCb = function successCb(recordId) { + + let updateCb = function updateCb(contactField) { + this.updateContactFieldIndexInIAP(pbr, contact.recordId, field, recordId, () => { + onsuccess(contactField); + }, onerror); + }.bind(this); + + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, recordId, contact.email, contact.recordId, + (updatedEmail) => { + updateCb({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + ICCRecordHelper.updateANR(pbr, recordId, contact.anr[0], contact.recordId, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + updateCb((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } + }.bind(this); + + let errorCb = function errorCb(errorMsg) { + if (DEBUG) { + this.context.debug(errorMsg + " USIM field " + field); + } + onerror(errorMsg); + }.bind(this); + + ICCRecordHelper.findFreeRecordId(pbr[field].fileId, successCb, errorCb); + }, + + /** + * Update IAP value. + * + * @param pbr The phonebook reference file. + * @param recordNumber The record identifier of EF_IAP. + * @param field Phonebook field. + * @param value The value of 'field' in IAP. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + * + */ + updateContactFieldIndexInIAP: function(pbr, recordNumber, field, value, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + let gotIAPCb = function gotIAPCb(iap) { + iap[pbr[field].indexInIAP] = value; + ICCRecordHelper.updateIAP(pbr.iap.fileId, recordNumber, iap, onsuccess, onerror); + }.bind(this); + ICCRecordHelper.readIAP(pbr.iap.fileId, recordNumber, gotIAPCb, onerror); + }, + + /** + * Update ICC ADN like EFs with Extension, like EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN or FDN. + * @param extFileId EF id of the EXT. + * @param contact The contact will be updated. (Shall have recordId property) + * @param pin2 PIN2 is required when updating ICC_EF_FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateADNLikeWithExtension: function(fileId, extFileId, contact, pin2, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let extNumber; + + if (contact.number) { + let numStart = contact.number[0] == "+" ? 1 : 0; + let number = contact.number.substring(0, numStart) + + this.context.GsmPDUHelper.stringToExtendedBcd( + contact.number.substring(numStart)); + extNumber = number.substr(numStart + ADN_MAX_NUMBER_DIGITS, + EXT_MAX_NUMBER_DIGITS); + } + + ICCRecordHelper.getADNLikeExtensionRecordNumber(fileId, contact.recordId, + (extRecordNumber) => { + let updateADNLike = (extRecordNumber) => { + ICCRecordHelper.updateADNLike(fileId, extRecordNumber, contact, + pin2, (updatedContact) => { + if (extNumber && extRecordNumber != 0xff) { + updatedContact.number = updatedContact.number.concat(extNumber); + } + onsuccess(updatedContact); + }, onerror); + }; + + let updateExtension = (extRecordNumber) => { + ICCRecordHelper.updateExtension(extFileId, extRecordNumber, extNumber, + () => updateADNLike(extRecordNumber), + () => updateADNLike(0xff)); + }; + + if (extNumber) { + if (extRecordNumber != 0xff) { + updateExtension(extRecordNumber); + return; + } + + ICCRecordHelper.findFreeRecordId(extFileId, + (extRecordNumber) => updateExtension(extRecordNumber), + (errorMsg) => { + if (DEBUG) { + this.context.debug("Couldn't find free extension record Id for " + extFileId + ": " + errorMsg); + } + updateADNLike(0xff); + }); + return; + } + + if (extRecordNumber != 0xff) { + ICCRecordHelper.cleanEFRecord(extFileId, extRecordNumber, + () => updateADNLike(0xff), onerror); + return; + } + + updateADNLike(0xff); + }, onerror); + }, +}; + +function IconLoaderObject(aContext) { + this.context = aContext; +} +IconLoaderObject.prototype = { + context: null, + + /** + * Load icons. + * + * @param recordNumbers Array of the record identifiers of EF_IMG. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + loadIcons: function(recordNumbers, onsuccess, onerror) { + if (!recordNumbers || !recordNumbers.length) { + if (onerror) { + onerror(); + } + return; + } + + this._start({ + recordNumbers: recordNumbers, + onsuccess: onsuccess, + onerror: onerror}); + }, + + _start: function(options) { + let callback = (function(icons) { + if (!options.icons) { + options.icons = []; + } + for (let i = 0; i < icons.length; i++) { + icons[i] = this._parseRawData(icons[i]); + } + options.icons[options.currentRecordIndex] = icons; + options.currentRecordIndex++; + + let recordNumbers = options.recordNumbers; + if (options.currentRecordIndex < recordNumbers.length) { + let recordNumber = recordNumbers[options.currentRecordIndex]; + this.context.SimRecordHelper.readIMG(recordNumber, + callback, + options.onerror); + } else { + if (options.onsuccess) { + options.onsuccess(options.icons); + } + } + }).bind(this); + + options.currentRecordIndex = 0; + this.context.SimRecordHelper.readIMG(options.recordNumbers[0], + callback, + options.onerror); + }, + + _parseRawData: function(rawData) { + let codingScheme = rawData.codingScheme; + + switch (codingScheme) { + case ICC_IMG_CODING_SCHEME_BASIC: + return this._decodeBasicImage(rawData.width, rawData.height, rawData.body); + + case ICC_IMG_CODING_SCHEME_COLOR: + case ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY: + return this._decodeColorImage(codingScheme, + rawData.width, rawData.height, + rawData.bitsPerImgPoint, + rawData.numOfClutEntries, + rawData.clut, rawData.body); + } + + return null; + }, + + _decodeBasicImage: function(width, height, body) { + let numOfPixels = width * height; + let pixelIndex = 0; + let currentByteIndex = 0; + let currentByte = 0x00; + + const BLACK = 0x000000FF; + const WHITE = 0xFFFFFFFF; + + let pixels = []; + while (pixelIndex < numOfPixels) { + // Reassign data and index for every byte (8 bits). + if (pixelIndex % 8 == 0) { + currentByte = body[currentByteIndex++]; + } + let bit = (currentByte >> (7 - (pixelIndex % 8))) & 0x01; + pixels[pixelIndex++] = bit ? WHITE : BLACK; + } + + return {pixels: pixels, + codingScheme: GECKO_IMG_CODING_SCHEME_BASIC, + width: width, + height: height}; + }, + + _decodeColorImage: function(codingScheme, width, height, bitsPerImgPoint, + numOfClutEntries, clut, body) { + let mask = 0xff >> (8 - bitsPerImgPoint); + let bitsStartOffset = 8 - bitsPerImgPoint; + let bitIndex = bitsStartOffset; + let numOfPixels = width * height; + let pixelIndex = 0; + let currentByteIndex = 0; + let currentByte = body[currentByteIndex++]; + + let pixels = []; + while (pixelIndex < numOfPixels) { + // Reassign data and index for every byte (8 bits). + if (bitIndex < 0) { + currentByte = body[currentByteIndex++]; + bitIndex = bitsStartOffset; + } + let clutEntry = ((currentByte >> bitIndex) & mask); + let clutIndex = clutEntry * ICC_CLUT_ENTRY_SIZE; + let alpha = codingScheme == ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY && + clutEntry == numOfClutEntries - 1; + pixels[pixelIndex++] = alpha ? 0x00 + : (clut[clutIndex] << 24 | + clut[clutIndex + 1] << 16 | + clut[clutIndex + 2] << 8 | + 0xFF) >>> 0; + bitIndex -= bitsPerImgPoint; + } + + return {pixels: pixels, + codingScheme: ICC_IMG_CODING_SCHEME_TO_GECKO[codingScheme], + width: width, + height: height}; + }, +}; + +/** + * Global stuff. + */ + +function Context(aClientId) { + this.clientId = aClientId; + + this.Buf = new BufObject(this); + this.RIL = new RilObject(this); + this.RIL.initRILState(); +} +Context.prototype = { + clientId: null, + Buf: null, + RIL: null, + + debug: function(aMessage) { + GLOBAL.debug("[" + this.clientId + "] " + aMessage); + } +}; + +(function() { + let lazySymbols = [ + "BerTlvHelper", "BitBufferHelper", "CdmaPDUHelper", + "ComprehensionTlvHelper", "GsmPDUHelper", "ICCContactHelper", + "ICCFileHelper", "ICCIOHelper", "ICCPDUHelper", "ICCRecordHelper", + "ICCUtilsHelper", "RuimRecordHelper", "SimRecordHelper", + "StkCommandParamsFactory", "StkProactiveCmdHelper", "IconLoader", + ]; + + for (let i = 0; i < lazySymbols.length; i++) { + let symbol = lazySymbols[i]; + Object.defineProperty(Context.prototype, symbol, { + get: function() { + let real = new GLOBAL[symbol + "Object"](this); + Object.defineProperty(this, symbol, { + value: real, + enumerable: true + }); + return real; + }, + configurable: true, + enumerable: true + }); + } +})(); + +var ContextPool = { + _contexts: [], + + handleRilMessage: function(aClientId, aUint8Array) { + let context = this._contexts[aClientId]; + context.Buf.processIncoming(aUint8Array); + }, + + handleChromeMessage: function(aMessage) { + let clientId = aMessage.rilMessageClientId; + if (clientId != null) { + let context = this._contexts[clientId]; + context.RIL.handleChromeMessage(aMessage); + return; + } + + if (DEBUG) debug("Received global chrome message " + JSON.stringify(aMessage)); + let method = this[aMessage.rilMessageType]; + if (typeof method != "function") { + if (DEBUG) { + debug("Don't know what to do"); + } + return; + } + method.call(this, aMessage); + }, + + setInitialOptions: function(aOptions) { + DEBUG = DEBUG_WORKER || aOptions.debug; + + let quirks = aOptions.quirks; + RILQUIRKS_CALLSTATE_EXTRA_UINT32 = quirks.callstateExtraUint32; + RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL = quirks.requestUseDialEmergencyCall; + RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS = quirks.simAppStateExtraFields; + RILQUIRKS_EXTRA_UINT32_2ND_CALL = quirks.extraUint2ndCall; + RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT = quirks.haveQueryIccLockRetryCount; + RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD = quirks.sendStkProfileDownload; + RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = quirks.dataRegistrationOnDemand; + RILQUIRKS_SUBSCRIPTION_CONTROL = quirks.subscriptionControl; + RILQUIRKS_SIGNAL_EXTRA_INT32 = quirks.signalExtraInt; + RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING = quirks.availableNetworkExtraStr; + RILQUIRKS_SMSC_ADDRESS_FORMAT = quirks.smscAddressFormat; + }, + + setDebugFlag: function(aOptions) { + DEBUG = DEBUG_WORKER || aOptions.debug; + }, + + registerClient: function(aOptions) { + let clientId = aOptions.clientId; + this._contexts[clientId] = new Context(clientId); + }, +}; + +function onRILMessage(aClientId, aUint8Array) { + ContextPool.handleRilMessage(aClientId, aUint8Array); +} + +onmessage = function onmessage(event) { + ContextPool.handleChromeMessage(event.data); +}; + +onerror = function onerror(event) { + if (DEBUG) debug("onerror" + event.message + "\n"); +}; |